@rt-tools/ui-kit-v2 0.15.0 → 0.17.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 (119) hide show
  1. package/README.md +7 -0
  2. package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs +96 -41
  3. package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs.map +1 -1
  4. package/fesm2022/rt-tools-ui-kit-v2.mjs +6824 -2700
  5. package/fesm2022/rt-tools-ui-kit-v2.mjs.map +1 -1
  6. package/package.json +1 -1
  7. package/rt-tools-ui-kit-v2-0.17.0.tgz +0 -0
  8. package/src/assets/icons/chat-error.svg +9 -0
  9. package/src/assets/icons/fork-spoon.svg +9 -0
  10. package/src/assets/icons/fullscreen.svg +9 -0
  11. package/src/assets/icons/home.svg +9 -0
  12. package/src/assets/icons/layers.svg +9 -0
  13. package/src/assets/icons/pin.svg +5 -0
  14. package/src/assets/icons/troubleshoot.svg +9 -0
  15. package/src/assets/icons-material/chart-bar.fill.svg +5 -0
  16. package/src/assets/icons-material/chart-bar.svg +5 -0
  17. package/src/assets/icons-material/chart-line.fill.svg +5 -0
  18. package/src/assets/icons-material/chart-line.svg +5 -0
  19. package/src/assets/icons-material/chat-error.fill.svg +5 -0
  20. package/src/assets/icons-material/chat-error.svg +5 -0
  21. package/src/assets/icons-material/comment.fill.svg +5 -0
  22. package/src/assets/icons-material/comment.svg +5 -0
  23. package/src/assets/icons-material/fork-spoon.fill.svg +5 -0
  24. package/src/assets/icons-material/fork-spoon.svg +5 -0
  25. package/src/assets/icons-material/fullscreen.fill.svg +5 -0
  26. package/src/assets/icons-material/fullscreen.svg +5 -0
  27. package/src/assets/icons-material/home.fill.svg +3 -0
  28. package/src/assets/icons-material/home.svg +5 -0
  29. package/src/assets/icons-material/ico-exit.fill.svg +5 -0
  30. package/src/assets/icons-material/ico-exit.svg +5 -0
  31. package/src/assets/icons-material/ico-sun.fill.svg +5 -0
  32. package/src/assets/icons-material/ico-sun.svg +5 -0
  33. package/src/assets/icons-material/ico-user.fill.svg +5 -0
  34. package/src/assets/icons-material/ico-user.svg +5 -0
  35. package/src/assets/icons-material/layers.fill.svg +3 -0
  36. package/src/assets/icons-material/layers.svg +5 -0
  37. package/src/assets/icons-material/minus.fill.svg +3 -0
  38. package/src/assets/icons-material/minus.svg +3 -0
  39. package/src/assets/icons-material/moon.fill.svg +5 -0
  40. package/src/assets/icons-material/moon.svg +5 -0
  41. package/src/assets/icons-material/pin.fill.svg +3 -0
  42. package/src/assets/icons-material/pin.svg +5 -0
  43. package/src/assets/icons-material/search-plus.fill.svg +5 -0
  44. package/src/assets/icons-material/search-plus.svg +5 -0
  45. package/src/assets/icons-material/th-large.fill.svg +3 -0
  46. package/src/assets/icons-material/th-large.svg +5 -0
  47. package/src/assets/icons-material/troubleshoot.fill.svg +5 -0
  48. package/src/assets/icons-material/troubleshoot.svg +5 -0
  49. package/src/lib/components/CONTEXT.md +1 -1
  50. package/src/lib/components/action-bar/CONTEXT.md +5 -0
  51. package/src/lib/components/action-bar/rt-action-bar.component.scss +30 -16
  52. package/src/lib/components/aside/CONTEXT.md +18 -1
  53. package/src/lib/components/aside/error-box/rt-aside-error-box.component.scss +37 -0
  54. package/src/lib/components/aside/footer/rt-aside-footer.component.scss +7 -8
  55. package/src/lib/components/aside/header/rt-aside-header.component.scss +16 -1
  56. package/src/lib/components/aside/rt-aside.component.scss +31 -3
  57. package/src/lib/components/button/CONTEXT.md +12 -11
  58. package/src/lib/components/button/rt-button.directive.scss +36 -0
  59. package/src/lib/components/data-list/settings/rt-data-list-settings-aside.component.scss +2 -2
  60. package/src/lib/components/data-table/cell/rt-data-table-cell.component.scss +1 -1
  61. package/src/lib/components/data-table/filter-cell/rt-data-table-filter-cell.component.scss +1 -1
  62. package/src/lib/components/data-table/rt-data-table.component.scss +1 -1
  63. package/src/lib/components/dialog/CONTEXT.md +9 -0
  64. package/src/lib/components/dialog/content/rt-dialog-content.component.scss +20 -0
  65. package/src/lib/components/dialog/footer/rt-dialog-footer.component.scss +18 -2
  66. package/src/lib/components/dialog/header/rt-dialog-header.component.scss +21 -3
  67. package/src/lib/components/dialog/rt-dialog.component.scss +9 -3
  68. package/src/lib/components/dynamic-selector/CONTEXT.md +50 -0
  69. package/src/lib/components/dynamic-selector/dynamic-input/rt-dynamic-input.component.scss +19 -0
  70. package/src/lib/components/dynamic-selector/list/rt-dynamic-selector-list.component.scss +108 -0
  71. package/src/lib/components/dynamic-selector/popup/rt-dynamic-selector-popup.component.scss +170 -0
  72. package/src/lib/components/dynamic-selector/rt-dynamic-selector.component.scss +23 -0
  73. package/src/lib/components/dynamic-selector/stories/component/test-dynamic-selector-matrix.component.scss +8 -0
  74. package/src/lib/components/dynamic-selector/stories/component/test-dynamic-selector.component.scss +7 -0
  75. package/src/lib/components/empty-state/CONTEXT.md +3 -0
  76. package/src/lib/components/empty-state/rt-empty-state.component.scss +9 -0
  77. package/src/lib/components/expansion-panel/CONTEXT.md +45 -0
  78. package/src/lib/components/expansion-panel/rt-expansion-panel.component.scss +134 -0
  79. package/src/lib/components/expansion-panel/stories/component/test-expansion-panel-matrix.component.scss +31 -0
  80. package/src/lib/components/header/CONTEXT.md +2 -2
  81. package/src/lib/components/icon/CONTEXT.md +11 -7
  82. package/src/lib/components/icon/rt-icon.component.scss +47 -0
  83. package/src/lib/components/icon-button/CONTEXT.md +27 -20
  84. package/src/lib/components/icon-button/rt-icon-button.component.scss +51 -25
  85. package/src/lib/components/image-cropper/CONTEXT.md +54 -0
  86. package/src/lib/components/image-cropper/rt-image-cropper.component.scss +220 -0
  87. package/src/lib/components/image-upload/CONTEXT.md +70 -0
  88. package/src/lib/components/image-upload/rt-image-upload.component.scss +89 -0
  89. package/src/lib/components/menu/CONTEXT.md +27 -0
  90. package/src/lib/components/menu/rt-menu-item.component.scss +42 -0
  91. package/src/lib/components/pagination/rt-pagination.component.scss +1 -1
  92. package/src/lib/components/scroll-area/CONTEXT.md +2 -0
  93. package/src/lib/components/scroll-area/rt-scroll-area.component.scss +20 -3
  94. package/src/lib/components/side-menu/CONTEXT.md +43 -0
  95. package/src/lib/components/side-menu/favorites/rt-side-menu-favorites.component.scss +126 -0
  96. package/src/lib/components/side-menu/rt-side-menu.component.scss +303 -0
  97. package/src/lib/components/side-menu/stories/component/test-side-menu-cell.component.scss +27 -0
  98. package/src/lib/components/side-menu/stories/component/test-side-menu.component.scss +39 -0
  99. package/src/lib/components/side-menu/sub-item/rt-side-menu-sub-item.component.scss +203 -0
  100. package/src/lib/components/spinner/CONTEXT.md +15 -8
  101. package/src/lib/components/spinner/rt-spinner.component.scss +108 -9
  102. package/src/lib/components/table/CONTEXT.md +6 -0
  103. package/src/lib/components/table/rt-table.component.scss +4 -2
  104. package/src/lib/components/tag/rt-tag.component.scss +43 -23
  105. package/src/lib/components/toast/CONTEXT.md +8 -0
  106. package/src/lib/components/toast/rt-toast.component.scss +74 -15
  107. package/src/lib/components/toast/rt-toaster.component.scss +5 -1
  108. package/src/lib/components/toggle-button-group/rt-toggle-button-group.component.scss +35 -37
  109. package/src/lib/components/toggle-switch/CONTEXT.md +11 -3
  110. package/src/lib/components/toggle-switch/rt-toggle-switch.component.scss +53 -14
  111. package/src/lib/components/toolbar/rt-toolbar.component.scss +15 -4
  112. package/src/lib/components/tooltip/CONTEXT.md +7 -4
  113. package/src/rich-editor/lib/components/chat/CONTEXT.md +7 -2
  114. package/src/rich-editor/lib/components/chat/rt-chat.component.scss +25 -0
  115. package/src/styles/_color-scheme.scss +50 -0
  116. package/styles/tokens.css +35 -0
  117. package/types/rt-tools-ui-kit-v2-rich-editor.d.ts +16 -20
  118. package/types/rt-tools-ui-kit-v2.d.ts +1588 -58
  119. package/rt-tools-ui-kit-v2-0.15.0.tgz +0 -0
