@rt-tools/ui-kit-v2 0.23.4 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,295 @@
1
+ import * as i0 from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, ViewEncapsulation, booleanAttribute, computed, inject, input, output } from "@angular/core";
3
+ import * as i1 from "@angular/forms";
4
+ import { FormsModule } from "@angular/forms";
5
+ import { BlockDirective, ElemDirective } from "@rt-tools/core";
6
+ import { RT_KIT_LABELS, ThemeService } from "@rt-tools/ui-kit-v2/core";
7
+ import { RtMenuItemComponent } from "@rt-tools/ui-kit-v2/menu";
8
+ import { RtSelectComponent } from "@rt-tools/ui-kit-v2/select";
9
+ const BEM_BLOCK = "rt-profile-menu";
10
+ var RtProfileMenuComponent = class RtProfileMenuComponent {
11
+ constructor() {
12
+ this.#theme = inject(ThemeService);
13
+ this.t = inject(RT_KIT_LABELS);
14
+ this.themeIcon = computed(() => this.#theme.isDark() ? "ico-sun" : "moon", ...ngDevMode ? [{ debugName: "themeIcon" }] : /* istanbul ignore next */ []);
15
+ this.avatar = computed(() => this.userName().trim().charAt(0).toUpperCase(), ...ngDevMode ? [{ debugName: "avatar" }] : /* istanbul ignore next */ []);
16
+ this.userName = input("", ...ngDevMode ? [{ debugName: "userName" }] : /* istanbul ignore next */ []);
17
+ this.organizationName = input("", ...ngDevMode ? [{ debugName: "organizationName" }] : /* istanbul ignore next */ []);
18
+ this.organizationChange = input("hidden", ...ngDevMode ? [{ debugName: "organizationChange" }] : /* istanbul ignore next */ []);
19
+ this.passwordChange = input(false, {
20
+ ...ngDevMode ? { debugName: "passwordChange" } : /* istanbul ignore next */ {},
21
+ transform: booleanAttribute
22
+ });
23
+ this.locales = input([], ...ngDevMode ? [{ debugName: "locales" }] : /* istanbul ignore next */ []);
24
+ this.locale = input("", ...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
25
+ this.signOutDisabled = input(false, {
26
+ ...ngDevMode ? { debugName: "signOutDisabled" } : /* istanbul ignore next */ {},
27
+ transform: booleanAttribute
28
+ });
29
+ this.changeOrganization = output();
30
+ this.changePassword = output();
31
+ this.localeChange = output();
32
+ this.signOut = output();
33
+ }
34
+ #theme;
35
+ onToggleTheme() {
36
+ this.#theme.toggle();
37
+ }
38
+ static {
39
+ this.ɵfac = i0.ɵɵngDeclareFactory({
40
+ minVersion: "12.0.0",
41
+ version: "22.2.2",
42
+ ngImport: i0,
43
+ type: RtProfileMenuComponent,
44
+ deps: [],
45
+ target: i0.ɵɵFactoryTarget.Component
46
+ });
47
+ }
48
+ static {
49
+ this.ɵcmp = i0.ɵɵngDeclareComponent({
50
+ minVersion: "17.0.0",
51
+ version: "22.2.2",
52
+ type: RtProfileMenuComponent,
53
+ isStandalone: true,
54
+ selector: "rt-profile-menu",
55
+ inputs: {
56
+ userName: {
57
+ classPropertyName: "userName",
58
+ publicName: "userName",
59
+ isSignal: true,
60
+ isRequired: false,
61
+ transformFunction: null
62
+ },
63
+ organizationName: {
64
+ classPropertyName: "organizationName",
65
+ publicName: "organizationName",
66
+ isSignal: true,
67
+ isRequired: false,
68
+ transformFunction: null
69
+ },
70
+ organizationChange: {
71
+ classPropertyName: "organizationChange",
72
+ publicName: "organizationChange",
73
+ isSignal: true,
74
+ isRequired: false,
75
+ transformFunction: null
76
+ },
77
+ passwordChange: {
78
+ classPropertyName: "passwordChange",
79
+ publicName: "passwordChange",
80
+ isSignal: true,
81
+ isRequired: false,
82
+ transformFunction: null
83
+ },
84
+ locales: {
85
+ classPropertyName: "locales",
86
+ publicName: "locales",
87
+ isSignal: true,
88
+ isRequired: false,
89
+ transformFunction: null
90
+ },
91
+ locale: {
92
+ classPropertyName: "locale",
93
+ publicName: "locale",
94
+ isSignal: true,
95
+ isRequired: false,
96
+ transformFunction: null
97
+ },
98
+ signOutDisabled: {
99
+ classPropertyName: "signOutDisabled",
100
+ publicName: "signOutDisabled",
101
+ isSignal: true,
102
+ isRequired: false,
103
+ transformFunction: null
104
+ }
105
+ },
106
+ outputs: {
107
+ changeOrganization: "changeOrganization",
108
+ changePassword: "changePassword",
109
+ localeChange: "localeChange",
110
+ signOut: "signOut"
111
+ },
112
+ host: { classAttribute: "rt-profile-menu" },
113
+ ngImport: i0,
114
+ template: "<ng-container rtBlock=\"rt-profile-menu\">\n <div rtElem=\"user\">\n <span rtElem=\"avatar\" aria-hidden=\"true\">{{ avatar() }}</span>\n <span rtElem=\"name\" qa-dataid=\"profile-user-name\">{{ userName() }}</span>\n @if (organizationName()) {\n <span rtElem=\"organization\" qa-dataid=\"profile-organization\">\n {{ organizationName() }}\n <span rtElem=\"organization-label\">{{ t().profileMenuOrganization }}</span>\n </span>\n }\n </div>\n\n <div rtElem=\"nav\" role=\"menu\">\n @switch (organizationChange()) {\n @case ('allowed') {\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-change-organization\"\n icon=\"refresh\"\n [label]=\"t().profileMenuChangeOrganization\"\n (selected)=\"changeOrganization.emit()\" />\n }\n @case ('denied') {\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-change-organization\"\n icon=\"refresh\"\n disabled\n [label]=\"t().profileMenuChangeOrganization\"\n [tooltip]=\"t().profileMenuChangeOrganizationDenied\" />\n }\n }\n\n @if (passwordChange()) {\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-password-change\"\n icon=\"ico-lock\"\n [label]=\"t().profileMenuChangePassword\"\n (selected)=\"changePassword.emit()\" />\n }\n\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-logout\"\n icon=\"ico-exit\"\n [disabled]=\"signOutDisabled()\"\n [label]=\"t().profileMenuSignOut\"\n (selected)=\"signOut.emit()\" />\n </div>\n\n <!-- Своего правила у переключателя языка нет: ширину ему задаёт колонка блока. -->\n @if (locales().length) {\n <rt-select\n qa-dataid=\"profile-locale-select\"\n name=\"rtProfileMenuLocale\"\n [options]=\"locales()\"\n [ngModel]=\"locale()\"\n (ngModelChange)=\"localeChange.emit($event)\" />\n }\n\n <rt-menu-item\n rtElem=\"theme\"\n qa-dataid=\"profile-theme-toggle\"\n [icon]=\"themeIcon()\"\n [label]=\"t().profileMenuSwitchTheme\"\n (selected)=\"onToggleTheme()\" />\n</ng-container>\n",
115
+ styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-profile-menu{display:flex;min-width:var(--rt-profile-menu-min-width);box-sizing:border-box;flex-direction:column;padding:var(--rt-profile-menu-padding);border:var(--rt-profile-menu-border-width) solid var(--rt-nav-panel-color-border);border-radius:var(--rt-profile-menu-radius);background-color:var(--rt-nav-panel-color-bg);box-shadow:var(--rt-profile-menu-shadow);gap:var(--rt-space-2)}.rt-profile-menu__user{display:flex;flex-direction:column;padding-bottom:var(--rt-space-2);gap:var(--rt-space-2)}.rt-profile-menu__avatar{display:inline-flex;width:var(--rt-profile-avatar-size);height:var(--rt-profile-avatar-size);align-items:center;justify-content:center;border:var(--rt-profile-avatar-border-width) solid var(--rt-nav-header-color-bg);border-radius:var(--rt-profile-avatar-radius);color:var(--rt-color-text-primary);font-size:var(--rt-profile-avatar-font-size);font-weight:var(--rt-font-weight-semibold);line-height:var(--rt-leading-none)}.rt-profile-menu__name{color:var(--rt-color-text-primary);font-size:var(--rt-text-md);font-weight:var(--rt-font-weight-semibold);line-height:var(--rt-leading-tight)}.rt-profile-menu__organization{color:var(--rt-nav-panel-item-color);font-size:var(--rt-text-md);font-weight:var(--rt-font-weight-regular);line-height:var(--rt-leading-tight)}.rt-profile-menu__organization-label{color:var(--rt-nav-panel-group-color)}.rt-profile-menu__nav{display:flex;width:calc(100% + var(--rt-profile-row-inset) + var(--rt-profile-row-inset));flex-direction:column;margin-left:calc(-1 * var(--rt-profile-row-inset))}.rt-profile-menu .rt-profile-menu__item,.rt-profile-menu .rt-profile-menu__theme{border-radius:var(--rt-profile-item-radius);color:var(--rt-nav-panel-item-color);font-size:var(--rt-profile-item-font-size);font-weight:var(--rt-profile-item-font-weight);gap:var(--rt-profile-item-content-gap)}.rt-profile-menu .rt-profile-menu__item{padding:var(--rt-profile-item-padding-y) var(--rt-profile-row-inset)}.rt-profile-menu .rt-profile-menu__theme{width:calc(100% + var(--rt-profile-row-inset) + var(--rt-profile-row-inset));padding:var(--rt-profile-row-inset);margin-left:calc(-1 * var(--rt-profile-row-inset));color:var(--rt-color-action-primary)}.rt-profile-menu .rt-profile-menu__theme .rt-menu-item__icon{box-sizing:content-box;padding:var(--rt-profile-theme-icon-padding);border-radius:var(--rt-profile-theme-icon-radius);background-color:var(--rt-color-action-primary-subtle)}}\n"],
116
+ dependencies: [
117
+ {
118
+ kind: "ngmodule",
119
+ type: FormsModule
120
+ },
121
+ {
122
+ kind: "directive",
123
+ type: i1.NgControlStatus,
124
+ selector: "[formControlName],[ngModel],[formControl]"
125
+ },
126
+ {
127
+ kind: "directive",
128
+ type: i1.NgModel,
129
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
130
+ inputs: [
131
+ "name",
132
+ "disabled",
133
+ "ngModel",
134
+ "ngModelOptions"
135
+ ],
136
+ outputs: ["ngModelChange"],
137
+ exportAs: ["ngModel"]
138
+ },
139
+ {
140
+ kind: "directive",
141
+ type: BlockDirective,
142
+ selector: "[rtBlock]",
143
+ inputs: ["rtMod"]
144
+ },
145
+ {
146
+ kind: "directive",
147
+ type: ElemDirective,
148
+ selector: "[rtElem]",
149
+ inputs: ["rtMod"]
150
+ },
151
+ {
152
+ kind: "component",
153
+ type: RtMenuItemComponent,
154
+ selector: "rt-menu-item",
155
+ inputs: [
156
+ "icon",
157
+ "label",
158
+ "danger",
159
+ "success",
160
+ "glyph",
161
+ "fill",
162
+ "current",
163
+ "disabled",
164
+ "confirmMessage",
165
+ "confirmTitle",
166
+ "confirmLabel",
167
+ "confirmCancelLabel",
168
+ "confirmTone",
169
+ "link"
170
+ ],
171
+ outputs: ["selected"]
172
+ },
173
+ {
174
+ kind: "component",
175
+ type: RtSelectComponent,
176
+ selector: "rt-select",
177
+ inputs: [
178
+ "appearance",
179
+ "options",
180
+ "placeholder",
181
+ "iconLeft",
182
+ "filter",
183
+ "filterPlaceholder",
184
+ "panelWidth",
185
+ "panelMaxHeight"
186
+ ],
187
+ outputs: ["selectionChange"]
188
+ }
189
+ ],
190
+ changeDetection: i0.ChangeDetectionStrategy.OnPush,
191
+ encapsulation: i0.ViewEncapsulation.None
192
+ });
193
+ }
194
+ };
195
+ i0.ɵɵngDeclareClassMetadata({
196
+ minVersion: "12.0.0",
197
+ version: "22.2.2",
198
+ ngImport: i0,
199
+ type: RtProfileMenuComponent,
200
+ decorators: [{
201
+ type: Component,
202
+ args: [{
203
+ selector: "rt-profile-menu",
204
+ changeDetection: ChangeDetectionStrategy.OnPush,
205
+ encapsulation: ViewEncapsulation.None,
206
+ imports: [
207
+ FormsModule,
208
+ BlockDirective,
209
+ ElemDirective,
210
+ RtMenuItemComponent,
211
+ RtSelectComponent
212
+ ],
213
+ host: { class: BEM_BLOCK },
214
+ template: "<ng-container rtBlock=\"rt-profile-menu\">\n <div rtElem=\"user\">\n <span rtElem=\"avatar\" aria-hidden=\"true\">{{ avatar() }}</span>\n <span rtElem=\"name\" qa-dataid=\"profile-user-name\">{{ userName() }}</span>\n @if (organizationName()) {\n <span rtElem=\"organization\" qa-dataid=\"profile-organization\">\n {{ organizationName() }}\n <span rtElem=\"organization-label\">{{ t().profileMenuOrganization }}</span>\n </span>\n }\n </div>\n\n <div rtElem=\"nav\" role=\"menu\">\n @switch (organizationChange()) {\n @case ('allowed') {\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-change-organization\"\n icon=\"refresh\"\n [label]=\"t().profileMenuChangeOrganization\"\n (selected)=\"changeOrganization.emit()\" />\n }\n @case ('denied') {\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-change-organization\"\n icon=\"refresh\"\n disabled\n [label]=\"t().profileMenuChangeOrganization\"\n [tooltip]=\"t().profileMenuChangeOrganizationDenied\" />\n }\n }\n\n @if (passwordChange()) {\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-password-change\"\n icon=\"ico-lock\"\n [label]=\"t().profileMenuChangePassword\"\n (selected)=\"changePassword.emit()\" />\n }\n\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-logout\"\n icon=\"ico-exit\"\n [disabled]=\"signOutDisabled()\"\n [label]=\"t().profileMenuSignOut\"\n (selected)=\"signOut.emit()\" />\n </div>\n\n <!-- Своего правила у переключателя языка нет: ширину ему задаёт колонка блока. -->\n @if (locales().length) {\n <rt-select\n qa-dataid=\"profile-locale-select\"\n name=\"rtProfileMenuLocale\"\n [options]=\"locales()\"\n [ngModel]=\"locale()\"\n (ngModelChange)=\"localeChange.emit($event)\" />\n }\n\n <rt-menu-item\n rtElem=\"theme\"\n qa-dataid=\"profile-theme-toggle\"\n [icon]=\"themeIcon()\"\n [label]=\"t().profileMenuSwitchTheme\"\n (selected)=\"onToggleTheme()\" />\n</ng-container>\n",
215
+ styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-profile-menu{display:flex;min-width:var(--rt-profile-menu-min-width);box-sizing:border-box;flex-direction:column;padding:var(--rt-profile-menu-padding);border:var(--rt-profile-menu-border-width) solid var(--rt-nav-panel-color-border);border-radius:var(--rt-profile-menu-radius);background-color:var(--rt-nav-panel-color-bg);box-shadow:var(--rt-profile-menu-shadow);gap:var(--rt-space-2)}.rt-profile-menu__user{display:flex;flex-direction:column;padding-bottom:var(--rt-space-2);gap:var(--rt-space-2)}.rt-profile-menu__avatar{display:inline-flex;width:var(--rt-profile-avatar-size);height:var(--rt-profile-avatar-size);align-items:center;justify-content:center;border:var(--rt-profile-avatar-border-width) solid var(--rt-nav-header-color-bg);border-radius:var(--rt-profile-avatar-radius);color:var(--rt-color-text-primary);font-size:var(--rt-profile-avatar-font-size);font-weight:var(--rt-font-weight-semibold);line-height:var(--rt-leading-none)}.rt-profile-menu__name{color:var(--rt-color-text-primary);font-size:var(--rt-text-md);font-weight:var(--rt-font-weight-semibold);line-height:var(--rt-leading-tight)}.rt-profile-menu__organization{color:var(--rt-nav-panel-item-color);font-size:var(--rt-text-md);font-weight:var(--rt-font-weight-regular);line-height:var(--rt-leading-tight)}.rt-profile-menu__organization-label{color:var(--rt-nav-panel-group-color)}.rt-profile-menu__nav{display:flex;width:calc(100% + var(--rt-profile-row-inset) + var(--rt-profile-row-inset));flex-direction:column;margin-left:calc(-1 * var(--rt-profile-row-inset))}.rt-profile-menu .rt-profile-menu__item,.rt-profile-menu .rt-profile-menu__theme{border-radius:var(--rt-profile-item-radius);color:var(--rt-nav-panel-item-color);font-size:var(--rt-profile-item-font-size);font-weight:var(--rt-profile-item-font-weight);gap:var(--rt-profile-item-content-gap)}.rt-profile-menu .rt-profile-menu__item{padding:var(--rt-profile-item-padding-y) var(--rt-profile-row-inset)}.rt-profile-menu .rt-profile-menu__theme{width:calc(100% + var(--rt-profile-row-inset) + var(--rt-profile-row-inset));padding:var(--rt-profile-row-inset);margin-left:calc(-1 * var(--rt-profile-row-inset));color:var(--rt-color-action-primary)}.rt-profile-menu .rt-profile-menu__theme .rt-menu-item__icon{box-sizing:content-box;padding:var(--rt-profile-theme-icon-padding);border-radius:var(--rt-profile-theme-icon-radius);background-color:var(--rt-color-action-primary-subtle)}}\n"]
216
+ }]
217
+ }],
218
+ propDecorators: {
219
+ userName: [{
220
+ type: i0.Input,
221
+ args: [{
222
+ isSignal: true,
223
+ alias: "userName",
224
+ required: false
225
+ }]
226
+ }],
227
+ organizationName: [{
228
+ type: i0.Input,
229
+ args: [{
230
+ isSignal: true,
231
+ alias: "organizationName",
232
+ required: false
233
+ }]
234
+ }],
235
+ organizationChange: [{
236
+ type: i0.Input,
237
+ args: [{
238
+ isSignal: true,
239
+ alias: "organizationChange",
240
+ required: false
241
+ }]
242
+ }],
243
+ passwordChange: [{
244
+ type: i0.Input,
245
+ args: [{
246
+ isSignal: true,
247
+ alias: "passwordChange",
248
+ required: false
249
+ }]
250
+ }],
251
+ locales: [{
252
+ type: i0.Input,
253
+ args: [{
254
+ isSignal: true,
255
+ alias: "locales",
256
+ required: false
257
+ }]
258
+ }],
259
+ locale: [{
260
+ type: i0.Input,
261
+ args: [{
262
+ isSignal: true,
263
+ alias: "locale",
264
+ required: false
265
+ }]
266
+ }],
267
+ signOutDisabled: [{
268
+ type: i0.Input,
269
+ args: [{
270
+ isSignal: true,
271
+ alias: "signOutDisabled",
272
+ required: false
273
+ }]
274
+ }],
275
+ changeOrganization: [{
276
+ type: i0.Output,
277
+ args: ["changeOrganization"]
278
+ }],
279
+ changePassword: [{
280
+ type: i0.Output,
281
+ args: ["changePassword"]
282
+ }],
283
+ localeChange: [{
284
+ type: i0.Output,
285
+ args: ["localeChange"]
286
+ }],
287
+ signOut: [{
288
+ type: i0.Output,
289
+ args: ["signOut"]
290
+ }]
291
+ }
292
+ });
293
+ export { RtProfileMenuComponent };
294
+
295
+ //# sourceMappingURL=rt-tools-ui-kit-v2-profile-menu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rt-tools-ui-kit-v2-profile-menu.mjs","names":[],"sources":["../../../projects/ui-kit-v2/src/lib/components/profile-menu/rt-profile-menu.component.ts","../../../projects/ui-kit-v2/src/lib/components/profile-menu/rt-profile-menu.component.html"],"sourcesContent":["import { BooleanInput } from '@angular/cdk/coercion';\nimport {\n booleanAttribute,\n computed,\n inject,\n input,\n output,\n ChangeDetectionStrategy,\n Component,\n InputSignal,\n InputSignalWithTransform,\n OutputEmitterRef,\n Signal,\n ViewEncapsulation,\n} from '@angular/core';\nimport { FormsModule } from '@angular/forms';\n\nimport { BlockDirective, ElemDirective } from '@rt-tools/core';\n\nimport { IRtIcon, RT_KIT_LABELS, ThemeService, TRtKitLabelMap } from '@rt-tools/ui-kit-v2/core';\n\nimport { RtMenuItemComponent } from '@rt-tools/ui-kit-v2/menu';\nimport { RtSelectComponent } from '@rt-tools/ui-kit-v2/select';\nimport { IRtProfileMenu } from './rt-profile-menu.model';\n\nconst BEM_BLOCK: string = 'rt-profile-menu';\n\n/**\n * Содержимое попапа профиля в шапке `rt-page-header`: кто вошёл и в какой организации, действия с\n * учётной записью, язык и тема.\n *\n * Свою панель меню рисует само — фон, рамку, скругление и тень: поповер кита оформления не даёт,\n * и голые пункты в нём легли бы прямо поверх страницы.\n *\n * Тему переключает `ThemeService`, как у `rt-theme-toggle`: служба одна на страницу, и вход\n * только повторял бы её. Куда ведут остальные действия, решает приложение — они уходят выходами.\n *\n * Стили без инкапсуляции: попап рисуется в перекрытии, у конца `<body>`, и правило по классу блока\n * достаёт до него, а до разметки `rt-menu-item` — тоже.\n */\n@Component({\n selector: 'rt-profile-menu',\n templateUrl: './rt-profile-menu.component.html',\n styleUrls: ['./rt-profile-menu.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n imports: [\n // angular\n FormsModule,\n\n // rt-tools\n BlockDirective,\n ElemDirective,\n\n // standalone components / directives\n RtMenuItemComponent,\n RtSelectComponent,\n ],\n host: {\n class: BEM_BLOCK,\n },\n})\nexport class RtProfileMenuComponent {\n readonly #theme: ThemeService = inject(ThemeService);\n\n /** Подписи кита: сигнал, потому что язык может смениться без перезагрузки. */\n protected readonly t: Signal<TRtKitLabelMap> = inject(RT_KIT_LABELS);\n\n /** Показан значок противоположной темы: нажатие переводит к ней. */\n protected readonly themeIcon: Signal<IRtIcon.Name> = computed((): IRtIcon.Name => (this.#theme.isDark() ? 'ico-sun' : 'moon'));\n\n /** Круг над именем несёт его первую букву: картинки профиля у кита нет. */\n protected readonly avatar: Signal<string> = computed((): string => this.userName().trim().charAt(0).toUpperCase());\n\n public readonly userName: InputSignal<string> = input<string>('');\n\n /** Пустое название — строки организации нет. */\n public readonly organizationName: InputSignal<string> = input<string>('');\n\n public readonly organizationChange: InputSignal<IRtProfileMenu.OrganizationChange> = input<IRtProfileMenu.OrganizationChange>('hidden');\n\n public readonly passwordChange: InputSignalWithTransform<boolean, BooleanInput> = input<boolean, BooleanInput>(false, {\n transform: booleanAttribute,\n });\n\n /** Пустой список — переключателя языка нет. */\n public readonly locales: InputSignal<ReadonlyArray<IRtProfileMenu.Locale>> = input<ReadonlyArray<IRtProfileMenu.Locale>>([]);\n\n public readonly locale: InputSignal<string> = input<string>('');\n\n /** Выход недоступен, пока приложение его уже ведёт. */\n public readonly signOutDisabled: InputSignalWithTransform<boolean, BooleanInput> = input<boolean, BooleanInput>(false, {\n transform: booleanAttribute,\n });\n\n public readonly changeOrganization: OutputEmitterRef<void> = output<void>();\n\n public readonly changePassword: OutputEmitterRef<void> = output<void>();\n\n public readonly localeChange: OutputEmitterRef<string> = output<string>();\n\n public readonly signOut: OutputEmitterRef<void> = output<void>();\n\n public onToggleTheme(): void {\n this.#theme.toggle();\n }\n}\n","<ng-container rtBlock=\"rt-profile-menu\">\n <div rtElem=\"user\">\n <span rtElem=\"avatar\" aria-hidden=\"true\">{{ avatar() }}</span>\n <span rtElem=\"name\" qa-dataid=\"profile-user-name\">{{ userName() }}</span>\n @if (organizationName()) {\n <span rtElem=\"organization\" qa-dataid=\"profile-organization\">\n {{ organizationName() }}\n <span rtElem=\"organization-label\">{{ t().profileMenuOrganization }}</span>\n </span>\n }\n </div>\n\n <div rtElem=\"nav\" role=\"menu\">\n @switch (organizationChange()) {\n @case ('allowed') {\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-change-organization\"\n icon=\"refresh\"\n [label]=\"t().profileMenuChangeOrganization\"\n (selected)=\"changeOrganization.emit()\" />\n }\n @case ('denied') {\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-change-organization\"\n icon=\"refresh\"\n disabled\n [label]=\"t().profileMenuChangeOrganization\"\n [tooltip]=\"t().profileMenuChangeOrganizationDenied\" />\n }\n }\n\n @if (passwordChange()) {\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-password-change\"\n icon=\"ico-lock\"\n [label]=\"t().profileMenuChangePassword\"\n (selected)=\"changePassword.emit()\" />\n }\n\n <rt-menu-item\n rtElem=\"item\"\n qa-dataid=\"profile-logout\"\n icon=\"ico-exit\"\n [disabled]=\"signOutDisabled()\"\n [label]=\"t().profileMenuSignOut\"\n (selected)=\"signOut.emit()\" />\n </div>\n\n <!-- Своего правила у переключателя языка нет: ширину ему задаёт колонка блока. -->\n @if (locales().length) {\n <rt-select\n qa-dataid=\"profile-locale-select\"\n name=\"rtProfileMenuLocale\"\n [options]=\"locales()\"\n [ngModel]=\"locale()\"\n (ngModelChange)=\"localeChange.emit($event)\" />\n }\n\n <rt-menu-item\n rtElem=\"theme\"\n qa-dataid=\"profile-theme-toggle\"\n [icon]=\"themeIcon()\"\n [label]=\"t().profileMenuSwitchTheme\"\n (selected)=\"onToggleTheme()\" />\n</ng-container>\n"],"mappings":";;;;;;;;AAyBA,MAAM,YAAoB;AAqC1B,IAAa,yBAAb,MAAa,uBAAsB;CAtBnC,cAAA;EAuBa,KAAA,SAAuB,OAAO,YAAY;EAGhC,KAAA,IAA4B,OAAO,aAAa;EAGhD,KAAA,YAAkC,eAA8B,KAAK,OAAO,OAAM,IAAK,YAAY,QAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;EAG3G,KAAA,SAAyB,eAAuB,KAAK,SAAQ,CAAE,CAAC,KAAI,CAAE,CAAC,OAAO,CAAC,CAAC,CAAC,YAAW,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;EAElG,KAAA,WAAgC,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;EAGjD,KAAA,mBAAwC,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;EAEzD,KAAA,qBAAqE,MAAyC,UAAS,GAAA,YAAA,CAAA,EAAA,WAAA,qBAAA,CAAA,+BAAA,CAAA,CAAA;EAEvH,KAAA,iBAAkE,MAA6B,OAAK;GAAA,GAAA,YAAA,EAAA,WAAA,iBAAA,+BAAA,CAAA;GAChH,WAAW;EAAgB,CAAA;EAIf,KAAA,UAA6D,MAA4C,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;EAE5G,KAAA,SAA8B,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;EAG/C,KAAA,kBAAmE,MAA6B,OAAK;GAAA,GAAA,YAAA,EAAA,WAAA,kBAAA,+BAAA,CAAA;GACjH,WAAW;EAAgB,CAAA;EAGf,KAAA,qBAA6C,OAAM;EAEnD,KAAA,iBAAyC,OAAM;EAE/C,KAAA,eAAyC,OAAM;EAE/C,KAAA,UAAkC,OAAM;CAK3D;CA3CY;CAwCF,gBAAa;EAChB,KAAK,OAAO,OAAM;CACtB;CA3CS;EAAA,KAAA,OAAA,GAAA,mBAAA;GAAA,YAAA;GAAA,SAAA;GAAA,UAAA;GAAA,MAAA;GAAsB,MAAA,CAAA;GAAA,QAAA,GAAA,gBAAA;EAAA,CAAA;CAAA;CAAtB;EAAA,KAAA,OAAA,GAAA,qBAAA;GAAA,YAAA;GAAA,SAAA;GAAA,MAAA;GAAsB,cAAA;GAAA,UAAA;GAAA,QAAA;IAAA,UAAA;KAAA,mBAAA;KAAA,YAAA;KAAA,UAAA;KAAA,YAAA;KAAA,mBAAA;IAAA;IAAA,kBAAA;KAAA,mBAAA;KAAA,YAAA;KAAA,UAAA;KAAA,YAAA;KAAA,mBAAA;IAAA;IAAA,oBAAA;KAAA,mBAAA;KAAA,YAAA;KAAA,UAAA;KAAA,YAAA;KAAA,mBAAA;IAAA;IAAA,gBAAA;KAAA,mBAAA;KAAA,YAAA;KAAA,UAAA;KAAA,YAAA;KAAA,mBAAA;IAAA;IAAA,SAAA;KAAA,mBAAA;KAAA,YAAA;KAAA,UAAA;KAAA,YAAA;KAAA,mBAAA;IAAA;IAAA,QAAA;KAAA,mBAAA;KAAA,YAAA;KAAA,UAAA;KAAA,YAAA;KAAA,mBAAA;IAAA;IAAA,iBAAA;KAAA,mBAAA;KAAA,YAAA;KAAA,UAAA;KAAA,YAAA;KAAA,mBAAA;IAAA;GAAA;GAAA,SAAA;IAAA,oBAAA;IAAA,gBAAA;IAAA,cAAA;IAAA,SAAA;GAAA;GAAA,MAAA,EAAA,gBAAA,kBAAA;GAAA,UAAA;GAAA,UC9DnC;GAoEA,QAAA,CAAA,y6EAAA;GAAA,cAAA;IAAA;KAAA,MAAA;KAAA,MDpBQ;IAAW;IAAA;KAAA,MAAA;KAAA,MAAA,GAAA;KAAA,UAAA;IAAA;IAAA;KAAA,MAAA;KAAA,MAAA,GAAA;KAAA,UAAA;KAAA,QAAA;MAAA;MAAA;MAAA;MAAA;KAAA;KAAA,SAAA,CAAA,eAAA;KAAA,UAAA,CAAA,SAAA;IAAA;IAAA;KAAA,MAAA;KAAA,MAGX;KAAc,UAAA;KAAA,QAAA,CAAA,OAAA;IAAA;IAAA;KAAA,MAAA;KAAA,MACd;KAAa,UAAA;KAAA,QAAA,CAAA,OAAA;IAAA;IAAA;KAAA,MAAA;KAAA,MAGb;KAAmB,UAAA;KAAA,QAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;KAAA;KAAA,SAAA,CAAA,UAAA;IAAA;IAAA;KAAA,MAAA;KAAA,MACnB;KAAiB,UAAA;KAAA,QAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;MAAA;KAAA;KAAA,SAAA,CAAA,iBAAA;IAAA;GAAA;GAAA,iBAAA,GAAA,wBAAA;GAAA,eAAA,GAAA,kBAAA;EAAA,CAAA;CAAA;;AAMZ,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAsB,YAAA,CAAA;EAtBlC,MAAA;EACa,MAAA,CAAA;GAAA,UAAA;GAAiB,iBAGV,wBAAwB;GAAM,eAChC,kBAAkB;GAAI,SAC5B;IAEL;IAGA;IACA;IAGA;IACA;GACH;GAAA,MACK,EACF,OAAO,UACV;GAAA,UAAA;GAAA,QAAA,CAAA,y6EAAA;EAAA,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rt-tools/ui-kit-v2",
3
- "version": "0.23.4",
3
+ "version": "0.24.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Yauheni Krumin",
6
6
  "type": "module",
