@rt-tools/ui-kit-v2 0.10.0 → 0.12.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 (166) hide show
  1. package/README.md +110 -5
  2. package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs +947 -0
  3. package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs.map +1 -0
  4. package/fesm2022/rt-tools-ui-kit-v2.mjs +11558 -8313
  5. package/fesm2022/rt-tools-ui-kit-v2.mjs.map +1 -1
  6. package/package.json +8 -1
  7. package/rt-tools-ui-kit-v2-0.12.0.tgz +0 -0
  8. package/src/assets/fonts/montserrat-cyrillic-ext-wght-italic.woff2 +0 -0
  9. package/src/assets/fonts/montserrat-cyrillic-ext-wght-normal.woff2 +0 -0
  10. package/src/assets/fonts/montserrat-cyrillic-wght-italic.woff2 +0 -0
  11. package/src/assets/fonts/montserrat-cyrillic-wght-normal.woff2 +0 -0
  12. package/src/assets/fonts/montserrat-latin-ext-wght-italic.woff2 +0 -0
  13. package/src/assets/fonts/montserrat-latin-ext-wght-normal.woff2 +0 -0
  14. package/src/assets/fonts/montserrat-latin-wght-italic.woff2 +0 -0
  15. package/src/assets/fonts/montserrat-latin-wght-normal.woff2 +0 -0
  16. package/src/assets/icons/arrows.svg +9 -0
  17. package/src/assets/icons/equals.svg +8 -0
  18. package/src/assets/icons/user-off.svg +16 -0
  19. package/src/assets/icons-material/NOTICE.md +6 -2
  20. package/src/assets/icons-material/arrow-left.fill.svg +3 -0
  21. package/src/assets/icons-material/arrow-left.svg +1 -1
  22. package/src/assets/icons-material/arrow-right.fill.svg +3 -0
  23. package/src/assets/icons-material/arrow-right.svg +3 -0
  24. package/src/assets/icons-material/arrows.fill.svg +5 -0
  25. package/src/assets/icons-material/arrows.svg +5 -0
  26. package/src/assets/icons-material/ban.fill.svg +5 -0
  27. package/src/assets/icons-material/ban.svg +1 -1
  28. package/src/assets/icons-material/bars.fill.svg +3 -0
  29. package/src/assets/icons-material/bars.svg +1 -1
  30. package/src/assets/icons-material/check.fill.svg +3 -0
  31. package/src/assets/icons-material/check.svg +1 -1
  32. package/src/assets/icons-material/chevron-down.fill.svg +3 -0
  33. package/src/assets/icons-material/chevron-down.svg +1 -1
  34. package/src/assets/icons-material/chevron-left.fill.svg +3 -0
  35. package/src/assets/icons-material/chevron-left.svg +1 -1
  36. package/src/assets/icons-material/chevron-right.fill.svg +3 -0
  37. package/src/assets/icons-material/chevron-right.svg +1 -1
  38. package/src/assets/icons-material/close.fill.svg +3 -0
  39. package/src/assets/icons-material/close.svg +1 -1
  40. package/src/assets/icons-material/cloud-upload.fill.svg +5 -0
  41. package/src/assets/icons-material/cloud-upload.svg +1 -1
  42. package/src/assets/icons-material/cog.fill.svg +5 -0
  43. package/src/assets/icons-material/cog.svg +1 -1
  44. package/src/assets/icons-material/copy.fill.svg +5 -0
  45. package/src/assets/icons-material/copy.svg +1 -1
  46. package/src/assets/icons-material/ellipsis-h.fill.svg +5 -0
  47. package/src/assets/icons-material/ellipsis-h.svg +1 -1
  48. package/src/assets/icons-material/ellipsis-v.fill.svg +5 -0
  49. package/src/assets/icons-material/ellipsis-v.svg +1 -1
  50. package/src/assets/icons-material/email.fill.svg +5 -0
  51. package/src/assets/icons-material/email.svg +5 -0
  52. package/src/assets/icons-material/equals.fill.svg +3 -0
  53. package/src/assets/icons-material/equals.svg +3 -0
  54. package/src/assets/icons-material/external-link.fill.svg +5 -0
  55. package/src/assets/icons-material/external-link.svg +1 -1
  56. package/src/assets/icons-material/eye-slash.fill.svg +5 -0
  57. package/src/assets/icons-material/eye-slash.svg +1 -1
  58. package/src/assets/icons-material/eye.fill.svg +5 -0
  59. package/src/assets/icons-material/eye.svg +1 -1
  60. package/src/assets/icons-material/ico-download.fill.svg +5 -0
  61. package/src/assets/icons-material/ico-download.svg +1 -1
  62. package/src/assets/icons-material/ico-plus.fill.svg +3 -0
  63. package/src/assets/icons-material/ico-plus.svg +1 -1
  64. package/src/assets/icons-material/info-circle.fill.svg +5 -0
  65. package/src/assets/icons-material/info-circle.svg +1 -1
  66. package/src/assets/icons-material/pencil.fill.svg +5 -0
  67. package/src/assets/icons-material/pencil.svg +1 -1
  68. package/src/assets/icons-material/refresh.fill.svg +5 -0
  69. package/src/assets/icons-material/refresh.svg +1 -1
  70. package/src/assets/icons-material/search.fill.svg +5 -0
  71. package/src/assets/icons-material/search.svg +1 -1
  72. package/src/assets/icons-material/star.fill.svg +3 -0
  73. package/src/assets/icons-material/star.svg +1 -1
  74. package/src/assets/icons-material/sync.fill.svg +5 -0
  75. package/src/assets/icons-material/sync.svg +1 -1
  76. package/src/assets/icons-material/table.fill.svg +5 -0
  77. package/src/assets/icons-material/table.svg +1 -1
  78. package/src/assets/icons-material/tag.fill.svg +5 -0
  79. package/src/assets/icons-material/tag.svg +1 -1
  80. package/src/assets/icons-material/trash.fill.svg +5 -0
  81. package/src/assets/icons-material/trash.svg +1 -1
  82. package/src/assets/icons-material/user-off.fill.svg +5 -0
  83. package/src/assets/icons-material/user-off.svg +5 -0
  84. package/src/assets/icons-material/user.fill.svg +5 -0
  85. package/src/assets/icons-material/user.svg +1 -1
  86. package/src/lib/components/CONTEXT.md +3 -0
  87. package/src/lib/components/action-bar/CONTEXT.md +53 -0
  88. package/src/lib/components/action-bar/rt-action-bar-holder.component.scss +70 -0
  89. package/src/lib/components/action-bar/rt-action-bar.component.scss +122 -0
  90. package/src/lib/components/aside/CONTEXT.md +5 -1
  91. package/src/lib/components/aside/_rt-aside-overlay.scss +9 -0
  92. package/src/lib/components/aside/header/rt-aside-header.component.scss +18 -2
  93. package/src/lib/components/aside/rt-aside.component.scss +5 -1
  94. package/src/lib/components/autocomplete/CONTEXT.md +2 -1
  95. package/src/lib/components/button/CONTEXT.md +11 -11
  96. package/src/lib/components/checkbox/rt-checkbox.component.scss +5 -1
  97. package/src/lib/components/data-list/CONTEXT.md +122 -0
  98. package/src/lib/components/data-list/rt-data-list.component.scss +57 -0
  99. package/src/lib/components/data-list/settings/rt-data-list-settings-aside.component.scss +256 -0
  100. package/src/lib/components/data-list/toolbar/rt-data-list-toolbar.component.scss +128 -0
  101. package/src/lib/components/data-table/CONTEXT.md +102 -0
  102. package/src/lib/components/data-table/cell/rt-data-table-cell.component.scss +115 -0
  103. package/src/lib/components/data-table/filter-cell/rt-data-table-filter-cell.component.scss +51 -0
  104. package/src/lib/components/data-table/header-cell/rt-data-table-header-cell.component.scss +111 -0
  105. package/src/lib/components/data-table/rt-data-table.component.scss +263 -0
  106. package/src/lib/components/date-picker/rt-date-picker.component.scss +13 -0
  107. package/src/lib/components/delta-view/CONTEXT.md +1 -1
  108. package/src/lib/components/dialog/CONTEXT.md +4 -0
  109. package/src/lib/components/dynamic-list/CONTEXT.md +90 -0
  110. package/src/lib/components/dynamic-list/rt-dynamic-list.component.scss +123 -0
  111. package/src/lib/components/empty-state/CONTEXT.md +5 -0
  112. package/src/lib/components/empty-state/rt-empty-state.component.scss +19 -5
  113. package/src/lib/components/file-card/rt-file-card.component.scss +14 -0
  114. package/src/lib/components/file-drop/rt-file-drop.component.scss +44 -8
  115. package/src/lib/components/icon-button/rt-icon-button.component.scss +28 -4
  116. package/src/lib/components/input/rt-input.component.scss +25 -1
  117. package/src/lib/components/input-number/rt-input-number.component.scss +13 -0
  118. package/src/lib/components/menu/CONTEXT.md +10 -0
  119. package/src/lib/components/menu/rt-menu-item.component.scss +35 -3
  120. package/src/lib/components/menu/rt-menu.component.scss +15 -5
  121. package/src/lib/components/multiselect/CONTEXT.md +3 -0
  122. package/src/lib/components/multiselect/rt-multiselect.component.scss +58 -1
  123. package/src/lib/components/pagination/CONTEXT.md +4 -1
  124. package/src/lib/components/pagination/rt-pagination.component.scss +43 -13
  125. package/src/lib/components/radio-button/CONTEXT.md +42 -0
  126. package/src/lib/components/radio-button/rt-radio-button.component.scss +105 -0
  127. package/src/lib/components/scroll-area/CONTEXT.md +52 -0
  128. package/src/lib/components/scroll-area/rt-scroll-area.component.scss +69 -0
  129. package/src/lib/components/select/CONTEXT.md +8 -1
  130. package/src/lib/components/select/rt-select.component.scss +110 -7
  131. package/src/lib/components/table/CONTEXT.md +46 -1
  132. package/src/lib/components/table/filter-header/CONTEXT.md +64 -0
  133. package/src/lib/components/table/filter-header/rt-table-filter-header.component.scss +76 -0
  134. package/src/lib/components/table/rt-table.component.scss +32 -1
  135. package/src/lib/components/tag/CONTEXT.md +9 -0
  136. package/src/lib/components/tag/rt-tag.component.scss +38 -0
  137. package/src/lib/components/textarea/CONTEXT.md +1 -1
  138. package/src/lib/components/thread-list/CONTEXT.md +6 -1
  139. package/src/lib/components/toolbar/CONTEXT.md +3 -0
  140. package/src/lib/components/toolbar/rt-toolbar.component.scss +9 -2
  141. package/src/lib/components/tooltip/CONTEXT.md +4 -4
  142. package/src/lib/components/tooltip/rt-tooltip.component.scss +2 -2
  143. package/src/{lib → rich-editor/lib}/components/chat/CONTEXT.md +1 -1
  144. package/src/{lib → rich-editor/lib}/components/message-composer/CONTEXT.md +5 -1
  145. package/src/{lib → rich-editor/lib}/components/message-composer/rt-message-composer.component.scss +9 -0
  146. package/src/{lib → rich-editor/lib}/components/rich-editor/CONTEXT.md +2 -2
  147. package/src/styles/_coarse-pointer.scss +15 -0
  148. package/src/styles/_coexist.scss +28 -0
  149. package/src/styles/_fonts.scss +99 -0
  150. package/src/styles/_index.scss +13 -0
  151. package/src/styles/_preset-material.scss +201 -26
  152. package/src/styles/_primitives.scss +41 -5
  153. package/src/styles/_scrollbar.scss +16 -12
  154. package/src/styles/_semantic-lists.scss +89 -0
  155. package/src/styles/_semantic.scss +80 -9
  156. package/src/styles/_theme-dark.scss +36 -0
  157. package/src/styles/_theme-scope.scss +111 -0
  158. package/src/styles/tokens.coexist.scss +3 -0
  159. package/src/styles/tokens.package.scss +6 -0
  160. package/styles/tokens.coexist.css +27 -0
  161. package/styles/tokens.css +5443 -43
  162. package/types/rt-tools-ui-kit-v2-rich-editor.d.ts +529 -0
  163. package/types/rt-tools-ui-kit-v2.d.ts +2760 -1381
  164. package/rt-tools-ui-kit-v2-0.10.0.tgz +0 -0
  165. /package/src/{lib → rich-editor/lib}/components/chat/rt-chat.component.scss +0 -0
  166. /package/src/{lib → rich-editor/lib}/components/rich-editor/rt-rich-editor.component.scss +0 -0
