@rt-tools/ui-kit-v2 0.34.3 → 0.34.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/fesm2022/rt-tools-ui-kit-v2-action-bar.mjs +2 -0
  2. package/fesm2022/rt-tools-ui-kit-v2-action-bar.mjs.map +1 -1
  3. package/fesm2022/rt-tools-ui-kit-v2-aside.mjs +2 -0
  4. package/fesm2022/rt-tools-ui-kit-v2-aside.mjs.map +1 -1
  5. package/fesm2022/rt-tools-ui-kit-v2-autocomplete.mjs +2 -0
  6. package/fesm2022/rt-tools-ui-kit-v2-autocomplete.mjs.map +1 -1
  7. package/fesm2022/rt-tools-ui-kit-v2-calendar.mjs +2 -0
  8. package/fesm2022/rt-tools-ui-kit-v2-calendar.mjs.map +1 -1
  9. package/fesm2022/rt-tools-ui-kit-v2-copy-value.mjs +2 -0
  10. package/fesm2022/rt-tools-ui-kit-v2-copy-value.mjs.map +1 -1
  11. package/fesm2022/rt-tools-ui-kit-v2-counter.mjs +2 -0
  12. package/fesm2022/rt-tools-ui-kit-v2-counter.mjs.map +1 -1
  13. package/fesm2022/rt-tools-ui-kit-v2-data-list.mjs +4 -0
  14. package/fesm2022/rt-tools-ui-kit-v2-data-list.mjs.map +1 -1
  15. package/fesm2022/rt-tools-ui-kit-v2-data-table.mjs +4 -0
  16. package/fesm2022/rt-tools-ui-kit-v2-data-table.mjs.map +1 -1
  17. package/fesm2022/rt-tools-ui-kit-v2-date-picker.mjs +4 -0
  18. package/fesm2022/rt-tools-ui-kit-v2-date-picker.mjs.map +1 -1
  19. package/fesm2022/rt-tools-ui-kit-v2-date-range.mjs +2 -0
  20. package/fesm2022/rt-tools-ui-kit-v2-date-range.mjs.map +1 -1
  21. package/fesm2022/rt-tools-ui-kit-v2-dialog.mjs +2 -0
  22. package/fesm2022/rt-tools-ui-kit-v2-dialog.mjs.map +1 -1
  23. package/fesm2022/rt-tools-ui-kit-v2-dynamic-list.mjs +2 -0
  24. package/fesm2022/rt-tools-ui-kit-v2-dynamic-list.mjs.map +1 -1
  25. package/fesm2022/rt-tools-ui-kit-v2-dynamic-selector.mjs +2 -0
  26. package/fesm2022/rt-tools-ui-kit-v2-dynamic-selector.mjs.map +1 -1
  27. package/fesm2022/rt-tools-ui-kit-v2-file-card.mjs +2 -0
  28. package/fesm2022/rt-tools-ui-kit-v2-file-card.mjs.map +1 -1
  29. package/fesm2022/rt-tools-ui-kit-v2-header.mjs +2 -0
  30. package/fesm2022/rt-tools-ui-kit-v2-header.mjs.map +1 -1
  31. package/fesm2022/rt-tools-ui-kit-v2-icon-button.mjs +103 -6
  32. package/fesm2022/rt-tools-ui-kit-v2-icon-button.mjs.map +1 -1
  33. package/fesm2022/rt-tools-ui-kit-v2-image-upload.mjs +2 -0
  34. package/fesm2022/rt-tools-ui-kit-v2-image-upload.mjs.map +1 -1
  35. package/fesm2022/rt-tools-ui-kit-v2-inline-edit.mjs +2 -0
  36. package/fesm2022/rt-tools-ui-kit-v2-inline-edit.mjs.map +1 -1
  37. package/fesm2022/rt-tools-ui-kit-v2-input-number.mjs +2 -0
  38. package/fesm2022/rt-tools-ui-kit-v2-input-number.mjs.map +1 -1
  39. package/fesm2022/rt-tools-ui-kit-v2-input.mjs +6 -1
  40. package/fesm2022/rt-tools-ui-kit-v2-input.mjs.map +1 -1
  41. package/fesm2022/rt-tools-ui-kit-v2-menu.mjs +2 -0
  42. package/fesm2022/rt-tools-ui-kit-v2-menu.mjs.map +1 -1
  43. package/fesm2022/rt-tools-ui-kit-v2-message.mjs +2 -0
  44. package/fesm2022/rt-tools-ui-kit-v2-message.mjs.map +1 -1
  45. package/fesm2022/rt-tools-ui-kit-v2-multiselect.mjs +2 -0
  46. package/fesm2022/rt-tools-ui-kit-v2-multiselect.mjs.map +1 -1
  47. package/fesm2022/rt-tools-ui-kit-v2-pagination.mjs +2 -0
  48. package/fesm2022/rt-tools-ui-kit-v2-pagination.mjs.map +1 -1
  49. package/fesm2022/rt-tools-ui-kit-v2-pdf-viewer.mjs +2 -0
  50. package/fesm2022/rt-tools-ui-kit-v2-pdf-viewer.mjs.map +1 -1
  51. package/fesm2022/rt-tools-ui-kit-v2-photo-viewer.mjs +2 -0
  52. package/fesm2022/rt-tools-ui-kit-v2-photo-viewer.mjs.map +1 -1
  53. package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs +10 -0
  54. package/fesm2022/rt-tools-ui-kit-v2-rich-editor.mjs.map +1 -1
  55. package/fesm2022/rt-tools-ui-kit-v2-select.mjs +2 -0
  56. package/fesm2022/rt-tools-ui-kit-v2-select.mjs.map +1 -1
  57. package/fesm2022/rt-tools-ui-kit-v2-side-menu.mjs +6 -0
  58. package/fesm2022/rt-tools-ui-kit-v2-side-menu.mjs.map +1 -1
  59. package/fesm2022/rt-tools-ui-kit-v2-speed-menu.mjs +2 -0
  60. package/fesm2022/rt-tools-ui-kit-v2-speed-menu.mjs.map +1 -1
  61. package/fesm2022/rt-tools-ui-kit-v2-table.mjs +8 -0
  62. package/fesm2022/rt-tools-ui-kit-v2-table.mjs.map +1 -1
  63. package/fesm2022/rt-tools-ui-kit-v2-tag.mjs +2 -0
  64. package/fesm2022/rt-tools-ui-kit-v2-tag.mjs.map +1 -1
  65. package/fesm2022/rt-tools-ui-kit-v2-theme-toggle.mjs +2 -0
  66. package/fesm2022/rt-tools-ui-kit-v2-theme-toggle.mjs.map +1 -1
  67. package/fesm2022/rt-tools-ui-kit-v2-thread-list.mjs +2 -0
  68. package/fesm2022/rt-tools-ui-kit-v2-thread-list.mjs.map +1 -1
  69. package/fesm2022/rt-tools-ui-kit-v2-toast.mjs +2 -0
  70. package/fesm2022/rt-tools-ui-kit-v2-toast.mjs.map +1 -1
  71. package/fesm2022/rt-tools-ui-kit-v2-workspace-details.mjs +2 -0
  72. package/fesm2022/rt-tools-ui-kit-v2-workspace-details.mjs.map +1 -1
  73. package/fesm2022/rt-tools-ui-kit-v2-workspace.mjs +2 -0
  74. package/fesm2022/rt-tools-ui-kit-v2-workspace.mjs.map +1 -1
  75. package/package.json +1 -1
  76. package/src/lib/components/icon-button/CONTEXT.md +32 -16
  77. package/src/lib/components/icon-button/rt-icon-button.component.scss +17 -9
  78. package/src/lib/components/input/CONTEXT.md +6 -0
  79. package/src/styles/_form.scss +14 -1
  80. package/styles/tokens.css +13 -1
  81. package/types/rt-tools-ui-kit-v2-icon-button.d.ts +38 -2
  82. package/types/rt-tools-ui-kit-v2-input.d.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"rt-tools-ui-kit-v2-photo-viewer.mjs","names":[],"sources":["../../../projects/ui-kit-v2/src/lib/components/photo-viewer/rt-photo-viewer.logic.ts","../../../projects/ui-kit-v2/src/lib/components/photo-viewer/rt-photo-viewer.component.ts","../../../projects/ui-kit-v2/src/lib/components/photo-viewer/rt-photo-viewer.component.html"],"sourcesContent":["/**\n * Чистая логика просмотрщика: переходы между кадрами и то, какие из них стоит\n * держать загруженными.\n */\n\n/** Листание по кругу: с последнего кадра стрелка вправо ведёт на первый */\nexport function nextPhotoIndex(current: number, total: number, step: number): number {\n if (total <= 0) {\n return 0;\n }\n return (((current + step) % total) + total) % total;\n}\n\n/** Индекс из адреса: в URL кадры нумеруются с единицы, внутри — с нуля */\nexport function indexFromParam(param: string | null, total: number): number {\n const parsed: number = Number(param);\n if (!Number.isInteger(parsed) || parsed < 1 || parsed > total) {\n return -1;\n }\n return parsed - 1;\n}\n\nexport function paramFromIndex(index: number): string {\n return String(index + 1);\n}\n\n/**\n * Индекс кадра по позиции прокрутки. Нужен, потому что листать можно свайпом:\n * без этого счётчик показывал бы один кадр, а на экране был бы другой.\n */\nexport function indexFromScroll(scrollLeft: number, slideWidth: number, total: number): number {\n if (slideWidth <= 0 || total <= 0) {\n return 0;\n }\n const index: number = Math.round(scrollLeft / slideWidth);\n return Math.min(Math.max(index, 0), total - 1);\n}\n\n/**\n * Кадры, которые держим в разметке: текущий и соседние. Полноразмерная\n * фотография весит сотни килобайт, и загружать все пятьдесят разом — значит\n * выесть трафик пользователя ради кадров, до которых он не долистает.\n */\nexport function shouldRenderPhoto(index: number, current: number, total: number): boolean {\n if (total <= 0) {\n return false;\n }\n const distance: number = Math.min(Math.abs(index - current), total - Math.abs(index - current));\n return distance <= 1;\n}\n","import { CdkTrapFocus } from '@angular/cdk/a11y';\nimport { NgTemplateOutlet } from '@angular/common';\nimport {\n afterNextRender,\n computed,\n inject,\n signal,\n viewChild,\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n Signal,\n TemplateRef,\n ViewEncapsulation,\n WritableSignal,\n} from '@angular/core';\n\n// rt-tools\nimport { BlockDirective, ElemDirective } from '@rt-tools/core';\n\nimport { RT_KIT_LABELS, TRtKitLabelMap } from '@rt-tools/ui-kit-v2/core';\nimport { RtDialogRef } from '@rt-tools/ui-kit-v2/dialog';\nimport { RT_DIALOG_DATA } from '@rt-tools/ui-kit-v2/dialog';\nimport { RtIconButtonComponent } from '@rt-tools/ui-kit-v2/icon-button';\nimport { indexFromScroll, nextPhotoIndex, shouldRenderPhoto } from './rt-photo-viewer.logic';\nimport { IRtPhotoViewer } from './rt-photo-viewer.model';\n\nconst BEM_BLOCK: string = 'rt-photo-viewer';\n\n/**\n * Полноэкранный просмотрщик фотографий поверх `RtDialogService`.\n *\n * Листание — нативный `scroll-snap`: свайп, инерцию и прилипание даёт сам\n * браузер, и на телефоне это ощущается правильнее любой реализации на\n * обработчиках. Готовую библиотеку не берём — весь кит здесь собственный, а\n * токены, BEM и `qa-dataid` ложатся естественно.\n *\n * Руками сделаны: стрелки, клавиши `←` `→`, счётчик и предзагрузка соседних\n * кадров. `Esc`, блокировку прокрутки фона и затемнение даёт сам диалог.\n */\n@Component({\n selector: 'rt-photo-viewer',\n templateUrl: './rt-photo-viewer.component.html',\n styleUrl: './rt-photo-viewer.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n imports: [\n // angular\n CdkTrapFocus,\n NgTemplateOutlet,\n\n // rt-tools\n BlockDirective,\n ElemDirective,\n\n // components\n RtIconButtonComponent,\n\n // transloco\n ],\n host: {\n class: BEM_BLOCK,\n role: 'dialog',\n 'aria-modal': 'true',\n // Клавиши слушает сам просмотрщик, поэтому фокус переводится на него\n // сразу после открытия — иначе стрелки и Esc не работали бы, пока\n // пользователь куда-нибудь не кликнет\n tabindex: '-1',\n '(keydown.escape)': 'close()',\n '(keydown.arrowLeft)': 'move(-1)',\n '(keydown.arrowRight)': 'move(1)',\n },\n})\nexport class RtPhotoViewerComponent {\n readonly #data: IRtPhotoViewer.Data = inject(RT_DIALOG_DATA) as IRtPhotoViewer.Data;\n readonly #dialogRef: RtDialogRef<number> = inject(RtDialogRef<number>);\n readonly #host: ElementRef<HTMLElement> = inject(ElementRef<HTMLElement>);\n\n protected readonly t: Signal<TRtKitLabelMap> = inject(RT_KIT_LABELS);\n\n protected readonly track: Signal<ElementRef<HTMLElement> | undefined> = viewChild<ElementRef<HTMLElement>>('track');\n\n protected readonly photos: readonly IRtPhotoViewer.Photo[] = this.#data.photos;\n\n /** Панель правки под дорожкой; там, где кадр только смотрят, её нет */\n protected readonly panel: TemplateRef<IRtPhotoViewer.PanelContext> | null = this.#data.panel ?? null;\n\n protected readonly currentIndex: WritableSignal<number> = signal<number>(this.#data.startIndex);\n\n protected readonly counter: Signal<string> = computed((): string => `${this.currentIndex() + 1} / ${this.photos.length}`);\n\n /** Соседние кадры готовятся заранее, дальние в разметку не попадают */\n protected readonly rendered: Signal<readonly boolean[]> = computed((): readonly boolean[] =>\n this.photos.map((_photo: IRtPhotoViewer.Photo, index: number): boolean =>\n shouldRenderPhoto(index, this.currentIndex(), this.photos.length)\n )\n );\n\n constructor() {\n // Открытие может быть не с первого кадра: без явной прокрутки счётчик\n // показывал бы второй кадр, а на экране остался бы первый\n afterNextRender((): void => {\n this.#host.nativeElement.focus();\n this.#scrollToCurrent();\n });\n }\n\n /** Наружу отдаётся кадр, на котором закрыли: по нему вызывающая сторона чистит адрес */\n public close(): void {\n this.#dialogRef.close(this.currentIndex());\n }\n\n protected move(step: number): void {\n this.select(nextPhotoIndex(this.currentIndex(), this.photos.length, step));\n }\n\n protected select(index: number): void {\n this.currentIndex.set(index);\n this.#scrollToCurrent();\n }\n\n /** Свайп листает мимо стрелок — счётчик догоняет позицию дорожки */\n protected onScroll(): void {\n const track: HTMLElement | undefined = this.track()?.nativeElement;\n if (track) {\n this.currentIndex.set(indexFromScroll(track.scrollLeft, track.clientWidth, this.photos.length));\n }\n }\n\n /**\n * Прокрутка ленты вслед за выбором. Позиция считается от ширины дорожки, а\n * не от `scrollWidth`: кадры за пределами окна ещё не отрисованы, и общая\n * ширина до их появления меньше реальной.\n *\n * Переход мгновенный: `behavior: 'smooth'` внутри оверлея молча не двигает\n * дорожку — счётчик менялся, а кадр оставался прежним. Свайп при этом\n * по-прежнему анимирует сам браузер через scroll-snap.\n */\n #scrollToCurrent(): void {\n const track: HTMLElement | undefined = this.track()?.nativeElement;\n if (!track) {\n return;\n }\n track.scrollTo({ left: track.clientWidth * this.currentIndex(), behavior: 'instant' });\n }\n}\n","<ng-container rtBlock=\"rt-photo-viewer\">\n <!-- Ловушка держит Tab внутри; фокус ставит сам компонент, поэтому\n авто-захвата здесь нет: он перебивал фокус на просмотрщике, и клавиши\n переставали листать -->\n <div rtElem=\"surface\" cdkTrapFocus>\n <header rtElem=\"bar\">\n <span rtElem=\"counter\" qa-dataid=\"photo-viewer-counter\">{{ counter() }}</span>\n <rt-icon-button\n qa-dataid=\"photo-viewer-close\"\n icon=\"close\"\n variant=\"ghost\"\n [ariaLabel]=\"t().photoViewerClose\"\n (clicked)=\"close()\" />\n </header>\n\n <div rtElem=\"track\" qa-dataid=\"photo-viewer-track\" #track (scroll)=\"onScroll()\">\n @for (photo of photos; track photo.id; let index = $index) {\n <figure rtElem=\"slide\" [attr.data-photo-index]=\"index\">\n @if (rendered()[index]) {\n <picture>\n @for (source of photo.picture.sources; track source.type) {\n <source sizes=\"100vw\" [type]=\"source.type\" [srcset]=\"source.srcset\" />\n }\n <img\n rtElem=\"image\"\n qa-dataid=\"photo-viewer-image\"\n decoding=\"async\"\n sizes=\"100vw\"\n [srcset]=\"photo.picture.fallbackSrcset\"\n [src]=\"photo.picture.fallbackSrc\"\n [alt]=\"photo.alt\" />\n </picture>\n }\n </figure>\n }\n </div>\n\n @if (photos.length > 1) {\n <rt-icon-button\n rtElem=\"arrow-prev\"\n qa-dataid=\"photo-viewer-prev\"\n icon=\"angle-left\"\n variant=\"ghost\"\n [ariaLabel]=\"t().photoViewerPrev\"\n (clicked)=\"move(-1)\" />\n\n <rt-icon-button\n rtElem=\"arrow-next\"\n qa-dataid=\"photo-viewer-next\"\n icon=\"angle-right\"\n variant=\"ghost\"\n [ariaLabel]=\"t().photoViewerNext\"\n (clicked)=\"move(1)\" />\n }\n\n <!-- Панель приходит снаружи: где-то кадр только смотрят, где-то им ещё и\n управляют, и решение принимается там же, где кадр виден крупно -->\n @if (panel) {\n <footer rtElem=\"panel\" qa-dataid=\"photo-viewer-panel\">\n <ng-container\n [ngTemplateOutlet]=\"panel\"\n [ngTemplateOutletContext]=\"{ $implicit: photos[currentIndex()], index: currentIndex() }\" />\n </footer>\n }\n </div>\n</ng-container>\n"],"mappings":"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"}
