@rt-tools/ui-kit-v2 0.14.0 → 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.
Files changed (83) hide show
  1. package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs +49 -7
  2. package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs.map +1 -1
  3. package/fesm2022/rt-tools-ui-kit-v2.mjs +5885 -2414
  4. package/fesm2022/rt-tools-ui-kit-v2.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/rt-tools-ui-kit-v2-0.16.0.tgz +0 -0
  7. package/src/assets/icons/chat-error.svg +9 -0
  8. package/src/assets/icons/fork-spoon.svg +9 -0
  9. package/src/assets/icons/fullscreen.svg +9 -0
  10. package/src/assets/icons/home.svg +9 -0
  11. package/src/assets/icons/layers.svg +9 -0
  12. package/src/assets/icons/pin.svg +5 -0
  13. package/src/assets/icons/troubleshoot.svg +9 -0
  14. package/src/assets/icons-material/chart-bar.fill.svg +5 -0
  15. package/src/assets/icons-material/chart-bar.svg +5 -0
  16. package/src/assets/icons-material/chart-line.fill.svg +5 -0
  17. package/src/assets/icons-material/chart-line.svg +5 -0
  18. package/src/assets/icons-material/chat-error.fill.svg +5 -0
  19. package/src/assets/icons-material/chat-error.svg +5 -0
  20. package/src/assets/icons-material/comment.fill.svg +5 -0
  21. package/src/assets/icons-material/comment.svg +5 -0
  22. package/src/assets/icons-material/fork-spoon.fill.svg +5 -0
  23. package/src/assets/icons-material/fork-spoon.svg +5 -0
  24. package/src/assets/icons-material/fullscreen.fill.svg +5 -0
  25. package/src/assets/icons-material/fullscreen.svg +5 -0
  26. package/src/assets/icons-material/home.fill.svg +3 -0
  27. package/src/assets/icons-material/home.svg +5 -0
  28. package/src/assets/icons-material/ico-exit.fill.svg +5 -0
  29. package/src/assets/icons-material/ico-exit.svg +5 -0
  30. package/src/assets/icons-material/ico-sun.fill.svg +5 -0
  31. package/src/assets/icons-material/ico-sun.svg +5 -0
  32. package/src/assets/icons-material/ico-user.fill.svg +5 -0
  33. package/src/assets/icons-material/ico-user.svg +5 -0
  34. package/src/assets/icons-material/layers.fill.svg +3 -0
  35. package/src/assets/icons-material/layers.svg +5 -0
  36. package/src/assets/icons-material/minus.fill.svg +3 -0
  37. package/src/assets/icons-material/minus.svg +3 -0
  38. package/src/assets/icons-material/moon.fill.svg +5 -0
  39. package/src/assets/icons-material/moon.svg +5 -0
  40. package/src/assets/icons-material/pin.fill.svg +3 -0
  41. package/src/assets/icons-material/pin.svg +5 -0
  42. package/src/assets/icons-material/search-plus.fill.svg +5 -0
  43. package/src/assets/icons-material/search-plus.svg +5 -0
  44. package/src/assets/icons-material/th-large.fill.svg +3 -0
  45. package/src/assets/icons-material/th-large.svg +5 -0
  46. package/src/assets/icons-material/troubleshoot.fill.svg +5 -0
  47. package/src/assets/icons-material/troubleshoot.svg +5 -0
  48. package/src/lib/components/CONTEXT.md +1 -1
  49. package/src/lib/components/aside/CONTEXT.md +9 -0
  50. package/src/lib/components/aside/error-box/rt-aside-error-box.component.scss +37 -0
  51. package/src/lib/components/aside/rt-aside.component.scss +14 -2
  52. package/src/lib/components/dynamic-selector/CONTEXT.md +44 -0
  53. package/src/lib/components/dynamic-selector/dynamic-input/rt-dynamic-input.component.scss +19 -0
  54. package/src/lib/components/dynamic-selector/list/rt-dynamic-selector-list.component.scss +108 -0
  55. package/src/lib/components/dynamic-selector/popup/rt-dynamic-selector-popup.component.scss +170 -0
  56. package/src/lib/components/dynamic-selector/rt-dynamic-selector.component.scss +23 -0
  57. package/src/lib/components/dynamic-selector/stories/component/test-dynamic-selector-matrix.component.scss +8 -0
  58. package/src/lib/components/dynamic-selector/stories/component/test-dynamic-selector.component.scss +7 -0
  59. package/src/lib/components/empty-state/CONTEXT.md +3 -0
  60. package/src/lib/components/empty-state/rt-empty-state.component.scss +9 -0
  61. package/src/lib/components/expansion-panel/CONTEXT.md +45 -0
  62. package/src/lib/components/expansion-panel/rt-expansion-panel.component.scss +134 -0
  63. package/src/lib/components/expansion-panel/stories/component/test-expansion-panel-matrix.component.scss +31 -0
  64. package/src/lib/components/image-cropper/CONTEXT.md +54 -0
  65. package/src/lib/components/image-cropper/rt-image-cropper.component.scss +220 -0
  66. package/src/lib/components/image-upload/CONTEXT.md +65 -0
  67. package/src/lib/components/image-upload/rt-image-upload.component.scss +77 -0
  68. package/src/lib/components/menu/CONTEXT.md +27 -0
  69. package/src/lib/components/menu/rt-menu-item.component.scss +42 -0
  70. package/src/lib/components/side-menu/CONTEXT.md +38 -0
  71. package/src/lib/components/side-menu/favorites/rt-side-menu-favorites.component.scss +126 -0
  72. package/src/lib/components/side-menu/rt-side-menu.component.scss +303 -0
  73. package/src/lib/components/side-menu/stories/component/test-side-menu-cell.component.scss +27 -0
  74. package/src/lib/components/side-menu/stories/component/test-side-menu.component.scss +39 -0
  75. package/src/lib/components/side-menu/sub-item/rt-side-menu-sub-item.component.scss +203 -0
  76. package/src/lib/components/table/CONTEXT.md +6 -0
  77. package/src/lib/components/table/rt-table.component.scss +4 -2
  78. package/src/lib/components/tooltip/CONTEXT.md +7 -4
  79. package/src/rich-editor/lib/components/message-composer/CONTEXT.md +15 -1
  80. package/src/styles/_color-scheme.scss +50 -0
  81. package/types/rt-tools-ui-kit-v2-rich-editor.d.ts +22 -2
  82. package/types/rt-tools-ui-kit-v2.d.ts +1236 -13
  83. package/rt-tools-ui-kit-v2-0.14.0.tgz +0 -0