@@ -0,0 +1,947 @@
1
+ import * as i0 from '@angular/core';
2
+ import { viewChild, signal, computed, input, output, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component, inject, booleanAttribute, numberAttribute, effect, TemplateRef, Directive, Pipe, contentChild, afterRenderEffect } from '@angular/core';
3
+ import { BlockDirective, ElemDirective, ModDirective } from '@rt-tools/core';
4
+ import { RtFormControlBase, coerceQuillDeltaOutput, rtKitLabel, RT_KIT_LABELS, RtFileCardComponent, RtIconButtonComponent, RT_KIT_LOCALE, RtButtonDirective, RtConfirmDirective, RtDeltaViewComponent, RtFileDropComponent, RtIconComponent, RtMenuComponent, RtNoteComponent, RtSkeletonWrapperComponent, RtSpinnerComponent, RtToolbarComponent, RtToolbarLeftDirective, RtToolbarRightDirective, fileNameFromUrl } from '@rt-tools/ui-kit-v2';
5
+ import { CdkTextareaAutosize } from '@angular/cdk/text-field';
6
+ import { toSignal } from '@angular/core/rxjs-interop';
7
+ import * as i1 from '@angular/forms';
8
+ import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
9
+ import { NgTemplateOutlet, DatePipe } from '@angular/common';
10
+
11
+ const BEM_BLOCK$2 = 'rt-rich-editor';
12
+ /** Модули тулбара — строго whitelist (без link/image/color/script). */
13
+ const TOOLBAR = {
14
+ full: [
15
+ ['bold', 'italic', 'underline', 'strike'],
16
+ [{ header: 1 }, { header: 2 }, { header: 3 }],
17
+ [{ list: 'ordered' }, { list: 'bullet' }],
18
+ ['blockquote', 'code-block'],
19
+ ],
20
+ minimal: [['bold'], [{ list: 'ordered' }, { list: 'bullet' }]],
21
+ };
22
+ /** Форматы, которые Quill вообще примет (вставленное из буфера лишнее вырезается). */
23
+ const FORMATS = {
24
+ full: ['bold', 'italic', 'underline', 'strike', 'header', 'list', 'blockquote', 'code-block'],
25
+ minimal: ['bold', 'list'],
26
+ };
27
+ /**
28
+ * Rich-редактор чата на Quill. Наследует CVA-ядро input-семейства
29
+ * (`RtFormControlBase`) — плагается в reactive-форму. Quill грузится **лениво**
30
+ * (`import("quill")` в `ngAfterViewInit`), тип импортируется `import type` и в
31
+ * бандл не попадает. Значение CVA — `IQuillDelta` (или `null`, когда пусто).
32
+ */
33
+ class RtRichEditorComponent extends RtFormControlBase {
34
+ constructor() {
35
+ super(...arguments);
36
+ // `viewChild` не работает на ES-private (`#`) поле — держим `protected`.
37
+ this.editorHost = viewChild.required('editor', /* @ts-ignore */
38
+ ...(ngDevMode ? [{ debugName: "editorHost" }] : /* istanbul ignore next */ []));
39
+ this.#quill = null;
40
+ /** Значение, пришедшее через writeValue до готовности Quill. */
41
+ this.#pending = null;
42
+ /**
43
+ * Смонтирована ли начинка редактора. Пока нет, хост несёт признак недостроенности: съёмке
44
+ * витрины и любой другой проверке снаружи иначе нечем отличить недостроенный редактор от
45
+ * достроенного пустого — оба выглядят пустым местом.
46
+ */
47
+ this.mounted = signal(false, /* @ts-ignore */
48
+ ...(ngDevMode ? [{ debugName: "mounted" }] : /* istanbul ignore next */ []));
49
+ this.hasValue = computed(() => this.value() !== null, /* @ts-ignore */
50
+ ...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
51
+ this.placeholder = input('', /* @ts-ignore */
52
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
53
+ /** Набор кнопок форматирования; сужает и принимаемые форматы (вставка из буфера). */
54
+ this.toolbar = input('full', /* @ts-ignore */
55
+ ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
56
+ /** Enter без Shift в редакторе — сигнал отправки (Shift+Enter остаётся переносом). */
57
+ this.enterPressed = output();
58
+ this.displayText = computed(() => {
59
+ const delta = this.value();
60
+ if (delta === null) {
61
+ return '';
62
+ }
63
+ return delta.ops.map((op) => op.insert).join('');
64
+ }, /* @ts-ignore */
65
+ ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
66
+ }
67
+ #quill;
68
+ /** Значение, пришедшее через writeValue до готовности Quill. */
69
+ #pending;
70
+ async ngAfterViewInit() {
71
+ const QuillCtor = (await import('quill')).default;
72
+ const quill = new QuillCtor(this.editorHost().nativeElement, {
73
+ theme: 'snow',
74
+ placeholder: this.placeholder(),
75
+ formats: [...FORMATS[this.toolbar()]],
76
+ modules: {
77
+ toolbar: [...TOOLBAR[this.toolbar()]],
78
+ keyboard: {
79
+ bindings: {
80
+ // Enter без Shift — отправка (перенос гасим возвратом false);
81
+ // Shift+Enter не совпадает с биндингом и уходит в дефолт (перенос).
82
+ enterSubmit: {
83
+ key: 'Enter',
84
+ shiftKey: false,
85
+ handler: () => {
86
+ this.enterPressed.emit();
87
+ return false;
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ });
94
+ this.#quill = quill;
95
+ if (this.#pending !== null) {
96
+ quill.setContents(this.#pending);
97
+ this.#pending = null;
98
+ }
99
+ if (this.isDisabled()) {
100
+ quill.disable();
101
+ }
102
+ quill.on('text-change', () => this.#onEditorChange());
103
+ this.mounted.set(true);
104
+ }
105
+ ngOnDestroy() {
106
+ this.#quill = null;
107
+ this.mounted.set(false);
108
+ }
109
+ writeValue(value) {
110
+ super.writeValue(value);
111
+ if (this.#quill === null) {
112
+ this.#pending = value;
113
+ return;
114
+ }
115
+ this.#quill.setContents((value ?? { ops: [] }));
116
+ }
117
+ setDisabledState(isDisabled) {
118
+ super.setDisabledState(isDisabled);
119
+ if (this.#quill === null) {
120
+ return;
121
+ }
122
+ if (isDisabled) {
123
+ this.#quill.disable();
124
+ return;
125
+ }
126
+ this.#quill.enable();
127
+ }
128
+ getEmptyValue() {
129
+ return null;
130
+ }
131
+ focusAfterClear() {
132
+ this.#quill?.focus();
133
+ }
134
+ /** Text-change: пустой слепок → null-канал; иначе delta. */
135
+ #onEditorChange() {
136
+ if (this.#quill === null) {
137
+ return;
138
+ }
139
+ const blank = this.#quill.getText().trim().length === 0;
140
+ const next = blank
141
+ ? null
142
+ : coerceQuillDeltaOutput({
143
+ ops: this.#quill.getContents().ops,
144
+ });
145
+ this.value.set(next);
146
+ this.emitChange(next);
147
+ this.markTouched();
148
+ }
149
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtRichEditorComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
150
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.6", type: RtRichEditorComponent, isStandalone: true, selector: "rt-rich-editor", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { enterPressed: "enterPressed" }, host: { properties: { "attr.data-rt-pending": "mounted() ? null : \"\"" }, classAttribute: "rt-rich-editor" }, providers: [
151
+ // Алиас базового токена — чтобы rt-field мог найти контрол через
152
+ // contentChild(RtFormControlBase) (query по абстрактному классу).
153
+ { provide: RtFormControlBase, useExisting: forwardRef(() => RtRichEditorComponent) },
154
+ ], viewQueries: [{ propertyName: "editorHost", first: true, predicate: ["editor"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<ng-container rtBlock=\"rt-rich-editor\">\n <div #editor rtElem=\"surface\" qa-dataid=\"rich-editor-surface\"></div>\n</ng-container>\n", styles: ["@layer rt-kit.components{.rt-rich-editor{--rt-rich-editor-radius: var(--rt-radius-md);display:flex;flex-direction:column;min-block-size:0}.rt-rich-editor .ql-toolbar.ql-snow,.rt-rich-editor .ql-container.ql-snow{border-color:var(--rt-color-border-default)}.rt-rich-editor .ql-toolbar.ql-snow{flex:0 0 auto;border-start-end-radius:var(--rt-rich-editor-radius);border-start-start-radius:var(--rt-rich-editor-radius)}.rt-rich-editor .ql-container.ql-snow{flex:1 1 auto;border-end-end-radius:var(--rt-rich-editor-radius);border-end-start-radius:var(--rt-rich-editor-radius);font-family:inherit;font-size:inherit;min-block-size:0;overflow-y:auto}.rt-rich-editor .ql-editor{background-color:var(--rt-color-bg-surface-subtle);color:var(--rt-color-text-primary);min-block-size:96px}.rt-rich-editor .ql-editor.ql-blank:before{color:var(--rt-color-text-muted);font-style:normal}.rt-rich-editor .ql-snow .ql-stroke{stroke:var(--rt-color-text-muted)}.rt-rich-editor .ql-snow .ql-fill{fill:var(--rt-color-text-muted)}.rt-rich-editor .ql-snow .ql-picker{color:var(--rt-color-text-muted)}.rt-rich-editor .ql-snow .ql-toolbar button:hover .ql-stroke,.rt-rich-editor .ql-snow button.ql-active .ql-stroke{stroke:var(--rt-color-text-link)}.rt-rich-editor .ql-snow .ql-toolbar button:hover .ql-fill,.rt-rich-editor .ql-snow button.ql-active .ql-fill{fill:var(--rt-color-text-link)}}\n"], dependencies: [{ kind: "directive", type:
155
+ // standalone components / directives
156
+ BlockDirective, selector: "[rtBlock]", inputs: ["rtMod"] }, { kind: "directive", type: ElemDirective, selector: "[rtElem]", inputs: ["rtMod"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
157
+ }
158
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtRichEditorComponent, decorators: [{
159
+ type: Component,
160
+ args: [{ selector: 'rt-rich-editor', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
161
+ // standalone components / directives
162
+ BlockDirective,
163
+ ElemDirective,
164
+ ], providers: [
165
+ // Алиас базового токена — чтобы rt-field мог найти контрол через
166
+ // contentChild(RtFormControlBase) (query по абстрактному классу).
167
+ { provide: RtFormControlBase, useExisting: forwardRef(() => RtRichEditorComponent) },
168
+ ], host: {
169
+ class: BEM_BLOCK$2,
170
+ // Пока начинка редактора не смонтирована, хост несёт признак недостроенности: снаружи
171
+ // недостроенный редактор ничем не отличается от достроенного пустого, и съёмка витрины
172
+ // ловила именно этот промежуток — кадр выходил втрое ниже эталона.
173
+ '[attr.data-rt-pending]': 'mounted() ? null : ""',
174
+ }, template: "<ng-container rtBlock=\"rt-rich-editor\">\n <div #editor rtElem=\"surface\" qa-dataid=\"rich-editor-surface\"></div>\n</ng-container>\n", styles: ["@layer rt-kit.components{.rt-rich-editor{--rt-rich-editor-radius: var(--rt-radius-md);display:flex;flex-direction:column;min-block-size:0}.rt-rich-editor .ql-toolbar.ql-snow,.rt-rich-editor .ql-container.ql-snow{border-color:var(--rt-color-border-default)}.rt-rich-editor .ql-toolbar.ql-snow{flex:0 0 auto;border-start-end-radius:var(--rt-rich-editor-radius);border-start-start-radius:var(--rt-rich-editor-radius)}.rt-rich-editor .ql-container.ql-snow{flex:1 1 auto;border-end-end-radius:var(--rt-rich-editor-radius);border-end-start-radius:var(--rt-rich-editor-radius);font-family:inherit;font-size:inherit;min-block-size:0;overflow-y:auto}.rt-rich-editor .ql-editor{background-color:var(--rt-color-bg-surface-subtle);color:var(--rt-color-text-primary);min-block-size:96px}.rt-rich-editor .ql-editor.ql-blank:before{color:var(--rt-color-text-muted);font-style:normal}.rt-rich-editor .ql-snow .ql-stroke{stroke:var(--rt-color-text-muted)}.rt-rich-editor .ql-snow .ql-fill{fill:var(--rt-color-text-muted)}.rt-rich-editor .ql-snow .ql-picker{color:var(--rt-color-text-muted)}.rt-rich-editor .ql-snow .ql-toolbar button:hover .ql-stroke,.rt-rich-editor .ql-snow button.ql-active .ql-stroke{stroke:var(--rt-color-text-link)}.rt-rich-editor .ql-snow .ql-toolbar button:hover .ql-fill,.rt-rich-editor .ql-snow button.ql-active .ql-fill{fill:var(--rt-color-text-link)}}\n"] }]
175
+ }], propDecorators: { editorHost: [{ type: i0.ViewChild, args: ['editor', { isSignal: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], enterPressed: [{ type: i0.Output, args: ["enterPressed"] }] } });
176
+
177
+ const BEM_BLOCK$1 = 'rt-message-composer';
178
+ /**
179
+ * Композер сообщения: авто-растущая textarea сверху, под разделительной линией —
180
+ * ряд контролов (слева — прикрепить файл, справа — отправить). Презентационный,
181
+ * без data-access: наружу торчит только `submitted` с текстом и файлами.
182
+ *
183
+ * Отправка — `Enter` (без Shift) или клик по иконке; `Shift+Enter` — перенос
184
+ * строки. Кнопка отправки активна, пока есть непустой текст или вложения.
185
+ */
186
+ class RtMessageComposerComponent {
187
+ #formValue;
188
+ #t_chatPlaceholder;
189
+ constructor() {
190
+ this.#t_chatPlaceholder = rtKitLabel('chatPlaceholder');
191
+ this.t = inject(RT_KIT_LABELS);
192
+ this.placeholderText = computed(() => this.placeholder() || this.#t_chatPlaceholder(), /* @ts-ignore */
193
+ ...(ngDevMode ? [{ debugName: "placeholderText" }] : /* istanbul ignore next */ []));
194
+ this.fileInput = viewChild('fileEl', /* @ts-ignore */
195
+ ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
196
+ this.form = new FormGroup({
197
+ message: new FormControl('', { nonNullable: true }),
198
+ files: new FormControl([], { nonNullable: true }),
199
+ delta: new FormControl(null),
200
+ });
201
+ this.files = computed(() => this.#formValue().files ?? [], /* @ts-ignore */
202
+ ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
203
+ /** Отправка доступна: есть контент (текст/delta/файлы), композер не занят. */
204
+ this.canSend = computed(() => {
205
+ if (this.sending() || this.disabled()) {
206
+ return false;
207
+ }
208
+ const value = this.#formValue();
209
+ const hasFiles = this.attachments() && (value.files?.length ?? 0) > 0;
210
+ if (this.formatting()) {
211
+ return (value.delta ?? null) !== null || hasFiles;
212
+ }
213
+ const hasText = (value.message ?? '').trim().length > 0;
214
+ return hasText || hasFiles;
215
+ }, /* @ts-ignore */
216
+ ...(ngDevMode ? [{ debugName: "canSend" }] : /* istanbul ignore next */ []));
217
+ /** Текст-подсказка textarea. Пусто — берётся переведённое умолчание. */
218
+ this.placeholder = input('', /* @ts-ignore */
219
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
220
+ /** Фильтр типов для file-input (`.pdf,.doc,...`). */
221
+ this.accept = input('', /* @ts-ignore */
222
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
223
+ /** Показывать кнопку вложения и список файлов. */
224
+ this.attachments = input(false, { ...(ngDevMode ? { debugName: "attachments" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
225
+ /** Отправка в процессе — блокирует submit и показывает её недоступность. */
226
+ this.sending = input(false, { ...(ngDevMode ? { debugName: "sending" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
227
+ /** Полная блокировка композера. */
228
+ this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
229
+ /** Rich-режим: вместо textarea — Quill-редактор, отправка уходит delta-каналом. */
230
+ this.formatting = input(false, { ...(ngDevMode ? { debugName: "formatting" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
231
+ /** Набор кнопок rich-редактора (проброс в `rt-rich-editor`). */
232
+ this.toolbar = input('full', /* @ts-ignore */
233
+ ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
234
+ /** Минимальная высота textarea в строках. */
235
+ this.minRows = input(1, { ...(ngDevMode ? { debugName: "minRows" } : /* istanbul ignore next */ {}), transform: numberAttribute });
236
+ /** Максимальная высота textarea в строках (дальше — скролл). */
237
+ this.maxRows = input(6, { ...(ngDevMode ? { debugName: "maxRows" } : /* istanbul ignore next */ {}), transform: numberAttribute });
238
+ /**
239
+ * Файлы, выброшенные drag-n-drop'ом в обёртку чата: пробрасываются сюда как
240
+ * вложения. Каждый физический drop даёт новый массив, поэтому effect
241
+ * перезапускается на каждое перетаскивание; файлы добавляются к уже выбранным.
242
+ */
243
+ this.droppedFiles = input(null, /* @ts-ignore */
244
+ ...(ngDevMode ? [{ debugName: "droppedFiles" }] : /* istanbul ignore next */ []));
245
+ /** Отправка сообщения: текст + файлы (последние — только при `attachments`). */
246
+ this.submitted = output();
247
+ this.#formValue = toSignal(this.form.valueChanges, {
248
+ initialValue: this.form.value,
249
+ });
250
+ effect(() => {
251
+ const blocked = this.disabled() || this.sending();
252
+ if (blocked && this.form.enabled) {
253
+ this.form.disable({ emitEvent: false });
254
+ }
255
+ if (!blocked && this.form.disabled) {
256
+ this.form.enable({ emitEvent: false });
257
+ }
258
+ });
259
+ effect(() => {
260
+ const dropped = this.droppedFiles();
261
+ if (dropped !== null && dropped.length > 0 && this.attachments()) {
262
+ this.form.controls.files.setValue([...this.form.controls.files.value, ...dropped]);
263
+ }
264
+ });
265
+ }
266
+ onKeydown(event) {
267
+ // Enter без модификаторов — отправка; Shift+Enter оставляем переносом строки.
268
+ // isComposing — не перехватываем подтверждение IME-композиции.
269
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
270
+ event.preventDefault();
271
+ this.submit();
272
+ }
273
+ }
274
+ openPicker() {
275
+ if (this.disabled() || this.sending()) {
276
+ return;
277
+ }
278
+ this.fileInput()?.nativeElement.click();
279
+ }
280
+ onFilesPicked(event) {
281
+ const el = event.target;
282
+ const picked = el.files ? Array.from(el.files) : [];
283
+ if (picked.length) {
284
+ this.form.controls.files.setValue([...this.form.controls.files.value, ...picked]);
285
+ }
286
+ // Сброс value — чтобы повторный выбор того же файла снова дал change-событие.
287
+ el.value = '';
288
+ }
289
+ removeFile(index) {
290
+ const next = this.form.controls.files.value.filter((_, i) => i !== index);
291
+ this.form.controls.files.setValue(next);
292
+ }
293
+ submit() {
294
+ if (!this.canSend()) {
295
+ return;
296
+ }
297
+ const files = this.attachments() ? this.form.controls.files.value : [];
298
+ if (this.formatting()) {
299
+ const delta = this.form.controls.delta.value;
300
+ this.submitted.emit({
301
+ text: '',
302
+ delta: delta === null ? null : JSON.stringify(delta),
303
+ files,
304
+ });
305
+ }
306
+ else {
307
+ const text = this.form.controls.message.value.trim();
308
+ this.submitted.emit({ text, files });
309
+ }
310
+ this.form.reset({ message: '', files: [], delta: null });
311
+ }
312
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtMessageComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
313
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: RtMessageComposerComponent, isStandalone: true, selector: "rt-message-composer", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, sending: { classPropertyName: "sending", publicName: "sending", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, formatting: { classPropertyName: "formatting", publicName: "formatting", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, minRows: { classPropertyName: "minRows", publicName: "minRows", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null }, droppedFiles: { classPropertyName: "droppedFiles", publicName: "droppedFiles", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitted: "submitted" }, host: { classAttribute: "rt-message-composer" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container rtBlock=\"rt-message-composer\" [formGroup]=\"form\">\n @if (formatting()) {\n <rt-rich-editor\n qa-dataid=\"message-composer-rich\"\n formControlName=\"delta\"\n [placeholder]=\"placeholderText()\"\n [toolbar]=\"toolbar()\"\n (enterPressed)=\"submit()\" />\n } @else {\n <textarea\n rtElem=\"input\"\n qa-dataid=\"message-composer-input\"\n cdkTextareaAutosize\n formControlName=\"message\"\n [attr.aria-label]=\"t().chatComposerAria\"\n [placeholder]=\"placeholderText()\"\n [cdkAutosizeMinRows]=\"minRows()\"\n [cdkAutosizeMaxRows]=\"maxRows()\"\n (keydown)=\"onKeydown($event)\"></textarea>\n }\n\n @if (attachments() && files().length) {\n <div rtElem=\"files\" qa-dataid=\"message-composer-files\">\n @for (file of files(); track $index) {\n <rt-file-card\n showRemove\n qa-dataid=\"message-composer-file\"\n size=\"sm\"\n [name]=\"file.name\"\n [sizeBytes]=\"file.size\"\n [disabled]=\"disabled() || sending()\"\n (removed)=\"removeFile($index)\" />\n }\n </div>\n }\n\n <div rtElem=\"divider\"></div>\n\n <div rtElem=\"controls\">\n @if (attachments()) {\n <input\n #fileEl\n qa-dataid=\"message-composer-file-input\"\n type=\"file\"\n hidden\n multiple\n [attr.accept]=\"accept() || null\"\n (change)=\"onFilesPicked($event)\" />\n <rt-icon-button\n qa-dataid=\"message-composer-attach\"\n icon=\"paperclip\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatAttachAria\"\n [tooltip]=\"t().chatAttachTooltip\"\n [disabled]=\"disabled() || sending()\"\n (clicked)=\"openPicker()\" />\n }\n\n <rt-icon-button\n rtElem=\"send\"\n qa-dataid=\"message-composer-send\"\n icon=\"send\"\n variant=\"primary\"\n [ariaLabel]=\"t().chatSendAria\"\n [tooltip]=\"t().chatSendLabel\"\n [disabled]=\"!canSend()\"\n (clicked)=\"submit()\" />\n </div>\n</ng-container>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-message-composer{--rt-message-composer-radius: var(--rt-radius-md);--rt-message-composer-border-width: var(--rt-border-width-thin);--rt-message-composer-divider-width: var(--rt-border-width-thin);display:flex;flex-direction:column;padding:var(--rt-space-4);border:var(--rt-message-composer-border-width) solid var(--rt-color-border-default);border-radius:var(--rt-message-composer-radius);background:var(--rt-color-bg-surface);gap:var(--rt-space-2);inline-size:100%}.rt-message-composer:focus-within{border-color:var(--rt-color-border-strong)}.rt-message-composer__input{padding:0;border:0;margin:0;background:transparent;color:inherit;font:inherit;font-size:var(--rt-text-sm);inline-size:100%;line-height:var(--rt-leading-snug);min-inline-size:0;outline:0;resize:none}.rt-message-composer__input::placeholder{color:var(--rt-color-text-muted)}.rt-message-composer__input:disabled{cursor:not-allowed}.rt-message-composer__files{display:flex;flex-direction:column;gap:var(--rt-space-1)}.rt-message-composer__divider{background:var(--rt-color-border-default);block-size:var(--rt-message-composer-divider-width);margin-inline:calc(-1 * var(--rt-space-4))}.rt-message-composer__controls{display:flex;align-items:center;gap:var(--rt-space-2)}.rt-message-composer__send{margin-inline-start:auto}}\n"], dependencies: [{ kind: "ngmodule", type:
314
+ // angular
315
+ ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type:
316
+ // cdk
317
+ CdkTextareaAutosize, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMinRows", "cdkAutosizeMaxRows", "cdkTextareaAutosize", "placeholder"], exportAs: ["cdkTextareaAutosize"] }, { kind: "component", type:
318
+ // standalone components / directives
319
+ RtFileCardComponent, selector: "rt-file-card", inputs: ["name", "sizeBytes", "size", "showDownload", "showRemove", "showRename", "disabled"], outputs: ["download", "removed", "rename"] }, { kind: "component", type: RtIconButtonComponent, selector: "rt-icon-button", inputs: ["icon", "ariaLabel", "variant", "iconColor", "iconFill", "size", "iconSize", "shape", "type", "tooltip", "tabIndex", "loading", "disabled", "active", "indicator"], outputs: ["clicked"] }, { kind: "component", type: RtRichEditorComponent, selector: "rt-rich-editor", inputs: ["placeholder", "toolbar"], outputs: ["enterPressed"] }, { kind: "directive", type: BlockDirective, selector: "[rtBlock]", inputs: ["rtMod"] }, { kind: "directive", type: ElemDirective, selector: "[rtElem]", inputs: ["rtMod"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
320
+ }
321
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtMessageComposerComponent, decorators: [{
322
+ type: Component,
323
+ args: [{ selector: 'rt-message-composer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
324
+ // angular
325
+ ReactiveFormsModule,
326
+ // cdk
327
+ CdkTextareaAutosize,
328
+ // standalone components / directives
329
+ RtFileCardComponent,
330
+ RtIconButtonComponent,
331
+ RtRichEditorComponent,
332
+ BlockDirective,
333
+ ElemDirective,
334
+ ], host: {
335
+ class: BEM_BLOCK$1,
336
+ }, template: "<ng-container rtBlock=\"rt-message-composer\" [formGroup]=\"form\">\n @if (formatting()) {\n <rt-rich-editor\n qa-dataid=\"message-composer-rich\"\n formControlName=\"delta\"\n [placeholder]=\"placeholderText()\"\n [toolbar]=\"toolbar()\"\n (enterPressed)=\"submit()\" />\n } @else {\n <textarea\n rtElem=\"input\"\n qa-dataid=\"message-composer-input\"\n cdkTextareaAutosize\n formControlName=\"message\"\n [attr.aria-label]=\"t().chatComposerAria\"\n [placeholder]=\"placeholderText()\"\n [cdkAutosizeMinRows]=\"minRows()\"\n [cdkAutosizeMaxRows]=\"maxRows()\"\n (keydown)=\"onKeydown($event)\"></textarea>\n }\n\n @if (attachments() && files().length) {\n <div rtElem=\"files\" qa-dataid=\"message-composer-files\">\n @for (file of files(); track $index) {\n <rt-file-card\n showRemove\n qa-dataid=\"message-composer-file\"\n size=\"sm\"\n [name]=\"file.name\"\n [sizeBytes]=\"file.size\"\n [disabled]=\"disabled() || sending()\"\n (removed)=\"removeFile($index)\" />\n }\n </div>\n }\n\n <div rtElem=\"divider\"></div>\n\n <div rtElem=\"controls\">\n @if (attachments()) {\n <input\n #fileEl\n qa-dataid=\"message-composer-file-input\"\n type=\"file\"\n hidden\n multiple\n [attr.accept]=\"accept() || null\"\n (change)=\"onFilesPicked($event)\" />\n <rt-icon-button\n qa-dataid=\"message-composer-attach\"\n icon=\"paperclip\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatAttachAria\"\n [tooltip]=\"t().chatAttachTooltip\"\n [disabled]=\"disabled() || sending()\"\n (clicked)=\"openPicker()\" />\n }\n\n <rt-icon-button\n rtElem=\"send\"\n qa-dataid=\"message-composer-send\"\n icon=\"send\"\n variant=\"primary\"\n [ariaLabel]=\"t().chatSendAria\"\n [tooltip]=\"t().chatSendLabel\"\n [disabled]=\"!canSend()\"\n (clicked)=\"submit()\" />\n </div>\n</ng-container>\n", styles: ["@charset \"UTF-8\";@layer rt-kit.components{.rt-message-composer{--rt-message-composer-radius: var(--rt-radius-md);--rt-message-composer-border-width: var(--rt-border-width-thin);--rt-message-composer-divider-width: var(--rt-border-width-thin);display:flex;flex-direction:column;padding:var(--rt-space-4);border:var(--rt-message-composer-border-width) solid var(--rt-color-border-default);border-radius:var(--rt-message-composer-radius);background:var(--rt-color-bg-surface);gap:var(--rt-space-2);inline-size:100%}.rt-message-composer:focus-within{border-color:var(--rt-color-border-strong)}.rt-message-composer__input{padding:0;border:0;margin:0;background:transparent;color:inherit;font:inherit;font-size:var(--rt-text-sm);inline-size:100%;line-height:var(--rt-leading-snug);min-inline-size:0;outline:0;resize:none}.rt-message-composer__input::placeholder{color:var(--rt-color-text-muted)}.rt-message-composer__input:disabled{cursor:not-allowed}.rt-message-composer__files{display:flex;flex-direction:column;gap:var(--rt-space-1)}.rt-message-composer__divider{background:var(--rt-color-border-default);block-size:var(--rt-message-composer-divider-width);margin-inline:calc(-1 * var(--rt-space-4))}.rt-message-composer__controls{display:flex;align-items:center;gap:var(--rt-space-2)}.rt-message-composer__send{margin-inline-start:auto}}\n"] }]
337
+ }], ctorParameters: () => [], propDecorators: { fileInput: [{ type: i0.ViewChild, args: ['fileEl', { isSignal: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], attachments: [{ type: i0.Input, args: [{ isSignal: true, alias: "attachments", required: false }] }], sending: [{ type: i0.Input, args: [{ isSignal: true, alias: "sending", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], formatting: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatting", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], minRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "minRows", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], droppedFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "droppedFiles", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }] } });
338
+
339
+ /**
340
+ * Захватывает `<ng-template rtChatMessageActions let-message>` — содержимое «…»-меню
341
+ * действий у реплики. `rt-chat` рисует этот шаблон рядом с точечными действиями реплики,
342
+ * прокидывая её через `$implicit`.
343
+ *
344
+ * Точечные действия остаются на месте: удаление, повтор отправки и скачивание общи для
345
+ * любой переписки. Шаблон нужен тем действиям, которых кит не знает и знать не должен, —
346
+ * без него потребителю оставалось ждать правки кита либо ставить кнопку над диалогом, то
347
+ * есть не там, где читают реплику.
348
+ *
349
+ * Дженерика у контекста нет: тип реплики у переписки один, и type-carrier, который у
350
+ * строки таблицы несёт тип строки, здесь не нужен.
351
+ *
352
+ * @example
353
+ * ```html
354
+ * <rt-chat [messages]="messages()" [messageHasActions]="canAct">
355
+ * <ng-template rtChatMessageActions let-message>
356
+ * <rt-menu-item icon="link" label="Скопировать ссылку" (selected)="copy(message)" />
357
+ * </ng-template>
358
+ * </rt-chat>
359
+ * ```
360
+ */
361
+ class RtChatMessageActionsDirective {
362
+ constructor() {
363
+ this.template = inject(TemplateRef);
364
+ }
365
+ /** Type-guard: сужает контекст шаблона до реплики при проверке шаблонов. */
366
+ static ngTemplateContextGuard(_directive,
367
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- второй довод стража контекста шаблона стоит только в типе-предикате; убрать его нечем, подпись задаёт каркас
368
+ context) {
369
+ return true;
370
+ }
371
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtChatMessageActionsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
372
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: RtChatMessageActionsDirective, isStandalone: true, selector: "ng-template[rtChatMessageActions]", ngImport: i0 }); }
373
+ }
374
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtChatMessageActionsDirective, decorators: [{
375
+ type: Directive,
376
+ args: [{
377
+ selector: 'ng-template[rtChatMessageActions]',
378
+ }]
379
+ }] });
380
+
381
+ /**
382
+ * Есть ли у реплики хотя бы одно доступное действие потребителя.
383
+ *
384
+ * Признак приходит предикатом от экрана, а не подсчётом отрисованных пунктов меню:
385
+ * содержимое спроецированного шаблона известно только после отрисовки, и кнопка
386
+ * успевала бы мигнуть на первом кадре.
387
+ *
388
+ * Предикат не задан — у реплики есть действия: экран, который ничего не гейтит,
389
+ * показывает кнопку меню. Так же это устроено у строки таблицы, и расхождение между
390
+ * двумя точками проекции одного кита читалось бы дефектом.
391
+ */
392
+ function messageHasAvailableActions(message, hasActions) {
393
+ return hasActions === null ? true : hasActions(message);
394
+ }
395
+
396
+ /**
397
+ * Есть ли у реплики доступные действия — гейт кнопки «…» у неё.
398
+ *
399
+ * Pure-pipe: пересчитывается только при смене реплики или предиката, а не на каждом
400
+ * цикле проверки. Значение приходит из контекста шаблона (реплика треда), поэтому
401
+ * `computed()` здесь неприменим.
402
+ */
403
+ class RtChatMessageHasActionsPipe {
404
+ transform(message, hasActions) {
405
+ return messageHasAvailableActions(message, hasActions);
406
+ }
407
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtChatMessageHasActionsPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
408
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.6", ngImport: i0, type: RtChatMessageHasActionsPipe, isStandalone: true, name: "rtChatMessageHasActions" }); }
409
+ }
410
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtChatMessageHasActionsPipe, decorators: [{
411
+ type: Pipe,
412
+ args: [{ name: 'rtChatMessageHasActions' }]
413
+ }] });
414
+
415
+ /**
416
+ * Модель общего чат-компонента `rt-chat`: один корневой неймспейс
417
+ * с префиксом `I`. Доменные стора маппят свою модель сообщения в `Message`, а сам
418
+ * `rt-chat` ничего не знает о доменных сущностях — только рисует тред и композер.
419
+ */
420
+ /**
421
+ * Судьба своего сообщения в ленте: пока летит на сервер — часы, после записи —
422
+ * двойной чек, после того как собеседник открыл переписку — тот же чек синим,
423
+ * при обрыве — «!» с повтором. Входящие сообщения статуса не имеют.
424
+ */
425
+ var ERtChatMessageStatus;
426
+ (function (ERtChatMessageStatus) {
427
+ ERtChatMessageStatus["Sending"] = "sending";
428
+ ERtChatMessageStatus["Sent"] = "sent";
429
+ ERtChatMessageStatus["Read"] = "read";
430
+ ERtChatMessageStatus["Failed"] = "failed";
431
+ })(ERtChatMessageStatus || (ERtChatMessageStatus = {}));
432
+
433
+ /**
434
+ * Ключи подписей статусов для скринридера: сам индикатор — иконка, и без текста
435
+ * различие «доставлено/прочитано» слышно не будет.
436
+ *
437
+ * Пайп отдаёт ключ, а переводит его `transloco` следующим звеном цепочки.
438
+ * Готовый текст здесь застыл бы на языке, активном в момент загрузки модуля.
439
+ */
440
+ const STATUS_KEYS = {
441
+ [ERtChatMessageStatus.Sending]: 'rtKit.chatStatusSending',
442
+ [ERtChatMessageStatus.Sent]: 'rtKit.chatStatusSent',
443
+ [ERtChatMessageStatus.Read]: 'rtKit.chatStatusRead',
444
+ [ERtChatMessageStatus.Failed]: 'rtKit.chatStatusFailed',
445
+ };
446
+ /** Ключ текстовой расшифровки иконки статуса сообщения. */
447
+ class RtChatStatusLabelPipe {
448
+ transform(status) {
449
+ return STATUS_KEYS[status];
450
+ }
451
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtChatStatusLabelPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
452
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.1.6", ngImport: i0, type: RtChatStatusLabelPipe, isStandalone: true, name: "rtChatStatusLabel" }); }
453
+ }
454
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtChatStatusLabelPipe, decorators: [{
455
+ type: Pipe,
456
+ args: [{ name: 'rtChatStatusLabel' }]
457
+ }] });
458
+
459
+ /** Выбранные файлы вместе с только что добавленными. Список всегда новый: его читает сигнал. */
460
+ function withAppendedFiles(current, added) {
461
+ return [...current, ...added];
462
+ }
463
+ /** Выбранные файлы без того, что стоит на этом месте. */
464
+ function withoutFileAt(current, index) {
465
+ return current.filter((_, position) => position !== index);
466
+ }
467
+ /** Файлы, названные полем выбора. Поле без выбора отдаёт пустой список, а не отсутствие. */
468
+ function pickedFiles(files) {
469
+ return Array.from(files ?? []);
470
+ }
471
+
472
+ /** Зазор от низа треда, в пределах которого читатель считается стоящим у нижнего края. */
473
+ const RT_CHAT_NEAR_BOTTOM_THRESHOLD_PX = 64;
474
+ /**
475
+ * Задержки повторных подтягиваний ленты к низу после первой прокрутки. Присланное
476
+ * содержимое дорисовывается несколькими проходами уже после того, как лента встала по
477
+ * промежуточной высоте, и набор тиков покрывает окно, за которое высота устаканивается.
478
+ */
479
+ const RT_CHAT_PIN_RETRY_DELAYS_MS = [50, 150, 300, 500, 800];
480
+ /** Стоит ли читатель у нижнего края треда. */
481
+ function isNearBottom(metrics) {
482
+ return metrics.scrollHeight - metrics.clientHeight - metrics.scrollTop <= RT_CHAT_NEAR_BOTTOM_THRESHOLD_PX;
483
+ }
484
+ /**
485
+ * Прилипает ли лента к низу после этой прокрутки.
486
+ *
487
+ * Прилипание снимается только тогда, когда читатель уехал вверх сам. Прокрутка, сделанная
488
+ * самим компонентом, и рост содержимого приходят с тем же или большим положением — на них
489
+ * прилипание лишь подтверждается: иначе позднее событие от собственной прокрутки читалось бы
490
+ * отрывом от низа, и лента переставала бы догонять последнее сообщение.
491
+ */
492
+ function nextAtBottom(previous, lastScrollTop, metrics) {
493
+ const nearBottom = isNearBottom(metrics);
494
+ return metrics.scrollTop < lastScrollTop - 1 ? nearBottom : previous || nearBottom;
495
+ }
496
+
497
+ const BEM_BLOCK = 'rt-chat';
498
+ /** Ключ заглушки имени вложения, когда домен не отдал `fileName` (только URL). */
499
+ const ATTACHMENT_FALLBACK_KEY = 'chatAttachmentFallback';
500
+ /**
501
+ * Общий презентационный чат: тред сообщений + композер. Никакого data-access/api — история
502
+ * приходит готовым массивом `IRtChat.Message[]`, отправка/обновление/скачивание уходят наружу
503
+ * через outputs, а стор и blob-загрузка остаются в feature-компоненте каждого домена.
504
+ *
505
+ * Композер держит свой `FormGroup` — наружу торчит только `send`. Полноэкранный
506
+ * режим (expand) — overlay внутри самого компонента (`--expanded`), потребителю
507
+ * лишь опционально сообщается через `expandedChange`.
508
+ *
509
+ * Шапка (тулбар «Чат» + refresh/expand) рендерится только когда включена хотя бы
510
+ * одна из иконок (`showRefresh`/`showExpand`): в диалогах своя header-обёртка,
511
+ * туда тулбар не нужен.
512
+ */
513
+ class RtChatComponent {
514
+ /**
515
+ * Пользователь у нижнего края треда. Плоское поле (не сигнал), чтобы
516
+ * afterRenderEffect не перезапускался на каждый scroll — обновляется в
517
+ * обработчике scroll и читается при автоскролле.
518
+ */
519
+ #atBottom;
520
+ /** Прошлый scrollTop треда — для различения прокрутки вверх и роста контента. */
521
+ #lastScrollTop;
522
+ /** One-shot: своё отправленное сообщение всегда прокручивает тред вниз. */
523
+ #forceScroll;
524
+ /** Текущее значение композера — источник реактивности для `canSend`. */
525
+ #formValue;
526
+ #defaultTitle;
527
+ #defaultPlaceholder;
528
+ #defaultEmptyHint;
529
+ constructor() {
530
+ /**
531
+ * Дата сообщения форматируется по активной локали приложения, а не по
532
+ * `LOCALE_ID`: тот вычисляется на подъёме приложения и там, где язык
533
+ * переключают на ходу, оставил бы часы в прежнем формате до перезагрузки.
534
+ *
535
+ * Данные локали для `DatePipe` регистрирует приложение: без них пайп падает
536
+ * `Missing locale data`, и кит за это не отвечает.
537
+ */
538
+ this.t = inject(RT_KIT_LABELS);
539
+ this.locale = inject(RT_KIT_LOCALE);
540
+ /**
541
+ * Пользователь у нижнего края треда. Плоское поле (не сигнал), чтобы
542
+ * afterRenderEffect не перезапускался на каждый scroll — обновляется в
543
+ * обработчике scroll и читается при автоскролле.
544
+ */
545
+ this.#atBottom = true;
546
+ /** Прошлый scrollTop треда — для различения прокрутки вверх и роста контента. */
547
+ this.#lastScrollTop = 0;
548
+ /** One-shot: своё отправленное сообщение всегда прокручивает тред вниз. */
549
+ this.#forceScroll = false;
550
+ /**
551
+ * Скролл-контейнер треда — для автопрокрутки к последнему сообщению.
552
+ * `viewChild` не работает на ES-private (`#`) поле, поэтому `protected`.
553
+ */
554
+ this.thread = viewChild('thread', /* @ts-ignore */
555
+ ...(ngDevMode ? [{ debugName: "thread" }] : /* istanbul ignore next */ []));
556
+ /** Скрытый нативный input — открывается кнопкой-скрепкой внутри поля ввода. */
557
+ this.composerFileEl = viewChild('composerFileEl', /* @ts-ignore */
558
+ ...(ngDevMode ? [{ debugName: "composerFileEl" }] : /* istanbul ignore next */ []));
559
+ this.attachmentFallbackName = rtKitLabel(ATTACHMENT_FALLBACK_KEY);
560
+ /** Подписи с учётом того, что передал хозяин компонента: своё важнее умолчания */
561
+ this.titleText = computed(() => this.title() || this.#defaultTitle(), /* @ts-ignore */
562
+ ...(ngDevMode ? [{ debugName: "titleText" }] : /* istanbul ignore next */ []));
563
+ this.placeholderText = computed(() => this.placeholder() || this.#defaultPlaceholder(), /* @ts-ignore */
564
+ ...(ngDevMode ? [{ debugName: "placeholderText" }] : /* istanbul ignore next */ []));
565
+ this.emptyHintText = computed(() => this.emptyHint() || this.#defaultEmptyHint(), /* @ts-ignore */
566
+ ...(ngDevMode ? [{ debugName: "emptyHintText" }] : /* istanbul ignore next */ []));
567
+ this.#defaultTitle = rtKitLabel('chatTitle');
568
+ this.#defaultPlaceholder = rtKitLabel('chatPlaceholder');
569
+ this.#defaultEmptyHint = rtKitLabel('chatEmptyHint');
570
+ this.MessageStatus = ERtChatMessageStatus;
571
+ /** Плейсхолдеры скелетонов треда при первичной загрузке. */
572
+ this.skeletonRows = [1, 2, 3];
573
+ /** Полноэкранный режим (overlay поверх страницы). */
574
+ this.expanded = signal(false, /* @ts-ignore */
575
+ ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
576
+ this.replyForm = new FormGroup({
577
+ message: new FormControl('', { nonNullable: true }),
578
+ files: new FormControl([], { nonNullable: true }),
579
+ });
580
+ /** Композер заблокирован при загрузке/рефреше треда или отправке in-flight. */
581
+ this.composerDisabled = computed(() => this.loading() || this.fetching() || this.sending(), /* @ts-ignore */
582
+ ...(ngDevMode ? [{ debugName: "composerDisabled" }] : /* istanbul ignore next */ []));
583
+ /**
584
+ * Можно ли отправить: композер активен и есть содержимое — непустой текст или
585
+ * (при включённых вложениях) хотя бы один файл. Сообщение само по себе не
586
+ * бывает «невалидным» — пустоту гасим дизейблом кнопки, а не подсветкой поля.
587
+ */
588
+ this.canSend = computed(() => {
589
+ if (this.composerDisabled()) {
590
+ return false;
591
+ }
592
+ const value = this.#formValue();
593
+ const hasText = (value.message ?? '').trim().length > 0;
594
+ const hasFiles = this.attachments() && (value.files?.length ?? 0) > 0;
595
+ return hasText || hasFiles;
596
+ }, /* @ts-ignore */
597
+ ...(ngDevMode ? [{ debugName: "canSend" }] : /* istanbul ignore next */ []));
598
+ /** Показывать ли шапку: только когда включена хотя бы одна иконка. */
599
+ this.showHeader = computed(() => this.showRefresh() || this.showExpand(), /* @ts-ignore */
600
+ ...(ngDevMode ? [{ debugName: "showHeader" }] : /* istanbul ignore next */ []));
601
+ /** Подпись кнопки fullscreen: тернарник в шаблоне переводом не покрыть. */
602
+ this.expandLabel = rtKitLabel(computed(() => (this.expanded() ? 'chatCollapse' : 'chatExpand')));
603
+ /** Выбранные, но ещё не отправленные файлы — карточки над полем ввода. */
604
+ this.selectedFiles = computed(() => this.#formValue().files ?? [], /* @ts-ignore */
605
+ ...(ngDevMode ? [{ debugName: "selectedFiles" }] : /* istanbul ignore next */ []));
606
+ /**
607
+ * Drop-зона активна только когда вложения включены, ответ доступен и композер
608
+ * не занят (загрузка/рефреш/отправка). Иначе обёртка `rt-file-drop` просто
609
+ * пропускает контент без оверлея.
610
+ */
611
+ this.dropDisabled = computed(() => !this.attachments() || !this.canReply() || this.composerDisabled(), /* @ts-ignore */
612
+ ...(ngDevMode ? [{ debugName: "dropDisabled" }] : /* istanbul ignore next */ []));
613
+ /**
614
+ * Файл из drag-n-drop для rich-композера: каждый drop кладёт новый `File`,
615
+ * пробрасывается в `rt-message-composer` через input. Классический композер
616
+ * принимает drop напрямую в свой `replyForm`.
617
+ */
618
+ this.droppedFiles = signal(null, /* @ts-ignore */
619
+ ...(ngDevMode ? [{ debugName: "droppedFiles" }] : /* istanbul ignore next */ []));
620
+ /**
621
+ * Шаблон действий у реплики: `<ng-template rtChatMessageActions let-message>`.
622
+ *
623
+ * Не объявлен — разметка реплики прежняя, и лишней кнопки у неё не появляется.
624
+ */
625
+ this.messageActions = contentChild(RtChatMessageActionsDirective, /* @ts-ignore */
626
+ ...(ngDevMode ? [{ debugName: "messageActions" }] : /* istanbul ignore next */ []));
627
+ /** История сообщений (consumer уже смаппил из доменной модели). */
628
+ this.messages = input([], /* @ts-ignore */
629
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
630
+ /** Первичная загрузка — скелетоны вместо треда. */
631
+ this.loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
632
+ /** Рефреш при наличии данных — тред остаётся, композер дизейблится. */
633
+ this.fetching = input(false, { ...(ngDevMode ? { debugName: "fetching" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
634
+ /** Доступна ли отправка (иначе вместо композера — note с причиной). */
635
+ this.canReply = input(false, { ...(ngDevMode ? { debugName: "canReply" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
636
+ /** Причина блокировки ответа — note под тредом. */
637
+ this.replyBlockReason = input(null, /* @ts-ignore */
638
+ ...(ngDevMode ? [{ debugName: "replyBlockReason" }] : /* istanbul ignore next */ []));
639
+ /** Отправка in-flight — спиннер/дизейбл на кнопке. */
640
+ this.sending = input(false, { ...(ngDevMode ? { debugName: "sending" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
641
+ /**
642
+ * Подписи по умолчанию приходят пустыми и переводятся ниже: значение
643
+ * `input()` вычисляется при создании компонента, и готовый перевод застыл бы
644
+ * на языке, активном в этот момент.
645
+ */
646
+ this.placeholder = input('', /* @ts-ignore */
647
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
648
+ this.title = input('', /* @ts-ignore */
649
+ ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
650
+ /** Текст, когда треда ещё нет (заявка не создана). */
651
+ this.emptyHint = input('', /* @ts-ignore */
652
+ ...(ngDevMode ? [{ debugName: "emptyHint" }] : /* istanbul ignore next */ []));
653
+ /** Есть ли тред (иначе показываем `emptyHint`). */
654
+ this.hasThread = input(false, { ...(ngDevMode ? { debugName: "hasThread" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
655
+ /** Включить загрузку файлов в композере. */
656
+ this.attachments = input(false, { ...(ngDevMode ? { debugName: "attachments" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
657
+ /** `accept` для file-input (`.pdf,.doc,...`). */
658
+ this.accept = input('', /* @ts-ignore */
659
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
660
+ /**
661
+ * Компактный композер (`rt-message-composer`): авто-растущая textarea над
662
+ * разделителем + ряд иконок (прикрепить слева, отправить справа) вместо
663
+ * классического вертикального стека. По умолчанию выключен.
664
+ */
665
+ this.richComposer = input(false, { ...(ngDevMode ? { debugName: "richComposer" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
666
+ /**
667
+ * Rich-текст в композере (Quill delta). Требует `richComposer` — форматирование
668
+ * живёт только в компактном композере. Отправка уходит delta-каналом.
669
+ */
670
+ this.formatting = input(false, { ...(ngDevMode ? { debugName: "formatting" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
671
+ /** Набор кнопок rich-редактора (проброс до `rt-rich-editor` через композер). */
672
+ this.formattingToolbar = input('full', /* @ts-ignore */
673
+ ...(ngDevMode ? [{ debugName: "formattingToolbar" }] : /* istanbul ignore next */ []));
674
+ /**
675
+ * Растягивать чат на всю высоту контейнера: тред занимает свободное место,
676
+ * композер прижат к низу. По умолчанию выключен (тред с фикс. max-height).
677
+ * Требует, чтобы родитель давал высоту (flex-цепочка).
678
+ */
679
+ this.fill = input(false, { ...(ngDevMode ? { debugName: "fill" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
680
+ /** Показать иконку refresh в шапке. */
681
+ this.showRefresh = input(false, { ...(ngDevMode ? { debugName: "showRefresh" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
682
+ /** Показать иконку expand (fullscreen) в шапке. */
683
+ this.showExpand = input(false, { ...(ngDevMode ? { debugName: "showExpand" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
684
+ /** Отправка сообщения (+файлы) — consumer вызывает свой store/api. */
685
+ this.send = output();
686
+ /** Клик refresh — consumer перетягивает тред. */
687
+ this.refresh = output();
688
+ /** Клик по вложению — consumer качает blob своим api. Шаблон зовёт выход прямо: релей-метод,
689
+ * который только повторял `emit`, ничего не добавлял, а файл при этом рос. */
690
+ this.downloadFile = output();
691
+ /** Клик по карточке вложения из `attachments[]` — консюмер качает по `publicId`. */
692
+ this.downloadAttachment = output();
693
+ /** Смена fullscreen-состояния. */
694
+ this.expandedChange = output();
695
+ this.deleteMessage = output();
696
+ /** Повторная отправка своего сообщения со статусом `failed`. */
697
+ this.retryMessage = output();
698
+ /**
699
+ * Есть ли у реплики действия потребителя — гейт кнопки «…» у неё.
700
+ *
701
+ * Не задан — точка действий показывается у каждой реплики: экран, который ничего не
702
+ * гейтит, показывает её так же, как это делает строка таблицы.
703
+ */
704
+ this.messageHasActions = input(null, /* @ts-ignore */
705
+ ...(ngDevMode ? [{ debugName: "messageHasActions" }] : /* istanbul ignore next */ []));
706
+ this.#formValue = toSignal(this.replyForm.valueChanges, {
707
+ initialValue: this.replyForm.value,
708
+ });
709
+ // Поведение мессенджера: после каждого рендера, зависящего от треда
710
+ // (новые сообщения, окончание загрузки, fullscreen), прокручиваем к
711
+ // последнему сообщению — но только если это своё сообщение (forceScroll)
712
+ // или пользователь уже у нижнего края. Если он ушёл вверх читать историю,
713
+ // входящее сообщение его не дёргает. afterRenderEffect гарантирует, что DOM
714
+ // уже обновлён и scrollHeight посчитан по реальной высоте.
715
+ afterRenderEffect(() => {
716
+ this.messages();
717
+ this.loading();
718
+ this.fetching();
719
+ this.expanded();
720
+ const el = this.thread()?.nativeElement;
721
+ if (el === undefined) {
722
+ return;
723
+ }
724
+ if (this.#forceScroll || this.#atBottom) {
725
+ el.scrollTop = el.scrollHeight;
726
+ this.#atBottom = true;
727
+ this.#pinToBottomDeferred();
728
+ }
729
+ this.#forceScroll = false;
730
+ });
731
+ // `formControlName`-инпуты игнорируют атрибут `disabled` — состояние ввода
732
+ // держим через сам контрол: блок при рефреше треда или отправке in-flight.
733
+ effect(() => {
734
+ const blocked = this.composerDisabled();
735
+ if (blocked && this.replyForm.enabled) {
736
+ this.replyForm.disable({ emitEvent: false });
737
+ return;
738
+ }
739
+ if (!blocked && this.replyForm.disabled) {
740
+ this.replyForm.enable({ emitEvent: false });
741
+ }
742
+ });
743
+ }
744
+ onSubmit() {
745
+ // Кнопка уже дизейблится по `canSend` — guard лишь страхует от submit в
746
+ // обход кнопки (программный/Enter), молча и без подсветки поля.
747
+ if (!this.canSend()) {
748
+ return;
749
+ }
750
+ const text = this.replyForm.controls.message.value.trim();
751
+ const files = this.replyForm.controls.files.value;
752
+ // Своё сообщение всегда прокручивает тред вниз — независимо от позиции.
753
+ this.#forceScroll = true;
754
+ this.send.emit({ text, files: this.attachments() ? files : undefined });
755
+ this.replyForm.reset({ message: '', files: [] });
756
+ }
757
+ onComposerKeydown(event) {
758
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
759
+ event.preventDefault();
760
+ this.onSubmit();
761
+ }
762
+ }
763
+ /** Релей submit'а от `rt-message-composer` (rich-режим) наружу как `send`. */
764
+ onComposerSubmit(payload) {
765
+ // Своё сообщение всегда прокручивает тред вниз — независимо от позиции.
766
+ this.#forceScroll = true;
767
+ this.send.emit({
768
+ text: payload.text,
769
+ files: this.attachments() ? payload.files : undefined,
770
+ delta: payload.delta,
771
+ });
772
+ }
773
+ /**
774
+ * Файлы выброшены в обёртку чата: в rich-режиме отдаём массив композеру через
775
+ * input, в классическом — добавляем к уже выбранным в собственном `replyForm`.
776
+ */
777
+ onFilesDropped(files) {
778
+ if (files.length === 0) {
779
+ return;
780
+ }
781
+ if (this.richComposer()) {
782
+ // Новый массив на каждый drop — effect композера видит каждое перетаскивание.
783
+ this.droppedFiles.set([...files]);
784
+ return;
785
+ }
786
+ this.replyForm.controls.files.setValue(withAppendedFiles(this.replyForm.controls.files.value, files));
787
+ }
788
+ openFilePicker() {
789
+ this.composerFileEl()?.nativeElement.click();
790
+ }
791
+ onFilesPicked(event) {
792
+ const inputEl = event.target;
793
+ const picked = pickedFiles(inputEl.files);
794
+ if (picked.length > 0) {
795
+ this.replyForm.controls.files.setValue(withAppendedFiles(this.replyForm.controls.files.value, picked));
796
+ }
797
+ // Сброс значения: без него повторный выбор того же файла не даёт `change`.
798
+ inputEl.value = '';
799
+ }
800
+ removeFile(index) {
801
+ this.replyForm.controls.files.setValue(withoutFileAt(this.replyForm.controls.files.value, index));
802
+ }
803
+ /** Признак «читатель у нижнего края» обновляется по прокрутке: на нём стоит автопрокрутка. */
804
+ onThreadScroll() {
805
+ const el = this.thread()?.nativeElement;
806
+ if (el === undefined) {
807
+ return;
808
+ }
809
+ this.#atBottom = nextAtBottom(this.#atBottom, this.#lastScrollTop, el);
810
+ this.#lastScrollTop = el.scrollTop;
811
+ }
812
+ /**
813
+ * Серия отложенных подтягиваний ленты к низу: присланное содержимое дорисовывается несколькими
814
+ * проходами, и одной прокрутки мало. Каждый тик берёт тред заново — он бывает пересоздан — и
815
+ * дотягивает ленту, пока читатель стоит у нижнего края.
816
+ */
817
+ #pinToBottomDeferred() {
818
+ for (const delay of RT_CHAT_PIN_RETRY_DELAYS_MS) {
819
+ setTimeout(() => {
820
+ const el = this.thread()?.nativeElement;
821
+ if (el !== undefined && this.#atBottom) {
822
+ el.scrollTop = el.scrollHeight;
823
+ }
824
+ }, delay);
825
+ }
826
+ }
827
+ toggleExpand() {
828
+ const next = !this.expanded();
829
+ this.expanded.set(next);
830
+ this.expandedChange.emit(next);
831
+ }
832
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
833
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: RtChatComponent, isStandalone: true, selector: "rt-chat", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fetching: { classPropertyName: "fetching", publicName: "fetching", isSignal: true, isRequired: false, transformFunction: null }, canReply: { classPropertyName: "canReply", publicName: "canReply", isSignal: true, isRequired: false, transformFunction: null }, replyBlockReason: { classPropertyName: "replyBlockReason", publicName: "replyBlockReason", isSignal: true, isRequired: false, transformFunction: null }, sending: { classPropertyName: "sending", publicName: "sending", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, emptyHint: { classPropertyName: "emptyHint", publicName: "emptyHint", isSignal: true, isRequired: false, transformFunction: null }, hasThread: { classPropertyName: "hasThread", publicName: "hasThread", isSignal: true, isRequired: false, transformFunction: null }, attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, richComposer: { classPropertyName: "richComposer", publicName: "richComposer", isSignal: true, isRequired: false, transformFunction: null }, formatting: { classPropertyName: "formatting", publicName: "formatting", isSignal: true, isRequired: false, transformFunction: null }, formattingToolbar: { classPropertyName: "formattingToolbar", publicName: "formattingToolbar", isSignal: true, isRequired: false, transformFunction: null }, fill: { classPropertyName: "fill", publicName: "fill", isSignal: true, isRequired: false, transformFunction: null }, showRefresh: { classPropertyName: "showRefresh", publicName: "showRefresh", isSignal: true, isRequired: false, transformFunction: null }, showExpand: { classPropertyName: "showExpand", publicName: "showExpand", isSignal: true, isRequired: false, transformFunction: null }, messageHasActions: { classPropertyName: "messageHasActions", publicName: "messageHasActions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", refresh: "refresh", downloadFile: "downloadFile", downloadAttachment: "downloadAttachment", expandedChange: "expandedChange", deleteMessage: "deleteMessage", retryMessage: "retryMessage" }, host: { properties: { "class.rt-chat--expanded": "expanded()", "class.rt-chat--fill": "fill()" }, classAttribute: "rt-chat" }, queries: [{ propertyName: "messageActions", first: true, predicate: RtChatMessageActionsDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "thread", first: true, predicate: ["thread"], descendants: true, isSignal: true }, { propertyName: "composerFileEl", first: true, predicate: ["composerFileEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container rtBlock=\"rt-chat\">\n <rt-file-drop\n [overlayLabel]=\"t().chatDropOverlay\"\n [accept]=\"accept()\"\n [disabled]=\"dropDisabled()\"\n (filesDropped)=\"onFilesDropped($event)\">\n @if (showHeader()) {\n <rt-toolbar rtElem=\"header\" dense>\n <ng-template rtToolbarLeft>\n <h3 rtElem=\"title\" qa-dataid=\"chat-title\">{{ title() }}</h3>\n </ng-template>\n <ng-template rtToolbarRight>\n @if (showRefresh()) {\n <rt-icon-button\n qa-dataid=\"chat-refresh\"\n icon=\"sync\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatRefreshAria\"\n [tooltip]=\"t().chatRefreshTooltip\"\n [loading]=\"fetching()\"\n [disabled]=\"!hasThread()\"\n (clicked)=\"refresh.emit()\" />\n }\n @if (showExpand()) {\n <rt-icon-button\n qa-dataid=\"chat-expand\"\n variant=\"ghost\"\n [icon]=\"expanded() ? 'window-minimize' : 'window-maximize'\"\n [ariaLabel]=\"expandLabel()\"\n [tooltip]=\"expandLabel()\"\n (clicked)=\"toggleExpand()\" />\n }\n <ng-content select=\"[rtChatHeaderActions]\" />\n </ng-template>\n </rt-toolbar>\n }\n\n @if (!hasThread()) {\n <rt-note qa-dataid=\"chat-empty-hint\">{{ emptyHint() }}</rt-note>\n } @else {\n <div rtElem=\"thread-viewport\" qa-dataid=\"chat-thread-viewport\">\n @if (loading()) {\n <!-- \u0421\u043A\u0435\u043B\u0435\u0442\u043E\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0435\u0442 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F (\u0430\u0432\u0442\u043E\u0440 \u2192 \u0442\u0435\u043A\u0441\u0442 \u2192 \u0434\u0430\u0442\u0430),\n \u0447\u0435\u0440\u0435\u0434\u0443\u044F \u0432\u044B\u0440\u0430\u0432\u043D\u0438\u0432\u0430\u043D\u0438\u0435, \u0447\u0442\u043E\u0431\u044B \u0442\u0440\u0435\u0434 \u043D\u0435 \u00AB\u043F\u0440\u044B\u0433\u0430\u043B\u00BB \u043F\u0440\u0438 \u043F\u043E\u0434\u043C\u0435\u043D\u0435 \u043D\u0430 \u0440\u0435\u0430\u043B\u044C\u043D\u044B\u0435. -->\n <div rtElem=\"thread\" qa-dataid=\"chat-thread-skeleton\">\n @for (row of skeletonRows; track row; let even = $even) {\n <div rtElem=\"message-skeleton\" qa-dataid=\"chat-message-skeleton\" [rtMod]=\"{ own: even }\">\n <rt-skeleton-wrapper rtElem=\"skeleton-line\" width=\"72px\" height=\"10px\" [isLoading]=\"true\" />\n <rt-skeleton-wrapper rtElem=\"skeleton-line\" width=\"100%\" height=\"13px\" [isLoading]=\"true\" />\n <rt-skeleton-wrapper rtElem=\"skeleton-line\" width=\"55%\" height=\"13px\" [isLoading]=\"true\" />\n <rt-skeleton-wrapper rtElem=\"skeleton-line\" width=\"56px\" height=\"10px\" [isLoading]=\"true\" />\n </div>\n }\n </div>\n } @else {\n <div\n #thread\n rtElem=\"thread\"\n qa-dataid=\"chat-thread\"\n [rtMod]=\"{ frozen: fetching() }\"\n [attr.aria-busy]=\"fetching() ? 'true' : null\"\n (scroll)=\"onThreadScroll()\">\n @for (m of messages(); track m.id) {\n <div\n rtElem=\"message-row\"\n qa-dataid=\"chat-message\"\n [attr.data-own]=\"m.own\"\n [attr.data-status]=\"m.status || null\"\n [attr.data-deleted]=\"m.deleted || null\"\n [rtMod]=\"{ own: m.own }\">\n @if (m.canDelete && !m.deleted) {\n @if (m.deleteConfirm) {\n <rt-icon-button\n rtElem=\"message-delete\"\n qa-dataid=\"chat-message-delete\"\n icon=\"trash\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatDeleteAria\"\n [tooltip]=\"t().chatDeleteTooltip\"\n [rtConfirm]=\"m.deleteConfirm\"\n (confirmed)=\"deleteMessage.emit(m)\" />\n } @else {\n <rt-icon-button\n rtElem=\"message-delete\"\n qa-dataid=\"chat-message-delete\"\n icon=\"trash\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatDeleteAria\"\n [tooltip]=\"t().chatDeleteTooltip\"\n (clicked)=\"deleteMessage.emit(m)\" />\n }\n }\n @if (messageActions()?.template; as actionsTpl) {\n @if (m | rtChatMessageHasActions: messageHasActions()) {\n <div rtElem=\"message-actions\" qa-dataid=\"chat-message-actions\">\n <rt-menu>\n <ng-container\n [ngTemplateOutlet]=\"actionsTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: m }\" />\n </rt-menu>\n </div>\n }\n }\n <div rtElem=\"message\" [rtMod]=\"{ own: m.own, deleted: m.deleted }\">\n <p rtElem=\"message-author\" qa-dataid=\"chat-message-author\">{{ m.author }}</p>\n @if (m.deleted) {\n <p rtElem=\"message-text\" qa-dataid=\"chat-message-deleted\" rtMod=\"deleted\">\n {{ t().chatMessageDeleted }}\n </p>\n } @else {\n @if (m.delta) {\n <rt-delta-view rtElem=\"message-text\" qa-dataid=\"chat-message-text\" [delta]=\"m.delta\" />\n } @else {\n <p rtElem=\"message-text\" qa-dataid=\"chat-message-text\">{{ m.text }}</p>\n }\n @if (m.attachments?.length) {\n @for (att of m.attachments; track att.id) {\n <span rtElem=\"message-attachment\" qa-dataid=\"chat-message-attachment\">\n <rt-file-card\n showDownload\n size=\"sm\"\n [name]=\"att.name || attachmentFallbackName()\"\n (download)=\"downloadAttachment.emit(att)\" />\n </span>\n }\n } @else if (m.fileUrl) {\n <span rtElem=\"message-attachment\" qa-dataid=\"chat-message-attachment\">\n <rt-file-card\n showDownload\n size=\"sm\"\n [name]=\"m.fileName || attachmentFallbackName()\"\n (download)=\"downloadFile.emit(m)\" />\n </span>\n }\n @if (m.deepLink) {\n <span rtElem=\"message-deeplink\">\n <a\n rtButton\n qa-dataid=\"chat-message-deeplink\"\n appearance=\"outlined\"\n icon=\"send\"\n rel=\"noopener\"\n target=\"_blank\"\n [href]=\"m.deepLink\"\n [label]=\"m.deepLinkLabel || t().uiSubscribeNotifications\"\n [attr.aria-label]=\"m.deepLinkLabel || t().uiSubscribeNotifications\"></a>\n </span>\n }\n }\n <span rtElem=\"message-footer\">\n <span rtElem=\"message-date\" qa-dataid=\"chat-message-date\">\n {{ m.createdAt | date: 'short' : undefined : locale() }}\n </span>\n @if (m.status) {\n <span\n rtElem=\"message-status\"\n qa-dataid=\"chat-message-status\"\n [attr.data-status]=\"m.status\"\n [rtMod]=\"{ status: m.status }\"\n [attr.aria-label]=\"m.status | rtChatStatusLabel\">\n @if (m.status === MessageStatus.Failed) {\n <rt-icon rtElem=\"status-icon\" name=\"exclamation-circle\" size=\"sm\" />\n <rt-icon-button\n qa-dataid=\"chat-message-retry\"\n icon=\"sync\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatRetryAria\"\n [tooltip]=\"t().chatRetryTooltip\"\n (clicked)=\"retryMessage.emit(m)\" />\n } @else {\n <!-- \u0427\u0430\u0441\u044B, \u043F\u043E\u043A\u0430 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0432 \u043F\u0443\u0442\u0438; \u0434\u0430\u043B\u044C\u0448\u0435 \u0434\u0432\u043E\u0439\u043D\u043E\u0439 \u0447\u0435\u043A, \u0446\u0432\u0435\u0442 \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E\n (\u0441\u0435\u0440\u044B\u0439/\u0441\u0438\u043D\u0438\u0439) \u0440\u0430\u0437\u043B\u0438\u0447\u0430\u0435\u0442 \u0434\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E \u0438 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E. -->\n <rt-icon\n rtElem=\"status-icon\"\n size=\"sm\"\n [name]=\"m.status === MessageStatus.Sending ? 'clock' : 'check-double'\" />\n }\n </span>\n }\n </span>\n </div>\n </div>\n } @empty {\n <rt-note qa-dataid=\"chat-empty-thread\">{{ t().chatEmptyThread }}</rt-note>\n }\n </div>\n }\n @if (!loading() && fetching()) {\n <div rtElem=\"thread-overlay\" qa-dataid=\"chat-thread-overlay\" role=\"status\" [attr.aria-label]=\"t().chatRefreshAria\">\n <rt-spinner diameter=\"32\" />\n </div>\n }\n </div>\n\n @if (canReply()) {\n @if (richComposer()) {\n <rt-message-composer\n rtElem=\"composer-rich\"\n qa-dataid=\"chat-composer-rich\"\n [placeholder]=\"placeholder()\"\n [accept]=\"accept()\"\n [attachments]=\"attachments()\"\n [formatting]=\"formatting()\"\n [toolbar]=\"formattingToolbar()\"\n [sending]=\"sending()\"\n [disabled]=\"loading() || fetching()\"\n [droppedFiles]=\"droppedFiles()\"\n (submitted)=\"onComposerSubmit($event)\" />\n } @else {\n <form rtElem=\"composer\" qa-dataid=\"chat-composer\" [formGroup]=\"replyForm\" (ngSubmit)=\"onSubmit()\">\n @if (attachments() && selectedFiles().length > 0) {\n <div rtElem=\"composer-files\" qa-dataid=\"chat-composer-files\">\n @for (file of selectedFiles(); track $index) {\n <rt-file-card\n showRemove\n qa-dataid=\"chat-composer-file\"\n size=\"sm\"\n [name]=\"file.name\"\n [sizeBytes]=\"file.size\"\n [disabled]=\"composerDisabled()\"\n (removed)=\"removeFile($index)\" />\n }\n </div>\n }\n <div rtElem=\"composer-field\" [rtMod]=\"{ attach: attachments() }\">\n @if (attachments()) {\n <input\n #composerFileEl\n qa-dataid=\"chat-composer-file-input\"\n type=\"file\"\n hidden\n multiple\n [attr.accept]=\"accept() || null\"\n (change)=\"onFilesPicked($event)\" />\n <rt-icon-button\n qa-dataid=\"chat-composer-attach\"\n icon=\"paperclip\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatAttachAria\"\n [tooltip]=\"t().chatAttachTooltip\"\n [disabled]=\"composerDisabled()\"\n (clicked)=\"openFilePicker()\" />\n }\n <textarea\n rtElem=\"composer-input\"\n qa-dataid=\"chat-composer-input\"\n cdkTextareaAutosize\n formControlName=\"message\"\n cdkAutosizeMinRows=\"1\"\n cdkAutosizeMaxRows=\"4\"\n [attr.aria-label]=\"t().chatComposerAria\"\n [placeholder]=\"placeholder()\"\n (keydown)=\"onComposerKeydown($event)\"></textarea>\n </div>\n <button\n rtButton\n rtElem=\"composer-send\"\n qa-dataid=\"chat-composer-send\"\n type=\"submit\"\n theme=\"primary\"\n appearance=\"filled\"\n [label]=\"t().chatSendLabel\"\n [attr.aria-label]=\"t().chatSendAria\"\n [loading]=\"sending()\"\n [disabled]=\"!canSend()\"></button>\n </form>\n }\n } @else if (replyBlockReason()) {\n <rt-note rtElem=\"reply-block\" qa-dataid=\"chat-reply-block\">{{ replyBlockReason() }}</rt-note>\n }\n\n <!-- \u0421\u043B\u043E\u0442 \u0434\u043E\u043C\u0435\u043D\u043D\u044B\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F \u043F\u043E\u0434 \u043A\u043E\u043C\u043F\u043E\u0437\u0435\u0440\u043E\u043C: \u0432 fullscreen\n \u043F\u043E\u043F\u0430\u0434\u0430\u0435\u0442 \u0432\u043D\u0443\u0442\u0440\u044C overlay, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043A\u043D\u043E\u043F\u043A\u0438 \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u0432\u0438\u0434\u043D\u044B. -->\n <div rtElem=\"footer-slot\" qa-dataid=\"chat-footer\" [rtMod]=\"{ blocked: fetching() }\" [attr.inert]=\"fetching() ? '' : null\">\n <ng-content select=\"[rtChatFooter]\" />\n </div>\n }\n </rt-file-drop>\n</ng-container>\n", styles: ["@layer rt-kit.components{.rt-chat{--rt-chat-bubble-radius: var(--rt-radius-lg);--rt-chat-message-border-width: var(--rt-border-width-thin);--rt-chat-transition-duration: var(--rt-duration-fast);display:flex;flex-direction:column}.rt-chat>.rt-file-drop{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--rt-space-4);min-block-size:0}.rt-chat--expanded{position:fixed;z-index:var(--rt-z-modal);box-sizing:border-box;padding:var(--rt-space-md);background:var(--rt-color-bg-surface);inset:0}.rt-chat--expanded>.rt-file-drop{block-size:100%}@media(width>=1441px){.rt-chat--expanded>.rt-file-drop{inline-size:1000px;margin-inline:auto;max-inline-size:100%}}.rt-chat--expanded .rt-chat__thread-viewport{flex:1 1 auto;min-block-size:0}.rt-chat--expanded .rt-chat__thread{max-height:none}.rt-chat--fill{block-size:100%}.rt-chat--fill .rt-chat__thread-viewport{flex:1 1 auto;min-block-size:0}.rt-chat--fill .rt-chat__thread{max-height:none}.rt-chat__title{margin:0;color:var(--rt-color-text-primary);font-family:var(--rt-font-family-sans);font-size:var(--rt-text-md);font-weight:var(--rt-font-weight-bold);letter-spacing:var(--rt-letter-spacing-wide);line-height:var(--rt-leading-snug)}.rt-chat__thread-viewport{position:relative;display:flex;flex-direction:column;contain:inline-size;min-block-size:0}.rt-chat__thread{display:flex;max-height:280px;flex:1 1 auto;flex-direction:column;gap:var(--rt-space-4);min-block-size:0;overflow-y:auto}.rt-chat__thread--frozen{overflow-y:hidden}.rt-chat__thread-overlay{position:absolute;z-index:1;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--rt-color-bg-surface) 60%,transparent);inset:0}.rt-chat__footer-slot{display:flex;flex-direction:column}.rt-chat__footer-slot:empty{display:none}.rt-chat__footer-slot--blocked{opacity:.6;pointer-events:none}.rt-chat__message-row{display:flex;max-width:min(80%,480px);align-items:center;align-self:flex-start;gap:var(--rt-space-2)}.rt-chat__message-row--own{align-self:flex-end}.rt-chat__message-row:hover .rt-chat__message-delete,.rt-chat__message-row:focus-within .rt-chat__message-delete,.rt-chat__message-row:hover .rt-chat__message-actions,.rt-chat__message-row:focus-within .rt-chat__message-actions{opacity:1;pointer-events:auto}.rt-chat__message{display:flex;min-width:0;flex:0 1 auto;flex-direction:column;padding:var(--rt-space-2);border-radius:var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) 0;background:var(--rt-chat-bubble-in-bg);gap:var(--rt-space-2)}.rt-chat__message--own{border-radius:var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) 0 var(--rt-chat-bubble-radius);background:var(--rt-chat-bubble-own-bg)}.rt-chat__message--deleted{border:var(--rt-chat-message-border-width) dashed var(--rt-color-border-default);background:transparent}.rt-chat__message-author{margin:0;color:var(--rt-chat-bubble-in-author);font-size:var(--rt-text-2xs);font-weight:var(--rt-font-weight-bold);letter-spacing:var(--rt-letter-spacing-wide);line-height:var(--rt-leading-snug)}.rt-chat__message--own .rt-chat__message-author{color:var(--rt-chat-bubble-own-author)}.rt-chat__message-text{margin:0;color:var(--rt-color-text-primary);font-size:var(--rt-text-xs);font-weight:var(--rt-font-weight-regular);line-height:var(--rt-leading-snug);white-space:pre-wrap}.rt-chat__message-text--deleted{color:var(--rt-color-text-muted);font-style:italic}.rt-chat__message-delete,.rt-chat__message-actions{flex:0 0 auto;opacity:0;pointer-events:none;transition:opacity var(--rt-chat-transition-duration) ease}.rt-chat__message-attachment{display:block}.rt-chat__message-footer{display:flex;align-items:center;gap:var(--rt-space-1)}.rt-chat__message-date{color:var(--rt-chat-bubble-date);font-size:var(--rt-text-xs);line-height:var(--rt-leading-none)}.rt-chat__message-status{display:inline-flex;align-items:center;color:var(--rt-color-text-muted);gap:var(--rt-space-0-5)}.rt-chat__message-status--status--read{color:var(--rt-chat-status-read)}.rt-chat__message-status--status--failed{color:var(--rt-color-state-danger)}.rt-chat__status-icon{font-size:var(--rt-text-sm)}.rt-chat__message-skeleton{display:flex;max-width:min(80%,480px);flex-direction:column;align-self:flex-start;padding:var(--rt-space-2);border-radius:var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) 0;background:var(--rt-chat-bubble-in-bg);gap:var(--rt-space-1)}.rt-chat__message-skeleton--own{align-self:flex-end;border-radius:var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) 0 var(--rt-chat-bubble-radius);background:var(--rt-chat-bubble-own-bg)}.rt-chat__skeleton-line{display:block}.rt-chat__message-deeplink{display:flex;justify-content:flex-start;margin-top:var(--rt-space-1)}.rt-chat__composer,.rt-chat__composer-rich,.rt-chat__reply-block{margin-top:var(--rt-space-4)}.rt-chat__composer{display:flex;flex-direction:column;gap:var(--rt-space-2)}.rt-chat__composer-files{display:flex;flex-wrap:wrap;gap:var(--rt-space-2)}.rt-chat__composer-field{display:flex;box-sizing:border-box;align-items:center;border:var(--rt-input-border-width) solid var(--rt-input-color-border);border-radius:var(--rt-input-radius);background-color:var(--rt-input-color-bg);gap:var(--rt-space-1);min-block-size:var(--rt-input-height);padding-inline:var(--rt-input-padding-x);transition:var(--rt-input-transition)}.rt-chat__composer-field:focus-within{border-color:var(--rt-input-color-border-focus);box-shadow:var(--rt-input-focus-shadow)}.rt-chat__composer-field--attach{padding-inline-start:var(--rt-space-1)}.rt-chat__composer-input{box-sizing:content-box;flex:1 1 auto;border:0;background:transparent;color:var(--rt-input-color-text);font-family:inherit;font-size:var(--rt-input-font-size);inline-size:100%;line-height:var(--rt-textarea-line-height);padding-block:var(--rt-space-2);resize:none}.rt-chat__composer-input::placeholder{color:var(--rt-input-color-text-placeholder);opacity:1}.rt-chat__composer-input:focus-visible{outline:none}.rt-chat__composer-input:disabled{cursor:not-allowed}.rt-chat__composer-send{inline-size:100%}}\n"], dependencies: [{ kind: "directive", type:
834
+ // Angular
835
+ CdkTextareaAutosize, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMinRows", "cdkAutosizeMaxRows", "cdkTextareaAutosize", "placeholder"], exportAs: ["cdkTextareaAutosize"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type:
836
+ // standalone components / directives
837
+ RtButtonDirective, selector: "button[rtButton], a[rtButton]", inputs: ["label", "icon", "iconPos", "theme", "appearance", "size", "rounded", "loading", "loadingIcon", "pressed"] }, { kind: "directive", type: RtConfirmDirective, selector: "[rtConfirm]", inputs: ["rtConfirm", "rtConfirmTitle", "rtConfirmLabel", "rtConfirmCancelLabel", "rtConfirmTone", "rtConfirmDisabled"], outputs: ["confirmed"], exportAs: ["rtConfirm"] }, { kind: "component", type: RtDeltaViewComponent, selector: "rt-delta-view", inputs: ["delta"] }, { kind: "component", type: RtFileCardComponent, selector: "rt-file-card", inputs: ["name", "sizeBytes", "size", "showDownload", "showRemove", "showRename", "disabled"], outputs: ["download", "removed", "rename"] }, { kind: "component", type: RtFileDropComponent, selector: "rt-file-drop", inputs: ["disabled", "overlayLabel", "zones", "accept"], outputs: ["filesDropped", "zoneFilesDropped", "draggingChange"] }, { kind: "component", type: RtIconButtonComponent, selector: "rt-icon-button", inputs: ["icon", "ariaLabel", "variant", "iconColor", "iconFill", "size", "iconSize", "shape", "type", "tooltip", "tabIndex", "loading", "disabled", "active", "indicator"], outputs: ["clicked"] }, { kind: "component", type: RtIconComponent, selector: "rt-icon", inputs: ["name", "size", "color", "fill", "rotate"] }, { kind: "component", type: RtMenuComponent, selector: "rt-menu", inputs: ["icon", "ariaLabel", "align", "panelClass", "size", "disabled"], outputs: ["openedChange"] }, { kind: "component", type: RtMessageComposerComponent, selector: "rt-message-composer", inputs: ["placeholder", "accept", "attachments", "sending", "disabled", "formatting", "toolbar", "minRows", "maxRows", "droppedFiles"], outputs: ["submitted"] }, { kind: "component", type: RtNoteComponent, selector: "rt-note" }, { kind: "component", type: RtSkeletonWrapperComponent, selector: "rt-skeleton-wrapper", inputs: ["width", "height", "shape", "size", "borderRadius", "animation", "isLoading"] }, { kind: "component", type: RtSpinnerComponent, selector: "rt-spinner", inputs: ["diameter", "color"] }, { kind: "component", type: RtToolbarComponent, selector: "rt-toolbar", inputs: ["dense"] }, { kind: "directive", type: RtToolbarLeftDirective, selector: "[rtToolbarLeft]" }, { kind: "directive", type: RtToolbarRightDirective, selector: "[rtToolbarRight]" }, { kind: "directive", type: BlockDirective, selector: "[rtBlock]", inputs: ["rtMod"] }, { kind: "directive", type: ElemDirective, selector: "[rtElem]", inputs: ["rtMod"] }, { kind: "directive", type: ModDirective, selector: "[rtMod]" }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: RtChatStatusLabelPipe, name: "rtChatStatusLabel" }, { kind: "pipe", type: RtChatMessageHasActionsPipe, name: "rtChatMessageHasActions" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
838
+ }
839
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: RtChatComponent, decorators: [{
840
+ type: Component,
841
+ args: [{ selector: 'rt-chat', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
842
+ // Angular
843
+ CdkTextareaAutosize,
844
+ DatePipe,
845
+ NgTemplateOutlet,
846
+ ReactiveFormsModule,
847
+ // standalone components / directives
848
+ RtButtonDirective,
849
+ RtChatStatusLabelPipe,
850
+ RtConfirmDirective,
851
+ RtDeltaViewComponent,
852
+ RtFileCardComponent,
853
+ RtFileDropComponent,
854
+ RtIconButtonComponent,
855
+ RtIconComponent,
856
+ RtChatMessageHasActionsPipe,
857
+ RtMenuComponent,
858
+ RtMessageComposerComponent,
859
+ RtNoteComponent,
860
+ RtSkeletonWrapperComponent,
861
+ RtSpinnerComponent,
862
+ RtToolbarComponent,
863
+ RtToolbarLeftDirective,
864
+ RtToolbarRightDirective,
865
+ BlockDirective,
866
+ ElemDirective,
867
+ ModDirective,
868
+ ], host: {
869
+ class: BEM_BLOCK,
870
+ '[class.rt-chat--expanded]': 'expanded()',
871
+ '[class.rt-chat--fill]': 'fill()',
872
+ }, template: "<ng-container rtBlock=\"rt-chat\">\n <rt-file-drop\n [overlayLabel]=\"t().chatDropOverlay\"\n [accept]=\"accept()\"\n [disabled]=\"dropDisabled()\"\n (filesDropped)=\"onFilesDropped($event)\">\n @if (showHeader()) {\n <rt-toolbar rtElem=\"header\" dense>\n <ng-template rtToolbarLeft>\n <h3 rtElem=\"title\" qa-dataid=\"chat-title\">{{ title() }}</h3>\n </ng-template>\n <ng-template rtToolbarRight>\n @if (showRefresh()) {\n <rt-icon-button\n qa-dataid=\"chat-refresh\"\n icon=\"sync\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatRefreshAria\"\n [tooltip]=\"t().chatRefreshTooltip\"\n [loading]=\"fetching()\"\n [disabled]=\"!hasThread()\"\n (clicked)=\"refresh.emit()\" />\n }\n @if (showExpand()) {\n <rt-icon-button\n qa-dataid=\"chat-expand\"\n variant=\"ghost\"\n [icon]=\"expanded() ? 'window-minimize' : 'window-maximize'\"\n [ariaLabel]=\"expandLabel()\"\n [tooltip]=\"expandLabel()\"\n (clicked)=\"toggleExpand()\" />\n }\n <ng-content select=\"[rtChatHeaderActions]\" />\n </ng-template>\n </rt-toolbar>\n }\n\n @if (!hasThread()) {\n <rt-note qa-dataid=\"chat-empty-hint\">{{ emptyHint() }}</rt-note>\n } @else {\n <div rtElem=\"thread-viewport\" qa-dataid=\"chat-thread-viewport\">\n @if (loading()) {\n <!-- \u0421\u043A\u0435\u043B\u0435\u0442\u043E\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0435\u0442 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F (\u0430\u0432\u0442\u043E\u0440 \u2192 \u0442\u0435\u043A\u0441\u0442 \u2192 \u0434\u0430\u0442\u0430),\n \u0447\u0435\u0440\u0435\u0434\u0443\u044F \u0432\u044B\u0440\u0430\u0432\u043D\u0438\u0432\u0430\u043D\u0438\u0435, \u0447\u0442\u043E\u0431\u044B \u0442\u0440\u0435\u0434 \u043D\u0435 \u00AB\u043F\u0440\u044B\u0433\u0430\u043B\u00BB \u043F\u0440\u0438 \u043F\u043E\u0434\u043C\u0435\u043D\u0435 \u043D\u0430 \u0440\u0435\u0430\u043B\u044C\u043D\u044B\u0435. -->\n <div rtElem=\"thread\" qa-dataid=\"chat-thread-skeleton\">\n @for (row of skeletonRows; track row; let even = $even) {\n <div rtElem=\"message-skeleton\" qa-dataid=\"chat-message-skeleton\" [rtMod]=\"{ own: even }\">\n <rt-skeleton-wrapper rtElem=\"skeleton-line\" width=\"72px\" height=\"10px\" [isLoading]=\"true\" />\n <rt-skeleton-wrapper rtElem=\"skeleton-line\" width=\"100%\" height=\"13px\" [isLoading]=\"true\" />\n <rt-skeleton-wrapper rtElem=\"skeleton-line\" width=\"55%\" height=\"13px\" [isLoading]=\"true\" />\n <rt-skeleton-wrapper rtElem=\"skeleton-line\" width=\"56px\" height=\"10px\" [isLoading]=\"true\" />\n </div>\n }\n </div>\n } @else {\n <div\n #thread\n rtElem=\"thread\"\n qa-dataid=\"chat-thread\"\n [rtMod]=\"{ frozen: fetching() }\"\n [attr.aria-busy]=\"fetching() ? 'true' : null\"\n (scroll)=\"onThreadScroll()\">\n @for (m of messages(); track m.id) {\n <div\n rtElem=\"message-row\"\n qa-dataid=\"chat-message\"\n [attr.data-own]=\"m.own\"\n [attr.data-status]=\"m.status || null\"\n [attr.data-deleted]=\"m.deleted || null\"\n [rtMod]=\"{ own: m.own }\">\n @if (m.canDelete && !m.deleted) {\n @if (m.deleteConfirm) {\n <rt-icon-button\n rtElem=\"message-delete\"\n qa-dataid=\"chat-message-delete\"\n icon=\"trash\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatDeleteAria\"\n [tooltip]=\"t().chatDeleteTooltip\"\n [rtConfirm]=\"m.deleteConfirm\"\n (confirmed)=\"deleteMessage.emit(m)\" />\n } @else {\n <rt-icon-button\n rtElem=\"message-delete\"\n qa-dataid=\"chat-message-delete\"\n icon=\"trash\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatDeleteAria\"\n [tooltip]=\"t().chatDeleteTooltip\"\n (clicked)=\"deleteMessage.emit(m)\" />\n }\n }\n @if (messageActions()?.template; as actionsTpl) {\n @if (m | rtChatMessageHasActions: messageHasActions()) {\n <div rtElem=\"message-actions\" qa-dataid=\"chat-message-actions\">\n <rt-menu>\n <ng-container\n [ngTemplateOutlet]=\"actionsTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: m }\" />\n </rt-menu>\n </div>\n }\n }\n <div rtElem=\"message\" [rtMod]=\"{ own: m.own, deleted: m.deleted }\">\n <p rtElem=\"message-author\" qa-dataid=\"chat-message-author\">{{ m.author }}</p>\n @if (m.deleted) {\n <p rtElem=\"message-text\" qa-dataid=\"chat-message-deleted\" rtMod=\"deleted\">\n {{ t().chatMessageDeleted }}\n </p>\n } @else {\n @if (m.delta) {\n <rt-delta-view rtElem=\"message-text\" qa-dataid=\"chat-message-text\" [delta]=\"m.delta\" />\n } @else {\n <p rtElem=\"message-text\" qa-dataid=\"chat-message-text\">{{ m.text }}</p>\n }\n @if (m.attachments?.length) {\n @for (att of m.attachments; track att.id) {\n <span rtElem=\"message-attachment\" qa-dataid=\"chat-message-attachment\">\n <rt-file-card\n showDownload\n size=\"sm\"\n [name]=\"att.name || attachmentFallbackName()\"\n (download)=\"downloadAttachment.emit(att)\" />\n </span>\n }\n } @else if (m.fileUrl) {\n <span rtElem=\"message-attachment\" qa-dataid=\"chat-message-attachment\">\n <rt-file-card\n showDownload\n size=\"sm\"\n [name]=\"m.fileName || attachmentFallbackName()\"\n (download)=\"downloadFile.emit(m)\" />\n </span>\n }\n @if (m.deepLink) {\n <span rtElem=\"message-deeplink\">\n <a\n rtButton\n qa-dataid=\"chat-message-deeplink\"\n appearance=\"outlined\"\n icon=\"send\"\n rel=\"noopener\"\n target=\"_blank\"\n [href]=\"m.deepLink\"\n [label]=\"m.deepLinkLabel || t().uiSubscribeNotifications\"\n [attr.aria-label]=\"m.deepLinkLabel || t().uiSubscribeNotifications\"></a>\n </span>\n }\n }\n <span rtElem=\"message-footer\">\n <span rtElem=\"message-date\" qa-dataid=\"chat-message-date\">\n {{ m.createdAt | date: 'short' : undefined : locale() }}\n </span>\n @if (m.status) {\n <span\n rtElem=\"message-status\"\n qa-dataid=\"chat-message-status\"\n [attr.data-status]=\"m.status\"\n [rtMod]=\"{ status: m.status }\"\n [attr.aria-label]=\"m.status | rtChatStatusLabel\">\n @if (m.status === MessageStatus.Failed) {\n <rt-icon rtElem=\"status-icon\" name=\"exclamation-circle\" size=\"sm\" />\n <rt-icon-button\n qa-dataid=\"chat-message-retry\"\n icon=\"sync\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatRetryAria\"\n [tooltip]=\"t().chatRetryTooltip\"\n (clicked)=\"retryMessage.emit(m)\" />\n } @else {\n <!-- \u0427\u0430\u0441\u044B, \u043F\u043E\u043A\u0430 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0432 \u043F\u0443\u0442\u0438; \u0434\u0430\u043B\u044C\u0448\u0435 \u0434\u0432\u043E\u0439\u043D\u043E\u0439 \u0447\u0435\u043A, \u0446\u0432\u0435\u0442 \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E\n (\u0441\u0435\u0440\u044B\u0439/\u0441\u0438\u043D\u0438\u0439) \u0440\u0430\u0437\u043B\u0438\u0447\u0430\u0435\u0442 \u0434\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E \u0438 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E. -->\n <rt-icon\n rtElem=\"status-icon\"\n size=\"sm\"\n [name]=\"m.status === MessageStatus.Sending ? 'clock' : 'check-double'\" />\n }\n </span>\n }\n </span>\n </div>\n </div>\n } @empty {\n <rt-note qa-dataid=\"chat-empty-thread\">{{ t().chatEmptyThread }}</rt-note>\n }\n </div>\n }\n @if (!loading() && fetching()) {\n <div rtElem=\"thread-overlay\" qa-dataid=\"chat-thread-overlay\" role=\"status\" [attr.aria-label]=\"t().chatRefreshAria\">\n <rt-spinner diameter=\"32\" />\n </div>\n }\n </div>\n\n @if (canReply()) {\n @if (richComposer()) {\n <rt-message-composer\n rtElem=\"composer-rich\"\n qa-dataid=\"chat-composer-rich\"\n [placeholder]=\"placeholder()\"\n [accept]=\"accept()\"\n [attachments]=\"attachments()\"\n [formatting]=\"formatting()\"\n [toolbar]=\"formattingToolbar()\"\n [sending]=\"sending()\"\n [disabled]=\"loading() || fetching()\"\n [droppedFiles]=\"droppedFiles()\"\n (submitted)=\"onComposerSubmit($event)\" />\n } @else {\n <form rtElem=\"composer\" qa-dataid=\"chat-composer\" [formGroup]=\"replyForm\" (ngSubmit)=\"onSubmit()\">\n @if (attachments() && selectedFiles().length > 0) {\n <div rtElem=\"composer-files\" qa-dataid=\"chat-composer-files\">\n @for (file of selectedFiles(); track $index) {\n <rt-file-card\n showRemove\n qa-dataid=\"chat-composer-file\"\n size=\"sm\"\n [name]=\"file.name\"\n [sizeBytes]=\"file.size\"\n [disabled]=\"composerDisabled()\"\n (removed)=\"removeFile($index)\" />\n }\n </div>\n }\n <div rtElem=\"composer-field\" [rtMod]=\"{ attach: attachments() }\">\n @if (attachments()) {\n <input\n #composerFileEl\n qa-dataid=\"chat-composer-file-input\"\n type=\"file\"\n hidden\n multiple\n [attr.accept]=\"accept() || null\"\n (change)=\"onFilesPicked($event)\" />\n <rt-icon-button\n qa-dataid=\"chat-composer-attach\"\n icon=\"paperclip\"\n variant=\"ghost\"\n [ariaLabel]=\"t().chatAttachAria\"\n [tooltip]=\"t().chatAttachTooltip\"\n [disabled]=\"composerDisabled()\"\n (clicked)=\"openFilePicker()\" />\n }\n <textarea\n rtElem=\"composer-input\"\n qa-dataid=\"chat-composer-input\"\n cdkTextareaAutosize\n formControlName=\"message\"\n cdkAutosizeMinRows=\"1\"\n cdkAutosizeMaxRows=\"4\"\n [attr.aria-label]=\"t().chatComposerAria\"\n [placeholder]=\"placeholder()\"\n (keydown)=\"onComposerKeydown($event)\"></textarea>\n </div>\n <button\n rtButton\n rtElem=\"composer-send\"\n qa-dataid=\"chat-composer-send\"\n type=\"submit\"\n theme=\"primary\"\n appearance=\"filled\"\n [label]=\"t().chatSendLabel\"\n [attr.aria-label]=\"t().chatSendAria\"\n [loading]=\"sending()\"\n [disabled]=\"!canSend()\"></button>\n </form>\n }\n } @else if (replyBlockReason()) {\n <rt-note rtElem=\"reply-block\" qa-dataid=\"chat-reply-block\">{{ replyBlockReason() }}</rt-note>\n }\n\n <!-- \u0421\u043B\u043E\u0442 \u0434\u043E\u043C\u0435\u043D\u043D\u044B\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F \u043F\u043E\u0434 \u043A\u043E\u043C\u043F\u043E\u0437\u0435\u0440\u043E\u043C: \u0432 fullscreen\n \u043F\u043E\u043F\u0430\u0434\u0430\u0435\u0442 \u0432\u043D\u0443\u0442\u0440\u044C overlay, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043A\u043D\u043E\u043F\u043A\u0438 \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u0432\u0438\u0434\u043D\u044B. -->\n <div rtElem=\"footer-slot\" qa-dataid=\"chat-footer\" [rtMod]=\"{ blocked: fetching() }\" [attr.inert]=\"fetching() ? '' : null\">\n <ng-content select=\"[rtChatFooter]\" />\n </div>\n }\n </rt-file-drop>\n</ng-container>\n", styles: ["@layer rt-kit.components{.rt-chat{--rt-chat-bubble-radius: var(--rt-radius-lg);--rt-chat-message-border-width: var(--rt-border-width-thin);--rt-chat-transition-duration: var(--rt-duration-fast);display:flex;flex-direction:column}.rt-chat>.rt-file-drop{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--rt-space-4);min-block-size:0}.rt-chat--expanded{position:fixed;z-index:var(--rt-z-modal);box-sizing:border-box;padding:var(--rt-space-md);background:var(--rt-color-bg-surface);inset:0}.rt-chat--expanded>.rt-file-drop{block-size:100%}@media(width>=1441px){.rt-chat--expanded>.rt-file-drop{inline-size:1000px;margin-inline:auto;max-inline-size:100%}}.rt-chat--expanded .rt-chat__thread-viewport{flex:1 1 auto;min-block-size:0}.rt-chat--expanded .rt-chat__thread{max-height:none}.rt-chat--fill{block-size:100%}.rt-chat--fill .rt-chat__thread-viewport{flex:1 1 auto;min-block-size:0}.rt-chat--fill .rt-chat__thread{max-height:none}.rt-chat__title{margin:0;color:var(--rt-color-text-primary);font-family:var(--rt-font-family-sans);font-size:var(--rt-text-md);font-weight:var(--rt-font-weight-bold);letter-spacing:var(--rt-letter-spacing-wide);line-height:var(--rt-leading-snug)}.rt-chat__thread-viewport{position:relative;display:flex;flex-direction:column;contain:inline-size;min-block-size:0}.rt-chat__thread{display:flex;max-height:280px;flex:1 1 auto;flex-direction:column;gap:var(--rt-space-4);min-block-size:0;overflow-y:auto}.rt-chat__thread--frozen{overflow-y:hidden}.rt-chat__thread-overlay{position:absolute;z-index:1;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--rt-color-bg-surface) 60%,transparent);inset:0}.rt-chat__footer-slot{display:flex;flex-direction:column}.rt-chat__footer-slot:empty{display:none}.rt-chat__footer-slot--blocked{opacity:.6;pointer-events:none}.rt-chat__message-row{display:flex;max-width:min(80%,480px);align-items:center;align-self:flex-start;gap:var(--rt-space-2)}.rt-chat__message-row--own{align-self:flex-end}.rt-chat__message-row:hover .rt-chat__message-delete,.rt-chat__message-row:focus-within .rt-chat__message-delete,.rt-chat__message-row:hover .rt-chat__message-actions,.rt-chat__message-row:focus-within .rt-chat__message-actions{opacity:1;pointer-events:auto}.rt-chat__message{display:flex;min-width:0;flex:0 1 auto;flex-direction:column;padding:var(--rt-space-2);border-radius:var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) 0;background:var(--rt-chat-bubble-in-bg);gap:var(--rt-space-2)}.rt-chat__message--own{border-radius:var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) 0 var(--rt-chat-bubble-radius);background:var(--rt-chat-bubble-own-bg)}.rt-chat__message--deleted{border:var(--rt-chat-message-border-width) dashed var(--rt-color-border-default);background:transparent}.rt-chat__message-author{margin:0;color:var(--rt-chat-bubble-in-author);font-size:var(--rt-text-2xs);font-weight:var(--rt-font-weight-bold);letter-spacing:var(--rt-letter-spacing-wide);line-height:var(--rt-leading-snug)}.rt-chat__message--own .rt-chat__message-author{color:var(--rt-chat-bubble-own-author)}.rt-chat__message-text{margin:0;color:var(--rt-color-text-primary);font-size:var(--rt-text-xs);font-weight:var(--rt-font-weight-regular);line-height:var(--rt-leading-snug);white-space:pre-wrap}.rt-chat__message-text--deleted{color:var(--rt-color-text-muted);font-style:italic}.rt-chat__message-delete,.rt-chat__message-actions{flex:0 0 auto;opacity:0;pointer-events:none;transition:opacity var(--rt-chat-transition-duration) ease}.rt-chat__message-attachment{display:block}.rt-chat__message-footer{display:flex;align-items:center;gap:var(--rt-space-1)}.rt-chat__message-date{color:var(--rt-chat-bubble-date);font-size:var(--rt-text-xs);line-height:var(--rt-leading-none)}.rt-chat__message-status{display:inline-flex;align-items:center;color:var(--rt-color-text-muted);gap:var(--rt-space-0-5)}.rt-chat__message-status--status--read{color:var(--rt-chat-status-read)}.rt-chat__message-status--status--failed{color:var(--rt-color-state-danger)}.rt-chat__status-icon{font-size:var(--rt-text-sm)}.rt-chat__message-skeleton{display:flex;max-width:min(80%,480px);flex-direction:column;align-self:flex-start;padding:var(--rt-space-2);border-radius:var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) 0;background:var(--rt-chat-bubble-in-bg);gap:var(--rt-space-1)}.rt-chat__message-skeleton--own{align-self:flex-end;border-radius:var(--rt-chat-bubble-radius) var(--rt-chat-bubble-radius) 0 var(--rt-chat-bubble-radius);background:var(--rt-chat-bubble-own-bg)}.rt-chat__skeleton-line{display:block}.rt-chat__message-deeplink{display:flex;justify-content:flex-start;margin-top:var(--rt-space-1)}.rt-chat__composer,.rt-chat__composer-rich,.rt-chat__reply-block{margin-top:var(--rt-space-4)}.rt-chat__composer{display:flex;flex-direction:column;gap:var(--rt-space-2)}.rt-chat__composer-files{display:flex;flex-wrap:wrap;gap:var(--rt-space-2)}.rt-chat__composer-field{display:flex;box-sizing:border-box;align-items:center;border:var(--rt-input-border-width) solid var(--rt-input-color-border);border-radius:var(--rt-input-radius);background-color:var(--rt-input-color-bg);gap:var(--rt-space-1);min-block-size:var(--rt-input-height);padding-inline:var(--rt-input-padding-x);transition:var(--rt-input-transition)}.rt-chat__composer-field:focus-within{border-color:var(--rt-input-color-border-focus);box-shadow:var(--rt-input-focus-shadow)}.rt-chat__composer-field--attach{padding-inline-start:var(--rt-space-1)}.rt-chat__composer-input{box-sizing:content-box;flex:1 1 auto;border:0;background:transparent;color:var(--rt-input-color-text);font-family:inherit;font-size:var(--rt-input-font-size);inline-size:100%;line-height:var(--rt-textarea-line-height);padding-block:var(--rt-space-2);resize:none}.rt-chat__composer-input::placeholder{color:var(--rt-input-color-text-placeholder);opacity:1}.rt-chat__composer-input:focus-visible{outline:none}.rt-chat__composer-input:disabled{cursor:not-allowed}.rt-chat__composer-send{inline-size:100%}}\n"] }]
873
+ }], ctorParameters: () => [], propDecorators: { thread: [{ type: i0.ViewChild, args: ['thread', { isSignal: true }] }], composerFileEl: [{ type: i0.ViewChild, args: ['composerFileEl', { isSignal: true }] }], messageActions: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RtChatMessageActionsDirective), { isSignal: true }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], fetching: [{ type: i0.Input, args: [{ isSignal: true, alias: "fetching", required: false }] }], canReply: [{ type: i0.Input, args: [{ isSignal: true, alias: "canReply", required: false }] }], replyBlockReason: [{ type: i0.Input, args: [{ isSignal: true, alias: "replyBlockReason", required: false }] }], sending: [{ type: i0.Input, args: [{ isSignal: true, alias: "sending", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], emptyHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyHint", required: false }] }], hasThread: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasThread", required: false }] }], attachments: [{ type: i0.Input, args: [{ isSignal: true, alias: "attachments", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], richComposer: [{ type: i0.Input, args: [{ isSignal: true, alias: "richComposer", required: false }] }], formatting: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatting", required: false }] }], formattingToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "formattingToolbar", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], showRefresh: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRefresh", required: false }] }], showExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExpand", required: false }] }], send: [{ type: i0.Output, args: ["send"] }], refresh: [{ type: i0.Output, args: ["refresh"] }], downloadFile: [{ type: i0.Output, args: ["downloadFile"] }], downloadAttachment: [{ type: i0.Output, args: ["downloadAttachment"] }], expandedChange: [{ type: i0.Output, args: ["expandedChange"] }], deleteMessage: [{ type: i0.Output, args: ["deleteMessage"] }], retryMessage: [{ type: i0.Output, args: ["retryMessage"] }], messageHasActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageHasActions", required: false }] }] } });
874
+
875
+ /**
876
+ * Статус уже сохранённого своего сообщения: сервер знает только, дошло ли оно до
877
+ * собеседника. Часы и ошибка — состояния клиентской очереди, сюда не попадают.
878
+ */
879
+ function deliveryStatus(read) {
880
+ return read ? ERtChatMessageStatus.Read : ERtChatMessageStatus.Sent;
881
+ }
882
+ /**
883
+ * Неподтверждённое сообщение → строка ленты. Показывается сразу после отправки:
884
+ * ждать ответа сервера, глядя в пустоту, незачем ни одной из сторон.
885
+ *
886
+ * Скрыть такое сообщение нельзя: на сервере его ещё нет, и удалять нечего.
887
+ */
888
+ function pendingToRtMessage(pending, authorLabel) {
889
+ return {
890
+ id: pending.tempId,
891
+ tempId: pending.tempId,
892
+ author: authorLabel,
893
+ own: true,
894
+ text: pending.text,
895
+ delta: pending.delta ?? null,
896
+ createdAt: pending.createdAt,
897
+ status: pending.status,
898
+ canDelete: false,
899
+ attachments: pending.files.map((file, index) => ({
900
+ id: index,
901
+ name: file.name,
902
+ // Файл ещё не на сервере — скачивать нечего, карточка только именует.
903
+ publicId: '',
904
+ })),
905
+ };
906
+ }
907
+ /**
908
+ * Собрать `IRtChat.Message` из доменного сообщения. Общие поля (текст, delta,
909
+ * дата, вложения, файл) берутся из контракта; автор/own/system/имя файла —
910
+ * из опций. Ключ `id` — `tempId ?? id` в оптимистичном режиме (пока сервер не
911
+ * подтвердил), иначе серверный `id`.
912
+ */
913
+ function toRtMessage(message, options) {
914
+ const optimistic = options.optimistic ?? true;
915
+ const fileNameFromUrlOrNull = message.fileUrl ? fileNameFromUrl(message.fileUrl) : null;
916
+ return {
917
+ id: optimistic ? (message.tempId ?? message.id) : message.id,
918
+ tempId: optimistic ? message.tempId : undefined,
919
+ status: optimistic ? message.status : undefined,
920
+ author: options.roleLabel(message),
921
+ own: options.isOwn(message),
922
+ system: options.isSystem?.(message) ?? false,
923
+ text: message.message,
924
+ delta: message.messageDelta,
925
+ createdAt: message.createdAt,
926
+ fileUrl: message.fileUrl,
927
+ fileName: options.fileName ? options.fileName(message) : fileNameFromUrlOrNull,
928
+ attachments: message.attachments.map((attachment) => ({
929
+ id: attachment.id,
930
+ name: attachment.name,
931
+ publicId: attachment.publicId,
932
+ })),
933
+ };
934
+ }
935
+
936
+ /*
937
+ * Вход @rt-tools/ui-kit-v2/rich-editor — компоненты, которые работают на библиотеке quill: редактор
938
+ * текста, поле ввода сообщения и чат. Они стоят отдельным входом, чтобы приложение без них
939
+ * собиралось без quill: из основного входа сборщик разрешал бы import('quill') всегда.
940
+ */
941
+
942
+ /**
943
+ * Generated bundle index. Do not edit.
944
+ */
945
+
946
+ export { ERtChatMessageStatus, RtChatComponent, RtChatMessageActionsDirective, RtChatMessageHasActionsPipe, RtChatStatusLabelPipe, RtMessageComposerComponent, RtRichEditorComponent, deliveryStatus, messageHasAvailableActions, pendingToRtMessage, toRtMessage };
947
+ //# sourceMappingURL=rt-tools-ui-kit-v2-rich-editor.mjs.map