1
+ {"version":3,"file":"rt-tools-ui-kit-v2-photo-viewer.mjs","names":[],"sources":["../../../projects/ui-kit-v2/src/lib/components/photo-viewer/rt-photo-viewer.logic.ts","../../../projects/ui-kit-v2/src/lib/components/photo-viewer/rt-photo-viewer.component.ts","../../../projects/ui-kit-v2/src/lib/components/photo-viewer/rt-photo-viewer.component.html"],"sourcesContent":["/**\n * Чистая логика просмотрщика: переходы между кадрами и то, какие из них стоит\n * держать загруженными.\n */\n\n/** Листание по кругу: с последнего кадра стрелка вправо ведёт на первый */\nexport function nextPhotoIndex(current: number, total: number, step: number): number {\n if (total <= 0) {\n return 0;\n }\n return (((current + step) % total) + total) % total;\n}\n\n/** Индекс из адреса: в URL кадры нумеруются с единицы, внутри — с нуля */\nexport function indexFromParam(param: string | null, total: number): number {\n const parsed: number = Number(param);\n if (!Number.isInteger(parsed) || parsed < 1 || parsed > total) {\n return -1;\n }\n return parsed - 1;\n}\n\nexport function paramFromIndex(index: number): string {\n return String(index + 1);\n}\n\n/**\n * Индекс кадра по позиции прокрутки. Нужен, потому что листать можно свайпом:\n * без этого счётчик показывал бы один кадр, а на экране был бы другой.\n */\nexport function indexFromScroll(scrollLeft: number, slideWidth: number, total: number): number {\n if (slideWidth <= 0 || total <= 0) {\n return 0;\n }\n const index: number = Math.round(scrollLeft / slideWidth);\n return Math.min(Math.max(index, 0), total - 1);\n}\n\n/**\n * Кадры, которые держим в разметке: текущий и соседние. Полноразмерная\n * фотография весит сотни килобайт, и загружать все пятьдесят разом — значит\n * выесть трафик пользователя ради кадров, до которых он не долистает.\n */\nexport function shouldRenderPhoto(index: number, current: number, total: number): boolean {\n if (total <= 0) {\n return false;\n }\n const distance: number = Math.min(Math.abs(index - current), total - Math.abs(index - current));\n return distance <= 1;\n}\n","import { CdkTrapFocus } from '@angular/cdk/a11y';\nimport { NgTemplateOutlet } from '@angular/common';\nimport {\n afterNextRender,\n computed,\n inject,\n signal,\n viewChild,\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n Signal,\n TemplateRef,\n ViewEncapsulation,\n WritableSignal,\n} from '@angular/core';\n\n// rt-tools\nimport { BlockDirective, ElemDirective } from '@rt-tools/core';\n\nimport { RT_KIT_LABELS, TRtKitLabelMap } from '@rt-tools/ui-kit-v2/core';\nimport { RtDialogRef } from '@rt-tools/ui-kit-v2/dialog';\nimport { RT_DIALOG_DATA } from '@rt-tools/ui-kit-v2/dialog';\nimport { RtIconButtonComponent } from '@rt-tools/ui-kit-v2/icon-button';\nimport { indexFromScroll, nextPhotoIndex, shouldRenderPhoto } from './rt-photo-viewer.logic';\nimport { IRtPhotoViewer } from './rt-photo-viewer.model';\n\nconst BEM_BLOCK: string = 'rt-photo-viewer';\n\n/**\n * Полноэкранный просмотрщик фотографий поверх `RtDialogService`.\n *\n * Листание — нативный `scroll-snap`: свайп, инерцию и прилипание даёт сам\n * браузер, и на телефоне это ощущается правильнее любой реализации на\n * обработчиках. Готовую библиотеку не берём — весь кит здесь собственный, а\n * токены, BEM и `qa-dataid` ложатся естественно.\n *\n * Руками сделаны: стрелки, клавиши `←` `→`, счётчик и предзагрузка соседних\n * кадров. `Esc`, блокировку прокрутки фона и затемнение даёт сам диалог.\n */\n@Component({\n selector: 'rt-photo-viewer',\n templateUrl: './rt-photo-viewer.component.html',\n styleUrl: './rt-photo-viewer.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n imports: [\n // angular\n CdkTrapFocus,\n NgTemplateOutlet,\n\n // rt-tools\n BlockDirective,\n ElemDirective,\n\n // components\n RtIconButtonComponent,\n\n // transloco\n ],\n host: {\n class: BEM_BLOCK,\n role: 'dialog',\n 'aria-modal': 'true',\n // Клавиши слушает сам просмотрщик, поэтому фокус переводится на него\n // сразу после открытия — иначе стрелки и Esc не работали бы, пока\n // пользователь куда-нибудь не кликнет\n tabindex: '-1',\n '(keydown.escape)': 'close()',\n '(keydown.arrowLeft)': 'move(-1)',\n '(keydown.arrowRight)': 'move(1)',\n },\n})\nexport class RtPhotoViewerComponent {\n readonly #data: IRtPhotoViewer.Data = inject(RT_DIALOG_DATA) as IRtPhotoViewer.Data;\n readonly #dialogRef: RtDialogRef<number> = inject(RtDialogRef<number>);\n readonly #host: ElementRef<HTMLElement> = inject(ElementRef<HTMLElement>);\n\n protected readonly t: Signal<TRtKitLabelMap> = inject(RT_KIT_LABELS);\n\n protected readonly track: Signal<ElementRef<HTMLElement> | undefined> = viewChild<ElementRef<HTMLElement>>('track');\n\n protected readonly photos: readonly IRtPhotoViewer.Photo[] = this.#data.photos;\n\n /** Панель правки под дорожкой; там, где кадр только смотрят, её нет */\n protected readonly panel: TemplateRef<IRtPhotoViewer.PanelContext> | null = this.#data.panel ?? null;\n\n protected readonly currentIndex: WritableSignal<number> = signal<number>(this.#data.startIndex);\n\n protected readonly counter: Signal<string> = computed((): string => `${this.currentIndex() + 1} / ${this.photos.length}`);\n\n /** Соседние кадры готовятся заранее, дальние в разметку не попадают */\n protected readonly rendered: Signal<readonly boolean[]> = computed((): readonly boolean[] =>\n this.photos.map((_photo: IRtPhotoViewer.Photo, index: number): boolean =>\n shouldRenderPhoto(index, this.currentIndex(), this.photos.length)\n )\n );\n\n constructor() {\n // Открытие может быть не с первого кадра: без явной прокрутки счётчик\n // показывал бы второй кадр, а на экране остался бы первый\n afterNextRender((): void => {\n this.#host.nativeElement.focus();\n this.#scrollToCurrent();\n });\n }\n\n /** Наружу отдаётся кадр, на котором закрыли: по нему вызывающая сторона чистит адрес */\n public close(): void {\n this.#dialogRef.close(this.currentIndex());\n }\n\n protected move(step: number): void {\n this.select(nextPhotoIndex(this.currentIndex(), this.photos.length, step));\n }\n\n protected select(index: number): void {\n this.currentIndex.set(index);\n this.#scrollToCurrent();\n }\n\n /** Свайп листает мимо стрелок — счётчик догоняет позицию дорожки */\n protected onScroll(): void {\n const track: HTMLElement | undefined = this.track()?.nativeElement;\n if (track) {\n this.currentIndex.set(indexFromScroll(track.scrollLeft, track.clientWidth, this.photos.length));\n }\n }\n\n /**\n * Прокрутка ленты вслед за выбором. Позиция считается от ширины дорожки, а\n * не от `scrollWidth`: кадры за пределами окна ещё не отрисованы, и общая\n * ширина до их появления меньше реальной.\n *\n * Переход мгновенный: `behavior: 'smooth'` внутри оверлея молча не двигает\n * дорожку — счётчик менялся, а кадр оставался прежним. Свайп при этом\n * по-прежнему анимирует сам браузер через scroll-snap.\n */\n #scrollToCurrent(): void {\n const track: HTMLElement | undefined = this.track()?.nativeElement;\n if (!track) {\n return;\n }\n track.scrollTo({ left: track.clientWidth * this.currentIndex(), behavior: 'instant' });\n }\n}\n","<ng-container rtBlock=\"rt-photo-viewer\">\n <!-- Ловушка держит Tab внутри; фокус ставит сам компонент, поэтому\n авто-захвата здесь нет: он перебивал фокус на просмотрщике, и клавиши\n переставали листать -->\n <div rtElem=\"surface\" cdkTrapFocus>\n <header rtElem=\"bar\">\n <span rtElem=\"counter\" qa-dataid=\"photo-viewer-counter\">{{ counter() }}</span>\n <rt-icon-button\n qa-dataid=\"photo-viewer-close\"\n icon=\"close\"\n variant=\"ghost\"\n [ariaLabel]=\"t().photoViewerClose\"\n (clicked)=\"close()\" />\n </header>\n\n <div rtElem=\"track\" qa-dataid=\"photo-viewer-track\" #track (scroll)=\"onScroll()\">\n @for (photo of photos; track photo.id; let index = $index) {\n <figure rtElem=\"slide\" [attr.data-photo-index]=\"index\">\n @if (rendered()[index]) {\n <picture>\n @for (source of photo.picture.sources; track source.type) {\n <source sizes=\"100vw\" [type]=\"source.type\" [srcset]=\"source.srcset\" />\n }\n <img\n rtElem=\"image\"\n qa-dataid=\"photo-viewer-image\"\n decoding=\"async\"\n sizes=\"100vw\"\n [srcset]=\"photo.picture.fallbackSrcset\"\n [src]=\"photo.picture.fallbackSrc\"\n [alt]=\"photo.alt\" />\n </picture>\n }\n </figure>\n }\n </div>\n\n @if (photos.length > 1) {\n <rt-icon-button\n rtElem=\"arrow-prev\"\n qa-dataid=\"photo-viewer-prev\"\n icon=\"angle-left\"\n variant=\"ghost\"\n [ariaLabel]=\"t().photoViewerPrev\"\n (clicked)=\"move(-1)\" />\n\n <rt-icon-button\n rtElem=\"arrow-next\"\n qa-dataid=\"photo-viewer-next\"\n icon=\"angle-right\"\n variant=\"ghost\"\n [ariaLabel]=\"t().photoViewerNext\"\n (clicked)=\"move(1)\" />\n }\n\n <!-- Панель приходит снаружи: где-то кадр только смотрят, где-то им ещё и\n управляют, и решение принимается там же, где кадр виден крупно -->\n @if (panel) {\n <footer rtElem=\"panel\" qa-dataid=\"photo-viewer-panel\">\n <ng-container\n [ngTemplateOutlet]=\"panel\"\n [ngTemplateOutletContext]=\"{ $implicit: photos[currentIndex()], index: currentIndex() }\" />\n </footer>\n }\n </div>\n</ng-container>\n"],"mappings":"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"}
@@ -631,6 +631,8 @@ var RtMessageComposerComponent = class RtMessageComposerComponent {
631
631
  "disabled",
632
632
  "active",
633
633
  "expanded",
634
+ "ariaHaspopup",
635
+ "link",
634
636
  "indicator"
635
637
  ],
