@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,171 @@
1
+ /* ─── Drawer ──────────────────────────────────────────────────
2
+ * Выезжающая панель. Анатомия — как у модального окна: корень — сама панель и
3
+ * несёт слой, подложка — её сосед, раньше в DOM. Подложка, сетка панели,
4
+ * заголовок, тело, подвал и кнопка закрытия — общие с окном (`modal.css`).
5
+ * Своё у панели — край, размер по оси края, место полосы жеста и место в
6
+ * контейнере: раскладка без замеров. Код отдаёт значения модификатором края
7
+ * (`--placement-*`), переменными размера и `data-*` (`data-swiping`,
8
+ * `data-contained`).
9
+ *
10
+ * Край. Боковая панель — во всю высоту, ширина — `--so-drawer-width`; верхняя
11
+ * и нижняя — во всю ширину, высота — `--so-drawer-height`, по умолчанию — по
12
+ * содержимому. Умолчания — у корня документа, а свойства `width` и `height`
13
+ * код пишет на панель инлайном. Потолок — экран (контейнер) без полосы
14
+ * подложки (`--so-drawer-gap`): по ней панель закрывают нажатием мимо, и из-под
15
+ * панели видно, откуда её открыли. Длинное содержимое прокручивается в теле.
16
+ * `start` и `end` — логические стороны: в RTL панель сама встаёт у другого
17
+ * края.
18
+ *
19
+ * Внутри контейнера (`contained`) панель и подложка стоят в ближайшем
20
+ * позиционированном предке, а не на экране.
21
+ *
22
+ * Полоса жеста — посередине края, за который тянут, то есть края со стороны
23
+ * страницы. Своя дорожка сетки, а не наложение: зона захвата
24
+ * (`--so-target-size`) не накрывает заголовок и кнопку закрытия. Отступ
25
+ * содержимого с её стороны держит сама зона: он шире обычного, и второй
26
+ * отступ сверх неё отодвинул бы содержимое дальше, чем у панели без полосы.
27
+ * У бокового края штрих полосы — поперёк строки (`swipe.css`). Пока панель
28
+ * тянут, курсор «держу» — по всей панели, и панель идёт за указателем: сдвиг
29
+ * плагина жеста (`--so-swipe-offset`, `swipe.css`) — её `translate` по оси
30
+ * края. Сдвиг — `translate`, а не отступ: он от размера самой панели и ничего
31
+ * не раскладывает заново.
32
+ *
33
+ * Облик — поверхность, кромка, тень, скругление углов со стороны страницы,
34
+ * вуаль подложки, въезд и выезд, возврат отпущенной — рисует тема. */
35
+
36
+ :root {
37
+ --so-drawer-width: 20rem;
38
+ --so-drawer-height: auto;
39
+ --so-drawer-gap: 3rem;
40
+ }
41
+
42
+ .so-drawer__backdrop[data-contained='true'] {
43
+ position: absolute;
44
+ }
45
+
46
+ .so-drawer {
47
+ position: fixed;
48
+ grid-template-columns: minmax(0, 1fr) auto;
49
+ grid-template-rows: auto minmax(0, 1fr) auto;
50
+ grid-template-areas:
51
+ 'title close'
52
+ 'body body'
53
+ 'footer footer';
54
+ }
55
+
56
+ .so-drawer[data-contained='true'] {
57
+ position: absolute;
58
+ }
59
+
60
+ .so-drawer--placement-start,
61
+ .so-drawer--placement-end {
62
+ inset-block: 0;
63
+ width: var(--so-drawer-width);
64
+ max-width: calc(100% - var(--so-drawer-gap));
65
+ translate: var(--so-swipe-offset) 0;
66
+ }
67
+
68
+ .so-drawer--placement-top,
69
+ .so-drawer--placement-bottom {
70
+ inset-inline: 0;
71
+ height: var(--so-drawer-height);
72
+ max-height: calc(100% - var(--so-drawer-gap));
73
+ translate: 0 var(--so-swipe-offset);
74
+ }
75
+
76
+ .so-drawer--placement-start {
77
+ inset-inline-start: 0;
78
+ }
79
+
80
+ .so-drawer--placement-end {
81
+ inset-inline-end: 0;
82
+ }
83
+
84
+ .so-drawer--placement-top {
85
+ inset-block-start: 0;
86
+ }
87
+
88
+ .so-drawer--placement-bottom {
89
+ inset-block-end: 0;
90
+ }
91
+
92
+ .so-drawer[data-swiping='true'] {
93
+ cursor: grabbing;
94
+ }
95
+
96
+ .so-drawer__handle {
97
+ grid-area: handle;
98
+ }
99
+
100
+ .so-drawer--placement-top > .so-drawer__handle,
101
+ .so-drawer--placement-bottom > .so-drawer__handle {
102
+ block-size: var(--so-target-size);
103
+ }
104
+
105
+ .so-drawer--placement-start > .so-drawer__handle,
106
+ .so-drawer--placement-end > .so-drawer__handle {
107
+ inline-size: var(--so-target-size);
108
+ }
109
+
110
+ .so-drawer--placement-start > .so-drawer__handle::before,
111
+ .so-drawer--placement-end > .so-drawer__handle::before {
112
+ inline-size: var(--so-swipe-grip-thickness);
113
+ block-size: var(--so-swipe-grip-length);
114
+ }
115
+
116
+ /* Дорожка полосы — у края со стороны страницы. */
117
+ .so-drawer--placement-bottom:has(> .so-drawer__handle) {
118
+ grid-template-rows: auto auto minmax(0, 1fr) auto;
119
+ grid-template-areas:
120
+ 'handle handle'
121
+ 'title close'
122
+ 'body body'
123
+ 'footer footer';
124
+ }
125
+
126
+ .so-drawer--placement-bottom:has(> .so-drawer__handle) > .so-drawer__title {
127
+ padding-block-start: 0;
128
+ }
129
+
130
+ .so-drawer--placement-bottom:has(> .so-drawer__handle) > .so-drawer__close {
131
+ margin-block-start: calc((var(--so-modal-title-line) - var(--so-button-height-normal)) / 2);
132
+ }
133
+
134
+ .so-drawer--placement-top:has(> .so-drawer__handle) {
135
+ grid-template-rows: auto minmax(0, 1fr) auto auto;
136
+ grid-template-areas:
137
+ 'title close'
138
+ 'body body'
139
+ 'footer footer'
140
+ 'handle handle';
141
+ }
142
+
143
+ .so-drawer--placement-top:has(> .so-drawer__handle) > .so-drawer__footer {
144
+ padding-block-end: 0;
145
+ }
146
+
147
+ .so-drawer--placement-end:has(> .so-drawer__handle) {
148
+ grid-template-columns: auto minmax(0, 1fr) auto;
149
+ grid-template-areas:
150
+ 'handle title close'
151
+ 'handle body body'
152
+ 'handle footer footer';
153
+ }
154
+
155
+ .so-drawer--placement-end:has(> .so-drawer__handle)
156
+ > :is(.so-drawer__title, .so-drawer__body, .so-drawer__footer) {
157
+ padding-inline-start: 0;
158
+ }
159
+
160
+ .so-drawer--placement-start:has(> .so-drawer__handle) {
161
+ grid-template-columns: minmax(0, 1fr) auto auto;
162
+ grid-template-areas:
163
+ 'title close handle'
164
+ 'body body handle'
165
+ 'footer footer handle';
166
+ }
167
+
168
+ .so-drawer--placement-start:has(> .so-drawer__handle)
169
+ > :is(.so-drawer__title, .so-drawer__body, .so-drawer__footer) {
170
+ padding-inline-end: 0;
171
+ }
@@ -0,0 +1,440 @@
1
+ /*
2
+ * Field contains its label, control wrapper, helper and error (with icon and
3
+ * text). Core supplies --position-top/start/end and --size-* on the root. Field
4
+ * has no control states of its own: read data-disabled, data-readonly and
5
+ * data-required from the direct child of __control. Descendant states may belong
6
+ * to clear buttons or Select tags and must not affect the whole Field.
7
+ *
8
+ * DOM order is label, control, helper, error for every position. Top is a column;
9
+ * start/end use a grid and logical directions, including RTL. Helper and error
10
+ * start under the control. Only one renders, so validation does not shift the
11
+ * form. Type sizes and gaps follow Label's caption scale. Parts own font sizes
12
+ * so the control retains its own text and em-based icons. Without a size modifier
13
+ * the size is normal. The theme paints text, errors and required dots.
14
+ */
15
+
16
+ /* Колонки поля с подписью в начале строки — `--so-field-columns`, её ставит
17
+ * приложение (см. `--position-start` ниже). Остальное — геометрия частей:
18
+ * зазор значка ошибки до текста и точка обязательного поля — размер, отступ
19
+ * от текста подписи и подъём к высоте прописных, всё в `em` кегля подписи. */
20
+ :root {
21
+ --so-field-columns: fit-content(40%) minmax(0, 1fr);
22
+ --so-field-error-gap: 0.375em;
23
+ --so-field-required-size: 0.3em;
24
+ --so-field-required-gap: 0.25em;
25
+ --so-field-required-lift: 0.5em;
26
+ }
27
+
28
+ .so-field {
29
+ --so-field-gap: var(--so-caption-gap-normal);
30
+ --so-field-stack: var(--so-caption-stack-normal);
31
+ }
32
+
33
+ .so-field--size-sm {
34
+ --so-field-gap: var(--so-caption-gap-sm);
35
+ --so-field-stack: var(--so-caption-stack-sm);
36
+ }
37
+
38
+ .so-field--size-sm > .so-field__label,
39
+ .so-field--size-sm > .so-field__control::before {
40
+ font-size: var(--so-caption-font-size-sm);
41
+ line-height: var(--so-caption-line-height-sm);
42
+ }
43
+
44
+ .so-field--size-sm > .so-field__helper,
45
+ .so-field--size-sm > .so-field__error {
46
+ font-size: var(--so-caption-note-font-size-sm);
47
+ line-height: var(--so-caption-note-line-height-sm);
48
+ }
49
+
50
+ /* Распорка ползунка — кеглем подписи (см. «ползунок» ниже). Кегль без
51
+ * интерлиньяжа: высота её строки — полоса ручек. */
52
+ .so-field--size-sm > .so-field__control > .so-slider--orientation-horizontal::before {
53
+ font-size: var(--so-caption-font-size-sm);
54
+ }
55
+
56
+ .so-field--size-normal {
57
+ --so-field-gap: var(--so-caption-gap-normal);
58
+ --so-field-stack: var(--so-caption-stack-normal);
59
+ }
60
+
61
+ .so-field--size-normal > .so-field__helper,
62
+ .so-field--size-normal > .so-field__error {
63
+ font-size: var(--so-caption-note-font-size-normal);
64
+ line-height: var(--so-caption-note-line-height-normal);
65
+ }
66
+
67
+ .so-field--size-lg {
68
+ --so-field-gap: var(--so-caption-gap-lg);
69
+ --so-field-stack: var(--so-caption-stack-lg);
70
+ }
71
+
72
+ .so-field--size-lg > .so-field__label,
73
+ .so-field--size-lg > .so-field__control::before {
74
+ font-size: var(--so-caption-font-size-lg);
75
+ line-height: var(--so-caption-line-height-lg);
76
+ }
77
+
78
+ .so-field--size-lg > .so-field__helper,
79
+ .so-field--size-lg > .so-field__error {
80
+ font-size: var(--so-caption-note-font-size-lg);
81
+ line-height: var(--so-caption-note-line-height-lg);
82
+ }
83
+
84
+ .so-field--size-lg > .so-field__control > .so-slider--orientation-horizontal::before {
85
+ font-size: var(--so-caption-font-size-lg);
86
+ }
87
+
88
+ .so-field--size-xl {
89
+ --so-field-gap: var(--so-caption-gap-xl);
90
+ --so-field-stack: var(--so-caption-stack-xl);
91
+ }
92
+
93
+ .so-field--size-xl > .so-field__label,
94
+ .so-field--size-xl > .so-field__control::before {
95
+ font-size: var(--so-caption-font-size-xl);
96
+ line-height: var(--so-caption-line-height-xl);
97
+ }
98
+
99
+ .so-field--size-xl > .so-field__helper,
100
+ .so-field--size-xl > .so-field__error {
101
+ font-size: var(--so-caption-note-font-size-xl);
102
+ line-height: var(--so-caption-note-line-height-xl);
103
+ }
104
+
105
+ .so-field--size-xl > .so-field__control > .so-slider--orientation-horizontal::before {
106
+ font-size: var(--so-caption-font-size-xl);
107
+ }
108
+
109
+ .so-field--size-2xl {
110
+ --so-field-gap: var(--so-caption-gap-2xl);
111
+ --so-field-stack: var(--so-caption-stack-2xl);
112
+ }
113
+
114
+ .so-field--size-2xl > .so-field__label,
115
+ .so-field--size-2xl > .so-field__control::before {
116
+ font-size: var(--so-caption-font-size-2xl);
117
+ line-height: var(--so-caption-line-height-2xl);
118
+ }
119
+
120
+ .so-field--size-2xl > .so-field__helper,
121
+ .so-field--size-2xl > .so-field__error {
122
+ font-size: var(--so-caption-note-font-size-2xl);
123
+ line-height: var(--so-caption-note-line-height-2xl);
124
+ }
125
+
126
+ .so-field--size-2xl > .so-field__control > .so-slider--orientation-horizontal::before {
127
+ font-size: var(--so-caption-font-size-2xl);
128
+ }
129
+
130
+ /* Подпись над контролом — столбец. Это сторона по умолчанию, `top`: правила
131
+ * у `--position-top` нет, и блок без модификатора выглядит так же. */
132
+ .so-field {
133
+ display: flex;
134
+ flex-direction: column;
135
+ }
136
+
137
+ /* Подпись — по ширине текста: клик мимо текста в поле не уводит. */
138
+ .so-field__label {
139
+ align-self: flex-start;
140
+ min-inline-size: 0;
141
+ }
142
+
143
+ /* Обёртка контрола — флекс, а не блок: строчный корень контрола (Input,
144
+ * DatePicker — `inline-flex`) в блоке встал бы на строку текста и взял бы
145
+ * под собой место под выносные элементы. Контрол — по своей ширине: её
146
+ * решает он сам (Select и горизонтальный Slider — во всю обёртку, поле —
147
+ * своим минимумом). */
148
+ .so-field__control {
149
+ display: flex;
150
+ align-items: flex-start;
151
+ min-inline-size: 0;
152
+ }
153
+
154
+ /* Зазор подписи до контрола — `stack`: на глаз он равен боковому. */
155
+ .so-field > .so-field__label + .so-field__control {
156
+ margin-block-start: var(--so-field-stack);
157
+ }
158
+
159
+ /* Справка — подсказка и ошибка: кегль справки при контроле, отступ от
160
+ * контрола тот же, что у подписи над ним. Длинное слово переносится, а не
161
+ * выталкивает колонку. */
162
+ .so-field__helper,
163
+ .so-field__error {
164
+ margin-block-start: var(--so-field-stack);
165
+ min-inline-size: 0;
166
+ overflow-wrap: break-word;
167
+ font-size: var(--so-caption-note-font-size-normal);
168
+ line-height: var(--so-caption-note-line-height-normal);
169
+ }
170
+
171
+ /* Ошибка — значок и текст в строку. Значок — не украшение: в режиме
172
+ * принудительных цветов цвет текста системный, и ошибку от подсказки
173
+ * отличает только он. */
174
+ .so-field__error {
175
+ display: flex;
176
+ align-items: flex-start;
177
+ column-gap: var(--so-field-error-gap);
178
+ }
179
+
180
+ /* Значок — в кегль ошибки и по центру её первой строки: коробка иконки
181
+ * высотой в строку, рисунок встаёт в её середину сам (`viewBox` квадратный, а
182
+ * коробка — нет). Значок — часть текста ошибки, и размер ему даёт поле, а не
183
+ * шкала иконок. */
184
+ .so-field__error > .so-field__error-icon {
185
+ flex-shrink: 0;
186
+ inline-size: 1em;
187
+ block-size: 1lh;
188
+ }
189
+
190
+ .so-field__error-text {
191
+ min-inline-size: 0;
192
+ }
193
+
194
+ /* Подпись сбоку — сетка: колонка подписи и колонка контрола, подсказка и
195
+ * ошибка под колонкой контрола. Зазор между колонками — `gap` шкалы подписи,
196
+ * тот же, что у `Label` сбоку; между строками — `stack`, как у подписи над
197
+ * контролом.
198
+ *
199
+ * Подпись стоит на первой строке контрола. Правило одно, как у `Label`: в
200
+ * обёртке контрола — распорка, строка нулевой ширины кеглем подписи; она
201
+ * стоит по центру контрола, а сетка ровняет её базовую линию с первой строкой
202
+ * подписи. Первая строка подписи встаёт по центру контрола: у поля ввода,
203
+ * выбора и даты — на строку его текста, у флажка, переключателя и оценки — на
204
+ * их середину. Контрол ниже строки — он встаёт по центру первой строки
205
+ * подписи. Высоту контрола знать не нужно. Исключения — контролы, которые
206
+ * бывают выше своей первой строки (ниже). */
207
+ .so-field--position-start,
208
+ .so-field--position-end {
209
+ display: grid;
210
+ align-items: baseline;
211
+ column-gap: var(--so-field-gap);
212
+ row-gap: var(--so-field-stack);
213
+ }
214
+
215
+ .so-field--position-start > .so-field__label,
216
+ .so-field--position-end > .so-field__label {
217
+ align-self: baseline;
218
+ justify-self: start;
219
+ }
220
+
221
+ .so-field--position-start > .so-field__label + .so-field__control,
222
+ .so-field--position-start > .so-field__helper,
223
+ .so-field--position-start > .so-field__error,
224
+ .so-field--position-end > .so-field__label + .so-field__control,
225
+ .so-field--position-end > .so-field__helper,
226
+ .so-field--position-end > .so-field__error {
227
+ margin-block-start: 0;
228
+ }
229
+
230
+ .so-field--position-start > .so-field__control,
231
+ .so-field--position-end > .so-field__control {
232
+ align-items: center;
233
+ }
234
+
235
+ /* `hidden` убирает распорку из доступного имени. */
236
+ .so-field--position-start > .so-field__control::before,
237
+ .so-field--position-end > .so-field__control::before {
238
+ content: '\200b';
239
+ visibility: hidden;
240
+ }
241
+
242
+ /* Контролы выше своей первой строки ровняются по ней, а не по центру: на
243
+ * центре всей коробки подпись опустилась бы к середине списка.
244
+ *
245
+ * - RadioGroup, `Label` вокруг контрола и Select, чьи теги переносятся по
246
+ * строкам (`wrap`), — по базовой линии своей первой строки: первого радио,
247
+ * текста подписи, первого ряда тегов. Распорки нет, и базовая линия обёртки
248
+ * — их собственная.
249
+ * - Ползунок — по полосе ручек: подписи меток под ней и подсказка над ней
250
+ * (`tooltip-always`) коробку растят. Распорка — внутри ползунка: строка
251
+ * нулевой ширины в начале его флекс-ряда, высотой в полосу ручек
252
+ * (`--so-slider-thumb-size` — сторона ручки его размера,
253
+ * `components/slider.css`), прижатая к верху его поля, — ровно туда, где
254
+ * лежит полоса, какой бы ни была подсказка над ней. Её базовая линия —
255
+ * базовая линия ползунка и обёртки. Вертикальный — по верху: первая строка
256
+ * подписи у его начала.
257
+ * - Многострочное поле Textarea — по своей первой строке текста: она стоит по
258
+ * центру строки поля его размера — высоты Input того же размера
259
+ * (`textarea.css`). Распорка — у верха обёртки, высотой в эту строку:
260
+ * подпись встаёт на первую строку, как у Input, сколько бы строк ни было
261
+ * ниже. Высота строки — по размеру самого поля, из шкалы поля
262
+ * (`input.css`): размер подписи и размер поля задают отдельно. */
263
+ .so-field--position-start > .so-field__control:has(> :is(.so-radio-group, .so-label))::before,
264
+ .so-field--position-start
265
+ > .so-field__control:has(> .so-select .so-tags[data-overflow='wrap'] .so-tags-item)::before,
266
+ .so-field--position-end > .so-field__control:has(> :is(.so-radio-group, .so-label))::before,
267
+ .so-field--position-end
268
+ > .so-field__control:has(> .so-select .so-tags[data-overflow='wrap'] .so-tags-item)::before,
269
+ .so-field--position-start > .so-field__control:has(> .so-slider--orientation-horizontal)::before,
270
+ .so-field--position-end > .so-field__control:has(> .so-slider--orientation-horizontal)::before {
271
+ content: none;
272
+ }
273
+
274
+ .so-field--position-start > .so-field__control > .so-slider--orientation-horizontal::before,
275
+ .so-field--position-end > .so-field__control > .so-slider--orientation-horizontal::before {
276
+ content: '\200b';
277
+ visibility: hidden;
278
+ align-self: flex-start;
279
+ line-height: var(--so-slider-thumb-size);
280
+ }
281
+
282
+ .so-field--position-start > .so-field__control:has(> .so-slider--orientation-vertical),
283
+ .so-field--position-end > .so-field__control:has(> .so-slider--orientation-vertical),
284
+ .so-field--position-start > .so-field__control:has(> .so-textarea),
285
+ .so-field--position-end > .so-field__control:has(> .so-textarea) {
286
+ align-items: flex-start;
287
+ }
288
+
289
+ .so-field--position-start > .so-field__control:has(> .so-textarea--size-sm)::before,
290
+ .so-field--position-end > .so-field__control:has(> .so-textarea--size-sm)::before {
291
+ line-height: var(--so-input-height-sm);
292
+ }
293
+
294
+ .so-field--position-start > .so-field__control:has(> .so-textarea--size-normal)::before,
295
+ .so-field--position-end > .so-field__control:has(> .so-textarea--size-normal)::before {
296
+ line-height: var(--so-input-height-normal);
297
+ }
298
+
299
+ .so-field--position-start > .so-field__control:has(> .so-textarea--size-lg)::before,
300
+ .so-field--position-end > .so-field__control:has(> .so-textarea--size-lg)::before {
301
+ line-height: var(--so-input-height-lg);
302
+ }
303
+
304
+ .so-field--position-start > .so-field__control:has(> .so-textarea--size-xl)::before,
305
+ .so-field--position-end > .so-field__control:has(> .so-textarea--size-xl)::before {
306
+ line-height: var(--so-input-height-xl);
307
+ }
308
+
309
+ .so-field--position-start > .so-field__control:has(> .so-textarea--size-2xl)::before,
310
+ .so-field--position-end > .so-field__control:has(> .so-textarea--size-2xl)::before {
311
+ line-height: var(--so-input-height-2xl);
312
+ }
313
+
314
+ /* Подпись в начале строки. Колонка подписи — по тексту, но не шире 40 %
315
+ * поля: длинная подпись переносится, а не забирает место контрола.
316
+ *
317
+ * Колонки задаёт переменная `--so-field-columns`, и её ставит приложение
318
+ * (умолчание — у корня документа, выше):
319
+ *
320
+ * - форма — сетка из двух колонок, поля в ней — во всю строку, а в переменной
321
+ * `subgrid`: колонка подписей у всех полей формы одна, по самой длинной
322
+ * подписи (или своей ширины — её задаёт сетка формы);
323
+ * - узкое место — одна колонка (`minmax(0, 1fr)`): подпись встаёт над
324
+ * контролом, как у `top`, без смены `position`. Так же поля формы, когда
325
+ * сетка формы сама переходит на одну колонку.
326
+ *
327
+ * Поэтому места частей — линии с краёв, а не номера колонок, и строки не
328
+ * заданы: подпись — первая колонка, контрол и справка — последняя, и при одной
329
+ * колонке части встают друг под другом в порядке разметки. Без подписи место
330
+ * её колонки остаётся: в форме контрол стоит в колонке контролов. */
331
+ .so-field--position-start {
332
+ grid-template-columns: var(--so-field-columns);
333
+ }
334
+
335
+ .so-field--position-start > .so-field__label {
336
+ grid-column: 1 / 2;
337
+ }
338
+
339
+ /* Подпись в несколько строк выше контрола — она занимает и строку справки:
340
+ * иначе справка встала бы под подписью, в отрыве от своего контрола. */
341
+ .so-field--position-start:has(> .so-field__helper, > .so-field__error) > .so-field__label {
342
+ grid-row: span 2;
343
+ }
344
+
345
+ .so-field--position-start > .so-field__control,
346
+ .so-field--position-start > .so-field__helper,
347
+ .so-field--position-start > .so-field__error {
348
+ grid-column: -2 / -1;
349
+ }
350
+
351
+ /* Подпись в конце строки — как у флажка с текстом: контрол по своей ширине,
352
+ * подпись — остаток строки. Подсказка и ошибка начинаются под контролом и
353
+ * идут во всю строку: в колонке флажка они не поместились бы. Колонок формы
354
+ * эта сторона не берёт — её колонка подписей стоит перед контролом, — и в
355
+ * одну колонку не складывается: контрол с подписью в конце узок и так. */
356
+ .so-field--position-end {
357
+ grid-template-columns: auto minmax(0, 1fr);
358
+ }
359
+
360
+ .so-field--position-end > .so-field__label,
361
+ .so-field--position-end > .so-field__control {
362
+ grid-row: 1;
363
+ }
364
+
365
+ .so-field--position-end > .so-field__control {
366
+ grid-column: 1;
367
+ }
368
+
369
+ .so-field--position-end > .so-field__label {
370
+ grid-column: 2;
371
+ }
372
+
373
+ .so-field--position-end > .so-field__helper,
374
+ .so-field--position-end > .so-field__error {
375
+ grid-column: 1 / -1;
376
+ }
377
+
378
+ /* Подпись переключателя — часть его цели: её `for` ведёт на поле CheckBox или
379
+ * Switch, клик по ней переключает, и курсор на ней — рука, как у `Label`.
380
+ * Остальным контролам клик по подписи только ставит фокус, а у DateInput,
381
+ * DatePicker и RadioGroup она только называет группу: курсор обычный. */
382
+ .so-field:has(> .so-field__control > :is(.so-check-box, .so-switch)) > .so-field__label {
383
+ cursor: pointer;
384
+ -webkit-user-select: none;
385
+ user-select: none;
386
+ }
387
+
388
+ /* Выключенный контрол — гаснет текст поля: подпись, подсказка и ошибка, как
389
+ * текст `Label` (`disabled.css`). Контрол гасит себя сам. Своего `disabled` у
390
+ * Field нет — второй путь к состоянию контрола разошёлся бы с первым. */
391
+ .so-field:has(> .so-field__control > [data-disabled='true']) > .so-field__label,
392
+ .so-field:has(> .so-field__control > [data-disabled='true']) > .so-field__helper,
393
+ .so-field:has(> .so-field__control > [data-disabled='true']) > .so-field__error {
394
+ opacity: var(--so-disabled-opacity);
395
+ cursor: default;
396
+ }
397
+
398
+ .so-field:has(> .so-field__control > [data-disabled='true']) > .so-field__label > *,
399
+ .so-field:has(> .so-field__control > [data-disabled='true']) > .so-field__helper > *,
400
+ .so-field:has(> .so-field__control > [data-disabled='true']) > .so-field__error > * {
401
+ --so-disabled-opacity: 1;
402
+ }
403
+
404
+ /* Контрол только для чтения — гаснет один курсор, как у `Label`: клик по
405
+ * подписи его не меняет. Имени блока контрола подпись не знает — отсюда
406
+ * совпадение по части класса. */
407
+ .so-field:has(> .so-field__control > [data-readonly='true']) > .so-field__label {
408
+ cursor: default;
409
+ }
410
+
411
+ /* Обязательное поле — точка у подписи, после её текста, на высоте прописных:
412
+ * так же стоит звёздочка, но точка — не текст и в имя поля не входит. Здесь —
413
+ * её место и размер, от кегля подписи; цвет и форма — у темы, те же, что у
414
+ * точки обязательного контрола. Точка у поля одна: с подписью точку контрола
415
+ * гасит переменная `--so-required-dot` (`required.css`), без подписи она
416
+ * остаётся у контрола. */
417
+ .so-field:has(> .so-field__control > [data-required='true']) > .so-field__label::after {
418
+ content: '';
419
+ display: inline-block;
420
+ inline-size: var(--so-field-required-size);
421
+ block-size: var(--so-field-required-size);
422
+ margin-inline-start: var(--so-field-required-gap);
423
+ vertical-align: var(--so-field-required-lift);
424
+ }
425
+
426
+ /* Переменная наследуется, поэтому каждая обёртка ставит свою: поле без
427
+ * подписи внутри поля с подписью точку контрола не теряет. */
428
+ .so-field__control {
429
+ --so-required-dot: initial;
430
+ }
431
+
432
+ .so-field:has(> .so-field__label) > .so-field__control {
433
+ --so-required-dot: none;
434
+ }
435
+
436
+ /* Ползунок в поле — во всю обёртку: своей ширины по содержимому у него нет,
437
+ * и по ней он встал бы в свою нижнюю границу. */
438
+ .so-field__control > .so-slider--orientation-horizontal {
439
+ flex: 1 1 auto;
440
+ }
@@ -0,0 +1,61 @@
1
+ /* ─── Icon ────────────────────────────────────────────────────
2
+ * Размер иконки — в `em` от кегля места, где она стоит: у строки на крупных
3
+ * размерах кегль растёт и сам. Шкала — переменные корня документа по размеру
4
+ * (`sm`…`2xl`); по ней же отметке CheckBox даёт размер его флажок
5
+ * (`check-box.css`). После `lg` шаг мелкий: на `2xl` крестик Tabs и очистка
6
+ * Select уже почти во всю высоту строки без её поля. Умолчания — числа темы
7
+ * oren; тема задаёт свои на корне документа.
8
+ *
9
+ * Правило нужно на каждой ступени шкалы: своего размера у `svg` нет. Без него
10
+ * иконка в кнопке, сжатой по содержимому, схлопывалась в 0×0 (крестики Tabs и
11
+ * Tags, очистка и стрелка Select на `xl` и `2xl`), а во флекс-строке
12
+ * растягивалась на её ширину (стрелка Accordion). Сторожит
13
+ * `playground/vue/browser/icon-size.spec.ts`.
14
+ *
15
+ * Цвет иконки — дело темы (`--so-icon-color`, `docs/agents/theming.md`,
16
+ * «Пакеты иконок»). */
17
+
18
+ :root {
19
+ --so-icon-size-sm: 0.875em;
20
+ --so-icon-size-normal: 1em;
21
+ --so-icon-size-lg: 1.35em;
22
+ --so-icon-size-xl: 1.4em;
23
+ --so-icon-size-2xl: 1.45em;
24
+ }
25
+
26
+ /* Корень иконки любого тега — блок, как `svg` библиотеки под сбросом
27
+ * (`reset.css`). У строчного корня размер ступени не действует вовсе: `span` —
28
+ * корень `<Icon>` без тега и элемента Web Components `<span is="so-icon">`,
29
+ * рисунок которого — `<svg>` внутри, растянутый на корень, — оставался пустым
30
+ * при любом `size`. Не `inline-block`: строчная иконка встала бы на базовую
31
+ * линию текста, ниже центра строки, а блок в области текста кнопки центруют
32
+ * авто-отступы (`button.css`). Корень нулевой специфичности: свой `display` у
33
+ * иконки потребителя его перекрывает. */
34
+ :where(.so-icon) {
35
+ display: block;
36
+ }
37
+
38
+ .so-icon--size-sm {
39
+ width: var(--so-icon-size-sm);
40
+ height: var(--so-icon-size-sm);
41
+ }
42
+
43
+ .so-icon--size-normal {
44
+ width: var(--so-icon-size-normal);
45
+ height: var(--so-icon-size-normal);
46
+ }
47
+
48
+ .so-icon--size-lg {
49
+ width: var(--so-icon-size-lg);
50
+ height: var(--so-icon-size-lg);
51
+ }
52
+
53
+ .so-icon--size-xl {
54
+ width: var(--so-icon-size-xl);
55
+ height: var(--so-icon-size-xl);
56
+ }
57
+
58
+ .so-icon--size-2xl {
59
+ width: var(--so-icon-size-2xl);
60
+ height: var(--so-icon-size-2xl);
61
+ }