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.
@@ -0,0 +1,229 @@
1
+ // Griffincss UI — Dropdown
2
+ // Выпадающая панель под кнопкой. Внутрь кладут .gr-menu, но не обязательно:
3
+ // панель — просто поверхность, на ней может стоять форма или список ссылок.
4
+ //
5
+ // Раскрытие берётся у платформы двумя способами, и оба работают без скрипта:
6
+ //
7
+ // 1. <details class="gr-dropdown"> — работает везде. Панель позиционируется
8
+ // абсолютно внутри обёртки. Закрытия по клику мимо и по Esc платформа
9
+ // здесь не даёт: <details> о них не знает.
10
+ //
11
+ // 2. <button popovertarget> + панель с атрибутом popover — верхний слой,
12
+ // закрытие по Esc и по клику мимо даёт браузер. Панель в верхнем слое
13
+ // позиционируется относительно окна, а не обёртки, поэтому привязать
14
+ // её к кнопке может только CSS anchor positioning. Там, где его нет,
15
+ // панель раскрывается листом у нижнего края экрана — положение не то,
16
+ // но меню доступно и закрывается штатно.
17
+ //
18
+ // Оба пути — один класс панели: .gr-dropdown-panel. Третий, .gr-dropdown-hover,
19
+ // раскрывает ту же панель наведением — со своими ограничениями, см. ниже.
20
+
21
+ // --- Обёртка ----------------------------------------------------------------
22
+
23
+ .gr-dropdown {
24
+ position: relative;
25
+ display: inline-block;
26
+ }
27
+
28
+ // Кнопка на <summary>: родной треугольник снимается, остальное оформление
29
+ // берётся у .gr-btn — класс ставится на тот же элемент.
30
+ .gr-dropdown > summary {
31
+ list-style: none;
32
+ cursor: pointer;
33
+ }
34
+
35
+ .gr-dropdown > summary::-webkit-details-marker {
36
+ display: none;
37
+ }
38
+
39
+ // --- Панель -----------------------------------------------------------------
40
+
41
+ .gr-dropdown-panel {
42
+ position: absolute;
43
+ z-index: var(--gr-z-dropdown);
44
+ inset-block-start: calc(100% + var(--gr-ui-gap) * 0.5);
45
+ inset-inline-start: 0;
46
+ box-sizing: border-box;
47
+ min-inline-size: max-content;
48
+ max-inline-size: min(20rem, 90vw);
49
+ margin: 0;
50
+ }
51
+
52
+ // Раскрытие к концу строки — для панели у правого края шапки, которая иначе
53
+ // вылезла бы за экран.
54
+ .gr-dropdown-end .gr-dropdown-panel {
55
+ inset-inline: auto 0;
56
+ }
57
+
58
+ // Раскрытие вверх — для панели в подвале страницы.
59
+ .gr-dropdown-up .gr-dropdown-panel {
60
+ inset-block: auto calc(100% + var(--gr-ui-gap) * 0.5);
61
+ }
62
+
63
+ // --- Панель в верхнем слое --------------------------------------------------
64
+
65
+ // UA-стиль попапа задаёт inset: 0 и margin: auto — панель повисла бы
66
+ // посреди экрана. Сбрасываем и то, и другое; рамка и фон приходят
67
+ // от .gr-menu или от того, что положат внутрь.
68
+ // margin: 0 обязателен. У попапа UA-стиль ставит margin: auto, и при
69
+ // раскладке по position-area свободное место уходит в автополя — заданный
70
+ // отступ от кнопки съедается, панель прилипает к ней вплотную.
71
+ .gr-dropdown-panel[popover] {
72
+ position: fixed;
73
+ inset: auto 0 0;
74
+ max-inline-size: 100%;
75
+ margin: 0;
76
+ border: 0;
77
+ padding: 0;
78
+ background: none;
79
+ color: inherit;
80
+ overflow: visible;
81
+ }
82
+
83
+ // Привязка к кнопке. anchor-scope ограничивает имя якоря поддеревом обёртки:
84
+ // без него все дропдауны страницы с одинаковым --gr-dropdown-anchor
85
+ // притягивались бы к последнему в документе — правило выбора якоря берёт
86
+ // последний подходящий элемент в порядке дерева.
87
+ //
88
+ // Вылеты заданы функцией anchor() поимённо, а не через position-area:
89
+ // при раскладке по области поля панели уходят в выравнивание внутри неё,
90
+ // и отступ от кнопки съедается — панель прилипает к ней вплотную.
91
+ // С anchor() зазор считается там же, где у обычной, не всплывающей панели.
92
+ @supports (anchor-name: --gr-dropdown-anchor) {
93
+ .gr-dropdown {
94
+ anchor-scope: --gr-dropdown-anchor;
95
+ }
96
+
97
+ .gr-dropdown > :where(summary, button, .gr-btn) {
98
+ anchor-name: --gr-dropdown-anchor;
99
+ }
100
+
101
+ .gr-dropdown-panel[popover] {
102
+ position-anchor: --gr-dropdown-anchor;
103
+ inset-block: calc(anchor(end) + var(--gr-ui-gap) * 0.5) auto;
104
+ inset-inline: anchor(start) auto;
105
+ min-inline-size: max-content;
106
+ max-inline-size: min(20rem, 90%);
107
+
108
+ // Если снизу места нет — панель переворачивается вверх, если не хватает
109
+ // вбок — к другому краю кнопки. Без этого меню у нижнего края окна
110
+ // уезжало бы за пределы экрана.
111
+ position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
112
+ }
113
+
114
+ .gr-dropdown-end .gr-dropdown-panel[popover] {
115
+ inset-inline: auto anchor(end);
116
+ }
117
+
118
+ .gr-dropdown-up .gr-dropdown-panel[popover] {
119
+ inset-block: auto calc(anchor(start) + var(--gr-ui-gap) * 0.5);
120
+ }
121
+ }
122
+
123
+ // --- Раскрытие по наведению --------------------------------------------------
124
+
125
+ // Третий способ раскрытия, для навигационных меню: панель показывается,
126
+ // пока указатель на обёртке. Скрипт не нужен и здесь — но у способа есть
127
+ // цена, и она честная:
128
+ //
129
+ // * на сенсорном экране наведения нет вовсе, поэтому меню открывается
130
+ // по фокусу: тап по кнопке ставит фокус, :focus-within раскрывает
131
+ // панель. Кнопка обязана быть <button> — на <div> фокуса не будет;
132
+ // * состояние не сообщается разметкой: aria-expanded без скрипта
133
+ // не обновить, поэтому кнопке ставят aria-haspopup="true" и не ставят
134
+ // aria-expanded — лгать об открытости хуже, чем промолчать;
135
+ // * закрытия по Esc платформа тут не даёт: наведение — не «открытое
136
+ // состояние» с точки зрения браузера.
137
+ //
138
+ // Для меню с действиями — «Удалить», «Выйти» — берут <details> или попап:
139
+ // там состояние настоящее. Наведение годится для навигации, где случайное
140
+ // раскрытие ничего не стоит.
141
+ //
142
+ // Панель прячется visibility, а не display: скрытая через display, она
143
+ // не участвовала бы в переходе, а visibility и из дерева доступности убирает,
144
+ // и анимируется.
145
+ .gr-dropdown-hover .gr-dropdown-panel {
146
+ visibility: hidden;
147
+ opacity: 0;
148
+ transition:
149
+ opacity var(--gr-transition),
150
+ visibility var(--gr-transition);
151
+
152
+ // Задержка на закрытие: указатель, соскользнувший с кнопки мимо панели,
153
+ // не должен захлопывать меню мгновенно.
154
+ transition-delay: 0.25s;
155
+ }
156
+
157
+ // Мостик через зазор между кнопкой и панелью. Без него указатель, идущий
158
+ // от кнопки к меню, пересекает пустое место, наведение прерывается,
159
+ // и панель закрывается на полпути.
160
+ .gr-dropdown-hover .gr-dropdown-panel::before {
161
+ content: "";
162
+ position: absolute;
163
+ inset-block-end: 100%;
164
+ inset-inline: 0;
165
+ block-size: calc(var(--gr-ui-gap) * 0.5);
166
+ }
167
+
168
+ // У раскрытой вверх панели зазор с другой стороны.
169
+ .gr-dropdown-hover.gr-dropdown-up .gr-dropdown-panel::before {
170
+ inset-block: 100% auto;
171
+ }
172
+
173
+ @media (hover: hover) {
174
+ .gr-dropdown-hover:hover .gr-dropdown-panel {
175
+ visibility: visible;
176
+ opacity: 1;
177
+ transition-delay: 0s;
178
+ }
179
+ }
180
+
181
+ // Клавиатура и сенсорный экран: фокус внутри обёртки раскрывает панель
182
+ // без всякого наведения.
183
+ .gr-dropdown-hover:focus-within .gr-dropdown-panel {
184
+ visibility: visible;
185
+ opacity: 1;
186
+ transition-delay: 0s;
187
+ }
188
+
189
+ @media (prefers-reduced-motion: reduce) {
190
+ .gr-dropdown-hover .gr-dropdown-panel {
191
+ transition-duration: 0.01ms;
192
+ }
193
+ }
194
+
195
+ // --- Появление --------------------------------------------------------------
196
+
197
+ // Панель появляется сдвигом на её же зазор и проявлением. Переход в display
198
+ // объявлен через allow-discrete, иначе анимации не было бы вовсе: попап
199
+ // переключается сразу из display: none.
200
+ @supports (transition-behavior: allow-discrete) {
201
+ .gr-dropdown-panel[popover] {
202
+ opacity: 0;
203
+ translate: 0 calc(-1 * var(--gr-ui-gap) * 0.5);
204
+ transition:
205
+ opacity var(--gr-transition),
206
+ translate var(--gr-transition),
207
+ overlay var(--gr-transition) allow-discrete,
208
+ display var(--gr-transition) allow-discrete;
209
+ }
210
+
211
+ .gr-dropdown-panel[popover]:popover-open {
212
+ opacity: 1;
213
+ translate: 0;
214
+ }
215
+
216
+ @starting-style {
217
+ .gr-dropdown-panel[popover]:popover-open {
218
+ opacity: 0;
219
+ translate: 0 calc(-1 * var(--gr-ui-gap) * 0.5);
220
+ }
221
+ }
222
+ }
223
+
224
+ @media (prefers-reduced-motion: reduce) {
225
+ .gr-dropdown-panel[popover] {
226
+ translate: none;
227
+ transition-duration: 0.01ms;
228
+ }
229
+ }
@@ -0,0 +1,70 @@
1
+ // Griffincss UI — Empty state
2
+ // Пустое состояние: список без записей, поиск без результатов, папка,
3
+ // в которую ещё ничего не положили.
4
+ //
5
+ // <div class="gr-empty">
6
+ // <span class="gr-empty-icon" aria-hidden="true">📭</span>
7
+ // <p class="gr-empty-title">Пока пусто</p>
8
+ // <p class="gr-empty-text">Здесь появятся заказы, как только вы
9
+ // оформите первый.</p>
10
+ // <div class="gr-empty-actions">
11
+ // <a class="gr-btn gr-btn-primary" href="/catalog">В каталог</a>
12
+ // </div>
13
+ // </div>
14
+ //
15
+ // Пустое состояние без действия — тупик: человек прочитал, что записей нет,
16
+ // и остался с этим один на один. Поэтому в модуле есть слот под кнопку,
17
+ // и ставить её стоит везде, где следующий шаг вообще существует.
18
+
19
+ .gr-empty {
20
+ display: flex;
21
+ box-sizing: border-box;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ gap: var(--gr-ui-gap);
25
+ padding-block: calc(var(--gr-gap) * 2);
26
+ padding-inline: var(--gr-gap);
27
+ color: var(--gr-color-text-secondary);
28
+ text-align: center;
29
+ }
30
+
31
+ .gr-empty-icon {
32
+ color: var(--gr-color-text-muted);
33
+ font-size: 2.5rem;
34
+ line-height: 1;
35
+ }
36
+
37
+ .gr-empty-title {
38
+ margin: 0;
39
+ color: var(--gr-color-text);
40
+ font-size: 1.125rem;
41
+ font-weight: 600;
42
+ line-height: 1.3;
43
+ }
44
+
45
+ // Пояснение — единственный текст на пустом экране, и во всю ширину таблицы
46
+ // оно читается плохо: строка уходит далеко за комфортные 60–70 знаков.
47
+ // Ограничение задано в ch, поэтому оно следует кеглю и переживает режим
48
+ // для слабовидящих.
49
+ .gr-empty-text {
50
+ max-inline-size: 46ch;
51
+ margin: 0;
52
+ line-height: 1.5;
53
+ }
54
+
55
+ .gr-empty-actions {
56
+ display: flex;
57
+ flex-wrap: wrap;
58
+ gap: var(--gr-ui-gap);
59
+ justify-content: center;
60
+ margin-block-start: calc(var(--gr-ui-gap) * 0.5);
61
+ }
62
+
63
+ // Вариант в рамке: то же состояние внутри пунктирной коробки — годится
64
+ // там, где пустое место иначе не читается как часть интерфейса, например
65
+ // в колонке доски задач.
66
+ .gr-empty-boxed {
67
+ border: var(--gr-border-width) dashed var(--gr-color-border);
68
+ border-radius: var(--gr-radius);
69
+ background-color: color-mix(in srgb, currentcolor 3%, transparent);
70
+ }
@@ -0,0 +1,236 @@
1
+ // Griffincss UI — Form
2
+ // Поля ввода, метки, подсказки и сообщения об ошибке.
3
+ //
4
+ // Три поля — .gr-input, .gr-textarea, .gr-select — одна семья элементов
5
+ // управления: общая высота, общий кегль, общие состояния. Поэтому и размер
6
+ // у них задаётся одной парой классов, .gr-input-sm / .gr-input-lg,
7
+ // на любом из трёх.
8
+ //
9
+ // Родной вид элемента не подменяется там, где его можно оставить:
10
+ // у <input type="date"> остаётся системный календарь, у <input type="color"> —
11
+ // системная палитра. Меняются рамка, фон и кегль.
12
+ //
13
+ // Исключение — стрелка <select>. Её положение задаёт браузер, и на
14
+ // padding-inline-end она не реагирует: стрелка прижимается вплотную к краю
15
+ // поля, а в разных браузерах ещё и выглядит по-разному. Поэтому у списка
16
+ // appearance: none и своя стрелка фоном, отбитая от края на --gr-control-padding-x.
17
+ // Сам элемент при этом остаётся <select> — как <input type="checkbox">
18
+ // остаётся собой под классом .gr-switch.
19
+
20
+ // --- Обёртка ----------------------------------------------------------------
21
+
22
+ // Метка, поле и подсказка одной колонкой. Зазор вдвое меньше --gr-ui-gap:
23
+ // подпись должна читаться как часть поля, а не как соседний блок.
24
+ .gr-field {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: calc(var(--gr-ui-gap) * 0.5);
28
+ }
29
+
30
+ .gr-label {
31
+ color: var(--gr-color-text);
32
+ font-size: 0.875rem;
33
+ font-weight: 600;
34
+ line-height: 1.4;
35
+ }
36
+
37
+ .gr-hint {
38
+ color: var(--gr-color-text-muted);
39
+ font-size: 0.8125rem;
40
+ line-height: 1.4;
41
+ }
42
+
43
+ // Сообщение об ошибке. Цвет — не единственный признак: рядом с полем стоит
44
+ // текст, который переживает и печать, и дальтонизм.
45
+ .gr-error {
46
+ color: var(--gr-color-danger);
47
+ font-size: 0.8125rem;
48
+ line-height: 1.4;
49
+ }
50
+
51
+ // --- Поля -------------------------------------------------------------------
52
+
53
+ .gr-input,
54
+ .gr-textarea,
55
+ .gr-select {
56
+ --gr-control-h: var(--gr-control-height);
57
+
58
+ display: block;
59
+ box-sizing: border-box;
60
+ inline-size: 100%;
61
+ min-block-size: var(--gr-control-h);
62
+ margin: 0;
63
+ padding-block: 0.375rem;
64
+ padding-inline: var(--gr-control-padding-x);
65
+ border: var(--gr-border-width) solid var(--gr-color-border);
66
+ border-radius: var(--gr-radius);
67
+ background-color: var(--gr-color-bg);
68
+ color: var(--gr-color-text);
69
+ font-family: inherit;
70
+ font-size: var(--gr-control-font-size);
71
+ line-height: 1.4;
72
+ transition: border-color var(--gr-transition);
73
+ }
74
+
75
+ // Многострочное поле выше однострочного и тянется только по вертикали:
76
+ // горизонтальное растягивание ломает колонку разметки.
77
+ .gr-textarea {
78
+ min-block-size: calc(var(--gr-control-h) * 2);
79
+ resize: vertical;
80
+ }
81
+
82
+ // Стрелка — один треугольник из конического градиента. Не картинка
83
+ // и не data-URI: цвет берётся из currentcolor, поэтому стрелка следует
84
+ // и теме, и состоянию :disabled, а SVG с зашитым цветом не следовал бы
85
+ // ни тому, ни другому.
86
+ //
87
+ // Раньше её складывали из двух линейных градиентов, стыкующихся встык.
88
+ // Геометрия сходилась точно — сдвиг между слоями был ровно равен ширине
89
+ // половины, — но координаты обеих половин дробные, а округляются при
90
+ // растеризации независимо. Стоило стилю оформления сдвинуть отступ так,
91
+ // что одна половина попадала на 10.5px, а другая на 15.94px, как половины
92
+ // расходились на полпикселя и «галочка» выглядела сломанной. Одна фигура
93
+ // одним слоем такого шва не имеет в принципе.
94
+ //
95
+ // Сектор 90°, отсчёт от −45°: из нижней центральной точки лучи идут
96
+ // в верхние углы коробки, вдвое более широкой, чем высокой.
97
+ .gr-select {
98
+ --gr-select-arrow: 0.34rem;
99
+
100
+ padding-inline-end: calc(var(--gr-control-padding-x) * 2 + var(--gr-select-arrow) * 2);
101
+ background-image: conic-gradient(from -45deg at 50% 100%, currentcolor 0 90deg, transparent 0);
102
+ background-repeat: no-repeat;
103
+
104
+ // Отсчёт от правого края в три значения: так отступ задаётся точно,
105
+ // а не долей от разницы размеров, как при записи процентом.
106
+ background-position: right var(--gr-control-padding-x) center;
107
+ background-size: calc(var(--gr-select-arrow) * 2) var(--gr-select-arrow);
108
+ appearance: none;
109
+ }
110
+
111
+ // Список с несколькими строками не выпадает — стрелке там нечего показывать.
112
+ .gr-select[multiple],
113
+ .gr-select[size] {
114
+ padding-inline-end: var(--gr-control-padding-x);
115
+ background-image: none;
116
+ }
117
+
118
+ .gr-input::placeholder,
119
+ .gr-textarea::placeholder {
120
+ color: var(--gr-color-text-muted);
121
+ opacity: 1;
122
+ }
123
+
124
+ .gr-input:focus-visible,
125
+ .gr-textarea:focus-visible,
126
+ .gr-select:focus-visible {
127
+ border-color: var(--gr-color-accent);
128
+ outline: var(--gr-focus-width) solid var(--gr-color-focus);
129
+ outline-offset: 1px;
130
+ }
131
+
132
+ // :user-invalid, а не :invalid — пустое обязательное поле не должно краснеть
133
+ // до того, как человек его тронул. Атрибут aria-invalid рядом: его ставит
134
+ // серверная проверка, которой правила браузера не касаются.
135
+ .gr-input:user-invalid,
136
+ .gr-textarea:user-invalid,
137
+ .gr-select:user-invalid,
138
+ .gr-input[aria-invalid="true"],
139
+ .gr-textarea[aria-invalid="true"],
140
+ .gr-select[aria-invalid="true"] {
141
+ border-color: var(--gr-color-danger);
142
+ }
143
+
144
+ .gr-input:disabled,
145
+ .gr-textarea:disabled,
146
+ .gr-select:disabled {
147
+ border-color: var(--gr-color-border);
148
+ background-color: var(--gr-color-surface-sunken);
149
+ color: var(--gr-color-text-disabled);
150
+ cursor: not-allowed;
151
+ }
152
+
153
+ .gr-input:read-only:not(:disabled),
154
+ .gr-textarea:read-only:not(:disabled) {
155
+ background-color: var(--gr-color-surface);
156
+ }
157
+
158
+ // --- Размеры ----------------------------------------------------------------
159
+
160
+ .gr-input-sm {
161
+ --gr-control-h: var(--gr-control-height-sm);
162
+
163
+ padding-block: 0.1875rem;
164
+ padding-inline: calc(var(--gr-control-padding-x) * 0.7);
165
+ font-size: 0.875rem;
166
+ }
167
+
168
+ .gr-input-lg {
169
+ --gr-control-h: var(--gr-control-height-lg);
170
+
171
+ padding-block: 0.5rem;
172
+ padding-inline: calc(var(--gr-control-padding-x) * 1.4);
173
+ font-size: 1.125rem;
174
+ }
175
+
176
+ // --- Группа с аддонами ------------------------------------------------------
177
+
178
+ // Поле и приклеенные к нему подпись или кнопка. Как и в .gr-btn-group,
179
+ // соседние границы наезжают друг на друга на свою толщину.
180
+ .gr-input-group {
181
+ display: flex;
182
+ inline-size: 100%;
183
+ }
184
+
185
+ .gr-input-group > * {
186
+ border-radius: 0;
187
+ }
188
+
189
+ .gr-input-group > :first-child {
190
+ border-start-start-radius: var(--gr-radius);
191
+ border-end-start-radius: var(--gr-radius);
192
+ }
193
+
194
+ .gr-input-group > :last-child {
195
+ border-start-end-radius: var(--gr-radius);
196
+ border-end-end-radius: var(--gr-radius);
197
+ }
198
+
199
+ .gr-input-group > * + * {
200
+ margin-inline-start: calc(-1 * var(--gr-border-width));
201
+ }
202
+
203
+ // Поле в группе тянется, аддон — нет.
204
+ .gr-input-group > .gr-input,
205
+ .gr-input-group > .gr-select {
206
+ flex: 1 1 auto;
207
+ inline-size: auto;
208
+ min-inline-size: 0;
209
+ }
210
+
211
+ // Сфокусированное поле поднимается над соседями: иначе контур фокуса
212
+ // оказался бы наполовину закрыт аддоном.
213
+ .gr-input-group > :focus-within,
214
+ .gr-input-group > :focus-visible {
215
+ position: relative;
216
+ z-index: 1;
217
+ }
218
+
219
+ // Неинтерактивная подпись у края поля — единица измерения, валюта, префикс.
220
+ // Своя --gr-control-h, а не унаследованная: аддон полю не потомок, а сосед,
221
+ // поэтому размер ему задаётся тем же .gr-input-sm на нём самом.
222
+ .gr-input-addon {
223
+ --gr-control-h: var(--gr-control-height);
224
+
225
+ display: inline-flex;
226
+ box-sizing: border-box;
227
+ flex: 0 0 auto;
228
+ align-items: center;
229
+ min-block-size: var(--gr-control-h);
230
+ padding-inline: var(--gr-control-padding-x);
231
+ border: var(--gr-border-width) solid var(--gr-color-border);
232
+ background-color: var(--gr-color-surface-raised);
233
+ color: var(--gr-color-text-secondary);
234
+ font-size: var(--gr-control-font-size);
235
+ white-space: nowrap;
236
+ }
@@ -0,0 +1,39 @@
1
+ // Griffincss UI — Keyframes
2
+ // Отдельный файл, а не части модулей: внутри @scope правила @keyframes
3
+ // недействительны, поэтому в scoped-сборке они подключаются вне области
4
+ // видимости. Имена префиксованы gr-, чтобы не столкнуться с анимациями
5
+ // проекта.
6
+
7
+ // Тот же keyframe, что у .gr-animate-spin в griffincss-utils: имя одно,
8
+ // а пакеты независимы, и определения обязаны совпадать посимвольно.
9
+ // Разойдись они — картинка зависела бы от того, какой файл подключён,
10
+ // потому что слой утилит старше слоя компонентов. Совпадение сторожит
11
+ // тест «кольцо спиннера крутит тот же keyframe, что и утилита».
12
+ @keyframes gr-spin {
13
+ to {
14
+ transform: rotate(360deg);
15
+ }
16
+ }
17
+
18
+ // Бегущая полоса неопределённого прогресса. Ездит физическим направлением,
19
+ // а не логическим: индикатор занятости ничего не сообщает о направлении
20
+ // чтения, и переворачивать его в RTL значило бы приписать ему смысл,
21
+ // которого у него нет.
22
+ @keyframes gr-progress-slide {
23
+ from {
24
+ background-position-x: 0%;
25
+ }
26
+
27
+ to {
28
+ background-position-x: 100%;
29
+ }
30
+ }
31
+
32
+ // Мерцание заглушки. Меняется прозрачность, а не цвет: заглушка залита
33
+ // оттенком currentcolor, и анимировать цвет пришлось бы через color-mix
34
+ // в каждом кадре.
35
+ @keyframes gr-skeleton-pulse {
36
+ 50% {
37
+ opacity: 0.45;
38
+ }
39
+ }