636
638
  outputs: ["clicked"]
@@ -1588,6 +1590,8 @@ var RtChatComponent = class RtChatComponent {
1588
1590
  "disabled",
1589
1591
  "active",
1590
1592
  "expanded",
1593
+ "ariaHaspopup",
1594
+ "link",
1591
1595
  "indicator"
1592
1596
  ],
1593
1597
  outputs: ["clicked"]
@@ -2124,6 +2128,8 @@ var RtAiChatCopyComponent = class RtAiChatCopyComponent {
2124
2128
  "disabled",
2125
2129
  "active",
2126
2130
  "expanded",
2131
+ "ariaHaspopup",
2132
+ "link",
2127
2133
  "indicator"
2128
2134
  ],
2129
2135
  outputs: ["clicked"]
@@ -2297,6 +2303,8 @@ var RtAiChatAnswerComponent = class RtAiChatAnswerComponent {
2297
2303
  "disabled",
2298
2304
  "active",
2299
2305
  "expanded",
2306
+ "ariaHaspopup",
2307
+ "link",
2300
2308
  "indicator"
2301
2309
  ],
2302
2310
  outputs: ["clicked"]
@@ -2968,6 +2976,8 @@ var RtAiChatComponent = class RtAiChatComponent {
2968
2976
  "disabled",
2969
2977
  "active",
2970
2978
  "expanded",
2979
+ "ariaHaspopup",
2980
+ "link",
2971
2981
  "indicator"
2972
2982
  ],
2973
2983
  outputs: ["clicked"]