@soxa-ui/theme-base 0.0.0-stage → 0.3.0-beta.1

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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +399 -2
  3. package/dist/setup/index.d.ts +31 -0
  4. package/dist/setup/index.js +4 -0
  5. package/dist/setup/plugins/index.d.ts +3 -0
  6. package/dist/setup/plugins/table-head.plugin.d.ts +44 -0
  7. package/dist/setup/plugins/table-head.plugin.js +86 -0
  8. package/dist/setup/plugins/tabs-view.plugin.d.ts +32 -0
  9. package/dist/setup/plugins/tabs-view.plugin.js +100 -0
  10. package/dist/setup/plugins/toasts-front.plugin.d.ts +63 -0
  11. package/dist/setup/plugins/toasts-front.plugin.js +131 -0
  12. package/dist/setup/plugins/transitions.d.ts +43 -0
  13. package/dist/setup/plugins/transitions.js +75 -0
  14. package/package.json +55 -4
  15. package/src/anchored.css +75 -0
  16. package/src/caption.css +71 -0
  17. package/src/components/accordion.css +96 -0
  18. package/src/components/button.css +139 -0
  19. package/src/components/calendar.css +594 -0
  20. package/src/components/check-box.css +121 -0
  21. package/src/components/date-input.css +224 -0
  22. package/src/components/date-picker.css +208 -0
  23. package/src/components/dialog.css +95 -0
  24. package/src/components/drag-and-drop.css +18 -0
  25. package/src/components/drawer.css +171 -0
  26. package/src/components/field.css +440 -0
  27. package/src/components/icon.css +61 -0
  28. package/src/components/input.css +189 -0
  29. package/src/components/label.css +160 -0
  30. package/src/components/list-box.css +165 -0
  31. package/src/components/menu.css +224 -0
  32. package/src/components/number-input.css +183 -0
  33. package/src/components/pagination.css +164 -0
  34. package/src/components/popover.css +164 -0
  35. package/src/components/progress-linear.css +88 -0
  36. package/src/components/progress-spinner.css +127 -0
  37. package/src/components/radio-group.css +130 -0
  38. package/src/components/rating.css +141 -0
  39. package/src/components/scroller.css +136 -0
  40. package/src/components/select.css +233 -0
  41. package/src/components/skeleton.css +33 -0
  42. package/src/components/slider.css +482 -0
  43. package/src/components/switch.css +142 -0
  44. package/src/components/table.css +1513 -0
  45. package/src/components/tabs.css +166 -0
  46. package/src/components/tags.css +371 -0
  47. package/src/components/textarea.css +179 -0
  48. package/src/components/toasts.css +447 -0
  49. package/src/components/tooltip.css +59 -0
  50. package/src/direction.css +30 -0
  51. package/src/disabled.css +26 -0
  52. package/src/group.css +90 -0
  53. package/src/index.css +97 -0
  54. package/src/modal.css +183 -0
  55. package/src/place.css +30 -0
  56. package/src/required.css +40 -0
  57. package/src/reset.css +172 -0
  58. package/src/swipe.css +71 -0
  59. package/src/target.css +46 -0
  60. package/theme-contract.json +3373 -0
  61. package/theme-variables.json +336 -0