@@ -9,11 +9,6 @@
9
9
  rt-icon-button {
10
10
  display: inline-flex;
11
11
 
12
- /* Дефолт = md; перезаписывается модификаторами sm/lg ниже либо inline стилем потребителя.
13
- Ряд берётся из общей шкалы высот контролов: иконочная кнопка стоит в строке
14
- рядом с кнопкой и полем и обязана совпадать с ними по высоте. */
15
- --rt-icon-button-size: var(--rt-control-height-md);
16
-
17
12
  /* Цвет иконки объявлен здесь, а не приходил бы запасным значением на месте
18
13
  обращения: запасное переживает смену темы — переопределять нечего. Своё
19
14
  свойство блока перебивает тот, кому нужен другой цвет: так делает просмотр
@@ -38,20 +33,26 @@
38
33
  --rt-icon-button-color-hover: var(--rt-icon-button-color);
39
34
  --rt-icon-button-transition-duration: var(--rt-duration-fast);
40
35
 
41
- /* Заливка покоя — своим свойством: неактивная кнопка берёт из неё долю, которую задаёт
42
- набор оформления. */
43
- --rt-icon-button-bg: transparent;
36
+ /* Размер и заливка покоя — частные шаги: их пишут модификаторы размера и вида, а кнопка
37
+ читает их под публичными `--rt-icon-button-size` и `--rt-icon-button-bg`. Публичные
38
+ кнопка не объявляет, поэтому значение на теге `<rt-icon-button>` или выше доходит до
39
+ неё; раньше модификатор объявлял публичное свойство на самой кнопке и перебивал тег.
40
+ Компоненты кита, которым нужен свой размер кнопки, тоже пишут шаг, а не публичное
41
+ свойство: последнее слово остаётся за приложением.
42
+ Ряд размеров — из общей шкалы высот контролов: кнопка стоит в строке рядом с полем. */
43
+ --rt-icon-button-bg-variant: transparent;
44
+ --rt-icon-button-size-step: var(--rt-control-height-md);
44
45
 
45
46
  position: relative;
46
47
  display: inline-flex;
47
- width: var(--rt-icon-button-size);
48
- height: var(--rt-icon-button-size);
48
+ width: var(--rt-icon-button-size, var(--rt-icon-button-size-step));
49
+ height: var(--rt-icon-button-size, var(--rt-icon-button-size-step));
49
50
  align-items: center;
50
51
  justify-content: center;
51
52
  padding: 0;
52
53
  border: 0;
53
54
  border-radius: var(--rt-icon-button-radius);
54
- background-color: var(--rt-icon-button-bg);
55
+ background-color: var(--rt-icon-button-bg, var(--rt-icon-button-bg-variant));
55
56
  box-shadow: var(--rt-icon-button-shadow);
56
57
  color: var(--rt-icon-button-color);
57
58
  cursor: pointer;
@@ -59,24 +60,34 @@
59
60
  background-color var(--rt-icon-button-transition-duration) ease,
60
61
  box-shadow var(--rt-icon-button-transition-duration) ease;
61
62
 
63
+ /* Два шага меньше `sm` — для плотных строк. У шкалы нет ступени 22px, и ступень ради одной
64
+ кнопки легла бы мимо её сетки в 4px: шаг `xs` записан на месте. */
65
+ &--2xs {
66
+ --rt-icon-button-size-step: var(--rt-size-5);
67
+ }
68
+
69
+ &--xs {
70
+ --rt-icon-button-size-step: 22px;
71
+ }
72
+
62
73
  &--sm {
63
- --rt-icon-button-size: var(--rt-control-height-sm);
74
+ --rt-icon-button-size-step: var(--rt-control-height-sm);
64
75
  }
65
76
 
66
77
  &--md {
67
- --rt-icon-button-size: var(--rt-control-height-md);
78
+ --rt-icon-button-size-step: var(--rt-control-height-md);
68
79
  }
69
80
 
70
81
  &--lg {
71
- --rt-icon-button-size: var(--rt-control-height-lg);
82
+ --rt-icon-button-size-step: var(--rt-control-height-lg);
72
83
  }
73
84
 
74
85
  &--xl {
75
- --rt-icon-button-size: var(--rt-control-height-xl);
86
+ --rt-icon-button-size-step: var(--rt-control-height-xl);
76
87
  }
77
88
 
78
89
  &--2xl {
79
- --rt-icon-button-size: var(--rt-control-height-2xl);
90
+ --rt-icon-button-size-step: var(--rt-control-height-2xl);
80
91
  }
81
92
 
82
93
  /* Подъём под указателем — ручка для потребителя, как тень покоя; по умолчанию равен
@@ -85,9 +96,11 @@
85
96
  box-shadow: var(--rt-icon-button-shadow-hover);
86
97
  }
87
98
 
99
+ /* Заливка под указателем читается под ручкой `--rt-icon-button-bg-hover`; без неё каждый
100
+ вид берёт свою, как прежде. */
88
101
  &--ghost {
89
102
  &:hover:not(:disabled) {
90
- background-color: var(--rt-icon-button-color-bg-hover);
103
+ background-color: var(--rt-icon-button-bg-hover, var(--rt-icon-button-color-bg-hover));
91
104
  color: var(--rt-icon-button-color-hover);
92
105
  }
93
106
 
@@ -97,22 +110,22 @@
97
110
  }
98
111
 
99
112
  &--primary {
100
- --rt-icon-button-bg: var(--rt-color-action-primary);
113
+ --rt-icon-button-bg-variant: var(--rt-color-action-primary);
101
114
 
102
115
  color: var(--rt-color-action-on-primary);
103
116
 
104
117
  &:hover:not(:disabled) {
105
- background-color: var(--rt-color-action-primary-hover);
118
+ background-color: var(--rt-icon-button-bg-hover, var(--rt-color-action-primary-hover));
106
119
  }
107
120
  }
108
121
 
109
122
  &--secondary {
110
- --rt-icon-button-bg: var(--rt-color-bg-surface-subtle);
123
+ --rt-icon-button-bg-variant: var(--rt-color-bg-surface-subtle);
111
124
 
112
125
  color: var(--rt-color-text-primary);
113
126
 
114
127
  &:hover:not(:disabled) {
115
- background-color: var(--rt-icon-button-color-bg-hover);
128
+ background-color: var(--rt-icon-button-bg-hover, var(--rt-icon-button-color-bg-hover));
116
129
  }
117
130
  }
118
131
 
@@ -120,7 +133,7 @@
120
133
  color: var(--rt-color-state-danger);
121
134
 
122
135
  &:hover:not(:disabled) {
123
- background-color: var(--rt-icon-button-color-bg-hover);
136
+ background-color: var(--rt-icon-button-bg-hover, var(--rt-icon-button-color-bg-hover));
124
137
  }
125
138
 
126
139
  &.rt-icon-button--active {
@@ -132,7 +145,7 @@
132
145
  color: var(--rt-color-state-success);
133
146
 
134
147
  &:hover:not(:disabled) {
135
- background-color: var(--rt-icon-button-color-bg-hover);
148
+ background-color: var(--rt-icon-button-bg-hover, var(--rt-icon-button-color-bg-hover));
136
149
  }
137
150
 
138
151
  &.rt-icon-button--active {
@@ -144,7 +157,7 @@
144
157
  color: var(--rt-color-state-warning);
145
158
 
146
159
  &:hover:not(:disabled) {
147
- background-color: var(--rt-icon-button-color-bg-hover);
160
+ background-color: var(--rt-icon-button-bg-hover, var(--rt-icon-button-color-bg-hover));
148
161
  }
149
162
 
150
163
  &.rt-icon-button--active {
@@ -157,7 +170,11 @@
157
170
  встроенным стилем, и правилом его не перебить. Заливка остаётся долей заливки покоя: по умолчанию
158
171
  вся, и свой вид прежний. */
159
172
  &:disabled {
160
- background-color: color-mix(in srgb, var(--rt-icon-button-bg) var(--rt-icon-button-disabled-fill), transparent);
173
+ background-color: color-mix(
174
+ in srgb,
175
+ var(--rt-icon-button-bg, var(--rt-icon-button-bg-variant)) var(--rt-icon-button-disabled-fill),
176
+ transparent
177
+ );
161
178
  cursor: not-allowed;
162
179
  opacity: var(--rt-icon-button-disabled-opacity);
163
180
 
@@ -172,6 +189,15 @@
172
189
  outline-offset: 2px;
173
190
  }
174
191
 
192
+ /* Своё содержимое вместо значка по имени: встаёт по центру и берёт цвет кнопки. */
193
+ &__content {
194
+ display: inline-flex;
195
+ align-items: center;
196
+ justify-content: center;
197
+ color: inherit;
198
+ line-height: 1;
199
+ }
200
+
175
201
  &__spinner {
176
202
  --rt-icon-button-spin-duration: 1s;
177
203
 
@@ -0,0 +1,54 @@
1
+ # `rt-image-cropper`
2
+
3
+ Обрезка изображения. Исходник вписан в поле целиком, поверх — рамка с восемью ручками; компонент
4
+ отдаёт файл, вырезанный рамкой.
5
+
6
+ ```html
7
+ <rt-image-cropper round format="webp" [file]="photo" [ratio]="1" [quality]="85" (cropped)="save($event.file)" (loadFailed)="warn()" />
8
+ ```
9
+
10
+ | вход | тип | умолчание |
11
+ | ------------- | ----------------------------------- | ---------------------------------- |
12
+ | `placeholder` | `string` | `''` → `uiImageCropperPlaceholder` |
13
+ | `file` | `Blob \| null` | `null` |
14
+ | `ratio` | `number \| null` | `null` |
15
+ | `round` | `boolean` | `false` |
16
+ | `minSize` | `number` | `16` |
17
+ | `format` | `'png' \| 'jpeg' \| 'webp' \| null` | `null` |
18
+ | `quality` | `number` | `92` |
19
+ | `disabled` | `boolean` | `false` |
20
+
21
+ Выходы: `cropped` (`{ file, frame }`), `loadFailed`.
22
+
23
+ ## Главное, что нужно знать
24
+
25
+ **Рамка живёт в пикселях исходника, а не поля.** Поле меняется с окном, исходник — нет; `frame` в
26
+ результате — прямоугольник в пикселях исходника.
27
+
28
+ **Результат приходит после прочтения исходника и после каждого законченного изменения.** Во время
29
+ перетаскивания — ни одного: один жест — один файл. Каждая стрелка — законченное изменение.
30
+
31
+ **Круглый вид — только маска на экране.** Результат остаётся квадратом, который режет рамка:
32
+ скругляет картинку приложение там, где её показывает.
33
+
34
+ **Без выбранного формата берётся формат исходника**, а если он не png, jpeg или webp — png.
35
+
36
+ **Поворот снимка по EXIF делает браузер.** Размеры, которые видит компонент, уже повёрнутые.
37
+
38
+ **В материальном наборе обрезка выглядит как в первом ките.** Поле без подложки и скругления,
39
+ вокруг рамки светлая вуаль, по рамке пунктир, ручки мелкие и тёмные. Свой набор держит тёмную
40
+ вуаль и крупные белые ручки.
41
+
42
+ ## Края
43
+
44
+ - Рамка не выходит за исходник и не становится меньше `minSize`; исходник меньше `minSize` не
45
+ выпускает рамку за край.
46
+ - Смена `ratio` или `round` ставит рамку заново — наибольшую по центру.
47
+ - Исходник, который не прочитался, показывает отказ и поднимает `loadFailed`; рамки нет.
48
+ - Недоступная обрезка показывает рамку и не меняет её ни указателем, ни клавишами.
49
+ - Высота поля — свойство `--rt-image-cropper-height`.
50
+
51
+ ## Рядом
52
+
53
+ - [`rt-file-drop`](../file-drop/CONTEXT.md) — приём файла перетаскиванием.
54
+ - [`rt-file-input`](../file-input/CONTEXT.md) — выбор файла кнопкой.
@@ -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,70 @@
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
+ | `downloadIconSize` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| null` | `null` → по размеру кнопки |
23
+ | `chooseAppearance` | `'filled' \| 'outlined' \| 'text'` | `'outlined'` |
24
+ | `chooseIcon` | `string \| null` | `'ico-upload'` |
25
+ | `autoApply` | `boolean` | `false` |
26
+ | `loading` | `boolean` | `false` |
27
+ | `disabled` | `boolean` | `false` |
28
+ | `ratio` | `number \| null` | `null` |
29
+ | `round` | `boolean` | `false` |
30
+ | `format` | `'png' \| 'jpeg' \| 'webp' \| null` | `null` |
31
+ | `quality` | `number` | `92` |
32
+
33
+ Выходы: `imageChanged` (`File`), `downloaded`.
34
+
35
+ ## Главное, что нужно знать
36
+
37
+ **Место одно, и в нём одно состояние.** Загрузка приложения важнее всего, выбранный файл важнее
38
+ картинки, картинка важнее зоны загрузки. Картинка стоит на месте зоны, а не под ней.
39
+
40
+ **Нажатие на картинку выбирает другой файл**, а выбранный открывает обрезку. «Отмена» возвращает
41
+ прежнюю картинку, «Применить» ставит результат и отдаёт файл в `imageChanged`.
42
+
43
+ **Картинка — адрес.** Приложение даёт свой; адрес применённого файла загрузчик делает сам и отпускает,
44
+ когда картинку сменили или компонент ушёл. Новый `imageUrl` от приложения снова берёт верх.
45
+
46
+ **`autoApply` убирает кнопки:** каждый результат обрезки сразу становится картинкой и уходит
47
+ приложению, а обрезка остаётся открытой.
48
+
49
+ ## Оформление снаружи
50
+
51
+ Свойства блока: `--rt-image-upload-image-max-height`, `--rt-image-upload-image-radius`,
52
+ `--rt-image-upload-hover-opacity`, `--rt-image-upload-focus-shadow`,
53
+ `--rt-image-upload-download-overhang`, `--rt-image-upload-download-bg`,
54
+ `--rt-image-upload-download-radius`, `--rt-image-upload-download-size` (размер кнопки скачивания,
55
+ по умолчанию шаг `--rt-control-height-md`), `--rt-image-upload-download-blur` (размытие под ней,
56
+ 8px). Они объявлены на самом блоке, поэтому переопределяются
57
+ правилом на `.rt-image-upload`, а не на `:root`. Зона загрузки берёт свойства
58
+ `--rt-empty-state-frame-*`, обрезка — `--rt-image-cropper-*`.
59
+
60
+ ## Края
61
+
62
+ - Брошенный файл не-изображение пропускается, зона остаётся как была.
63
+ - Недоступный загрузчик не берёт файлов и не открывает выбор; скачивание остаётся.
64
+ - Обрезка, не прочитавшая файл, закрывается, как по «Отмене».
65
+
66
+ ## Рядом
67
+
68
+ - [`rt-image-cropper`](../image-cropper/CONTEXT.md) — обрезка сама по себе, без выбора файла.
69
+ - [`rt-file-drop`](../file-drop/CONTEXT.md) и [`rt-empty-state`](../empty-state/CONTEXT.md) — из них
70
+ собрана зона загрузки.
@@ -0,0 +1,89 @@
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
+ --rt-image-upload-download-blur: 8px;
14
+ --rt-image-upload-download-size: var(--rt-control-height-md);
15
+
16
+ display: block;
17
+ width: 100%;
18
+
19
+ &__loading {
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ padding: var(--rt-space-xl);
24
+ }
25
+
26
+ &__actions {
27
+ display: flex;
28
+ justify-content: flex-end;
29
+ margin-top: var(--rt-space-sm);
30
+ gap: var(--rt-space-sm);
31
+ }
32
+
33
+ /* Кнопка скачивания заходит за правый верхний угол картинки, как у первого кита: у превью
34
+ отступ вокруг картинки, а кнопка стоит в углу превью и накрывает край картинки. */
35
+ &__preview {
36
+ position: relative;
37
+ display: inline-block;
38
+ max-width: 100%;
39
+ box-sizing: border-box;
40
+ padding: var(--rt-image-upload-download-overhang);
41
+
42
+ /* На верх строки: на базовой линии под картинкой оставался зазор под нижние выносные. */
43
+ vertical-align: top;
44
+ }
45
+
46
+ &__download {
47
+ position: absolute;
48
+ top: 0;
49
+ right: 0;
50
+ border-radius: var(--rt-image-upload-download-radius);
51
+ /* stylelint-disable-next-line property-no-vendor-prefix -- Safari < 18 */
52
+ -webkit-backdrop-filter: blur(var(--rt-image-upload-download-blur));
53
+ backdrop-filter: blur(var(--rt-image-upload-download-blur));
54
+ background: var(--rt-image-upload-download-bg);
55
+
56
+ &--round {
57
+ --rt-image-upload-download-radius: var(--rt-radius-full);
58
+ }
59
+
60
+ /* Размер отдаётся кнопке её частным шагом, а не публичным `--rt-icon-button-size`: так
61
+ последнее слово остаётся за приложением. Шаг кнопка объявляет на корне своего
62
+ шаблона, а не на хосте, поэтому правило адресует тот же корень. */
63
+ .rt-icon-button {
64
+ --rt-icon-button-size-step: var(--rt-image-upload-download-size);
65
+ }
66
+ }
67
+
68
+ &__image {
69
+ display: block;
70
+ max-width: 100%;
71
+ max-height: var(--rt-image-upload-image-max-height);
72
+ border-radius: var(--rt-image-upload-image-radius);
73
+ object-fit: contain;
74
+
75
+ &--active {
76
+ cursor: pointer;
77
+
78
+ &:hover {
79
+ opacity: var(--rt-image-upload-hover-opacity);
80
+ }
81
+
82
+ &:focus-visible {
83
+ box-shadow: var(--rt-image-upload-focus-shadow);
84
+ outline: none;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
@@ -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