@@ -321,6 +321,10 @@
321
321
  "types": "./types/rt-tools-ui-kit-v2-popover.d.ts",
322
322
  "default": "./fesm2022/rt-tools-ui-kit-v2-popover.mjs"
323
323
  },
324
+ "./profile-menu": {
325
+ "types": "./types/rt-tools-ui-kit-v2-profile-menu.d.ts",
326
+ "default": "./fesm2022/rt-tools-ui-kit-v2-profile-menu.mjs"
327
+ },
324
328
  "./prompt-suggestion": {
325
329
  "types": "./types/rt-tools-ui-kit-v2-prompt-suggestion.d.ts",
326
330
  "default": "./fesm2022/rt-tools-ui-kit-v2-prompt-suggestion.mjs"
@@ -10,6 +10,7 @@
10
10
  --rt-bottom-sheet-handle-radius: var(--rt-radius-xs);
11
11
  --rt-bottom-sheet-shadow: var(--rt-shadow-card-soft);
12
12
  --rt-bottom-sheet-transition-duration: var(--rt-duration-base);
13
+ --rt-bottom-sheet-footer-border-width: var(--rt-border-width-thin);
13
14
 
14
15
  position: fixed;
15
16
  z-index: 1000;
@@ -91,7 +92,7 @@
91
92
  flex-wrap: wrap;
92
93
  align-items: center;
93
94
  padding: var(--rt-space-4) var(--rt-space-5) calc(var(--rt-space-4) + env(safe-area-inset-bottom));
94
- border-top: var(--rt-border-width-thin) solid var(--rt-color-border-default);
95
+ border-top: var(--rt-bottom-sheet-footer-border-width) solid var(--rt-color-border-default);
95
96
  gap: var(--rt-space-2);
96
97
 
97
98
  &:empty {
@@ -0,0 +1,32 @@
1
+ # `rt-profile-menu`
2
+
3
+ Содержимое попапа профиля в шапке `rt-page-header`: человек, организация, действия, язык и тема.
4
+
5
+ ```html
6
+ <rt-profile-menu organizationChange="allowed" [userName]="name" [organizationName]="org" (signOut)="signOut()" />
7
+ ```
8
+
9
+ | вход | тип | умолчание |
10
+ | -------------------- | -------------------------------------- | ---------- |
11
+ | `userName` | `string` | `''` |
12
+ | `organizationName` | `string` | `''` |
13
+ | `organizationChange` | `'hidden' \| 'denied' \| 'allowed'` | `'hidden'` |
14
+ | `passwordChange` | `boolean` | `false` |
15
+ | `locales` | `ReadonlyArray<IRtProfileMenu.Locale>` | `[]` |
16
+ | `locale` | `string` | `''` |
17
+ | `signOutDisabled` | `boolean` | `false` |
18
+
19
+ Выходы: `changeOrganization`, `changePassword`, `localeChange` (код языка), `signOut`.
20
+
21
+ ## Главное, что нужно знать
22
+
23
+ **Панель рисует само меню.** У поповера кита оформления нет, и голые пункты меню в попапе шапки
24
+ легли бы прямо поверх страницы. Стили без инкапсуляции: попап живёт в перекрытии у конца `<body>`.
25
+
26
+ **Тему переключает `ThemeService`**, как у `rt-theme-toggle`: значок показывает тему, к которой
27
+ переведёт нажатие.
28
+
29
+ ## Края
30
+
31
+ - Пустое название организации убирает её строку, пустой список языков — переключатель языка.
32
+ - Смена организации в режиме `denied` стоит приглушённой и объясняет причину подсказкой.
@@ -0,0 +1,104 @@
1
+ @layer rt-kit.components {
2
+ /* rt-profile-menu — ViewEncapsulation.None: попап рисуется в перекрытии у конца `<body>`, и
3
+ правило по классу блока достаёт до него. Отсюда же красится круг под значком темы: он
4
+ рисуется разметкой `rt-menu-item`, и заскоупленное правило до чужого шаблона не дотянулось бы.
5
+ Подложку блок берёт у панели навигации кита, а тень своя: узкой панели нужен очерченный край. */
6
+
7
+ .rt-profile-menu {
8
+ display: flex;
9
+ min-width: var(--rt-profile-menu-min-width);
10
+ box-sizing: border-box;
11
+ flex-direction: column;
12
+ padding: var(--rt-profile-menu-padding);
13
+ border: var(--rt-profile-menu-border-width) solid var(--rt-nav-panel-color-border);
14
+ border-radius: var(--rt-profile-menu-radius);
15
+ background-color: var(--rt-nav-panel-color-bg);
16
+ box-shadow: var(--rt-profile-menu-shadow);
17
+ gap: var(--rt-space-2);
18
+
19
+ &__user {
20
+ display: flex;
21
+ flex-direction: column;
22
+ padding-bottom: var(--rt-space-2);
23
+ gap: var(--rt-space-2);
24
+ }
25
+
26
+ &__avatar {
27
+ display: inline-flex;
28
+ width: var(--rt-profile-avatar-size);
29
+ height: var(--rt-profile-avatar-size);
30
+ align-items: center;
31
+ justify-content: center;
32
+ border: var(--rt-profile-avatar-border-width) solid var(--rt-nav-header-color-bg);
33
+ border-radius: var(--rt-profile-avatar-radius);
34
+ color: var(--rt-color-text-primary);
35
+ font-size: var(--rt-profile-avatar-font-size);
36
+ font-weight: var(--rt-font-weight-semibold);
37
+ line-height: var(--rt-leading-none);
38
+ }
39
+
40
+ &__name {
41
+ color: var(--rt-color-text-primary);
42
+ font-size: var(--rt-text-md);
43
+ font-weight: var(--rt-font-weight-semibold);
44
+ line-height: var(--rt-leading-tight);
45
+ }
46
+
47
+ // Название заведения и слово-пояснение стоят в одной строке: без него непонятно, что это за
48
+ // вторая строка под именем.
49
+ &__organization {
50
+ color: var(--rt-nav-panel-item-color);
51
+ font-size: var(--rt-text-md);
52
+ font-weight: var(--rt-font-weight-regular);
53
+ line-height: var(--rt-leading-tight);
54
+ }
55
+
56
+ &__organization-label {
57
+ color: var(--rt-nav-panel-group-color);
58
+ }
59
+
60
+ // Строки выступают за содержимое панели на половину её отступа: подсветка на наведении иначе
61
+ // упирается в рамку. Выступ и горизонтальный отступ строки — одно число, поэтому значок
62
+ // стоит на краю содержимого панели.
63
+ &__nav {
64
+ display: flex;
65
+ width: calc(100% + var(--rt-profile-row-inset) + var(--rt-profile-row-inset));
66
+ flex-direction: column;
67
+ margin-left: calc(-1 * var(--rt-profile-row-inset));
68
+ }
69
+
70
+ // Строки просторнее и крупнее пунктов обычного меню кита: их четыре, и они читаются как
71
+ // список действий, а не как выпадающее меню.
72
+ // Правила строк идут потомком блока: свои умолчания кит объявляет одним классом, и без
73
+ // лишнего веса исход решал бы порядок подключения стилей.
74
+ & &__item,
75
+ & &__theme {
76
+ border-radius: var(--rt-profile-item-radius);
77
+ color: var(--rt-nav-panel-item-color);
78
+ font-size: var(--rt-profile-item-font-size);
79
+ font-weight: var(--rt-profile-item-font-weight);
80
+ gap: var(--rt-profile-item-content-gap);
81
+ }
82
+
83
+ & &__item {
84
+ padding: var(--rt-profile-item-padding-y) var(--rt-profile-row-inset);
85
+ }
86
+
87
+ // Переключатель темы выделен из списка акцентом: значок садится в круг на подложке акцента,
88
+ // подпись красится им же. Круг набирается отступом по content-box — размер самого значка
89
+ // приходит инлайном от `rt-icon`.
90
+ & &__theme {
91
+ width: calc(100% + var(--rt-profile-row-inset) + var(--rt-profile-row-inset));
92
+ padding: var(--rt-profile-row-inset);
93
+ margin-left: calc(-1 * var(--rt-profile-row-inset));
94
+ color: var(--rt-color-action-primary);
95
+
96
+ .rt-menu-item__icon {
97
+ box-sizing: content-box;
98
+ padding: var(--rt-profile-theme-icon-padding);
99
+ border-radius: var(--rt-profile-theme-icon-radius);
100
+ background-color: var(--rt-color-action-primary-subtle);
101
+ }
102
+ }
103
+ }
104
+ }
@@ -399,8 +399,12 @@
399
399
  --rt-profile-menu-min-width: var(--rt-size-60);
400
400
  --rt-profile-menu-padding: var(--rt-space-5);
401
401
  --rt-profile-menu-shadow: var(--rt-shadow-profile-menu);
402
+ --rt-profile-menu-border-width: var(--rt-border-width-thin);
403
+ --rt-profile-menu-radius: var(--rt-radius-md);
402
404
  --rt-profile-avatar-size: var(--rt-size-10);
403
405
  --rt-profile-avatar-font-size: var(--rt-text-2xl);
406
+ --rt-profile-avatar-border-width: var(--rt-border-width-medium);
407
+ --rt-profile-avatar-radius: var(--rt-radius-full);
404
408
  --rt-profile-item-padding-y: var(--rt-space-4);
405
409
  --rt-profile-item-content-gap: var(--rt-space-2);
406
410
  --rt-profile-item-font-size: var(--rt-text-md);
@@ -412,6 +416,7 @@
412
416
  вертикали с содержимым. Это же число — горизонтальный отступ строки. */
413
417
  --rt-profile-row-inset: var(--rt-space-2);
414
418
  --rt-profile-theme-icon-padding: var(--rt-space-2);
419
+ --rt-profile-theme-icon-radius: var(--rt-radius-full);
415
420
 
416
421
  /* Габариты панели второго уровня: ширина считается числом колонок, поэтому
417
422
  колонка, промежуток и внутренний отступ — токены, из которых панель