@@ -0,0 +1,179 @@
1
+ /* ─── Textarea ────────────────────────────────────────────────
2
+ * Многострочное поле семьи Input: рамка поля без строки поля — высоты у
3
+ * размера нет, её ведут строки текста.
4
+ *
5
+ * .so-textarea корень — рамка поля
6
+ * textarea нативное поле, без класса — как `<input>` у Input
7
+ * .so-textarea__counter счётчик символов — под полем, внутри рамки
8
+ *
9
+ * Высоту `<textarea>` база не задаёт — ни `height`, ни `min-height`, ни
10
+ * `max-height`: без автороста её дают `rows` и ручка, с авторостом — плагин,
11
+ * инлайновой `height` на самой `<textarea>`. Корень растёт за ней сам.
12
+ * Плагин автороста считает высоту по вычисленным стилям `<textarea>`:
13
+ * интерлиньяж, поля по оси блока и рамка (у поля её нет — рамку рисует
14
+ * корень), `box-sizing: border-box` (`reset.css`). Поэтому интерлиньяж задан
15
+ * на каждом размере, без `normal`: число `normal` из стиля не прочесть.
16
+ * Перехода у высоты нет и быть не должно: поле растёт вместе с набором.
17
+ *
18
+ * Шкала — шкала поля (`input.css`): кегль, интерлиньяж и отступ текста от
19
+ * рамки те же, что у Input того же размера, а от высоты Input берётся строка
20
+ * поля (`--so-textarea-row`): поля текста над первой строкой и под последней
21
+ * считаются так, чтобы первая строка стояла по центру этой строки — там же,
22
+ * где текст Input. Поэтому Textarea в одну строку (`rows="1"`) — ровно высоты
23
+ * Input того же размера. Счётчик — кеглем справки при контроле (`caption.css`):
24
+ * на ступень мельче текста поля, как подсказка и ошибка `Field`.
25
+ *
26
+ * Рамку рисует тема и задаёт её толщину на блоке (`--so-textarea-border`,
27
+ * контракт): поля текста считаются без неё, и у рамки потолще высота поля та
28
+ * же — уже поля.
29
+ *
30
+ * Облик — рамка, поверхность, скругление, кольцо фокуса, подсказка пустого
31
+ * поля, цвет счётчика и его цифры, цвет точки обязательного поля, варианты и
32
+ * ошибка — рисует тема. */
33
+
34
+ /* Столбец: поле, под ним счётчик. Строчный, как Input: в строке текста и в
35
+ * обёртке контрола `Field` поле стоит по своей ширине — её задают `cols` поля
36
+ * и класс приложения на корне. Минимум — как у Input. Размер без
37
+ * модификатора — `normal`: ядро пишет `--size-*` всегда, но блок и без него
38
+ * обязан выглядеть умолчанием. */
39
+ .so-textarea {
40
+ --so-textarea-row: var(--so-input-height-normal);
41
+ --so-textarea-indent: var(--so-input-indent-normal);
42
+ position: relative;
43
+ display: inline-flex;
44
+ flex-direction: column;
45
+ min-width: var(--so-input-min-width);
46
+ }
47
+
48
+ .so-textarea > textarea {
49
+ line-height: var(--so-input-line-height-normal);
50
+ }
51
+
52
+ .so-textarea > .so-textarea__counter {
53
+ font-size: var(--so-caption-note-font-size-normal);
54
+ line-height: var(--so-caption-note-line-height-normal);
55
+ }
56
+
57
+ /* Выключенное поле гаснет рамкой целиком, с полем и счётчиком
58
+ * (`disabled.css`). Корень — `div`, и выключенность читается с вложенного
59
+ * поля, как у Input: нативный `disabled` поля ставит и выключенный предок
60
+ * `<fieldset>`. */
61
+ .so-textarea:has(> textarea:disabled) {
62
+ opacity: var(--so-disabled-opacity);
63
+ }
64
+
65
+ .so-textarea:has(> textarea:disabled) > * {
66
+ --so-disabled-opacity: 1;
67
+ }
68
+
69
+ /* Поле — во всю ширину рамки и во всю её высоту над счётчиком: задало
70
+ * приложение корню высоту или его растянула сетка, лишнее место забирает
71
+ * поле, и счётчик остаётся у нижней рамки.
72
+ *
73
+ * Поля текста — сбоку отступ шкалы поля: текст начинается там же, где у
74
+ * Input того же размера. Сверху и снизу — остаток строки поля от интерлиньяжа
75
+ * и рамки, пополам (`1lh` — интерлиньяж самой `<textarea>`). Поля — внутри
76
+ * прокрутки: текст уезжает под рамку, а не обрывается в отступе.
77
+ *
78
+ * Ручка — только по высоте: поле растёт вниз, а рамка и счётчик едут за ним;
79
+ * по ширине ручка выдвигала бы поле из колонки формы. Ручку без автороста
80
+ * база оставляет: ею поле расширяют и без него, как нативное. */
81
+ .so-textarea > textarea {
82
+ flex-grow: 1;
83
+ width: 100%;
84
+ padding-inline: var(--so-textarea-indent);
85
+ padding-block: calc((var(--so-textarea-row) - 2 * var(--so-textarea-border, 0px) - 1lh) / 2);
86
+ resize: vertical;
87
+ }
88
+
89
+ /* Размер выключенного поля не меняют. */
90
+ .so-textarea > textarea:disabled {
91
+ cursor: default;
92
+ resize: none;
93
+ }
94
+
95
+ /* Счётчик — строка под полем, в рамке, у конца строки: подсказка и ошибка
96
+ * `Field` встают под всем компонентом, и снаружи счётчик спорил бы с ними за
97
+ * строку. Сторона логическая: в RTL счётчик у левого края. Поля — те же, что
98
+ * у текста: сбоку отступ шкалы поля, снизу — его половина. */
99
+ .so-textarea__counter {
100
+ align-self: flex-end;
101
+ padding-inline: var(--so-textarea-indent);
102
+ padding-block-end: calc(var(--so-textarea-indent) / 2);
103
+ white-space: nowrap;
104
+ cursor: default;
105
+ }
106
+
107
+ .so-textarea--size-sm {
108
+ --so-textarea-row: var(--so-input-height-sm);
109
+ --so-textarea-indent: var(--so-input-indent-sm);
110
+ }
111
+
112
+ .so-textarea--size-sm > textarea {
113
+ font-size: var(--so-input-font-size-sm);
114
+ line-height: var(--so-input-line-height-sm);
115
+ }
116
+
117
+ .so-textarea--size-sm > .so-textarea__counter {
118
+ font-size: var(--so-caption-note-font-size-sm);
119
+ line-height: var(--so-caption-note-line-height-sm);
120
+ }
121
+
122
+ .so-textarea--size-normal {
123
+ --so-textarea-row: var(--so-input-height-normal);
124
+ --so-textarea-indent: var(--so-input-indent-normal);
125
+ }
126
+
127
+ .so-textarea--size-normal > textarea {
128
+ line-height: var(--so-input-line-height-normal);
129
+ }
130
+
131
+ .so-textarea--size-normal > .so-textarea__counter {
132
+ font-size: var(--so-caption-note-font-size-normal);
133
+ line-height: var(--so-caption-note-line-height-normal);
134
+ }
135
+
136
+ .so-textarea--size-lg {
137
+ --so-textarea-row: var(--so-input-height-lg);
138
+ --so-textarea-indent: var(--so-input-indent-lg);
139
+ }
140
+
141
+ .so-textarea--size-lg > textarea {
142
+ font-size: var(--so-input-font-size-lg);
143
+ line-height: var(--so-input-line-height-lg);
144
+ }
145
+
146
+ .so-textarea--size-lg > .so-textarea__counter {
147
+ font-size: var(--so-caption-note-font-size-lg);
148
+ line-height: var(--so-caption-note-line-height-lg);
149
+ }
150
+
151
+ .so-textarea--size-xl {
152
+ --so-textarea-row: var(--so-input-height-xl);
153
+ --so-textarea-indent: var(--so-input-indent-xl);
154
+ }
155
+
156
+ .so-textarea--size-xl > textarea {
157
+ font-size: var(--so-input-font-size-xl);
158
+ line-height: var(--so-input-line-height-xl);
159
+ }
160
+
161
+ .so-textarea--size-xl > .so-textarea__counter {
162
+ font-size: var(--so-caption-note-font-size-xl);
163
+ line-height: var(--so-caption-note-line-height-xl);
164
+ }
165
+
166
+ .so-textarea--size-2xl {
167
+ --so-textarea-row: var(--so-input-height-2xl);
168
+ --so-textarea-indent: var(--so-input-indent-2xl);
169
+ }
170
+
171
+ .so-textarea--size-2xl > textarea {
172
+ font-size: var(--so-input-font-size-2xl);
173
+ line-height: var(--so-input-line-height-2xl);
174
+ }
175
+
176
+ .so-textarea--size-2xl > .so-textarea__counter {
177
+ font-size: var(--so-caption-note-font-size-2xl);
178
+ line-height: var(--so-caption-note-line-height-2xl);
179
+ }
@@ -0,0 +1,447 @@
1
+ /* ─── Toasts ──────────────────────────────────────────────────
2
+ * Всплывающие уведомления: регион у края окна и тосты в нём.
3
+ *
4
+ * .so-toasts регион — телепортирован в `body`
5
+ * .so-toasts__list список — тосты по порядку, новый первым
6
+ * .so-toasts-item тост — место в списке
7
+ * .so-toasts-item__card плашка — то, что видно
8
+ * .so-toasts-item__icon значок вида, кольцо ожидания или слот `icon`
9
+ * .so-toasts-item__body текст — заголовок, описание, своё содержимое
10
+ * .so-toasts-item__text заголовок
11
+ * .so-toasts-item__description описание
12
+ * .so-toasts-item__content своё содержимое (`render`)
13
+ * .so-toasts-item__close крестик — Button
14
+ *
15
+ * Регион стоит всегда, и пустым: живая область должна быть в документе до
16
+ * первого тоста. Места он не занимает — он `fixed`, — и нажатий мимо тостов не
17
+ * ловит: указатель берут только сами тосты (`pointer-events` наследуется, и
18
+ * тост возвращает его себе). Номер слоя код пишет региону инлайном.
19
+ *
20
+ * Место — модификатор региона `--placement-*`, код пишет его всегда: шесть
21
+ * мест у верхнего или нижнего края окна — у начала строки, по центру и у
22
+ * конца. Регион — во всю ширину окна без отступов, а тосты в нём встают у
23
+ * начала, по центру или у конца (`align-items` списка): `start` и `end`
24
+ * логические, в RTL тосты сами встают у другого края. Стороны отступа —
25
+ * физические и обе сразу: поправки на вырез экрана (`safe-area-inset-*`)
26
+ * физические тоже, и логическая сторона в RTL взяла бы поправку чужого края.
27
+ *
28
+ * Раскладка — модификатор региона `--layout-*`, тоже всегда: столбец (`list`)
29
+ * или стопка (`cascade`). Столбец: новый тост в разметке первый, а на экране —
30
+ * у края — у верхнего края столбец идёт сверху вниз, у нижнего — снизу вверх,
31
+ * и список растёт от края. Тост — узла два: место в списке и плашка. Место —
32
+ * сетка в одну строку (`minmax(0, 1fr)`): тема схлопывает строку переходом, и
33
+ * соседи сдвигаются, а высоту тоста никто не мерит. Плашка своей высоты не
34
+ * меняет — она прижата к стороне места, дальней от края окна, а зазор до
35
+ * соседа — поле места с другой стороны (у последнего тоста его нет: полоса
36
+ * зазора за столбцом ловила бы указатель). Что не поместилось в строку, режет
37
+ * `clip-path` места — с запасом в зазор под тень, кромку и кольцо фокуса
38
+ * плашки; `overflow` резал бы ровно по краю места. Ширина тоста — строка около
39
+ * сорока знаков, на узком экране — во всю ширину региона. Закрытый тост
40
+ * нажатий не ловит: нажатие сквозь него — нажатие по тому, что под ним. Тянут
41
+ * — курсор «держу», где бы указатель ни был.
42
+ *
43
+ * Плашка — ряд: значок, текст, крестик. Строка текста (`--so-toasts-line`) —
44
+ * переменная: значок стоит в её квадрате (кегль коробки — её сторона, иконка
45
+ * soxa меряется в `em`), а крестик — квадрат высоты Button `normal` — по
46
+ * центру первой строки; поля по оси блока у него — минус половина разницы со
47
+ * строкой, и тост в одну строку крестик не раздвигает, а у конца строки он
48
+ * заходит в поле плашки на четверть своей стороны — значок встаёт на край
49
+ * текста, как у кнопок шапки окна (`modal.css`). Кегль текста — облик, его
50
+ * задаёт тема без интерлиньяжа. Кольцо ожидания на месте значка — по кругу
51
+ * глифа значка (`--so-toasts-spinner-size`), а не по своей шкале. Пустая часть
52
+ * места не занимает. Длинное слово в тексте переносится, а не раздвигает тост.
53
+ *
54
+ * Стопка: тосты перекрывают друг друга в одной ячейке сетки списка. Передний
55
+ * — тост под фокусом, без фокуса — первый, а за закрытым первым — следующий;
56
+ * он в потоке ячейки и своей высоты, содержимое открыто. Задние — за ним
57
+ * (`z-index` по ступени), во всю ячейку стопки и места в потоке не занимают
58
+ * (`contain: size`): высота стопки — высота переднего. Высоту переднего
59
+ * задним пишет плагин раскладки `TToastsFrontPlugin` (`--so-toasts-front-height`
60
+ * на регионе, `setup/` общего слоя; ставит его тема), и тема меняет её
61
+ * переходом, когда передний сменился; без плагина задний — во всю ячейку, той
62
+ * же высоты. Ступень — глубина тоста (`--so-toasts-depth`, место в составе,
63
+ * его пишет разметка): на одну дальше
64
+ * за тостом под фокусом и на одну ближе за закрытым, пока он гаснет. Задний
65
+ * уже переднего за каждую ступень и выглядывает из-под него на зазор столбца к
66
+ * дальней от края окна стороне; глубже последнего видимого слоя тост стоит за
67
+ * ним и прозрачен. Сжатие — от дальнего края: он и выглядывает, и на сжатии
68
+ * стоит на месте. Плашка заднего — во всю его высоту, её содержимое срезано и
69
+ * нажатий не ловит: край заднего ловит сам тост, и фокус на нём выводит тост
70
+ * вперёд. Закрытый передний встаёт поверх нового переднего, пока гаснет.
71
+ *
72
+ * Отступ региона, ширина, зазор, поля, строка, зазоры частей, кольцо и
73
+ * ступени стопки — переменные корня документа; умолчания — числа темы oren,
74
+ * тема задаёт свои на корне документа.
75
+ *
76
+ * Жест: тост идёт за указателем — сдвиг плагина жеста (`--so-swipe-offset`,
77
+ * `swipe.css`) — `translate` места: место уезжает целиком, вместе с обрезкой,
78
+ * и плашку она не режет. У боковых мест — по строке, к своему краю, у мест по
79
+ * центру — по оси блока, к краю окна. В стопке то же свойство несёт и
80
+ * ступень: сдвиг и ступень — один `translate`.
81
+ *
82
+ * Облик — поверхность, кромка, тень и скругление плашки, цвет вида сообщения,
83
+ * кегль и начертание текста, цвет значка и описания, кольцо фокуса, вид
84
+ * крестика и кнопок на заливке, закрытое содержимое заднего, а движение —
85
+ * проявление и исчезание, въезд, уход, возврат отпущенного и переход ступени —
86
+ * рисует тема. */
87
+
88
+ :root {
89
+ --so-toasts-offset: 1rem;
90
+ --so-toasts-width: 22rem;
91
+ --so-toasts-gap: 0.75rem;
92
+ --so-toasts-padding-block: 0.875rem;
93
+ --so-toasts-padding-inline: 1rem;
94
+ --so-toasts-line: 1.25rem;
95
+ --so-toasts-card-gap: 0.75rem;
96
+ --so-toasts-body-gap: 0.125rem;
97
+ --so-toasts-content-gap: 0.5rem;
98
+ --so-toasts-content-offset: 0.375rem;
99
+ --so-toasts-spinner-size: calc(1em * 5 / 6);
100
+ --so-toasts-cascade-narrow: 0.05;
101
+ --so-toasts-cascade-layers: 2;
102
+ }
103
+
104
+ .so-toasts {
105
+ position: fixed;
106
+ left: calc(var(--so-toasts-offset) + env(safe-area-inset-left, 0px));
107
+ right: calc(var(--so-toasts-offset) + env(safe-area-inset-right, 0px));
108
+ margin: 0;
109
+ padding: 0;
110
+ pointer-events: none;
111
+ }
112
+
113
+ .so-toasts__list {
114
+ display: flex;
115
+ flex-direction: column;
116
+ margin: 0;
117
+ padding: 0;
118
+ list-style-type: none;
119
+ }
120
+
121
+ .so-toasts--placement-top-start,
122
+ .so-toasts--placement-top,
123
+ .so-toasts--placement-top-end {
124
+ top: calc(var(--so-toasts-offset) + env(safe-area-inset-top, 0px));
125
+ }
126
+
127
+ .so-toasts--placement-bottom-start,
128
+ .so-toasts--placement-bottom,
129
+ .so-toasts--placement-bottom-end {
130
+ bottom: calc(var(--so-toasts-offset) + env(safe-area-inset-bottom, 0px));
131
+ }
132
+
133
+ .so-toasts--placement-bottom-start > .so-toasts__list,
134
+ .so-toasts--placement-bottom > .so-toasts__list,
135
+ .so-toasts--placement-bottom-end > .so-toasts__list {
136
+ flex-direction: column-reverse;
137
+ }
138
+
139
+ .so-toasts--placement-top-start > .so-toasts__list,
140
+ .so-toasts--placement-bottom-start > .so-toasts__list {
141
+ align-items: flex-start;
142
+ }
143
+
144
+ .so-toasts--placement-top > .so-toasts__list,
145
+ .so-toasts--placement-bottom > .so-toasts__list {
146
+ align-items: center;
147
+ }
148
+
149
+ .so-toasts--placement-top-end > .so-toasts__list,
150
+ .so-toasts--placement-bottom-end > .so-toasts__list {
151
+ align-items: flex-end;
152
+ }
153
+
154
+ /* Зазор до соседа — `--so-toasts-item-gap`: у последнего тоста и в стопке его
155
+ * нет. */
156
+ .so-toasts-item {
157
+ --so-toasts-item-gap: var(--so-toasts-gap);
158
+ display: grid;
159
+ grid-template-rows: minmax(0, 1fr);
160
+ width: min(var(--so-toasts-width), 100%);
161
+ clip-path: inset(calc(-1 * var(--so-toasts-gap)));
162
+ pointer-events: auto;
163
+ }
164
+
165
+ .so-toasts-item:last-child {
166
+ --so-toasts-item-gap: 0px;
167
+ }
168
+
169
+ .so-toasts-item[data-swiping='true'] {
170
+ cursor: grabbing;
171
+ }
172
+
173
+ :is(
174
+ .so-toasts--placement-top-start,
175
+ .so-toasts--placement-top-end,
176
+ .so-toasts--placement-bottom-start,
177
+ .so-toasts--placement-bottom-end
178
+ )
179
+ > .so-toasts__list
180
+ > .so-toasts-item {
181
+ translate: var(--so-swipe-offset) 0;
182
+ }
183
+
184
+ :is(.so-toasts--placement-top, .so-toasts--placement-bottom) > .so-toasts__list > .so-toasts-item {
185
+ translate: 0 var(--so-swipe-offset);
186
+ }
187
+
188
+ .so-toasts-item[data-open='false'] {
189
+ pointer-events: none;
190
+ }
191
+
192
+ .so-toasts-item__card {
193
+ display: flex;
194
+ align-items: flex-start;
195
+ gap: var(--so-toasts-card-gap);
196
+ align-self: start;
197
+ padding: var(--so-toasts-padding-block) var(--so-toasts-padding-inline);
198
+ margin-block-start: var(--so-toasts-item-gap);
199
+ }
200
+
201
+ .so-toasts--placement-top-start > .so-toasts__list > .so-toasts-item > .so-toasts-item__card,
202
+ .so-toasts--placement-top > .so-toasts__list > .so-toasts-item > .so-toasts-item__card,
203
+ .so-toasts--placement-top-end > .so-toasts__list > .so-toasts-item > .so-toasts-item__card {
204
+ align-self: end;
205
+ margin-block: 0 var(--so-toasts-item-gap);
206
+ }
207
+
208
+ /* Коробка — квадрат строки текста и по ширине тоже: значок, кольцо и свой
209
+ * значок потребителя занимают одну колонку, и текст не сдвигается, когда
210
+ * кольцо сменилось значком. */
211
+ .so-toasts-item__icon {
212
+ display: flex;
213
+ flex-shrink: 0;
214
+ align-items: center;
215
+ justify-content: center;
216
+ min-inline-size: var(--so-toasts-line);
217
+ min-block-size: var(--so-toasts-line);
218
+ font-size: var(--so-toasts-line);
219
+ }
220
+
221
+ /* Специфичность — выше модификатора размера: ядро пишет его кольцу всегда. */
222
+ .so-toasts-item__icon > .so-progress-spinner {
223
+ --so-progress-spinner-size: var(--so-toasts-spinner-size);
224
+ }
225
+
226
+ .so-toasts-item__body {
227
+ display: flex;
228
+ flex-direction: column;
229
+ flex-grow: 1;
230
+ gap: var(--so-toasts-body-gap);
231
+ min-width: 0;
232
+ line-height: var(--so-toasts-line);
233
+ overflow-wrap: break-word;
234
+ }
235
+
236
+ .so-toasts-item__content {
237
+ display: flex;
238
+ flex-wrap: wrap;
239
+ align-items: center;
240
+ gap: var(--so-toasts-content-gap);
241
+ margin-block-start: var(--so-toasts-content-offset);
242
+ }
243
+
244
+ .so-toasts-item__icon:empty,
245
+ .so-toasts-item__text:empty,
246
+ .so-toasts-item__description:empty,
247
+ .so-toasts-item__content:empty {
248
+ display: none;
249
+ }
250
+
251
+ .so-toasts-item__close {
252
+ flex-shrink: 0;
253
+ aspect-ratio: 1;
254
+ padding-inline: 0;
255
+ margin-block: calc((var(--so-toasts-line) - var(--so-button-height-normal)) / 2);
256
+ margin-inline-end: calc(var(--so-button-height-normal) / -4);
257
+ }
258
+
259
+ /* Значок — по центру квадрата, как у кнопок шапки окна (`modal.css`). */
260
+ .so-toasts-item__close > .so-button__text {
261
+ display: flex;
262
+ justify-content: center;
263
+ }
264
+
265
+ /* Стопка. Ступени — `z-index` тостов, и за стопку они не выходят. */
266
+ .so-toasts--layout-cascade > .so-toasts__list {
267
+ display: grid;
268
+ grid-template-columns: minmax(0, 1fr);
269
+ isolation: isolate;
270
+ }
271
+
272
+ /* Стопка — у края окна, задние выглядывают к дальней от него стороне. */
273
+ .so-toasts--layout-cascade:is(
274
+ .so-toasts--placement-bottom-start,
275
+ .so-toasts--placement-bottom,
276
+ .so-toasts--placement-bottom-end
277
+ ) {
278
+ --so-toasts-peek-sign: -1;
279
+ }
280
+
281
+ .so-toasts--layout-cascade:is(
282
+ .so-toasts--placement-bottom-start,
283
+ .so-toasts--placement-bottom,
284
+ .so-toasts--placement-bottom-end
285
+ )
286
+ > .so-toasts__list {
287
+ align-items: end;
288
+ }
289
+
290
+ .so-toasts--layout-cascade:is(
291
+ .so-toasts--placement-top-start,
292
+ .so-toasts--placement-top,
293
+ .so-toasts--placement-top-end
294
+ ) {
295
+ --so-toasts-peek-sign: 1;
296
+ }
297
+
298
+ .so-toasts--layout-cascade:is(
299
+ .so-toasts--placement-top-start,
300
+ .so-toasts--placement-top,
301
+ .so-toasts--placement-top-end
302
+ )
303
+ > .so-toasts__list {
304
+ align-items: start;
305
+ }
306
+
307
+ .so-toasts--layout-cascade:is(.so-toasts--placement-top-start, .so-toasts--placement-bottom-start)
308
+ > .so-toasts__list {
309
+ justify-items: start;
310
+ }
311
+
312
+ .so-toasts--layout-cascade:is(.so-toasts--placement-top, .so-toasts--placement-bottom)
313
+ > .so-toasts__list {
314
+ justify-items: center;
315
+ }
316
+
317
+ .so-toasts--layout-cascade:is(.so-toasts--placement-top-end, .so-toasts--placement-bottom-end)
318
+ > .so-toasts__list {
319
+ justify-items: end;
320
+ }
321
+
322
+ /* Тост стопки — задний; передний — ниже. Ступень (`--so-toasts-lift`) — к
323
+ * дальней от края окна стороне, по оси блока, и стоит она в том же
324
+ * `translate`, что сдвиг жеста (ниже). */
325
+ .so-toasts--layout-cascade > .so-toasts__list > .so-toasts-item {
326
+ --so-toasts-item-gap: 0px;
327
+ --so-toasts-place: calc(
328
+ var(--so-toasts-depth, 0) + var(--so-toasts-behind-focus, 0) -
329
+ var(--so-toasts-after-closed, 0)
330
+ );
331
+ --so-toasts-lift: calc(
332
+ var(--so-toasts-peek-sign) * min(var(--so-toasts-place), var(--so-toasts-cascade-layers)) *
333
+ var(--so-toasts-gap)
334
+ );
335
+ grid-area: 1 / 1;
336
+ z-index: calc(-1 * var(--so-toasts-place));
337
+ contain: size;
338
+ height: var(--so-toasts-front-height, 100%);
339
+ scale: calc(
340
+ 1 - min(var(--so-toasts-place), var(--so-toasts-cascade-layers)) *
341
+ var(--so-toasts-cascade-narrow)
342
+ );
343
+ opacity: clamp(0, var(--so-toasts-cascade-layers) + 1 - var(--so-toasts-place), 1);
344
+ }
345
+
346
+ /* Сдвиг жеста — по своей оси, как у столбца, ступень — по оси блока. */
347
+ .so-toasts--layout-cascade:is(
348
+ .so-toasts--placement-top-start,
349
+ .so-toasts--placement-top-end,
350
+ .so-toasts--placement-bottom-start,
351
+ .so-toasts--placement-bottom-end
352
+ )
353
+ > .so-toasts__list
354
+ > .so-toasts-item {
355
+ translate: var(--so-swipe-offset) var(--so-toasts-lift);
356
+ }
357
+
358
+ .so-toasts--layout-cascade:is(.so-toasts--placement-top, .so-toasts--placement-bottom)
359
+ > .so-toasts__list
360
+ > .so-toasts-item {
361
+ translate: 0 calc(var(--so-swipe-offset) + var(--so-toasts-lift));
362
+ }
363
+
364
+ .so-toasts--layout-cascade:is(
365
+ .so-toasts--placement-bottom-start,
366
+ .so-toasts--placement-bottom,
367
+ .so-toasts--placement-bottom-end
368
+ )
369
+ > .so-toasts__list
370
+ > .so-toasts-item {
371
+ transform-origin: 50% 0;
372
+ }
373
+
374
+ .so-toasts--layout-cascade:is(
375
+ .so-toasts--placement-top-start,
376
+ .so-toasts--placement-top,
377
+ .so-toasts--placement-top-end
378
+ )
379
+ > .so-toasts__list
380
+ > .so-toasts-item {
381
+ transform-origin: 50% 100%;
382
+ }
383
+
384
+ /* Тост под фокусом — передний, стоявшие перед ним — на ступень дальше: видно
385
+ * так, будто его переставили первым. Перестановки нет: порядок Tab — порядок
386
+ * документа. */
387
+ .so-toasts--layout-cascade
388
+ > .so-toasts__list
389
+ > .so-toasts-item:has(~ .so-toasts-item:focus-within) {
390
+ --so-toasts-behind-focus: 1;
391
+ }
392
+
393
+ /* Стоявшие за закрытым — на ступень ближе, пока он гаснет: из коллекции он
394
+ * уйдёт, когда догаснет, и тогда глубина задних сойдётся со ступенью. */
395
+ .so-toasts--layout-cascade
396
+ > .so-toasts__list
397
+ > .so-toasts-item[data-open='false']
398
+ ~ .so-toasts-item {
399
+ --so-toasts-after-closed: 1;
400
+ }
401
+
402
+ .so-toasts--layout-cascade > .so-toasts__list > .so-toasts-item:focus-within {
403
+ --so-toasts-place: 0;
404
+ }
405
+
406
+ .so-toasts--layout-cascade > .so-toasts__list > .so-toasts-item > .so-toasts-item__card {
407
+ align-self: stretch;
408
+ margin-block: 0;
409
+ overflow: clip;
410
+ pointer-events: none;
411
+ }
412
+
413
+ /* Передний — тост под фокусом; без фокуса в стопке — первый, а за закрытым
414
+ * первым — следующий. Тот же выбор у поведения темы, которое меряет его
415
+ * высоту. */
416
+ .so-toasts--layout-cascade > .so-toasts__list > .so-toasts-item:focus-within,
417
+ .so-toasts--layout-cascade
418
+ > .so-toasts__list
419
+ > .so-toasts-item:first-child:not(:has(~ .so-toasts-item:focus-within)),
420
+ .so-toasts--layout-cascade
421
+ > .so-toasts__list
422
+ > .so-toasts-item[data-open='false']:first-child
423
+ + .so-toasts-item:not(:has(~ .so-toasts-item:focus-within)) {
424
+ contain: none;
425
+ height: auto;
426
+ }
427
+
428
+ .so-toasts--layout-cascade
429
+ > .so-toasts__list
430
+ > .so-toasts-item:focus-within
431
+ > .so-toasts-item__card,
432
+ .so-toasts--layout-cascade
433
+ > .so-toasts__list
434
+ > .so-toasts-item:first-child:not(:has(~ .so-toasts-item:focus-within))
435
+ > .so-toasts-item__card,
436
+ .so-toasts--layout-cascade
437
+ > .so-toasts__list
438
+ > .so-toasts-item[data-open='false']:first-child
439
+ + .so-toasts-item:not(:has(~ .so-toasts-item:focus-within))
440
+ > .so-toasts-item__card {
441
+ overflow: visible;
442
+ pointer-events: inherit;
443
+ }
444
+
445
+ .so-toasts--layout-cascade > .so-toasts__list > .so-toasts-item[data-open='false']:first-child {
446
+ z-index: 1;
447
+ }
@@ -0,0 +1,59 @@
1
+ /* ─── Tooltip ─────────────────────────────────────────────────
2
+ * Подсказка — короткий неинтерактивный текст у триггера.
3
+ *
4
+ * .so-tooltip корень — обёртка триггера, якорь панели и цель
5
+ * наведения
6
+ * .so-tooltip__panel плашка — Frame, телепортированный в `body`
7
+ * .so-tooltip__content текст подсказки
8
+ *
9
+ * Корень — строчный флекс, как у Popover (`components/popover.css`): у
10
+ * `inline-block` якорь встал бы выше строчного триггера, у `contents`
11
+ * прямоугольника нет вовсе. Корень ещё и цель наведения, и полоса вокруг
12
+ * триггера держала бы подсказку открытой, когда курсор уже ушёл с него.
13
+ *
14
+ * Плашка — с полями вокруг текста, шириной по тексту и не шире потолка и
15
+ * окна: минус `1rem` — чтобы она всегда помещалась в окно, сдвиг
16
+ * `TAnchorPlugin` держит её в окне, только пока она в него помещается.
17
+ * Потолок уже, чем у Popover: строка подсказки — пояснение, а не абзац.
18
+ * Потолка высоты нет: плашку не прокрутить — фокус в неё не уходит, а
19
+ * обрезанный текст не дочитать. Длинный текст — повод для Popover.
20
+ *
21
+ * Ширина — по тексту, а не по месту: панель позиционирована с `left`, и с
22
+ * шириной по содержимому место под текст было бы от `left` до края окна — у
23
+ * триггера справа текст сложился бы в узкий столбец. Текст просит ширину
24
+ * целиком и упирается в потолок плашки, а переносится уже внутри, длинное
25
+ * слово тоже. Ширину самой плашки не задать: `width` Frame пишет инлайном.
26
+ *
27
+ * Та же плашка — подсказка со значением у ручки Slider
28
+ * (`components/slider.css`): поля и строка у них одни. Строка — высота строки
29
+ * текста плашки: по ней ползунок держит место над ручками под подсказку,
30
+ * которая видна всегда, — поэтому её задаёт этот слой, а тема ставит плашке
31
+ * кегль без интерлиньяжа.
32
+ *
33
+ * Потолок, поля и строка — переменные корня документа; умолчания — числа темы
34
+ * oren, тема задаёт свои на корне документа.
35
+ *
36
+ * Облик — фон, текст, кегль, скругление и контур плашки — рисует тема. */
37
+
38
+ :root {
39
+ --so-tooltip-max-width: 16rem;
40
+ --so-tooltip-padding-inline: 0.5rem;
41
+ --so-tooltip-padding-block: 0.25rem;
42
+ --so-tooltip-line: 1.25rem;
43
+ }
44
+
45
+ .so-tooltip {
46
+ display: inline-flex;
47
+ }
48
+
49
+ .so-tooltip__panel {
50
+ max-width: min(var(--so-tooltip-max-width), calc(100vw - 1rem));
51
+ padding: var(--so-tooltip-padding-block) var(--so-tooltip-padding-inline);
52
+ line-height: var(--so-tooltip-line);
53
+ }
54
+
55
+ .so-tooltip__content {
56
+ width: max-content;
57
+ max-width: 100%;
58
+ overflow-wrap: break-word;
59
+ }