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,173 @@
1
+ // Griffincss UI — Toast
2
+ // Короткое уведомление в углу экрана. Живёт в регионе — закреплённом
3
+ // контейнере, объявленном в разметке живой областью:
4
+ //
5
+ // <div class="gr-toast-region gr-toast-region-bottom-end"
6
+ // role="status" aria-live="polite"></div>
7
+ //
8
+ // Регион ставится на страницу один раз и пустым: живая область обязана
9
+ // существовать в дереве ДО того, как в неё что-то положат, иначе программа
10
+ // чтения с экрана не заметит вставки. Пустой регион при этом ничего
11
+ // не занимает и ничего не сообщает.
12
+ //
13
+ // Сообщения об ошибке требуют своего региона с aria-live="assertive"
14
+ // и role="alert": «вежливая» область дочитает текущее и только потом
15
+ // сообщит, а потеря данных ждать не может.
16
+ //
17
+ // Показывает и убирает тосты griffincss-ui.js: Griffincss.ui.toast(text).
18
+ // Без скрипта модуль не используется — это единственный компонент
19
+ // библиотеки, у которого нет статического применения.
20
+
21
+ @use 'sass:list';
22
+
23
+ .gr-toast-region {
24
+ --gr-toast-slide: -0.75rem;
25
+
26
+ position: fixed;
27
+
28
+ // Регион растянут по краю экрана и лежит поверх всего. Не сними он с себя
29
+ // указатель — прозрачная полоса перехватывала бы щелчки по странице
30
+ // под ней. Сами тосты указатель принимают: в них бывают крестик и ссылка.
31
+ z-index: var(--gr-z-toast);
32
+ inset-block: 0 auto;
33
+ inset-inline: auto 0;
34
+ display: flex;
35
+ box-sizing: border-box;
36
+ flex-direction: column;
37
+ gap: var(--gr-ui-gap);
38
+ max-inline-size: min(26rem, 100%);
39
+ padding: var(--gr-gap);
40
+ pointer-events: none;
41
+ }
42
+
43
+ // --- Углы -------------------------------------------------------------------
44
+
45
+ // Новый тост вставляется в конец региона. У нижних углов это значит
46
+ // «ближе к краю экрана», поэтому колонка там перевёрнута: иначе свежее
47
+ // сообщение уезжало бы вверх, а старое оставалось бы на виду.
48
+ // Заодно переворачивается направление въезда.
49
+ .gr-toast-region-top-start {
50
+ inset-block: 0 auto;
51
+ inset-inline: 0 auto;
52
+ }
53
+
54
+ .gr-toast-region-top-end {
55
+ inset-block: 0 auto;
56
+ inset-inline: auto 0;
57
+ }
58
+
59
+ .gr-toast-region-bottom-start {
60
+ --gr-toast-slide: 0.75rem;
61
+
62
+ inset-block: auto 0;
63
+ inset-inline: 0 auto;
64
+ flex-direction: column-reverse;
65
+ }
66
+
67
+ .gr-toast-region-bottom-end {
68
+ --gr-toast-slide: 0.75rem;
69
+
70
+ inset-block: auto 0;
71
+ inset-inline: auto 0;
72
+ flex-direction: column-reverse;
73
+ }
74
+
75
+ // --- Сообщение --------------------------------------------------------------
76
+
77
+ .gr-toast {
78
+ --gr-toast-color: var(--gr-color-text-secondary);
79
+ --gr-toast-fill: var(--gr-color-border-strong);
80
+
81
+ display: flex;
82
+ box-sizing: border-box;
83
+ align-items: flex-start;
84
+ gap: var(--gr-ui-gap);
85
+ padding-block: calc(var(--gr-ui-gap) * 1.25);
86
+ padding-inline: var(--gr-gap);
87
+ border: var(--gr-border-width) solid var(--gr-color-border);
88
+ border-inline-start-width: calc(var(--gr-border-width) * 4);
89
+ border-inline-start-color: var(--gr-toast-fill);
90
+ border-radius: var(--gr-radius);
91
+ background-color: var(--gr-color-surface-overlay);
92
+ box-shadow: var(--gr-shadow-lg);
93
+ color: var(--gr-color-text);
94
+ font-size: 0.9375rem;
95
+ line-height: 1.45;
96
+ pointer-events: auto;
97
+ transition:
98
+ opacity var(--gr-transition),
99
+ translate var(--gr-transition);
100
+ }
101
+
102
+ // Появление объявлено начальным состоянием, а не классом от скрипта.
103
+ // Узел вставлен и сразу нарисован в конечном виде; @starting-style —
104
+ // единственный способ показать въезд, не требуя от рантайма второго кадра
105
+ // и пары «добавил класс — снял класс».
106
+ @starting-style {
107
+ .gr-toast {
108
+ opacity: 0;
109
+ translate: 0 var(--gr-toast-slide, -0.75rem);
110
+ }
111
+ }
112
+
113
+ // Уход. Класс вешает рантайм и ждёт конца перехода — так тост исчезает
114
+ // тем же движением, каким появился.
115
+ .gr-toast-leaving {
116
+ opacity: 0;
117
+ translate: 0 var(--gr-toast-slide, -0.75rem);
118
+
119
+ // Уходящий тост уже не сообщение, а картинка: щелчок по нему пришёлся бы
120
+ // на то, чего человек больше не видит.
121
+ pointer-events: none;
122
+ }
123
+
124
+ @media (prefers-reduced-motion: reduce) {
125
+ .gr-toast {
126
+ transition-duration: 0.01ms;
127
+ }
128
+ }
129
+
130
+ // Слот под иконку — как у сообщения о статусе: своих иконок библиотека
131
+ // не везёт, сюда кладут любой знак.
132
+ .gr-toast-icon {
133
+ flex: none;
134
+ inline-size: 1.25em;
135
+ block-size: 1.25em;
136
+ color: var(--gr-toast-color);
137
+ line-height: 1.25;
138
+ text-align: center;
139
+ }
140
+
141
+ .gr-toast-body {
142
+ flex: 1 1 auto;
143
+ min-inline-size: 0;
144
+ }
145
+
146
+ // Слово статуса. Тот же довод, что у .gr-alert-title: цвет не переживает
147
+ // чёрно-белую печать и дальтонизм, слово переживает.
148
+ .gr-toast-title {
149
+ display: block;
150
+ margin: 0;
151
+ color: var(--gr-toast-color);
152
+ font-size: inherit;
153
+ font-weight: 600;
154
+ }
155
+
156
+ // --- Статусы ----------------------------------------------------------------
157
+
158
+ // Парами: цвет слова и иконки, цвет полосы. Расходятся они у предупреждения —
159
+ // текстовый цвет обязан быть тёмным, чтобы читаться, а полоса такого цвета
160
+ // не читается как «внимание».
161
+ $gr-toast-statuses: (
162
+ info: (var(--gr-color-info), var(--gr-color-info)),
163
+ success: (var(--gr-color-success), var(--gr-color-success)),
164
+ warning: (var(--gr-color-warning), var(--gr-color-warning-surface)),
165
+ danger: (var(--gr-color-danger), var(--gr-color-danger))
166
+ );
167
+
168
+ @each $gr-name, $gr-value in $gr-toast-statuses {
169
+ .gr-toast-#{$gr-name} {
170
+ --gr-toast-color: #{list.nth($gr-value, 1)};
171
+ --gr-toast-fill: #{list.nth($gr-value, 2)};
172
+ }
173
+ }
@@ -0,0 +1,143 @@
1
+ // Griffincss UI — Tooltip
2
+ // Короткая подсказка у элемента. Показывается и указателем, и с клавиатуры,
3
+ // и то и другое — чистым CSS: подсказка лежит внутри обёртки, а обёртка
4
+ // реагирует на :hover и на :focus-within. Скрипт для этого не нужен.
5
+ //
6
+ // <span class="gr-tooltip-anchor">
7
+ // <button class="gr-btn" aria-describedby="t1">Сохранить</button>
8
+ // <span class="gr-tooltip" id="t1" role="tooltip">Ctrl + S</span>
9
+ // </span>
10
+ //
11
+ // aria-describedby в разметке обязателен: без него подсказку услышит
12
+ // только тот, кто её видит. Роль tooltip сообщает, что это пояснение
13
+ // к элементу, а не отдельная надпись рядом.
14
+ //
15
+ // Подсказка не прячется display: none и не появляется display: block:
16
+ // скрытая так, она исчезла бы из дерева доступности целиком, и связь
17
+ // по aria-describedby оборвалась бы. Прячут её прозрачность
18
+ // и visibility — то и другое анимируется и обратимо.
19
+
20
+ .gr-tooltip-anchor {
21
+ position: relative;
22
+ display: inline-flex;
23
+ vertical-align: middle;
24
+ }
25
+
26
+ .gr-tooltip {
27
+ --gr-tooltip-offset: calc(var(--gr-ui-gap) * 0.75);
28
+
29
+ position: absolute;
30
+ z-index: var(--gr-z-dropdown);
31
+ inset-block-end: calc(100% + var(--gr-tooltip-offset));
32
+ inset-inline-start: 50%;
33
+ box-sizing: border-box;
34
+ inline-size: max-content;
35
+ max-inline-size: min(16rem, 70vw);
36
+ padding-block: calc(var(--gr-ui-gap) * 0.5);
37
+ padding-inline: calc(var(--gr-control-padding-x) * 0.6);
38
+ border-radius: calc(var(--gr-radius) * 0.75);
39
+ background-color: var(--gr-color-text);
40
+ color: var(--gr-color-bg);
41
+ font-size: 0.8125rem;
42
+ line-height: 1.35;
43
+ text-align: center;
44
+ translate: -50% 0;
45
+ opacity: 0;
46
+ visibility: hidden;
47
+
48
+ // Скрытая подсказка не должна влиять на раскладку страницы. visibility
49
+ // убирает её с экрана и из дерева доступности, но место в области
50
+ // прокрутки за ней остаётся: боковая подсказка у правого края узкого
51
+ // экрана растягивала документ вбок — появлялась горизонтальная прокрутка
52
+ // страницы, на которой ничего не видно. content-visibility снимает
53
+ // содержимое и с раскладки; переход по нему объявлен allow-discrete,
54
+ // иначе появление шло бы рывком.
55
+ content-visibility: hidden;
56
+ transition:
57
+ opacity var(--gr-transition),
58
+ visibility var(--gr-transition),
59
+ content-visibility var(--gr-transition) allow-discrete;
60
+
61
+ // Подсказка не должна перехватывать указатель: наведённая мышью, она
62
+ // иначе закрыла бы собой элемент, к которому относится.
63
+ pointer-events: none;
64
+ }
65
+
66
+ // Клавиатура. :focus-within, а не :focus: фокус приходит на кнопку внутри
67
+ // обёртки, а правило написано на обёртку.
68
+ .gr-tooltip-anchor:focus-within .gr-tooltip {
69
+ opacity: 1;
70
+ visibility: visible;
71
+ content-visibility: visible;
72
+ }
73
+
74
+ // Указатель — отдельным правилом в @media (hover: hover): на сенсорном
75
+ // экране :hover залипает после тапа, и подсказка осталась бы висеть.
76
+ @media (hover: hover) {
77
+ .gr-tooltip-anchor:hover .gr-tooltip {
78
+ opacity: 1;
79
+ visibility: visible;
80
+ content-visibility: visible;
81
+ }
82
+ }
83
+
84
+ @media (prefers-reduced-motion: reduce) {
85
+ .gr-tooltip {
86
+ transition-duration: 0.01ms;
87
+ }
88
+ }
89
+
90
+ // --- Стороны ----------------------------------------------------------------
91
+
92
+ .gr-tooltip-bottom {
93
+ inset-block: calc(100% + var(--gr-tooltip-offset)) auto;
94
+ }
95
+
96
+ // У боковых подсказок смещение по другой оси: центрируется высота,
97
+ // а не ширина.
98
+ .gr-tooltip-start,
99
+ .gr-tooltip-end {
100
+ inset-block: 50% auto;
101
+ translate: 0 -50%;
102
+ }
103
+
104
+ .gr-tooltip-start {
105
+ inset-inline: auto calc(100% + var(--gr-tooltip-offset));
106
+ }
107
+
108
+ .gr-tooltip-end {
109
+ inset-inline: calc(100% + var(--gr-tooltip-offset)) auto;
110
+ }
111
+
112
+ // --- Подсказка в верхнем слое -----------------------------------------------
113
+
114
+ // Подсказка внутри прокручиваемого блока или таблицы с overflow: hidden
115
+ // обрезается его краем. Лечится верхним слоем: тот же .gr-tooltip
116
+ // с атрибутом popover рисуется поверх всего и притягивается к элементу
117
+ // якорем. Показывает такую подсказку скрипт (showPopover), поэтому
118
+ // правила :hover и :focus-within её не касаются.
119
+ @supports (anchor-name: --gr-tooltip-anchor) {
120
+ .gr-tooltip-anchor:has(> [popover].gr-tooltip) {
121
+ anchor-scope: --gr-tooltip-anchor;
122
+ anchor-name: --gr-tooltip-anchor;
123
+ }
124
+
125
+ [popover].gr-tooltip {
126
+ position: fixed;
127
+ position-anchor: --gr-tooltip-anchor;
128
+ position-area: block-start center;
129
+ inset: auto;
130
+ margin: 0;
131
+ margin-block-end: var(--gr-tooltip-offset);
132
+ border: 0;
133
+ translate: none;
134
+ overflow: visible;
135
+ position-try-fallbacks: flip-block;
136
+ }
137
+
138
+ [popover].gr-tooltip:popover-open {
139
+ opacity: 1;
140
+ visibility: visible;
141
+ content-visibility: visible;
142
+ }
143
+ }
@@ -0,0 +1,61 @@
1
+ // Griffincss UI — версия, ограниченная областью .griffin
2
+ // Компоненты действуют только внутри элементов с классом .griffin,
3
+ // что позволяет подключать библиотеку в чужую разметку без конфликтов.
4
+ // Токены (из griffincss-core) объявляются вне @scope: :root должен
5
+ // остаться глобальным.
6
+ //
7
+ // Слой тот же, что и у обычной сборки, — griffincss.ui.
8
+ //
9
+ // Требует поддержки CSS @scope (Chrome 118+, Safari 17.4+, Firefox 128+).
10
+ //
11
+ // Структурных правил оси оформления здесь нет: внутри @scope селектор
12
+ // [data-gr-style="…"] неявно получает :scope в начало и требует носитель
13
+ // атрибута в области, а он стоит на <html>. Ось при этом действует на уровне
14
+ // токенов — они объявлены вне области и работают в этой сборке полностью.
15
+ //
16
+ // Компоненты, живущие в верхнем слое, — модальное окно, выдвижная панель
17
+ // и попап дропдауна — областью не ограничены только визуально: узел
18
+ // остаётся в DOM внутри .griffin, поэтому @scope до него достаёт.
19
+
20
+ @use 'sass:meta';
21
+ @layer griffincss.reset, griffincss.core, griffincss.ui, griffincss.utils, griffincss.style;
22
+
23
+ // Весь вывод пакета — в слое griffincss.ui.
24
+ @layer griffincss.ui {
25
+ @include meta.load-css('griffincss-core/scss/tokens');
26
+
27
+ // Тема — тоже вне @scope: атрибут data-gr-theme стоит на <html>,
28
+ // то есть заведомо снаружи области .griffin.
29
+ @include meta.load-css('griffincss-core/scss/theme');
30
+
31
+ // @keyframes внутри @scope недействительны — подключаются вне области.
32
+ @include meta.load-css('keyframes');
33
+
34
+ @scope (.griffin) {
35
+ @include meta.load-css('accordion');
36
+ @include meta.load-css('alert');
37
+ @include meta.load-css('avatar');
38
+ @include meta.load-css('badge');
39
+ @include meta.load-css('breadcrumb');
40
+ @include meta.load-css('button');
41
+ @include meta.load-css('card');
42
+ @include meta.load-css('choice');
43
+ @include meta.load-css('drawer');
44
+ @include meta.load-css('dropdown');
45
+ @include meta.load-css('empty');
46
+ @include meta.load-css('form');
47
+ @include meta.load-css('link');
48
+ @include meta.load-css('menu');
49
+ @include meta.load-css('modal');
50
+ @include meta.load-css('nav');
51
+ @include meta.load-css('pagination');
52
+ @include meta.load-css('progress');
53
+ @include meta.load-css('skeleton');
54
+ @include meta.load-css('spinner');
55
+ @include meta.load-css('steps');
56
+ @include meta.load-css('table');
57
+ @include meta.load-css('tabs');
58
+ @include meta.load-css('toast');
59
+ @include meta.load-css('tooltip');
60
+ }
61
+ }
@@ -0,0 +1,64 @@
1
+ // Griffincss UI — компоненты интерфейса
2
+ // Кнопки, поля ввода, переключатели, карточки, таблицы, статусы, аватары,
3
+ // навигация, меню, вкладки, аккордеон, модальное окно, выдвижная панель,
4
+ // подсказки и пагинация.
5
+ //
6
+ // Надстройка над griffincss-core: токены, тема и карта брейкпоинтов берутся
7
+ // оттуда (peer-зависимость). От griffincss-utils пакет не зависит вовсе —
8
+ // компонент обязан выглядеть правильно сам по себе; утилиты нужны, чтобы
9
+ // его подвинуть, а не чтобы он собрался.
10
+ //
11
+ // Собранный griffincss-ui.css самодостаточен: блок :root входит в него
12
+ // целиком, подключать griffincss-core.css не требуется.
13
+ //
14
+ // Слой griffincss.ui стоит между ядром и утилитами. Компоненты старше ядра:
15
+ // .gr-card задаёт display: flex и не должен проигрывать .gr-flex. Утилиты
16
+ // старше компонентов: .gr-mb-0 на карточке обязан побеждать её собственный
17
+ // margin-bottom.
18
+
19
+ @use 'sass:meta';
20
+ @layer griffincss.reset, griffincss.core, griffincss.ui, griffincss.utils, griffincss.style;
21
+
22
+ // Весь вывод пакета — в слое griffincss.ui.
23
+ @layer griffincss.ui {
24
+ @include meta.load-css('griffincss-core/scss/tokens');
25
+
26
+ // Тема подключается и здесь, как в utils: слой griffincss.ui старше
27
+ // griffincss.core, поэтому переопределения темы, приехавшие только
28
+ // с griffincss-core.css, проиграли бы базовому :root из этой сборки —
29
+ // независимо от специфичности селектора.
30
+ @include meta.load-css('griffincss-core/scss/theme');
31
+
32
+ // @keyframes живут отдельным файлом ради scoped-сборки — см. _keyframes.scss.
33
+ @include meta.load-css('keyframes');
34
+ @include meta.load-css('accordion');
35
+ @include meta.load-css('alert');
36
+ @include meta.load-css('avatar');
37
+ @include meta.load-css('badge');
38
+ @include meta.load-css('breadcrumb');
39
+ @include meta.load-css('button');
40
+ @include meta.load-css('card');
41
+ @include meta.load-css('choice');
42
+ @include meta.load-css('drawer');
43
+ @include meta.load-css('dropdown');
44
+ @include meta.load-css('empty');
45
+ @include meta.load-css('form');
46
+ @include meta.load-css('link');
47
+ @include meta.load-css('menu');
48
+ @include meta.load-css('modal');
49
+ @include meta.load-css('nav');
50
+ @include meta.load-css('pagination');
51
+ @include meta.load-css('progress');
52
+ @include meta.load-css('skeleton');
53
+ @include meta.load-css('spinner');
54
+ @include meta.load-css('steps');
55
+ @include meta.load-css('table');
56
+ @include meta.load-css('tabs');
57
+ @include meta.load-css('toast');
58
+ @include meta.load-css('tooltip');
59
+
60
+ // Структурные правила оси оформления — последними: они уточняют компоненты
61
+ // и обязаны идти после их базовых правил. В griffincss-ui-scoped.scss
62
+ // модуля нет намеренно, см. его шапку.
63
+ @include meta.load-css('style-rules');
64
+ }