@rt-tools/ui-kit-v2 0.35.0 → 0.36.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.
- package/fesm2022/rt-tools-ui-kit-v2-action-bar.mjs +1 -0
- package/fesm2022/rt-tools-ui-kit-v2-action-bar.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-ai-run-status.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-ai-run-status.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-autocomplete.mjs +1 -0
- package/fesm2022/rt-tools-ui-kit-v2-autocomplete.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-breadcrumbs.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-breadcrumbs.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-color-palette.mjs +1 -0
- package/fesm2022/rt-tools-ui-kit-v2-color-palette.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-data-table.mjs +4 -2
- package/fesm2022/rt-tools-ui-kit-v2-data-table.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-date-picker.mjs +1 -0
- package/fesm2022/rt-tools-ui-kit-v2-date-picker.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-date-range.mjs +1 -0
- package/fesm2022/rt-tools-ui-kit-v2-date-range.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-draggable-tree.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-draggable-tree.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-dynamic-selector.mjs +5 -2
- package/fesm2022/rt-tools-ui-kit-v2-dynamic-selector.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-field.mjs +1 -0
- package/fesm2022/rt-tools-ui-kit-v2-field.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-file-card.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-file-card.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-hybrid-tree-selector.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-hybrid-tree-selector.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-hybrid-tree.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-hybrid-tree.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-icon-button.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-icon-button.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-image-upload.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-image-upload.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-inline-edit.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-inline-edit.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-multiselect.mjs +1 -0
- package/fesm2022/rt-tools-ui-kit-v2-multiselect.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-page-header.mjs +10 -6
- package/fesm2022/rt-tools-ui-kit-v2-page-header.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-popover.mjs +22 -2
- package/fesm2022/rt-tools-ui-kit-v2-popover.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs +36 -5
- package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-scroll-area.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-scroll-area.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-select.mjs +1 -0
- package/fesm2022/rt-tools-ui-kit-v2-select.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-side-menu.mjs +4 -2
- package/fesm2022/rt-tools-ui-kit-v2-side-menu.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-speed-menu.mjs +3 -1
- package/fesm2022/rt-tools-ui-kit-v2-speed-menu.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-split-button.mjs +1 -0
- package/fesm2022/rt-tools-ui-kit-v2-split-button.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-table.mjs +3 -1
- package/fesm2022/rt-tools-ui-kit-v2-table.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-tabs.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-tabs.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-tag.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-tag.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-thread-list.mjs +53 -5
- package/fesm2022/rt-tools-ui-kit-v2-thread-list.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-toggle-button-group.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-toggle-button-group.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-tooltip.mjs +37 -0
- package/fesm2022/rt-tools-ui-kit-v2-tooltip.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-tree-selector.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-tree-selector.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-tree.mjs +2 -1
- package/fesm2022/rt-tools-ui-kit-v2-tree.mjs.map +1 -1
- package/fesm2022/rt-tools-ui-kit-v2-usage-ring.mjs +4 -3
- package/fesm2022/rt-tools-ui-kit-v2-usage-ring.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/components/page-header/rt-page-header-mobile.scss +6 -0
- package/src/lib/components/popover/CONTEXT.md +4 -0
- package/src/lib/components/thread-list/rt-thread-list.component.scss +7 -0
- package/src/lib/components/tooltip/CONTEXT.md +4 -0
- package/src/rich-editor/lib/components/chat/rt-chat.component.scss +31 -1
- package/types/rt-tools-ui-kit-v2-popover.d.ts +11 -0
- package/types/rt-tools-ui-kit-v2-rich-editor.d.ts +7 -0
- package/types/rt-tools-ui-kit-v2-thread-list.d.ts +7 -1
- package/types/rt-tools-ui-kit-v2-tooltip.d.ts +12 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rt-tools-ui-kit-v2-popover.mjs","names":[],"sources":["../../../projects/ui-kit-v2/src/lib/components/popover/rt-popover.directive.ts"],"sourcesContent":["import { BooleanInput, NumberInput } from '@angular/cdk/coercion';\nimport {\n ConnectedPosition,\n FlexibleConnectedPositionStrategy,\n Overlay,\n OverlayConfig,\n OverlayRef,\n OverlaySizeConfig,\n ScrollStrategyOptions,\n} from '@angular/cdk/overlay';\nimport { TemplatePortal } from '@angular/cdk/portal';\nimport {\n booleanAttribute,\n DestroyRef,\n Directive,\n ElementRef,\n inject,\n input,\n InputSignal,\n InputSignalWithTransform,\n numberAttribute,\n OnDestroy,\n output,\n OutputEmitterRef,\n Signal,\n signal,\n TemplateRef,\n ViewContainerRef,\n WritableSignal,\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\n\nimport { map, merge, Observable, Subject, switchMap } from 'rxjs';\n\nimport { carryThemeScope, materialPresetClassesOf } from '@rt-tools/ui-kit-v2/core';\nimport { IRtPopover } from './rt-popover.model';\n\nconst HOVER_CLOSE_DELAY_MS: number = 100;\n\n/**\n * Высота, ниже которой ужимать панель под вьюпорт бессмысленно: в такой полосе не\n * видно и одного пункта. Нужна только режиму `fitViewport` — по ней CDK решает,\n * что свободного места под триггером хватает.\n */\nconst FIT_VIEWPORT_MIN_HEIGHT_PX: number = 120;\n\n/**\n * Переиспользуемый popover-примитив поверх CDK Overlay. Привязывает плавающую\n * панель (`<ng-template>`) к host-элементу. Движок — CDK Overlay\n * (`flexibleConnectedTo`): авто-flip у края viewport, reposition при скролле,\n * outside-click и Escape закрытие из коробки — без ручного `position: absolute`\n * в `:host` (тот резался `overflow: hidden` предками: таблицы, aside, карточки).\n *\n * Триггеры:\n * - `click` (default) — toggle по клику на host; outside-click закрывает.\n * - `hover` — открытие на `mouseenter`, закрытие с grace-периодом на `mouseleave`\n * (наведение на панель удерживает её открытой).\n * - `manual` — авто-listener'ы не вешаются; host сам зовёт `open()/close()`.\n * Используется form-control'ами с собственным жестом (autocomplete — открытие\n * по набору текста).\n *\n * `open()/close()/toggle()/isOpen()` публичны во ВСЕХ режимах — `trigger` лишь\n * определяет авто-listener'ы. `width: 'trigger'` синхронизирует ширину панели с\n * host'ом (dropdown'ы), `'trigger-min'` не даёт панели стать уже host'а и дальше пускает\n * её по содержимому, `'auto'` — content-sized (тултипы).\n */\n@Directive({\n selector: '[rtPopover]',\n exportAs: 'rtPopover',\n host: {\n '(click)': 'onHostClick()',\n '(mouseenter)': 'onHostMouseEnter()',\n '(mouseleave)': 'onHostMouseLeave($event)',\n },\n})\nexport class RtPopoverDirective implements OnDestroy {\n readonly #overlay: Overlay = inject(Overlay);\n readonly #elementRef: ElementRef<HTMLElement> = inject<ElementRef<HTMLElement>>(ElementRef);\n readonly #viewContainerRef: ViewContainerRef = inject(ViewContainerRef);\n readonly #scrollStrategies: ScrollStrategyOptions = inject(ScrollStrategyOptions);\n readonly #destroyRef: DestroyRef = inject(DestroyRef);\n\n readonly #overlayCreatedSource: Subject<OverlayRef> = new Subject<OverlayRef>();\n\n #overlayRef: OverlayRef | null = null;\n #positionStrategy: FlexibleConnectedPositionStrategy | null = null;\n #hoverCloseTimer: ReturnType<typeof setTimeout> | null = null;\n\n protected readonly isOpenState: WritableSignal<boolean> = signal<boolean>(false);\n\n public readonly template: InputSignal<TemplateRef<unknown> | null> = input<TemplateRef<unknown> | null>(null, { alias: 'rtPopover' });\n\n public readonly trigger: InputSignal<IRtPopover.Trigger> = input<IRtPopover.Trigger>('click', {\n alias: 'rtPopoverTrigger',\n });\n\n public readonly width: InputSignal<IRtPopover.Width> = input<IRtPopover.Width>('auto', {\n alias: 'rtPopoverWidth',\n });\n\n /** `end` — панель прижата к правому краю host'а (триггеры у правого края viewport). */\n public readonly align: InputSignal<IRtPopover.Align> = input<IRtPopover.Align>('start', {\n alias: 'rtPopoverAlign',\n });\n\n /**\n * Панель обязана уместиться во вьюпорт: не влезающая по ширине сдвигается от\n * края экрана внутрь, не влезающая по высоте ужимается до свободного места под\n * триггером, и содержимое в ней прокручивается.\n *\n * По умолчанию выключено: панель стоит ровно там, куда её ставит привязка к\n * триггеру, даже если часть её ушла за экран. Включать там, где панель широкая\n * или высокая настолько, что привязка сама по себе её не удерживает, — иначе\n * пункты навигации остаются за краем экрана и недостижимы.\n */\n public readonly fitViewport: InputSignalWithTransform<boolean, BooleanInput> = input<boolean, BooleanInput>(false, {\n alias: 'rtPopoverFitViewport',\n transform: booleanAttribute,\n });\n\n public readonly context: InputSignal<unknown> = input<unknown>(null, {\n alias: 'rtPopoverContext',\n });\n\n public readonly panelClass: InputSignal<string> = input<string>('', {\n alias: 'rtPopoverPanelClass',\n });\n\n public readonly disabled: InputSignalWithTransform<boolean, BooleanInput> = input<boolean, BooleanInput>(false, {\n alias: 'rtPopoverDisabled',\n transform: booleanAttribute,\n });\n\n public readonly offsetY: InputSignalWithTransform<number, NumberInput> = input<number, NumberInput>(4, {\n alias: 'rtPopoverOffsetY',\n transform: numberAttribute,\n });\n\n public readonly offsetX: InputSignalWithTransform<number, NumberInput> = input<number, NumberInput>(0, {\n alias: 'rtPopoverOffsetX',\n transform: numberAttribute,\n });\n\n /**\n * Свои места панели вокруг host'а: первое главное, остальные — запасные у края экрана. `null` —\n * прежние два: под host'ом и над ним. Заданные читаются при каждом открытии.\n */\n public readonly positions: InputSignal<readonly ConnectedPosition[] | null> = input<readonly ConnectedPosition[] | null>(null, {\n alias: 'rtPopoverPositions',\n });\n\n public readonly opened: OutputEmitterRef<void> = output<void>({ alias: 'rtPopoverOpened' });\n\n public readonly closed: OutputEmitterRef<void> = output<void>({ alias: 'rtPopoverClosed' });\n\n public readonly isOpen: Signal<boolean> = this.isOpenState.asReadonly();\n\n constructor() {\n // Outside-click / ESC подписки на единожды создаваемый overlay живут в\n // одном конструкторном стриме: #ensureOverlay эмитит OverlayRef после\n // создания (ровно один раз за жизнь директивы), стрим живёт до destroy.\n this.#overlayCreatedSource\n .pipe(\n switchMap((overlayRef: OverlayRef): Observable<() => void> =>\n merge(\n overlayRef.outsidePointerEvents().pipe(\n map((event: MouseEvent): (() => void) => (): void => {\n // Клик по host в режиме click обрабатывает onHostClick (toggle) —\n // не закрываем здесь, иначе двойной toggle (close + open).\n if (this.#elementRef.nativeElement.contains(event.target as Node)) {\n return;\n }\n // Клик по вложенной панели — не клик снаружи: закрытие\n // разрушило бы её вместе с нашей до того, как выбор\n // дойдёт до обработчика\n if (this.#isNestedOverlay(event.target)) {\n return;\n }\n this.close();\n })\n ),\n overlayRef.keydownEvents().pipe(\n map((event: KeyboardEvent): (() => void) => (): void => {\n if (event.key === 'Escape') {\n event.preventDefault();\n this.close();\n }\n })\n )\n )\n ),\n takeUntilDestroyed(this.#destroyRef)\n )\n .subscribe((handleCloseIntent: () => void): void => handleCloseIntent());\n }\n\n public ngOnDestroy(): void {\n this.#clearHoverTimer();\n this.#disposeOverlay();\n }\n\n public open(): void {\n if (this.disabled() || this.isOpenState() || this.template() === null) {\n return;\n }\n const overlayRef: OverlayRef = this.#ensureOverlay();\n const positions: readonly ConnectedPosition[] | null = this.positions();\n if (positions !== null) {\n this.#positionStrategy?.withPositions([...positions]);\n }\n const hostWidth: number = this.#elementRef.nativeElement.offsetWidth;\n const sizeByWidth: Record<IRtPopover.Width, OverlaySizeConfig> = {\n trigger: { width: hostWidth },\n 'trigger-min': { minWidth: hostWidth },\n auto: {},\n };\n overlayRef.updateSize(sizeByWidth[this.width()]);\n // Тема куска `rtTheme` вокруг host'а едет на коробку панели: панель лежит в конце\n // страницы, и без этого список из тёмной карточки рисовался темой страницы.\n carryThemeScope(overlayRef.overlayElement, this.#elementRef.nativeElement);\n const portal: TemplatePortal<unknown> = new TemplatePortal(this.template() as TemplateRef<unknown>, this.#viewContainerRef, {\n $implicit: this.context(),\n });\n overlayRef.attach(portal);\n this.#releasePaneMinHeight(overlayRef);\n this.isOpenState.set(true);\n this.opened.emit();\n }\n\n public close(): void {\n if (!this.isOpenState()) {\n return;\n }\n this.#overlayRef?.detach();\n this.isOpenState.set(false);\n this.closed.emit();\n }\n\n public toggle(): void {\n if (this.isOpenState()) {\n this.close();\n } else {\n this.open();\n }\n }\n\n // --- авто-triggers (HostListener'ы навешиваются на host безусловно, но\n // проверяют активный режим — это дешевле, чем условная подписка) ---\n\n protected onHostClick(): void {\n if (this.trigger() !== 'click' || this.disabled()) {\n return;\n }\n this.toggle();\n }\n\n protected onHostMouseEnter(): void {\n if (this.trigger() !== 'hover') {\n return;\n }\n this.#clearHoverTimer();\n this.open();\n }\n\n protected onHostMouseLeave(event: MouseEvent): void {\n if (this.trigger() !== 'hover') {\n return;\n }\n if (this.#isNestedOverlay(event.relatedTarget)) {\n return;\n }\n this.#scheduleHoverClose();\n }\n\n #ensureOverlay(): OverlayRef {\n if (this.#overlayRef !== null) {\n return this.#overlayRef;\n }\n const alignX: 'start' | 'end' = this.align();\n const positions: ConnectedPosition[] = [\n {\n originX: alignX,\n originY: 'bottom',\n overlayX: alignX,\n overlayY: 'top',\n offsetX: this.offsetX(),\n offsetY: this.offsetY(),\n },\n {\n originX: alignX,\n originY: 'top',\n overlayX: alignX,\n overlayY: 'bottom',\n offsetX: this.offsetX(),\n offsetY: -this.offsetY(),\n },\n ];\n // Сдвиг к краю экрана и ужатие по высоте включаются только по просьбе\n // потребителя: панель, которая и так помещается, обязана стоять у своего\n // триггера — меню строки таблицы, подтверждение, меню профиля.\n const fitViewport: boolean = this.fitViewport();\n const positionStrategy: FlexibleConnectedPositionStrategy = this.#overlay\n .position()\n .flexibleConnectedTo(this.#elementRef)\n .withPush(fitViewport)\n .withFlexibleDimensions(fitViewport)\n // Отступ от края экрана равен отступу от триггера: свободное место\n // считается без него, и ужатая по высоте панель свисала бы за нижний\n // край ровно на этот отступ — вместе со своей рамкой.\n .withViewportMargin(fitViewport ? Math.abs(this.offsetY()) : 0)\n .withPositions(positions);\n this.#positionStrategy = positionStrategy;\n\n const config: OverlayConfig = new OverlayConfig({\n positionStrategy,\n scrollStrategy: this.#scrollStrategies.reposition(),\n hasBackdrop: false,\n panelClass: this.#resolvePanelClasses(),\n // Порог, ниже которого свободного места под триггером считается\n // недостаточно и панель не ужимается, а сдвигается целиком.\n minHeight: fitViewport ? FIT_VIEWPORT_MIN_HEIGHT_PX : undefined,\n });\n const overlayRef: OverlayRef = this.#overlay.create(config);\n\n // Hover-bridge: при переходе курсора с host'а на панель отложенный close\n // (grace-период) гасится; уход с панели планирует его заново. Без этого\n // hover-popover закрывался бы по таймеру, как только мышь покидает host.\n // Listeners живут на overlay-элементе и умирают вместе с ним в dispose().\n overlayRef.overlayElement.addEventListener('mouseenter', (): void => {\n if (this.trigger() === 'hover') {\n this.#clearHoverTimer();\n }\n });\n overlayRef.overlayElement.addEventListener('mouseleave', (event: MouseEvent): void => {\n if (this.trigger() === 'hover' && !this.#isNestedOverlay(event.relatedTarget)) {\n this.#scheduleHoverClose();\n }\n });\n\n // Outside-click / ESC подписки объявлены один раз в конструкторе —\n // здесь только эмит созданного OverlayRef (см. #overlayCreatedSource-стрим).\n this.#overlayCreatedSource.next(overlayRef);\n\n this.#overlayRef = overlayRef;\n return overlayRef;\n }\n\n /**\n * Панель, открытая из нашей — список `rt-select`, календарь, вложенное меню.\n *\n * В DOM она лежит не внутри нашей панели, а рядом, отдельным оверлеем CDK.\n * Поэтому и уход курсора на неё, и клик по ней выглядят как «ушёл наружу»:\n * меню профиля закрывалось прямо под рукой, стоило потянуться к списку\n * языков, а выбранное значение до обработчика не доходило — панель со\n * `rt-select` разрушалась раньше.\n *\n * Вложенность определяется порядком в контейнере оверлеев: CDK добавляет их\n * по мере открытия, поэтому панель, открытая из нашей, всегда следует за\n * нашей. Соседний оверлей, открытый до нас, этой проверкой не удерживается —\n * клик по нему закрывает попап, как и любой клик снаружи.\n */\n #isNestedOverlay(target: EventTarget | null): boolean {\n const panel: HTMLElement | undefined = this.#overlayRef?.overlayElement;\n if (!panel || !(target instanceof Element)) {\n return false;\n }\n\n const nested: Element | null = target.closest('.cdk-overlay-pane');\n const container: HTMLElement | null = panel.parentElement;\n if (nested === null || nested === panel || container === null) {\n return false;\n }\n\n // `querySelectorAll` отдаёт узлы в порядке документа — им и меряется,\n // какая панель открыта позже\n const panes: readonly Element[] = Array.from(container.querySelectorAll('.cdk-overlay-pane'));\n\n return panes.indexOf(nested) > panes.indexOf(panel);\n }\n\n /**\n * Порог подгонки остаётся в настройке оверлея и уходит из стиля коробки.\n *\n * CDK читает порог из настройки, когда решает, сдвинуть панель или ужать. Но при `attach` и\n * при каждом `updateSize` он пишет то же число в стиль коробки, и короткая панель\n * растягивалась до порога: под двумя пунктами «Ещё» шапки стояла пустая полоса.\n */\n #releasePaneMinHeight(overlayRef: OverlayRef): void {\n overlayRef.overlayElement.style.minHeight = '';\n }\n\n #resolvePanelClasses(): string[] {\n const base: string = 'rt-popover-panel';\n const extra: string = this.panelClass().trim();\n const preset: string[] = materialPresetClassesOf(this.#elementRef.nativeElement);\n return extra ? [base, ...extra.split(/\\s+/), ...preset] : [base, ...preset];\n }\n\n #scheduleHoverClose(): void {\n this.#clearHoverTimer();\n this.#hoverCloseTimer = setTimeout((): void => this.close(), HOVER_CLOSE_DELAY_MS);\n }\n\n #clearHoverTimer(): void {\n if (this.#hoverCloseTimer !== null) {\n clearTimeout(this.#hoverCloseTimer);\n this.#hoverCloseTimer = null;\n }\n }\n\n #disposeOverlay(): void {\n this.#overlayRef?.dispose();\n this.#overlayRef = null;\n this.#positionStrategy = null;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rt-tools-ui-kit-v2-popover.mjs","names":[],"sources":["../../../projects/ui-kit-v2/src/lib/components/popover/rt-popover.directive.ts"],"sourcesContent":["import { BooleanInput, NumberInput } from '@angular/cdk/coercion';\nimport {\n ConnectedPosition,\n FlexibleConnectedPositionStrategy,\n Overlay,\n OverlayConfig,\n OverlayRef,\n OverlaySizeConfig,\n ScrollStrategyOptions,\n} from '@angular/cdk/overlay';\nimport { TemplatePortal } from '@angular/cdk/portal';\nimport {\n booleanAttribute,\n DestroyRef,\n Directive,\n ElementRef,\n inject,\n input,\n InputSignal,\n InputSignalWithTransform,\n numberAttribute,\n OnDestroy,\n output,\n OutputEmitterRef,\n Signal,\n signal,\n TemplateRef,\n ViewContainerRef,\n WritableSignal,\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\n\nimport { map, merge, Observable, Subject, switchMap } from 'rxjs';\n\nimport { carryThemeScope, materialPresetClassesOf } from '@rt-tools/ui-kit-v2/core';\nimport { IRtPopover } from './rt-popover.model';\n\nconst HOVER_CLOSE_DELAY_MS: number = 100;\n\n/**\n * Высота, ниже которой ужимать панель под вьюпорт бессмысленно: в такой полосе не\n * видно и одного пункта. Нужна только режиму `fitViewport` — по ней CDK решает,\n * что свободного места под триггером хватает.\n */\nconst FIT_VIEWPORT_MIN_HEIGHT_PX: number = 120;\n\n/**\n * Переиспользуемый popover-примитив поверх CDK Overlay. Привязывает плавающую\n * панель (`<ng-template>`) к host-элементу. Движок — CDK Overlay\n * (`flexibleConnectedTo`): авто-flip у края viewport, reposition при скролле,\n * outside-click и Escape закрытие из коробки — без ручного `position: absolute`\n * в `:host` (тот резался `overflow: hidden` предками: таблицы, aside, карточки).\n *\n * Триггеры:\n * - `click` (default) — toggle по клику на host; outside-click закрывает.\n * - `hover` — открытие на `mouseenter`, закрытие с grace-периодом на `mouseleave`\n * (наведение на панель удерживает её открытой).\n * - `manual` — авто-listener'ы не вешаются; host сам зовёт `open()/close()`.\n * Используется form-control'ами с собственным жестом (autocomplete — открытие\n * по набору текста).\n *\n * `open()/close()/toggle()/isOpen()` публичны во ВСЕХ режимах — `trigger` лишь\n * определяет авто-listener'ы. `width: 'trigger'` синхронизирует ширину панели с\n * host'ом (dropdown'ы), `'trigger-min'` не даёт панели стать уже host'а и дальше пускает\n * её по содержимому, `'auto'` — content-sized (тултипы).\n */\n@Directive({\n selector: '[rtPopover]',\n exportAs: 'rtPopover',\n host: {\n '(click)': 'onHostClick()',\n '(mouseenter)': 'onHostMouseEnter()',\n '(mouseleave)': 'onHostMouseLeave($event)',\n },\n})\nexport class RtPopoverDirective implements OnDestroy {\n readonly #overlay: Overlay = inject(Overlay);\n readonly #elementRef: ElementRef<HTMLElement> = inject<ElementRef<HTMLElement>>(ElementRef);\n readonly #viewContainerRef: ViewContainerRef = inject(ViewContainerRef);\n readonly #scrollStrategies: ScrollStrategyOptions = inject(ScrollStrategyOptions);\n readonly #destroyRef: DestroyRef = inject(DestroyRef);\n\n readonly #overlayCreatedSource: Subject<OverlayRef> = new Subject<OverlayRef>();\n\n #overlayRef: OverlayRef | null = null;\n #positionStrategy: FlexibleConnectedPositionStrategy | null = null;\n #hoverCloseTimer: ReturnType<typeof setTimeout> | null = null;\n\n protected readonly isOpenState: WritableSignal<boolean> = signal<boolean>(false);\n\n public readonly template: InputSignal<TemplateRef<unknown> | null> = input<TemplateRef<unknown> | null>(null, { alias: 'rtPopover' });\n\n public readonly trigger: InputSignal<IRtPopover.Trigger> = input<IRtPopover.Trigger>('click', {\n alias: 'rtPopoverTrigger',\n });\n\n public readonly width: InputSignal<IRtPopover.Width> = input<IRtPopover.Width>('auto', {\n alias: 'rtPopoverWidth',\n });\n\n /** `end` — панель прижата к правому краю host'а (триггеры у правого края viewport). */\n public readonly align: InputSignal<IRtPopover.Align> = input<IRtPopover.Align>('start', {\n alias: 'rtPopoverAlign',\n });\n\n /**\n * Панель обязана уместиться во вьюпорт: не влезающая по ширине сдвигается от\n * края экрана внутрь, не влезающая по высоте ужимается до свободного места под\n * триггером, и содержимое в ней прокручивается.\n *\n * По умолчанию выключено: панель стоит ровно там, куда её ставит привязка к\n * триггеру, даже если часть её ушла за экран. Включать там, где панель широкая\n * или высокая настолько, что привязка сама по себе её не удерживает, — иначе\n * пункты навигации остаются за краем экрана и недостижимы.\n */\n public readonly fitViewport: InputSignalWithTransform<boolean, BooleanInput> = input<boolean, BooleanInput>(false, {\n alias: 'rtPopoverFitViewport',\n transform: booleanAttribute,\n });\n\n public readonly context: InputSignal<unknown> = input<unknown>(null, {\n alias: 'rtPopoverContext',\n });\n\n public readonly panelClass: InputSignal<string> = input<string>('', {\n alias: 'rtPopoverPanelClass',\n });\n\n /**\n * Класс подложки под панелью. Пусто — подложки нет, и страница рядом с панелью остаётся\n * открытой. Заданный ставит подложку на весь экран: её вид задаёт класс, а нажатие по ней\n * закрывает панель так же, как нажатие снаружи. Читается при первом открытии.\n */\n public readonly backdropClass: InputSignal<string> = input<string>('', {\n alias: 'rtPopoverBackdropClass',\n });\n\n public readonly disabled: InputSignalWithTransform<boolean, BooleanInput> = input<boolean, BooleanInput>(false, {\n alias: 'rtPopoverDisabled',\n transform: booleanAttribute,\n });\n\n public readonly offsetY: InputSignalWithTransform<number, NumberInput> = input<number, NumberInput>(4, {\n alias: 'rtPopoverOffsetY',\n transform: numberAttribute,\n });\n\n public readonly offsetX: InputSignalWithTransform<number, NumberInput> = input<number, NumberInput>(0, {\n alias: 'rtPopoverOffsetX',\n transform: numberAttribute,\n });\n\n /**\n * Свои места панели вокруг host'а: первое главное, остальные — запасные у края экрана. `null` —\n * прежние два: под host'ом и над ним. Заданные читаются при каждом открытии.\n */\n public readonly positions: InputSignal<readonly ConnectedPosition[] | null> = input<readonly ConnectedPosition[] | null>(null, {\n alias: 'rtPopoverPositions',\n });\n\n public readonly opened: OutputEmitterRef<void> = output<void>({ alias: 'rtPopoverOpened' });\n\n public readonly closed: OutputEmitterRef<void> = output<void>({ alias: 'rtPopoverClosed' });\n\n public readonly isOpen: Signal<boolean> = this.isOpenState.asReadonly();\n\n constructor() {\n // Outside-click / ESC подписки на единожды создаваемый overlay живут в\n // одном конструкторном стриме: #ensureOverlay эмитит OverlayRef после\n // создания (ровно один раз за жизнь директивы), стрим живёт до destroy.\n this.#overlayCreatedSource\n .pipe(\n switchMap((overlayRef: OverlayRef): Observable<() => void> =>\n merge(\n overlayRef.outsidePointerEvents().pipe(\n map((event: MouseEvent): (() => void) => (): void => {\n // Клик по host в режиме click обрабатывает onHostClick (toggle) —\n // не закрываем здесь, иначе двойной toggle (close + open).\n if (this.#elementRef.nativeElement.contains(event.target as Node)) {\n return;\n }\n // Клик по вложенной панели — не клик снаружи: закрытие\n // разрушило бы её вместе с нашей до того, как выбор\n // дойдёт до обработчика\n if (this.#isNestedOverlay(event.target)) {\n return;\n }\n this.close();\n })\n ),\n // Подложка ловит нажатие раньше документа: снаружи оно уже не приходит\n overlayRef.backdropClick().pipe(map((): (() => void) => (): void => this.close())),\n overlayRef.keydownEvents().pipe(\n map((event: KeyboardEvent): (() => void) => (): void => {\n if (event.key === 'Escape') {\n event.preventDefault();\n this.close();\n }\n })\n )\n )\n ),\n takeUntilDestroyed(this.#destroyRef)\n )\n .subscribe((handleCloseIntent: () => void): void => handleCloseIntent());\n }\n\n public ngOnDestroy(): void {\n this.#clearHoverTimer();\n this.#disposeOverlay();\n }\n\n public open(): void {\n if (this.disabled() || this.isOpenState() || this.template() === null) {\n return;\n }\n const overlayRef: OverlayRef = this.#ensureOverlay();\n const positions: readonly ConnectedPosition[] | null = this.positions();\n if (positions !== null) {\n this.#positionStrategy?.withPositions([...positions]);\n }\n const hostWidth: number = this.#elementRef.nativeElement.offsetWidth;\n const sizeByWidth: Record<IRtPopover.Width, OverlaySizeConfig> = {\n trigger: { width: hostWidth },\n 'trigger-min': { minWidth: hostWidth },\n auto: {},\n };\n overlayRef.updateSize(sizeByWidth[this.width()]);\n // Тема куска `rtTheme` вокруг host'а едет на коробку панели: панель лежит в конце\n // страницы, и без этого список из тёмной карточки рисовался темой страницы.\n carryThemeScope(overlayRef.overlayElement, this.#elementRef.nativeElement);\n const portal: TemplatePortal<unknown> = new TemplatePortal(this.template() as TemplateRef<unknown>, this.#viewContainerRef, {\n $implicit: this.context(),\n });\n overlayRef.attach(portal);\n this.#releasePaneMinHeight(overlayRef);\n this.isOpenState.set(true);\n this.opened.emit();\n }\n\n public close(): void {\n if (!this.isOpenState()) {\n return;\n }\n this.#overlayRef?.detach();\n this.isOpenState.set(false);\n this.closed.emit();\n }\n\n public toggle(): void {\n if (this.isOpenState()) {\n this.close();\n } else {\n this.open();\n }\n }\n\n // --- авто-triggers (HostListener'ы навешиваются на host безусловно, но\n // проверяют активный режим — это дешевле, чем условная подписка) ---\n\n protected onHostClick(): void {\n if (this.trigger() !== 'click' || this.disabled()) {\n return;\n }\n this.toggle();\n }\n\n protected onHostMouseEnter(): void {\n if (this.trigger() !== 'hover') {\n return;\n }\n this.#clearHoverTimer();\n this.open();\n }\n\n protected onHostMouseLeave(event: MouseEvent): void {\n if (this.trigger() !== 'hover') {\n return;\n }\n if (this.#isNestedOverlay(event.relatedTarget)) {\n return;\n }\n this.#scheduleHoverClose();\n }\n\n #ensureOverlay(): OverlayRef {\n if (this.#overlayRef !== null) {\n return this.#overlayRef;\n }\n const alignX: 'start' | 'end' = this.align();\n const positions: ConnectedPosition[] = [\n {\n originX: alignX,\n originY: 'bottom',\n overlayX: alignX,\n overlayY: 'top',\n offsetX: this.offsetX(),\n offsetY: this.offsetY(),\n },\n {\n originX: alignX,\n originY: 'top',\n overlayX: alignX,\n overlayY: 'bottom',\n offsetX: this.offsetX(),\n offsetY: -this.offsetY(),\n },\n ];\n // Сдвиг к краю экрана и ужатие по высоте включаются только по просьбе\n // потребителя: панель, которая и так помещается, обязана стоять у своего\n // триггера — меню строки таблицы, подтверждение, меню профиля.\n const fitViewport: boolean = this.fitViewport();\n const positionStrategy: FlexibleConnectedPositionStrategy = this.#overlay\n .position()\n .flexibleConnectedTo(this.#elementRef)\n .withPush(fitViewport)\n .withFlexibleDimensions(fitViewport)\n // Отступ от края экрана равен отступу от триггера: свободное место\n // считается без него, и ужатая по высоте панель свисала бы за нижний\n // край ровно на этот отступ — вместе со своей рамкой.\n .withViewportMargin(fitViewport ? Math.abs(this.offsetY()) : 0)\n .withPositions(positions);\n this.#positionStrategy = positionStrategy;\n\n const config: OverlayConfig = new OverlayConfig({\n positionStrategy,\n scrollStrategy: this.#scrollStrategies.reposition(),\n hasBackdrop: this.backdropClass().trim() !== '',\n backdropClass: this.backdropClass().trim() || undefined,\n panelClass: this.#resolvePanelClasses(),\n // Порог, ниже которого свободного места под триггером считается\n // недостаточно и панель не ужимается, а сдвигается целиком.\n minHeight: fitViewport ? FIT_VIEWPORT_MIN_HEIGHT_PX : undefined,\n });\n const overlayRef: OverlayRef = this.#overlay.create(config);\n\n // Hover-bridge: при переходе курсора с host'а на панель отложенный close\n // (grace-период) гасится; уход с панели планирует его заново. Без этого\n // hover-popover закрывался бы по таймеру, как только мышь покидает host.\n // Listeners живут на overlay-элементе и умирают вместе с ним в dispose().\n overlayRef.overlayElement.addEventListener('mouseenter', (): void => {\n if (this.trigger() === 'hover') {\n this.#clearHoverTimer();\n }\n });\n overlayRef.overlayElement.addEventListener('mouseleave', (event: MouseEvent): void => {\n if (this.trigger() === 'hover' && !this.#isNestedOverlay(event.relatedTarget)) {\n this.#scheduleHoverClose();\n }\n });\n\n // Outside-click / ESC подписки объявлены один раз в конструкторе —\n // здесь только эмит созданного OverlayRef (см. #overlayCreatedSource-стрим).\n this.#overlayCreatedSource.next(overlayRef);\n\n this.#overlayRef = overlayRef;\n return overlayRef;\n }\n\n /**\n * Панель, открытая из нашей — список `rt-select`, календарь, вложенное меню.\n *\n * В DOM она лежит не внутри нашей панели, а рядом, отдельным оверлеем CDK.\n * Поэтому и уход курсора на неё, и клик по ней выглядят как «ушёл наружу»:\n * меню профиля закрывалось прямо под рукой, стоило потянуться к списку\n * языков, а выбранное значение до обработчика не доходило — панель со\n * `rt-select` разрушалась раньше.\n *\n * Вложенность определяется порядком в контейнере оверлеев: CDK добавляет их\n * по мере открытия, поэтому панель, открытая из нашей, всегда следует за\n * нашей. Соседний оверлей, открытый до нас, этой проверкой не удерживается —\n * клик по нему закрывает попап, как и любой клик снаружи.\n */\n #isNestedOverlay(target: EventTarget | null): boolean {\n const panel: HTMLElement | undefined = this.#overlayRef?.overlayElement;\n if (!panel || !(target instanceof Element)) {\n return false;\n }\n\n const nested: Element | null = target.closest('.cdk-overlay-pane');\n const container: HTMLElement | null = panel.parentElement;\n if (nested === null || nested === panel || container === null) {\n return false;\n }\n\n // `querySelectorAll` отдаёт узлы в порядке документа — им и меряется,\n // какая панель открыта позже\n const panes: readonly Element[] = Array.from(container.querySelectorAll('.cdk-overlay-pane'));\n\n return panes.indexOf(nested) > panes.indexOf(panel);\n }\n\n /**\n * Порог подгонки остаётся в настройке оверлея и уходит из стиля коробки.\n *\n * CDK читает порог из настройки, когда решает, сдвинуть панель или ужать. Но при `attach` и\n * при каждом `updateSize` он пишет то же число в стиль коробки, и короткая панель\n * растягивалась до порога: под двумя пунктами «Ещё» шапки стояла пустая полоса.\n */\n #releasePaneMinHeight(overlayRef: OverlayRef): void {\n overlayRef.overlayElement.style.minHeight = '';\n }\n\n #resolvePanelClasses(): string[] {\n const base: string = 'rt-popover-panel';\n const extra: string = this.panelClass().trim();\n const preset: string[] = materialPresetClassesOf(this.#elementRef.nativeElement);\n return extra ? [base, ...extra.split(/\\s+/), ...preset] : [base, ...preset];\n }\n\n #scheduleHoverClose(): void {\n this.#clearHoverTimer();\n this.#hoverCloseTimer = setTimeout((): void => this.close(), HOVER_CLOSE_DELAY_MS);\n }\n\n #clearHoverTimer(): void {\n if (this.#hoverCloseTimer !== null) {\n clearTimeout(this.#hoverCloseTimer);\n this.#hoverCloseTimer = null;\n }\n }\n\n #disposeOverlay(): void {\n this.#overlayRef?.dispose();\n this.#overlayRef = null;\n this.#positionStrategy = null;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from "@angular/core";
|
|
2
2
|
import { ChangeDetectionStrategy, Component, DestroyRef, Directive, ElementRef, Injector, Pipe, TemplateRef, ViewEncapsulation, afterNextRender, afterRenderEffect, booleanAttribute, computed, contentChild, effect, forwardRef, inject, input, model, numberAttribute, output, signal, viewChild } from "@angular/core";
|
|
3
3
|
import { BlockDirective, ElemDirective, ModDirective } from "@rt-tools/core";
|
|
4
|
-
import { RT_KIT_LABELS, RT_KIT_LOCALE, RtAiPromptInputComponent, RtAiRunStatusComponent, RtButtonDirective, RtConfirmDirective, RtCopyValueComponent, RtDeltaViewComponent, RtEmptyStateComponent, RtFileCardComponent, RtFileDropComponent, RtFormControlBase, RtIconButtonComponent, RtIconComponent, RtMarkdownTextComponent, RtMenuComponent, RtMenuItemComponent, RtMessageComponent, RtNoteComponent, RtPromptSuggestionComponent, RtServiceLogoComponent, RtSkeletonWrapperComponent, RtSpinnerComponent, RtThreadListComponent, RtThreadListRowActionsDirective, RtThreadListRowDirective, RtToolbarComponent, RtToolbarLeftDirective, RtToolbarRightDirective, coerceQuillDeltaOutput, fileNameFromUrl, markdownToPlainText, rtKitLabel, rtMatchParts } from "@rt-tools/ui-kit-v2";
|
|
4
|
+
import { RT_KIT_LABELS, RT_KIT_LOCALE, RtAiPromptInputComponent, RtAiRunStatusComponent, RtButtonDirective, RtConfirmDirective, RtCopyValueComponent, RtDeltaViewComponent, RtEmptyStateComponent, RtFileCardComponent, RtFileDropComponent, RtFormControlBase, RtIconButtonComponent, RtIconComponent, RtMarkdownTextComponent, RtMenuComponent, RtMenuItemComponent, RtMessageComponent, RtNoteComponent, RtPromptSuggestionComponent, RtServiceLogoComponent, RtSkeletonWrapperComponent, RtSpinnerComponent, RtThreadListComponent, RtThreadListRowActionsDirective, RtThreadListRowDirective, RtToolbarComponent, RtToolbarLeftDirective, RtToolbarRightDirective, RtTooltipDirective, coerceQuillDeltaOutput, fileNameFromUrl, markdownToPlainText, rtKitLabel, rtMatchParts } from "@rt-tools/ui-kit-v2";
|
|
5
5
|
import { CdkTextareaAutosize } from "@angular/cdk/text-field";
|
|
6
6
|
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
|
7
7
|
import * as i1 from "@angular/forms";
|
|
@@ -1094,6 +1094,7 @@ var RtChatComponent = class RtChatComponent {
|
|
|
1094
1094
|
this.placeholder = input("", ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
|
|
1095
1095
|
this.title = input("", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
1096
1096
|
this.sign = input(null, ...ngDevMode ? [{ debugName: "sign" }] : /* istanbul ignore next */ []);
|
|
1097
|
+
this.subtitle = input("", ...ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []);
|
|
1097
1098
|
this.emptyHint = input("", ...ngDevMode ? [{ debugName: "emptyHint" }] : /* istanbul ignore next */ []);
|
|
1098
1099
|
this.hasThread = input(false, {
|
|
1099
1100
|
...ngDevMode ? { debugName: "hasThread" } : /* istanbul ignore next */ {},
|
|
@@ -1305,6 +1306,13 @@ var RtChatComponent = class RtChatComponent {
|
|
|
1305
1306
|
isRequired: false,
|
|
1306
1307
|
transformFunction: null
|
|
1307
1308
|
},
|
|
1309
|
+
subtitle: {
|
|
1310
|
+
classPropertyName: "subtitle",
|
|
1311
|
+
publicName: "subtitle",
|
|
1312
|
+
isSignal: true,
|
|
1313
|
+
isRequired: false,
|
|
1314
|
+
transformFunction: null
|
|
1315
|
+
},
|
|
1308
1316
|
emptyHint: {
|
|
1309
1317
|
classPropertyName: "emptyHint",
|
|
1310
1318
|
publicName: "emptyHint",
|
|
@@ -1428,8 +1436,8 @@ var RtChatComponent = class RtChatComponent {
|
|
|
1428
1436
|
isSignal: true
|
|
1429
1437
|
}],
|
|
1430
1438
|
ngImport: i0,
|
|
1431
|
-
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 @if (sign(); as channel) {\n <rt-service-logo rtElem=\"sign\" qa-dataid=\"chat-sign\" size=\"xs\" [logos]=\"[channel]\" />\n }\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 <!-- Скелетон повторяет структуру сообщения (автор → текст → дата),\n чередуя выравнивание, чтобы тред не «прыгал» при подмене на реальные. -->\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 if (m.status === MessageStatus.Undelivered) {\n <!-- Ответ записан, служба собеседника его не приняла: подпись видна глазом,\n повтора нет — он записал бы ответ второй раз. -->\n <rt-icon rtElem=\"status-icon\" name=\"warning\" size=\"sm\" />\n <span rtElem=\"status-caption\" qa-dataid=\"chat-message-undelivered\">\n {{ t().chatStatusUndelivered }}\n </span>\n } @else {\n <!-- Часы, пока сообщение в пути; дальше двойной чек, цвет которого\n (серый/синий) различает доставлено и прочитано. -->\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 rtElem=\"typing\" qa-dataid=\"chat-typing\" aria-live=\"polite\" [rtMod]=\"{ shown: typingText() !== '' }\">\n {{ typingText() }}\n </div>\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 (input)=\"onReplyInput($event)\" />\n } @else {\n <form\n rtElem=\"composer\"\n qa-dataid=\"chat-composer\"\n [formGroup]=\"replyForm\"\n (ngSubmit)=\"onSubmit()\"\n (input)=\"onReplyInput($event)\">\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 <!-- Слот доменных действий потребителя под композером: в fullscreen\n попадает внутрь overlay, поэтому кнопки остаются видны. -->\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",
|
|
1432
|
-
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);--rt-chat-typing-radius: var(--rt-radius-full);--rt-chat-typing-shadow: var(--rt-shadow-sm);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%}@container rt-place (width > 1440px){.rt-chat--expanded>.rt-file-drop{inline-size:1000px;margin-inline:auto;max-inline-size:100%}}@media(width>1440px){:where(html:not(:has(.rt-container))) .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__sign{flex: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__typing{position:absolute;z-index:1;bottom:var(--rt-space-1);left:var(--rt-space-2);box-sizing:border-box;color:var(--rt-color-text-muted);font-size:var(--rt-text-xs);max-inline-size:calc(100% - 2 * var(--rt-space-2));overflow-wrap:anywhere;pointer-events:none}.rt-chat__typing--shown{padding:var(--rt-space-0-5) var(--rt-space-2);border-radius:var(--rt-chat-typing-radius);background:var(--rt-color-bg-surface);box-shadow:var(--rt-chat-typing-shadow)}.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__message-status--status--undelivered{color:var(--rt-color-state-warning-text)}.rt-chat__status-icon{font-size:var(--rt-text-sm)}.rt-chat__status-caption{font-size:var(--rt-text-xs);line-height:var(--rt-leading-none)}.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"],
|
|
1439
|
+
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 @if (sign(); as channel) {\n <rt-service-logo rtElem=\"sign\" qa-dataid=\"chat-sign\" size=\"xs\" [logos]=\"[channel]\" />\n }\n <div rtElem=\"names\">\n <h3 rtElem=\"title\" qa-dataid=\"chat-title\" rtTooltipWhenTruncated [rtTooltip]=\"title()\">{{ title() }}</h3>\n <p rtElem=\"subtitle\" qa-dataid=\"chat-subtitle\" [hidden]=\"!subtitle()\">{{ subtitle() }}</p>\n </div>\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 <!-- Скелетон повторяет структуру сообщения (автор → текст → дата),\n чередуя выравнивание, чтобы тред не «прыгал» при подмене на реальные. -->\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 if (m.status === MessageStatus.Undelivered) {\n <!-- Ответ записан, служба собеседника его не приняла: подпись видна глазом,\n повтора нет — он записал бы ответ второй раз. -->\n <rt-icon rtElem=\"status-icon\" name=\"warning\" size=\"sm\" />\n <span rtElem=\"status-caption\" qa-dataid=\"chat-message-undelivered\">\n {{ t().chatStatusUndelivered }}\n </span>\n } @else {\n <!-- Часы, пока сообщение в пути; дальше двойной чек, цвет которого\n (серый/синий) различает доставлено и прочитано. -->\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 rtElem=\"typing\" qa-dataid=\"chat-typing\" aria-live=\"polite\" [rtMod]=\"{ shown: typingText() !== '' }\">\n {{ typingText() }}\n </div>\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 (input)=\"onReplyInput($event)\" />\n } @else {\n <form\n rtElem=\"composer\"\n qa-dataid=\"chat-composer\"\n [formGroup]=\"replyForm\"\n (ngSubmit)=\"onSubmit()\"\n (input)=\"onReplyInput($event)\">\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 <!-- Слот доменных действий потребителя под композером: в fullscreen\n попадает внутрь overlay, поэтому кнопки остаются видны. -->\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",
|
|
1440
|
+
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);--rt-chat-typing-radius: var(--rt-radius-full);--rt-chat-typing-shadow: var(--rt-shadow-sm);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%}@container rt-place (width > 1440px){.rt-chat--expanded>.rt-file-drop{inline-size:1000px;margin-inline:auto;max-inline-size:100%}}@media(width>1440px){:where(html:not(:has(.rt-container))) .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__sign{flex:none}.rt-chat__title{overflow:hidden;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);text-overflow:ellipsis;white-space:nowrap}.rt-chat__names{display:flex;flex-direction:column;gap:var(--rt-space-0-5);min-inline-size:0}.rt-chat__subtitle{overflow:hidden;margin:0;color:var(--rt-color-text-muted);font-family:var(--rt-font-family-sans);font-size:var(--rt-text-xs);line-height:var(--rt-leading-snug);text-overflow:ellipsis;white-space:nowrap}.rt-chat__subtitle[hidden]{display:none}.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__typing{position:absolute;z-index:1;bottom:var(--rt-space-1);left:var(--rt-space-2);box-sizing:border-box;color:var(--rt-color-text-muted);font-size:var(--rt-text-xs);max-inline-size:calc(100% - 2 * var(--rt-space-2));overflow-wrap:anywhere;pointer-events:none}.rt-chat__typing--shown{padding:var(--rt-space-0-5) var(--rt-space-2);border-radius:var(--rt-chat-typing-radius);background:var(--rt-color-bg-surface);box-shadow:var(--rt-chat-typing-shadow)}.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__message-status--status--undelivered{color:var(--rt-color-state-warning-text)}.rt-chat__status-icon{font-size:var(--rt-text-sm)}.rt-chat__status-caption{font-size:var(--rt-text-xs);line-height:var(--rt-leading-none)}.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"],
|
|
1433
1441
|
dependencies: [
|
|
1434
1442
|
{
|
|
1435
1443
|
kind: "directive",
|
|
@@ -1705,6 +1713,20 @@ var RtChatComponent = class RtChatComponent {
|
|
|
1705
1713
|
type: RtToolbarRightDirective,
|
|
1706
1714
|
selector: "[rtToolbarRight]"
|
|
1707
1715
|
},
|
|
1716
|
+
{
|
|
1717
|
+
kind: "directive",
|
|
1718
|
+
type: RtTooltipDirective,
|
|
1719
|
+
selector: "[rtTooltip]",
|
|
1720
|
+
inputs: [
|
|
1721
|
+
"rtTooltip",
|
|
1722
|
+
"rtTooltipPlacement",
|
|
1723
|
+
"rtTooltipShowDelay",
|
|
1724
|
+
"rtTooltipWhenTruncated",
|
|
1725
|
+
"rtTooltipHideOnClick",
|
|
1726
|
+
"rtTooltipShowOnTap"
|
|
1727
|
+
],
|
|
1728
|
+
exportAs: ["rtTooltip"]
|
|
1729
|
+
},
|
|
1708
1730
|
{
|
|
1709
1731
|
kind: "directive",
|
|
1710
1732
|
type: BlockDirective,
|
|
@@ -1777,6 +1799,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1777
1799
|
RtToolbarComponent,
|
|
1778
1800
|
RtToolbarLeftDirective,
|
|
1779
1801
|
RtToolbarRightDirective,
|
|
1802
|
+
RtTooltipDirective,
|
|
1780
1803
|
BlockDirective,
|
|
1781
1804
|
ElemDirective,
|
|
1782
1805
|
ModDirective
|
|
@@ -1786,8 +1809,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1786
1809
|
"[class.rt-chat--expanded]": "expanded()",
|
|
1787
1810
|
"[class.rt-chat--fill]": "fill()"
|
|
1788
1811
|
},
|
|
1789
|
-
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 @if (sign(); as channel) {\n <rt-service-logo rtElem=\"sign\" qa-dataid=\"chat-sign\" size=\"xs\" [logos]=\"[channel]\" />\n }\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 <!-- Скелетон повторяет структуру сообщения (автор → текст → дата),\n чередуя выравнивание, чтобы тред не «прыгал» при подмене на реальные. -->\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 if (m.status === MessageStatus.Undelivered) {\n <!-- Ответ записан, служба собеседника его не приняла: подпись видна глазом,\n повтора нет — он записал бы ответ второй раз. -->\n <rt-icon rtElem=\"status-icon\" name=\"warning\" size=\"sm\" />\n <span rtElem=\"status-caption\" qa-dataid=\"chat-message-undelivered\">\n {{ t().chatStatusUndelivered }}\n </span>\n } @else {\n <!-- Часы, пока сообщение в пути; дальше двойной чек, цвет которого\n (серый/синий) различает доставлено и прочитано. -->\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 rtElem=\"typing\" qa-dataid=\"chat-typing\" aria-live=\"polite\" [rtMod]=\"{ shown: typingText() !== '' }\">\n {{ typingText() }}\n </div>\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 (input)=\"onReplyInput($event)\" />\n } @else {\n <form\n rtElem=\"composer\"\n qa-dataid=\"chat-composer\"\n [formGroup]=\"replyForm\"\n (ngSubmit)=\"onSubmit()\"\n (input)=\"onReplyInput($event)\">\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 <!-- Слот доменных действий потребителя под композером: в fullscreen\n попадает внутрь overlay, поэтому кнопки остаются видны. -->\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",
|
|
1790
|
-
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);--rt-chat-typing-radius: var(--rt-radius-full);--rt-chat-typing-shadow: var(--rt-shadow-sm);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%}@container rt-place (width > 1440px){.rt-chat--expanded>.rt-file-drop{inline-size:1000px;margin-inline:auto;max-inline-size:100%}}@media(width>1440px){:where(html:not(:has(.rt-container))) .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__sign{flex: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__typing{position:absolute;z-index:1;bottom:var(--rt-space-1);left:var(--rt-space-2);box-sizing:border-box;color:var(--rt-color-text-muted);font-size:var(--rt-text-xs);max-inline-size:calc(100% - 2 * var(--rt-space-2));overflow-wrap:anywhere;pointer-events:none}.rt-chat__typing--shown{padding:var(--rt-space-0-5) var(--rt-space-2);border-radius:var(--rt-chat-typing-radius);background:var(--rt-color-bg-surface);box-shadow:var(--rt-chat-typing-shadow)}.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__message-status--status--undelivered{color:var(--rt-color-state-warning-text)}.rt-chat__status-icon{font-size:var(--rt-text-sm)}.rt-chat__status-caption{font-size:var(--rt-text-xs);line-height:var(--rt-leading-none)}.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"]
|
|
1812
|
+
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 @if (sign(); as channel) {\n <rt-service-logo rtElem=\"sign\" qa-dataid=\"chat-sign\" size=\"xs\" [logos]=\"[channel]\" />\n }\n <div rtElem=\"names\">\n <h3 rtElem=\"title\" qa-dataid=\"chat-title\" rtTooltipWhenTruncated [rtTooltip]=\"title()\">{{ title() }}</h3>\n <p rtElem=\"subtitle\" qa-dataid=\"chat-subtitle\" [hidden]=\"!subtitle()\">{{ subtitle() }}</p>\n </div>\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 <!-- Скелетон повторяет структуру сообщения (автор → текст → дата),\n чередуя выравнивание, чтобы тред не «прыгал» при подмене на реальные. -->\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 if (m.status === MessageStatus.Undelivered) {\n <!-- Ответ записан, служба собеседника его не приняла: подпись видна глазом,\n повтора нет — он записал бы ответ второй раз. -->\n <rt-icon rtElem=\"status-icon\" name=\"warning\" size=\"sm\" />\n <span rtElem=\"status-caption\" qa-dataid=\"chat-message-undelivered\">\n {{ t().chatStatusUndelivered }}\n </span>\n } @else {\n <!-- Часы, пока сообщение в пути; дальше двойной чек, цвет которого\n (серый/синий) различает доставлено и прочитано. -->\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 rtElem=\"typing\" qa-dataid=\"chat-typing\" aria-live=\"polite\" [rtMod]=\"{ shown: typingText() !== '' }\">\n {{ typingText() }}\n </div>\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 (input)=\"onReplyInput($event)\" />\n } @else {\n <form\n rtElem=\"composer\"\n qa-dataid=\"chat-composer\"\n [formGroup]=\"replyForm\"\n (ngSubmit)=\"onSubmit()\"\n (input)=\"onReplyInput($event)\">\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 <!-- Слот доменных действий потребителя под композером: в fullscreen\n попадает внутрь overlay, поэтому кнопки остаются видны. -->\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",
|
|
1813
|
+
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);--rt-chat-typing-radius: var(--rt-radius-full);--rt-chat-typing-shadow: var(--rt-shadow-sm);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%}@container rt-place (width > 1440px){.rt-chat--expanded>.rt-file-drop{inline-size:1000px;margin-inline:auto;max-inline-size:100%}}@media(width>1440px){:where(html:not(:has(.rt-container))) .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__sign{flex:none}.rt-chat__title{overflow:hidden;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);text-overflow:ellipsis;white-space:nowrap}.rt-chat__names{display:flex;flex-direction:column;gap:var(--rt-space-0-5);min-inline-size:0}.rt-chat__subtitle{overflow:hidden;margin:0;color:var(--rt-color-text-muted);font-family:var(--rt-font-family-sans);font-size:var(--rt-text-xs);line-height:var(--rt-leading-snug);text-overflow:ellipsis;white-space:nowrap}.rt-chat__subtitle[hidden]{display:none}.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__typing{position:absolute;z-index:1;bottom:var(--rt-space-1);left:var(--rt-space-2);box-sizing:border-box;color:var(--rt-color-text-muted);font-size:var(--rt-text-xs);max-inline-size:calc(100% - 2 * var(--rt-space-2));overflow-wrap:anywhere;pointer-events:none}.rt-chat__typing--shown{padding:var(--rt-space-0-5) var(--rt-space-2);border-radius:var(--rt-chat-typing-radius);background:var(--rt-color-bg-surface);box-shadow:var(--rt-chat-typing-shadow)}.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__message-status--status--undelivered{color:var(--rt-color-state-warning-text)}.rt-chat__status-icon{font-size:var(--rt-text-sm)}.rt-chat__status-caption{font-size:var(--rt-text-xs);line-height:var(--rt-leading-none)}.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"]
|
|
1791
1814
|
}]
|
|
1792
1815
|
}],
|
|
1793
1816
|
ctorParameters: () => [],
|
|
@@ -1876,6 +1899,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1876
1899
|
required: false
|
|
1877
1900
|
}]
|
|
1878
1901
|
}],
|
|
1902
|
+
subtitle: [{
|
|
1903
|
+
type: i0.Input,
|
|
1904
|
+
args: [{
|
|
1905
|
+
isSignal: true,
|
|
1906
|
+
alias: "subtitle",
|
|
1907
|
+
required: false
|
|
1908
|
+
}]
|
|
1909
|
+
}],
|
|
1879
1910
|
emptyHint: [{
|
|
1880
1911
|
type: i0.Input,
|
|
1881
1912
|
args: [{
|