@@ -0,0 +1,220 @@
1
+ @layer rt-kit.components {
2
+ /* rt-image-cropper — обрезка изображения (ViewEncapsulation.None). Где лежат картинка и
3
+ рамка, считает компонент и кладёт на host пикселями поля; здесь только то, как они
4
+ выглядят. */
5
+
6
+ .rt-image-cropper {
7
+ --rt-image-cropper-height: calc(var(--rt-size-20) * 4);
8
+ --rt-image-cropper-radius: var(--rt-radius-md);
9
+ --rt-image-cropper-frame-radius: var(--rt-radius-none);
10
+ --rt-image-cropper-field-bg: var(--rt-color-bg-subtle);
11
+ --rt-image-cropper-shade: var(--rt-overlay-black-50);
12
+ --rt-image-cropper-rim-width: var(--rt-border-width-thin);
13
+ --rt-image-cropper-rim-color: var(--rt-overlay-white-50);
14
+ --rt-image-cropper-dash-style: none;
15
+ --rt-image-cropper-dash-color: transparent;
16
+ --rt-image-cropper-dash-opacity: 1;
17
+ --rt-image-cropper-handle-size: var(--rt-size-3);
18
+ --rt-image-cropper-handle-bg: var(--rt-color-action-on-primary);
19
+ --rt-image-cropper-handle-border: var(--rt-color-border-strong);
20
+ --rt-image-cropper-focus-shadow: var(--rt-shadow-focus-ring);
21
+ --rt-image-cropper-text-color: var(--rt-color-text-muted);
22
+ --rt-image-cropper-disabled-opacity: 0.5;
23
+
24
+ /* Где лежат картинка и рамка, компонент ставит на host пикселями поля; здесь — значения
25
+ до первого замера, чтобы у каждого свойства было объявление. */
26
+ --rt-image-cropper-image-x: 0;
27
+ --rt-image-cropper-image-y: 0;
28
+ --rt-image-cropper-image-width: 0;
29
+ --rt-image-cropper-image-height: 0;
30
+ --rt-image-cropper-frame-x: 0;
31
+ --rt-image-cropper-frame-y: 0;
32
+ --rt-image-cropper-frame-width: 0;
33
+ --rt-image-cropper-frame-height: 0;
34
+
35
+ /* Поле берёт ширину своей коробки: в ячейке, которая центрирует содержимое, блок без
36
+ ширины схлопнулся бы в ноль. */
37
+ display: block;
38
+ width: 100%;
39
+
40
+ &__field {
41
+ position: relative;
42
+ overflow: hidden;
43
+ height: var(--rt-image-cropper-height);
44
+ border-radius: var(--rt-image-cropper-radius);
45
+ background: var(--rt-image-cropper-field-bg);
46
+ user-select: none;
47
+ }
48
+
49
+ &__spinner {
50
+ position: absolute;
51
+ inset: 50% auto auto 50%;
52
+ translate: -50% -50%;
53
+ }
54
+
55
+ &__placeholder,
56
+ &__refusal {
57
+ position: absolute;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ padding: var(--rt-space-md);
62
+ margin: 0;
63
+ color: var(--rt-image-cropper-text-color);
64
+ inset: 0;
65
+ text-align: center;
66
+ }
67
+
68
+ &__image {
69
+ position: absolute;
70
+ top: var(--rt-image-cropper-image-y);
71
+ left: var(--rt-image-cropper-image-x);
72
+ width: var(--rt-image-cropper-image-width);
73
+ max-width: none;
74
+ height: var(--rt-image-cropper-image-height);
75
+ image-orientation: from-image;
76
+ pointer-events: none;
77
+ }
78
+
79
+ &__frame {
80
+ position: absolute;
81
+ top: var(--rt-image-cropper-frame-y);
82
+ left: var(--rt-image-cropper-frame-x);
83
+ width: var(--rt-image-cropper-frame-width);
84
+ height: var(--rt-image-cropper-frame-height);
85
+ box-sizing: border-box;
86
+ border: var(--rt-image-cropper-rim-width) solid var(--rt-image-cropper-rim-color);
87
+ border-radius: var(--rt-image-cropper-frame-radius);
88
+ cursor: move;
89
+
90
+ /* Затемнение всего, что вне рамки: обводка шире любого поля, а поле его обрезает.
91
+ Обводка повторяет скругление, поэтому в круглом виде светлое место — круг. */
92
+ outline: 100vmax solid var(--rt-image-cropper-shade);
93
+ touch-action: none;
94
+
95
+ /* Пунктир поверх светлой линии рамки; в своём наборе его нет, материальный рисует
96
+ его, как обрезка первого кита. */
97
+ &:after {
98
+ position: absolute;
99
+ border: var(--rt-image-cropper-rim-width) var(--rt-image-cropper-dash-style) var(--rt-image-cropper-dash-color);
100
+ border-radius: inherit;
101
+ content: '';
102
+ inset: calc(-1 * var(--rt-image-cropper-rim-width));
103
+ opacity: var(--rt-image-cropper-dash-opacity);
104
+ pointer-events: none;
105
+ }
106
+
107
+ &:focus-visible {
108
+ box-shadow: var(--rt-image-cropper-focus-shadow);
109
+ }
110
+ }
111
+
112
+ &__handle {
113
+ position: absolute;
114
+ width: var(--rt-image-cropper-handle-size);
115
+ height: var(--rt-image-cropper-handle-size);
116
+ box-sizing: border-box;
117
+ border: var(--rt-image-cropper-rim-width) solid var(--rt-image-cropper-handle-border);
118
+ background: var(--rt-image-cropper-handle-bg);
119
+ touch-action: none;
120
+
121
+ &:focus-visible {
122
+ box-shadow: var(--rt-image-cropper-focus-shadow);
123
+ outline: none;
124
+ }
125
+
126
+ &--side--nw {
127
+ top: 0;
128
+ left: 0;
129
+ cursor: nwse-resize;
130
+ }
131
+
132
+ &--side--n {
133
+ top: 0;
134
+ left: 50%;
135
+ cursor: ns-resize;
136
+ translate: -50% 0;
137
+ }
138
+
139
+ &--side--ne {
140
+ top: 0;
141
+ left: 100%;
142
+ cursor: nesw-resize;
143
+ translate: -100% 0;
144
+ }
145
+
146
+ &--side--e {
147
+ top: 50%;
148
+ left: 100%;
149
+ cursor: ew-resize;
150
+ translate: -100% -50%;
151
+ }
152
+
153
+ &--side--se {
154
+ top: 100%;
155
+ left: 100%;
156
+ cursor: nwse-resize;
157
+ translate: -100% -100%;
158
+ }
159
+
160
+ &--side--s {
161
+ top: 100%;
162
+ left: 50%;
163
+ cursor: ns-resize;
164
+ translate: -50% -100%;
165
+ }
166
+
167
+ &--side--sw {
168
+ top: 100%;
169
+ left: 0;
170
+ cursor: nesw-resize;
171
+ translate: 0 -100%;
172
+ }
173
+
174
+ &--side--w {
175
+ top: 50%;
176
+ left: 0;
177
+ cursor: ew-resize;
178
+ translate: 0 -50%;
179
+ }
180
+ }
181
+
182
+ /* Материальный набор повторяет обрезку первого кита: поле без подложки и скругления,
183
+ светлая вуаль вокруг рамки, пунктир по рамке и мелкие тёмные ручки. */
184
+ :is(.rt-preset-material, [data-preset='material']) & {
185
+ --rt-image-cropper-radius: var(--rt-radius-none);
186
+ --rt-image-cropper-field-bg: transparent;
187
+ --rt-image-cropper-shade: var(--rt-overlay-white-25);
188
+ --rt-image-cropper-dash-style: dashed;
189
+ --rt-image-cropper-dash-color: var(--rt-mat-dark-40);
190
+ --rt-image-cropper-dash-opacity: 0.75;
191
+ --rt-image-cropper-handle-size: var(--rt-size-2);
192
+ --rt-image-cropper-handle-bg: var(--rt-mat-dark-40);
193
+ --rt-image-cropper-handle-border: var(--rt-overlay-white-50);
194
+
195
+ @media (pointer: coarse) {
196
+ --rt-image-cropper-handle-size: var(--rt-size-6);
197
+ }
198
+ }
199
+
200
+ &--round {
201
+ --rt-image-cropper-frame-radius: var(--rt-radius-full);
202
+ }
203
+
204
+ /* Недоступность ниже вида: она отменяет курсоры и приглушает картинку при любом виде. */
205
+ &--disabled &__frame,
206
+ &--disabled &__handle {
207
+ cursor: default;
208
+ }
209
+
210
+ &--disabled &__handle,
211
+ &--disabled &__image {
212
+ opacity: var(--rt-image-cropper-disabled-opacity);
213
+ }
214
+
215
+ /* Палец крупнее указателя: ручки растут по способу нажатия, а не по ширине экрана. */
216
+ @media (pointer: coarse) {
217
+ --rt-image-cropper-handle-size: var(--rt-size-6);
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,65 @@
1
+ # `rt-image-upload`
2
+
3
+ Загрузчик одного изображения: логотип, аватар, обложка. Одно место, в котором по очереди стоят зона
4
+ загрузки, обрезка и сама картинка.
5
+
6
+ ```html
7
+ <rt-image-upload
8
+ downloadable
9
+ fileName="logo.png"
10
+ tooltip="Логотип. Нажмите, чтобы заменить"
11
+ [imageUrl]="logoUrl"
12
+ (imageChanged)="upload($event)" />
13
+ ```
14
+
15
+ | вход | тип | умолчание |
16
+ | --------------- | ----------------------------------- | ---------------------------- |
17
+ | `imageUrl` | `string \| null` | `null` |
18
+ | `fileName` | `string` | `'image'` |
19
+ | `tooltip` | `string` | `''` → `uiImageUploadChange` |
20
+ | `downloadable` | `boolean` | `false` |
21
+ | `downloadShape` | `'circle' \| 'square'` | `'circle'` |
22
+ | `autoApply` | `boolean` | `false` |
23
+ | `loading` | `boolean` | `false` |
24
+ | `disabled` | `boolean` | `false` |
25
+ | `ratio` | `number \| null` | `null` |
26
+ | `round` | `boolean` | `false` |
27
+ | `format` | `'png' \| 'jpeg' \| 'webp' \| null` | `null` |
28
+ | `quality` | `number` | `92` |
29
+
30
+ Выходы: `imageChanged` (`File`), `downloaded`.
31
+
32
+ ## Главное, что нужно знать
33
+
34
+ **Место одно, и в нём одно состояние.** Загрузка приложения важнее всего, выбранный файл важнее
35
+ картинки, картинка важнее зоны загрузки. Картинка стоит на месте зоны, а не под ней.
36
+
37
+ **Нажатие на картинку выбирает другой файл**, а выбранный открывает обрезку. «Отмена» возвращает
38
+ прежнюю картинку, «Применить» ставит результат и отдаёт файл в `imageChanged`.
39
+
40
+ **Картинка — адрес.** Приложение даёт свой; адрес применённого файла загрузчик делает сам и отпускает,
41
+ когда картинку сменили или компонент ушёл. Новый `imageUrl` от приложения снова берёт верх.
42
+
43
+ **`autoApply` убирает кнопки:** каждый результат обрезки сразу становится картинкой и уходит
44
+ приложению, а обрезка остаётся открытой.
45
+
46
+ ## Оформление снаружи
47
+
48
+ Свойства блока: `--rt-image-upload-image-max-height`, `--rt-image-upload-image-radius`,
49
+ `--rt-image-upload-hover-opacity`, `--rt-image-upload-focus-shadow`,
50
+ `--rt-image-upload-download-overhang`, `--rt-image-upload-download-bg`,
51
+ `--rt-image-upload-download-radius`. Они объявлены на самом блоке, поэтому переопределяются
52
+ правилом на `.rt-image-upload`, а не на `:root`. Зона загрузки берёт свойства
53
+ `--rt-empty-state-frame-*`, обрезка — `--rt-image-cropper-*`.
54
+
55
+ ## Края
56
+
57
+ - Брошенный файл не-изображение пропускается, зона остаётся как была.
58
+ - Недоступный загрузчик не берёт файлов и не открывает выбор; скачивание остаётся.
59
+ - Обрезка, не прочитавшая файл, закрывается, как по «Отмене».
60
+
61
+ ## Рядом
62
+
63
+ - [`rt-image-cropper`](../image-cropper/CONTEXT.md) — обрезка сама по себе, без выбора файла.
64
+ - [`rt-file-drop`](../file-drop/CONTEXT.md) и [`rt-empty-state`](../empty-state/CONTEXT.md) — из них
65
+ собрана зона загрузки.
@@ -0,0 +1,77 @@
1
+ @layer rt-kit.components {
2
+ /* rt-image-upload — одно место загрузчика (ViewEncapsulation.None, класс блока на host).
3
+ Место берёт ширину коробки: обрезка внутри меряет поле по ней. */
4
+
5
+ .rt-image-upload {
6
+ --rt-image-upload-image-max-height: var(--rt-size-100);
7
+ --rt-image-upload-image-radius: var(--rt-radius-md);
8
+ --rt-image-upload-focus-shadow: var(--rt-shadow-focus-ring);
9
+ --rt-image-upload-hover-opacity: 0.85;
10
+ --rt-image-upload-download-overhang: var(--rt-space-sm);
11
+ --rt-image-upload-download-bg: color-mix(in srgb, var(--rt-color-bg-surface) 75%, transparent);
12
+ --rt-image-upload-download-radius: var(--rt-radius-md);
13
+
14
+ display: block;
15
+ width: 100%;
16
+
17
+ &__loading {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: var(--rt-space-xl);
22
+ }
23
+
24
+ &__actions {
25
+ display: flex;
26
+ justify-content: flex-end;
27
+ margin-top: var(--rt-space-sm);
28
+ gap: var(--rt-space-sm);
29
+ }
30
+
31
+ /* Кнопка скачивания заходит за правый верхний угол картинки, как у первого кита: у превью
32
+ отступ вокруг картинки, а кнопка стоит в углу превью и накрывает край картинки. */
33
+ &__preview {
34
+ position: relative;
35
+ display: inline-block;
36
+ max-width: 100%;
37
+ box-sizing: border-box;
38
+ padding: var(--rt-image-upload-download-overhang);
39
+ }
40
+
41
+ &__download {
42
+ position: absolute;
43
+ top: 0;
44
+ right: 0;
45
+ border-radius: var(--rt-image-upload-download-radius);
46
+ /* stylelint-disable-next-line property-no-vendor-prefix -- Safari < 18 */
47
+ -webkit-backdrop-filter: blur(8px);
48
+ backdrop-filter: blur(8px);
49
+ background: var(--rt-image-upload-download-bg);
50
+
51
+ &--round {
52
+ --rt-image-upload-download-radius: var(--rt-radius-full);
53
+ }
54
+ }
55
+
56
+ &__image {
57
+ display: block;
58
+ max-width: 100%;
59
+ max-height: var(--rt-image-upload-image-max-height);
60
+ border-radius: var(--rt-image-upload-image-radius);
61
+ object-fit: contain;
62
+
63
+ &--active {
64
+ cursor: pointer;
65
+
66
+ &:hover {
67
+ opacity: var(--rt-image-upload-hover-opacity);
68
+ }
69
+
70
+ &:focus-visible {
71
+ box-shadow: var(--rt-image-upload-focus-shadow);
72
+ outline: none;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
@@ -44,6 +44,8 @@
44
44
  | `confirmTitle` | `string \| null` | `null` |
45
45
  | `confirmLabel` / `confirmCancelLabel` | `string` | `''` → словарь кита |
46
46
  | `confirmTone` | `'danger' \| 'warning' \| 'primary'` | `'danger'` |
47
+ | `link` | `RouterLink['routerLink']` | `null` |
48
+ | `fill` | `boolean` | `false` |
47
49
 
48
50
  | выход | тип |
49
51
  | ---------- | ------ |
@@ -60,6 +62,30 @@
60
62
  `confirmMessage` открывает `rt-menu-confirm-dialog`, и `selected` приходит **только** после
61
63
  подтверждения.
62
64
 
65
+ **Пункт-ссылка ведёт на страницу приложения.** Вход `link` принимает команды маршрута, как
66
+ `routerLink`, и пункт рисует внутри себя ссылку с адресом. Enter и клик переходят по ней, а
67
+ Ctrl/Cmd-click, Shift-click и средняя кнопка открывают новую вкладку или окно силами браузера.
68
+ Меню закрывается во всех этих случаях. Подтверждение у пункта-ссылки не спрашивается: переход
69
+ случается раньше, чем диалог успел бы спросить согласия.
70
+
71
+ **Свой значок пункта — шаблон `rtMenuItemIcon`.** Значок, которого нет ни в наборе кита, ни в
72
+ перечне имён Material, приложение кладёт внутрь пункта в `<ng-template rtMenuItemIcon>`. Кит
73
+ ставит его на место значка пункта его размером и красит цветом тона, в том числе у опасного
74
+ пункта и пункта-согласия; рисунок берёт цвет через `currentColor`. Свой значок сильнее `icon` и
75
+ `glyph`. Имя Material без пары и без своего значка рисует пункт без значка и в режиме
76
+ разработки пишет об этом предупреждение в консоль.
77
+
78
+ ```html
79
+ <rt-menu-item label="Сделать активным" (selected)="activate()">
80
+ <ng-template rtMenuItemIcon><svg viewBox="0 0 24 24">…</svg></ng-template>
81
+ </rt-menu-item>
82
+ ```
83
+
84
+ **Залитый значок пункта — вход `fill`.** Он уходит в `fill` значка: в материальном наборе пункт
85
+ рисует залитый рисунок, как пункты меню первого кита, а свой набор рисует значок одним рисунком.
86
+ Без входа значок контурный. Свой значок `rtMenuItemIcon` приложение рисует само, и заливка его не
87
+ касается.
88
+
63
89
  ## Края
64
90
 
65
91
  - Отключённый пункт не выбирается и меню не закрывает: он выпадает из таб-порядка (`tabindex`
@@ -68,6 +94,7 @@
68
94
  - Пункты для хода стрелками меню берёт из разметки панели, а не запросом компонентов: таблица
69
95
  кладёт их шаблоном приложения, и запрос содержимого меню их не видит.
70
96
  - Панель объявлена `role="menu"`, пункт — `role="menuitem"`.
97
+ - У пункта-ссылки роль пункта меню и фокус несёт ссылка внутри, а хост объявлен `role="none"`.
71
98
 
72
99
  ## Рядом
73
100
 
@@ -51,6 +51,23 @@
51
51
  max-height: var(--rt-menu-item-icon-size);
52
52
  flex-shrink: 0;
53
53
  color: var(--rt-color-text-muted);
54
+
55
+ /* Свой значок приложения: коробка значка держит размер и цвет тона, а рисунок внутри
56
+ занимает её целиком и красится через currentColor. */
57
+ &--own {
58
+ display: inline-flex;
59
+ width: var(--rt-menu-item-icon-size);
60
+ height: var(--rt-menu-item-icon-size);
61
+ align-items: center;
62
+ justify-content: center;
63
+ font-size: var(--rt-menu-item-icon-size);
64
+ line-height: 1;
65
+
66
+ > * {
67
+ max-width: 100%;
68
+ max-height: 100%;
69
+ }
70
+ }
54
71
  }
55
72
 
56
73
  &__label {
@@ -58,6 +75,31 @@
58
75
  white-space: nowrap;
59
76
  }
60
77
 
78
+ /* Пункт-ссылка: кликабельной зоной и узлом фокуса служит ссылка внутри, поэтому поля и
79
+ раскладка пункта переезжают на неё, а хост остаётся рамкой подсветки. */
80
+ &--link {
81
+ padding: 0;
82
+ }
83
+
84
+ &__link {
85
+ display: flex;
86
+ min-width: 0;
87
+ min-height: var(--rt-menu-item-min-height);
88
+ box-sizing: border-box;
89
+ flex: 1 1 auto;
90
+ align-items: center;
91
+ padding: var(--rt-menu-item-padding-y) var(--rt-menu-item-padding-x);
92
+ border-radius: var(--rt-menu-item-radius);
93
+ color: inherit;
94
+ gap: var(--rt-space-sm);
95
+ text-decoration: none;
96
+
97
+ &:focus-visible {
98
+ background: var(--rt-color-bg-hover);
99
+ outline: none;
100
+ }
101
+ }
102
+
61
103
  &--current {
62
104
  background: var(--rt-menu-item-current-bg);
63
105
  }
@@ -0,0 +1,38 @@
1
+ # `rt-side-menu`
2
+
3
+ Боковая навигация: полоса пунктов и подменю раздела рядом. Перенос бокового меню первого кита. Это
4
+ не `rt-menu`: то выпадающее меню действий, и одно другое не заменяет.
5
+
6
+ ```html
7
+ <rt-side-menu menuId="main" [menuItems]="sections()" [activeMenuIds]="activeIds()" />
8
+ ```
9
+
10
+ Входы, выходы и оси — в `Overview.mdx` рядом. Договорённость о поведении — описание второго кита,
11
+ поддомен бокового меню.
12
+
13
+ ## Главное, что нужно знать
14
+
15
+ **Активность приходит снаружи.** Меню не держит адрес само: какие пункты активны, говорит
16
+ `activeMenuIds`, и подсветку двигает новый набор после перехода.
17
+
18
+ **Режим и ширину подменю хранит сервис, а не меню.** `provideRtSideMenuSettings()` включает
19
+ хранение под номером меню; без провайдера меню ничего не помнит, и режим с шириной приходят
20
+ входами. Вход важнее сохранённого.
21
+
22
+ **Значок вне набора — шаблон `rtSideMenuIcon`.** Имя Material первого кита меню переводит парой
23
+ из перечня кита, как `rt-menu`. Имя без пары берёт свой шаблон меню, один на все пункты; без
24
+ шаблона на его месте пусто, а в режиме разработки консоль называет такое имя.
25
+
26
+ **Раздел и запрос меню держит само.** Их не задать входом: раздел открывает нажатие или наведение
27
+ на пункт полосы, запрос — набор в поле.
28
+
29
+ ## Края
30
+
31
+ - Незакреплённая панель стоит над страницей и закрывается уходом указателя. Если человек взялся за
32
+ поле поиска, её закрывает только нажатие снаружи.
33
+ - Закреплённая панель стоит в потоке рядом с полосой, у неё есть ручка ширины.
34
+ - На узком экране закрепления нет: меню рисует один столбец с кнопкой «назад».
35
+ - Столбец узкого экрана шириной `--rt-side-menu-mobile-width`, 15rem, как меню телефона первого кита:
36
+ длинная подпись режется многоточием и не раздувает меню. Строки стоят в 8 пикселях от края,
37
+ подпись — с 56.
38
+ - Отказ хранилища меню не останавливает.
@@ -0,0 +1,126 @@
1
+ @layer rt-kit.components {
2
+ /* rt-side-menu-favorites — блок избранного вверху подменю. ViewEncapsulation.None.
3
+ Блок — раскрывающаяся панель кита, оформленная как папка подменю; строки — те же подпункты,
4
+ что в списке. */
5
+
6
+ .rt-side-menu-favorites {
7
+ --rt-side-menu-favorites-radius: var(--rt-radius-full);
8
+ --rt-side-menu-favorites-divider-width: var(--rt-border-width-thin);
9
+
10
+ display: block;
11
+
12
+ /* Блок отделён от списка раздела чертой: без неё строка избранного и строка того же пункта
13
+ в списке стоят подряд и читаются как повтор. Свёрнутый блок держит черту так же. */
14
+ &:has(.rt-side-menu-favorites__panel) {
15
+ border-bottom: var(--rt-side-menu-favorites-divider-width) solid var(--rt-color-border-subtle);
16
+ margin-block-end: var(--rt-space-sm);
17
+ padding-block-end: var(--rt-space-sm);
18
+ }
19
+
20
+ /* Заголовок панели — как у папки подменю. Звезда стоит левее подписей строк, почти у края,
21
+ как у первого кита: так рядом со столбцом шевронов умещается и число строк. Свойства
22
+ переназначены правилом двух классов: так они сильнее значений самой панели на том же узле. */
23
+ &__panel.rt-expansion-panel {
24
+ --rt-expansion-panel-duration: var(--rt-duration-fast);
25
+ --rt-expansion-panel-header-radius: var(--rt-side-menu-favorites-radius);
26
+ --rt-expansion-panel-header-min-height: var(--rt-control-height-md);
27
+ --rt-expansion-panel-header-padding-block: var(--rt-space-2);
28
+ --rt-expansion-panel-header-padding-start: var(--rt-space-1);
29
+ --rt-expansion-panel-header-padding-end: var(--rt-space-sm);
30
+ --rt-expansion-panel-header-gap: var(--rt-space-sm);
31
+ --rt-expansion-panel-header-font-size: var(--rt-text-sm);
32
+ }
33
+
34
+ /* Звезда стоит на месте значка папки и красится цветом темы, как закрашенные звёзды списка. */
35
+ &__star {
36
+ flex-shrink: 0;
37
+ color: var(--rt-color-action-primary);
38
+ }
39
+
40
+ &__title {
41
+ overflow: hidden;
42
+ min-width: 0;
43
+ flex: 1 1 auto;
44
+ text-overflow: ellipsis;
45
+ white-space: nowrap;
46
+ }
47
+
48
+ &__list {
49
+ display: flex;
50
+ flex-direction: column;
51
+ }
52
+
53
+ &__item {
54
+ min-width: 0;
55
+ flex: 1 1 auto;
56
+ }
57
+ }
58
+
59
+ /* Строка и ручка объявлены вне корня блока: строку в руке CDK выносит в конец страницы, и правило
60
+ из корня до неё не доходит. */
61
+ .rt-side-menu-favorites__row {
62
+ display: flex;
63
+ align-items: center;
64
+
65
+ &.cdk-drag-preview {
66
+ --rt-side-menu-favorites-preview-radius: var(--rt-radius-full);
67
+ --rt-side-menu-favorites-preview-shadow: var(--rt-shadow-md);
68
+
69
+ border-radius: var(--rt-side-menu-favorites-preview-radius);
70
+ box-shadow: var(--rt-side-menu-favorites-preview-shadow);
71
+ }
72
+
73
+ /* Фон строки в руке — на строке внутри превью, а не на самом превью: CDK выносит превью в
74
+ верхний слой как `popover` и ставит ему встроенный прозрачный фон, который правило
75
+ класса не перебивает. */
76
+ &.cdk-drag-preview .rt-side-menu-favorites__item {
77
+ min-width: 0;
78
+ flex: 1 1 auto;
79
+ border-radius: var(--rt-side-menu-favorites-preview-radius);
80
+ background: var(--rt-color-bg-surface);
81
+ }
82
+
83
+ &.cdk-drag-placeholder {
84
+ opacity: 0;
85
+ }
86
+ }
87
+
88
+ /* Ручка видна так же, как «убрать»: под наведением и фокусом клавиатуры своей строки, а где
89
+ указатель не умеет наводиться, — всегда. Прячется прозрачностью: остаётся в порядке Tab. */
90
+ .rt-side-menu-favorites__handle {
91
+ flex-shrink: 0;
92
+ cursor: grab;
93
+ opacity: 0;
94
+
95
+ &:active {
96
+ cursor: grabbing;
97
+ }
98
+
99
+ @media (hover: none) {
100
+ opacity: 1;
101
+ }
102
+ }
103
+
104
+ .rt-side-menu-favorites__row:hover .rt-side-menu-favorites__handle,
105
+ .rt-side-menu-favorites__row:has(:focus-visible) .rt-side-menu-favorites__handle,
106
+ .rt-side-menu-favorites__row.cdk-drag-preview .rt-side-menu-favorites__handle {
107
+ opacity: 1;
108
+ }
109
+
110
+ /* Меню с `favoriteActionsReserve="none"`: ручка, ждущая наведения, в покое не занимает ширины. */
111
+ @media (hover: hover) {
112
+ .rt-side-menu--favorite-actions-none .rt-side-menu-favorites__handle {
113
+ overflow: hidden;
114
+ width: 0;
115
+ margin-inline-start: calc(var(--rt-space-sm) * -1);
116
+ }
117
+
118
+ /* Возврат места — прямым правилом: отрицание с `:has()` браузер при наведении не пересчитывал. */
119
+ .rt-side-menu--favorite-actions-none .rt-side-menu-favorites__row:hover .rt-side-menu-favorites__handle,
120
+ .rt-side-menu--favorite-actions-none .rt-side-menu-favorites__row:has(:focus-visible) .rt-side-menu-favorites__handle {
121
+ overflow: visible;
122
+ width: auto;
123
+ margin-inline-start: 0;
124
+ }
125
+ }
126
+ }