@rt-tools/ui-kit-v2 0.4.0 → 0.5.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.
@@ -5,7 +5,7 @@ import * as i1$1 from '@angular/cdk/overlay';
5
5
  import { Overlay, ScrollStrategyOptions, OverlayConfig, OverlayModule } from '@angular/cdk/overlay';
6
6
  import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
7
7
  import { takeUntilDestroyed, outputToObservable, toSignal, toObservable } from '@angular/core/rxjs-interop';
8
- import { Subject, mergeMap, map, EMPTY, merge, takeUntil, of, shareReplay, forkJoin, tap, firstValueFrom, switchMap, debounce, timer, filter, catchError, exhaustMap, startWith, take, fromEvent, debounceTime, distinctUntilChanged } from 'rxjs';
8
+ import { Subject, mergeMap, map, EMPTY, merge, takeUntil, of, shareReplay, forkJoin, tap, firstValueFrom, switchMap, debounce, timer, filter, catchError, exhaustMap, startWith, take, finalize, defer, fromEvent, debounceTime, distinctUntilChanged } from 'rxjs';
9
9
  import { DOCUMENT, NgTemplateOutlet, DatePipe, DecimalPipe, formatDate } from '@angular/common';
10
10
  import { HttpClient } from '@angular/common/http';
11
11
  import * as i1 from '@angular/forms';
@@ -1665,7 +1665,13 @@ function provideRtIcons(baseUrl) {
1665
1665
  // Не блокируем bootstrap: sprite иконок грузится в фоне, приложение
1666
1666
  // стартует сразу. Иначе Angular ждёт forkJoin из сотен SVG-запросов
1667
1667
  // (первая загрузка +5с). rt-icon рендерит через <use> по мере готовности sprite.
1668
- void firstValueFrom(registry.preloadAll());
1668
+ //
1669
+ // Умолчание обязательно. Приложение, снесённое до прихода набора, обрывает запросы,
1670
+ // `forkJoin` завершается ничего не отдав, и без умолчания обещание отказывает
1671
+ // необработанным `no elements in sequence`. Видно это там, где приложения живут
1672
+ // коротко: витрина роняла на этом по одной-две истории за заход, и причина читалась
1673
+ // как дефект обвязки снимков.
1674
+ void firstValueFrom(registry.preloadAll(), { defaultValue: undefined });
1669
1675
  }),
1670
1676
  ]);
1671
1677
  }
@@ -2663,7 +2669,7 @@ const LOADING_FADE_OUT_BUFFER_MS = 50;
2663
2669
  * ```html
2664
2670
  * <button rtButton label="Войти" theme="primary"></button>
2665
2671
  * <button rtButton label="Скачать" icon="ico-download" theme="success"></button>
2666
- * <button rtButton icon="ico-pencil" theme="info" appearance="text"></button>
2672
+ * <button rtButton icon="pencil" theme="info" appearance="text"></button>
2667
2673
  * <button rtButton label="Сохранение..." [loading]="saving()" [disabled]="saving()"></button>
2668
2674
  * <a rtButton label="Главная" [routerLink]="'/'"></a>
2669
2675
  * ```
@@ -6534,6 +6540,77 @@ function rootSegmentsOf(commands) {
6534
6540
  return [...head.split('/').filter(Boolean), ...rest];
6535
6541
  }
6536
6542
 
6543
+ /**
6544
+ * Учёт панелей, стоящих на экране: из него роутерный гард узнаёт, кого спрашивать
6545
+ * о несохранённых правках.
6546
+ *
6547
+ * Спрашивать компонент, который роутер подставляет в гард, нельзя: когда
6548
+ * ro-маршрут объявлен под уровнем без своего компонента, Angular отдаёт детям
6549
+ * контекст родителя, и в гард приходит компонент первичного аутлета — экран
6550
+ * раздела, а не панель.
6551
+ *
6552
+ * Ключ записи — объявление маршрута, а не порядок постановки. Гард стоит на
6553
+ * каждом ro-маршруте, и одна навигация зовёт его за каждый уходящий маршрут
6554
+ * отдельно: учёт стеком дал бы ответ не той панели и два вопроса о правках
6555
+ * подряд. Объявление маршрута — та же ссылка на объект и у панели, и у снимка,
6556
+ * который роутер отдаёт гарду, поэтому два каркаса на экране не путаются даже
6557
+ * при одинаково названных аутлетах: маршруты у них разные.
6558
+ *
6559
+ * Служба корневая: гард живёт в инжекторе маршрута и до службы, поставленной на
6560
+ * уровне компонента, не дотянулся бы вовсе. Одного учёта на приложение хватает —
6561
+ * записи различает маршрут. Переменной уровня модуля учёт быть не может: та
6562
+ * пережила бы запрос при отдаче страницы сервером и оказалась бы общей на два
6563
+ * приложения, поднятых на одной странице.
6564
+ */
6565
+ class RtRouteAsideRegistry {
6566
+ #panels = new Map();
6567
+ /**
6568
+ * Панель встала на экран. Повторная постановка под тем же маршрутом заменяет
6569
+ * запись: аутлет держит одну панель, и отвечать должна нынешняя.
6570
+ */
6571
+ register(route, panel) {
6572
+ const key = route.routeConfig;
6573
+ if (key === null) {
6574
+ return;
6575
+ }
6576
+ this.#panels.set(key, panel);
6577
+ }
6578
+ /**
6579
+ * Панель ушла с экрана. Запись снимается только своя: пока уходящая панель
6580
+ * доигрывает уход, её место мог занять сосед, и снятие вслепую стёрло бы
6581
+ * запись того, кто на экране остался.
6582
+ */
6583
+ unregister(route, panel) {
6584
+ const key = route.routeConfig;
6585
+ if (key === null || this.#panels.get(key) !== panel) {
6586
+ return;
6587
+ }
6588
+ this.#panels.delete(key);
6589
+ }
6590
+ /** Снятие панели, чей ответ о правках отказал: спрашивать её больше нечем. */
6591
+ forget(panel) {
6592
+ for (const [key, registered] of this.#panels) {
6593
+ if (registered === panel) {
6594
+ this.#panels.delete(key);
6595
+ }
6596
+ }
6597
+ }
6598
+ /** Панель, отвечающая за уход с этого маршрута, или её отсутствие. */
6599
+ find(route) {
6600
+ const key = route.routeConfig;
6601
+ if (key === null) {
6602
+ return null;
6603
+ }
6604
+ return this.#panels.get(key) ?? null;
6605
+ }
6606
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RtRouteAsideRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6607
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RtRouteAsideRegistry, providedIn: 'root' }); }
6608
+ }
6609
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RtRouteAsideRegistry, decorators: [{
6610
+ type: Injectable,
6611
+ args: [{ providedIn: 'root' }]
6612
+ }] });
6613
+
6537
6614
  /**
6538
6615
  * Открытие ждёт `panel.ready()`, а не `ngAfterViewInit`: на F5 rt-container
6539
6616
  * создаёт overlay асинхронно, иначе `open()` гонится с ещё не готовым overlay.
@@ -6563,7 +6640,20 @@ class RtRouteAsideComponent {
6563
6640
  #notificationBus;
6564
6641
  #dialog;
6565
6642
  #injector;
6643
+ #registry;
6566
6644
  #opened;
6645
+ /**
6646
+ * Уход санкционирован самой панелью, и о правках спрашивать не надо: они либо
6647
+ * записаны, либо пользователь уже решил их судьбу. Признак живёт до первого
6648
+ * же вопроса о правках и снимается вместе с ответом на него.
6649
+ */
6650
+ #leaveAllowed;
6651
+ /**
6652
+ * Вопрос о правках, заданный роутерному уходу и ещё не отвеченный. Второй
6653
+ * уход, пришедший пока окно открыто, ждёт того же ответа: своё окно он
6654
+ * поставил бы поверх первого, и убрать его было бы некому.
6655
+ */
6656
+ #pendingAsk$;
6567
6657
  constructor() {
6568
6658
  this.#entity = signal(null, /* @ts-ignore */
6569
6659
  ...(ngDevMode ? [{ debugName: "#entity" }] : /* istanbul ignore next */ []));
@@ -6587,7 +6677,20 @@ class RtRouteAsideComponent {
6587
6677
  this.#notificationBus = inject(NotificationBus);
6588
6678
  this.#dialog = inject(RtDialogService);
6589
6679
  this.#injector = inject(Injector);
6680
+ this.#registry = inject(RtRouteAsideRegistry);
6590
6681
  this.#opened = false;
6682
+ /**
6683
+ * Уход санкционирован самой панелью, и о правках спрашивать не надо: они либо
6684
+ * записаны, либо пользователь уже решил их судьбу. Признак живёт до первого
6685
+ * же вопроса о правках и снимается вместе с ответом на него.
6686
+ */
6687
+ this.#leaveAllowed = false;
6688
+ /**
6689
+ * Вопрос о правках, заданный роутерному уходу и ещё не отвеченный. Второй
6690
+ * уход, пришедший пока окно открыто, ждёт того же ответа: своё окно он
6691
+ * поставил бы поверх первого, и убрать его было бы некому.
6692
+ */
6693
+ this.#pendingAsk$ = null;
6591
6694
  this.router = inject(Router);
6592
6695
  this.route = inject(ActivatedRoute);
6593
6696
  this.destroyRef = inject(DestroyRef);
@@ -6623,7 +6726,14 @@ class RtRouteAsideComponent {
6623
6726
  }
6624
6727
  this.#opened = true;
6625
6728
  panel.open();
6729
+ // Панель встала в аутлет — с этого мгновения о правках спрашивают её,
6730
+ // а не тот компонент, который роутер подставит гарду.
6731
+ this.#registry.register(this.route.snapshot, this);
6626
6732
  });
6733
+ // Снятие с учёта идёт по уничтожению, а не по уходу с экрана: уход может и
6734
+ // не состояться — роутер отклоняет навигацию молча, — и панель, снятая с
6735
+ // учёта раньше времени, осталась бы на экране, не отвечая о своих правках.
6736
+ this.destroyRef.onDestroy(() => this.#registry.unregister(this.route.snapshot, this));
6627
6737
  // Мутации из runMutation(): mergeMap подписывает каждый op$ независимо
6628
6738
  // (как раньше — по подписке на вызов, без отмены предыдущей in-flight
6629
6739
  // мутации); handler'ы замыкают opts вызова, catchError гасит ошибку
@@ -6692,16 +6802,21 @@ class RtRouteAsideComponent {
6692
6802
  */
6693
6803
  canDeactivate() {
6694
6804
  const intent = this.#closeIntent();
6805
+ // Признак идущей записи старше разрешения на уход: панель, выдавшая себе
6806
+ // разрешение и не дождавшаяся ответа сервера, уходить не должна — исход
6807
+ // записи решит её судьбу сам.
6695
6808
  if (intent === ERtAsideCloseIntent.Ignore) {
6696
6809
  return of(false);
6697
6810
  }
6811
+ // Уход начала сама панель: правки либо записаны секунду назад, либо их
6812
+ // судьбу пользователь уже решил. Вопрос был бы задан про то, чего нет.
6813
+ if (this.#consumeLeaveAllowance()) {
6814
+ return of(true);
6815
+ }
6698
6816
  if (intent === ERtAsideCloseIntent.Close) {
6699
6817
  return of(true);
6700
6818
  }
6701
- return this.#dialog
6702
- .open(RtAsideUnsavedDialogComponent)
6703
- .afterClosed()
6704
- .pipe(switchMap((outcome) => this.#deactivateAfter(outcome)));
6819
+ return this.#askUnsaved();
6705
6820
  }
6706
6821
  setEntity(entity) {
6707
6822
  this.#entity.set(entity);
@@ -6802,9 +6917,11 @@ class RtRouteAsideComponent {
6802
6917
  opts.onSuccess();
6803
6918
  return;
6804
6919
  }
6805
- // Закрытие после успеха идёт мимо гарда: форма к этому моменту
6806
- // ещё тронута, и вопрос о несохранённых правках был бы задан
6807
- // ровно про то, что только что сохранилось.
6920
+ // Закрытие после успеха идёт мимо обоих гардов — и кнопочного, и
6921
+ // роутерного. Форма к этому моменту ещё тронута, и вопрос о
6922
+ // несохранённых правках был бы задан ровно про то, что только что
6923
+ // сохранилось; роутерный гард закрывает разрешение на уход,
6924
+ // которое панель выдаёт себе перед вызовом роутера.
6808
6925
  if (opts?.closeOnSuccess === true || this.#isCreateMode()) {
6809
6926
  this.#closePanel();
6810
6927
  return;
@@ -6885,6 +7002,53 @@ class RtRouteAsideComponent {
6885
7002
  }
6886
7003
  return toObservable(this.#submitting, { injector: this.#injector }).pipe(filter((submitting) => !submitting), take(1), map(() => this.submitError() === null));
6887
7004
  }
7005
+ /**
7006
+ * Вопрос о правках роутерному уходу. Окно открывается одно на все уходы,
7007
+ * пришедшие пока оно висит: второе поставило бы поверх первого оверлей,
7008
+ * убрать который было бы некому — от того же и `exhaustMap` на пути кнопок.
7009
+ */
7010
+ #askUnsaved() {
7011
+ const pending = this.#pendingAsk$;
7012
+ if (pending !== null) {
7013
+ return pending;
7014
+ }
7015
+ const ask$ = this.#dialog
7016
+ .open(RtAsideUnsavedDialogComponent)
7017
+ .afterClosed()
7018
+ .pipe(switchMap((outcome) => this.#deactivateAfter(outcome)), finalize(() => {
7019
+ this.#pendingAsk$ = null;
7020
+ }), shareReplay({ bufferSize: 1, refCount: false }));
7021
+ this.#pendingAsk$ = ask$;
7022
+ return ask$;
7023
+ }
7024
+ /**
7025
+ * Разрешение на уход читается один раз: иначе первое же закрытие после записи
7026
+ * сняло бы вопрос о правках со всех следующих уходов, и тронутая форма
7027
+ * уезжала бы молча.
7028
+ */
7029
+ #consumeLeaveAllowance() {
7030
+ const allowed = this.#leaveAllowed;
7031
+ this.#leaveAllowed = false;
7032
+ return allowed;
7033
+ }
7034
+ /**
7035
+ * Уход, начатый самой панелью. Разрешение выдаётся здесь, вплотную к вызову
7036
+ * роутера, а не при закрытии панели: между закрытием и навигацией оверлей
7037
+ * доигрывает уход, и разрешение, выданное раньше, достаётся чужому уходу,
7038
+ * пришедшему в это окно, — тронутая форма уехала бы без вопроса.
7039
+ *
7040
+ * Невостребованное разрешение снимается итогом навигации: роутер отклоняет
7041
+ * навигацию молча, её может отменить другой гард, и оставшееся разрешение
7042
+ * сняло бы вопрос о правках со следующего ухода, которого панель не начинала.
7043
+ */
7044
+ #navigateAllowed(navigate) {
7045
+ this.#leaveAllowed = true;
7046
+ void navigate()
7047
+ .catch(() => false)
7048
+ .then(() => {
7049
+ this.#leaveAllowed = false;
7050
+ });
7051
+ }
6888
7052
  #closePanel() {
6889
7053
  this.panel()?.close();
6890
7054
  }
@@ -6892,7 +7056,7 @@ class RtRouteAsideComponent {
6892
7056
  const params = queryParams ?? this.#relatedQueryParams;
6893
7057
  this.#relatedCommands = null;
6894
7058
  this.#relatedQueryParams = null;
6895
- void this.router.navigateByUrl(this.#relatedTree(commands, params));
7059
+ this.#navigateAllowed(() => this.router.navigateByUrl(this.#relatedTree(commands, params)));
6896
7060
  }
6897
7061
  /**
6898
7062
  * Адрес, на который уходит панель. Свой аутлет она снимает здесь же, тем же
@@ -6931,9 +7095,9 @@ class RtRouteAsideComponent {
6931
7095
  this.#relatedCommands = null;
6932
7096
  }
6933
7097
  #navigateAway() {
6934
- void this.router.navigate([{ outlets: this.closeOutlets() }], {
7098
+ this.#navigateAllowed(() => this.router.navigate([{ outlets: this.closeOutlets() }], {
6935
7099
  relativeTo: this.route.parent,
6936
- });
7100
+ }));
6937
7101
  }
6938
7102
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RtRouteAsideComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
6939
7103
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.0.5", type: RtRouteAsideComponent, isStandalone: true, viewQueries: [{ propertyName: "panel", first: true, predicate: RtContainerRightSidenavPanelDirective, descendants: true, isSignal: true }], ngImport: i0 }); }
@@ -6950,12 +7114,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
6950
7114
  * Ставится на каждый ro-маршрут: гард висит на кнопках, нажатии мимо и Esc, а
6951
7115
  * смену маршрута ловит только роутер.
6952
7116
  *
6953
- * Панели может не быть: роутер зовёт гард и для маршрута, чей компонент не создан.
6954
- * Спрашивать тогда не о чем — уступить место можно. Без этой проверки первая же
6955
- * такая навигация падает, а следом отказывает любая другая: маршрут остаётся
6956
- * активным, и приложение застревает на адресе панели.
6957
- */
6958
- const rtAsideUnsavedGuard = (component) => component?.canDeactivate() ?? of(true);
7117
+ * Спрашивается панель, учтённая под маршрутом ухода, а не компонент, который
7118
+ * подставил роутер. Компонент — не панель, когда ro-маршрут объявлен под уровнем
7119
+ * без своего компонента: Angular отдаёт детям контекст родителя, и в гард
7120
+ * приходит компонент первичного аутлета. Ответ такого компонента к правкам
7121
+ * отношения не имеет, а вызов у него метода, которого нет, ронял навигацию — она
7122
+ * падала необработанным отказом, адрес не менялся, и приложение оставалось
7123
+ * запертым на панели.
7124
+ *
7125
+ * Компонент от роутера остаётся запасным путём: гард выведен наружу кита, и
7126
+ * панель приложения может отвечать о правках, не наследуя базовый класс, — на
7127
+ * учёт такая панель не встаёт никогда. Без запасного пути у неё вопрос о правках
7128
+ * исчез бы молча, а это тихая потеря правок.
7129
+ *
7130
+ * Спрашивать некого — уступить место можно: роутер зовёт гард и для маршрута,
7131
+ * чей компонент не создан. Без этого первая же такая навигация падает, а следом
7132
+ * отказывает любая другая.
7133
+ *
7134
+ * Отказом навигации гард не отвечает никогда, в том числе когда ответ панели
7135
+ * отказал: разрешить уход значило бы унести правки, о которых спросить не
7136
+ * удалось, а отменить и оставить всё как есть — запереть приложение на панели
7137
+ * навсегда. Поэтому уход отменяется один раз, а панель снимается с учёта, и
7138
+ * следующий уход идёт запасным путём.
7139
+ */
7140
+ /** Компонент от роутера годится в ответчики, только если умеет отвечать. */
7141
+ function fallbackPanel(component) {
7142
+ return typeof component?.canDeactivate === 'function' ? component : null;
7143
+ }
7144
+ const rtAsideUnsavedGuard = (component, currentRoute) => {
7145
+ const registry = inject(RtRouteAsideRegistry);
7146
+ const registered = registry.find(currentRoute);
7147
+ const panel = registered ?? fallbackPanel(component);
7148
+ if (panel === null) {
7149
+ return of(true);
7150
+ }
7151
+ // defer: ответ панели может отказать и синхронным броском, а гард обязан
7152
+ // пережить и его.
7153
+ return defer(() => panel.canDeactivate()).pipe(catchError(() => {
7154
+ registry.forget(panel);
7155
+ return of(false);
7156
+ }));
7157
+ };
6959
7158
 
6960
7159
  /** Контракт счётчика common/ui. */
6961
7160
  /**
@@ -11221,11 +11420,11 @@ class RtTableSortHeaderComponent {
11221
11420
  this.#table.toggleSort(this.rtSortHeader());
11222
11421
  }
11223
11422
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RtTableSortHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
11224
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: RtTableSortHeaderComponent, isStandalone: true, selector: "rt-table-sort-header, th[rtSortHeader]", inputs: { rtSortHeader: { classPropertyName: "rtSortHeader", publicName: "rtSortHeader", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.aria-sort": "ariaSort()" }, classAttribute: "rt-table-sort-header" }, ngImport: i0, template: "<ng-template #label>\n <ng-content />\n</ng-template>\n\n@if (sortable()) {\n <button\n rtElem=\"button\"\n qa-dataid=\"table-sort-header\"\n type=\"button\"\n [attr.data-column]=\"rtSortHeader()\"\n [attr.data-direction]=\"direction()\"\n [rtMod]=\"{ active: direction() !== null }\"\n (click)=\"onToggle()\">\n <ng-container [ngTemplateOutlet]=\"label\" />\n <rt-icon rtElem=\"icon\" size=\"xs\" [name]=\"icon()\" />\n </button>\n} @else {\n <ng-container [ngTemplateOutlet]=\"label\" />\n}\n", styles: ["@charset \"UTF-8\";.rt-table-sort-header__button{display:inline-flex;align-items:center;padding:0;border:none;background:none;color:inherit;cursor:pointer;font:inherit;gap:var(--rt-space-xs);text-align:inherit}.rt-table-sort-header__button:hover{color:var(--rt-color-text-primary)}.rt-table-sort-header__button:focus-visible{outline:2px solid var(--rt-color-action-primary);outline-offset:2px}.rt-table-sort-header__icon{flex:0 0 auto;color:var(--rt-color-text-muted)}.rt-table-sort-header__button--active{color:var(--rt-color-text-primary)}.rt-table-sort-header__button--active .rt-table-sort-header__icon{color:var(--rt-color-action-primary)}\n"], dependencies: [{ kind: "directive", type:
11423
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: RtTableSortHeaderComponent, isStandalone: true, selector: "rt-table-sort-header, th[rtSortHeader]", inputs: { rtSortHeader: { classPropertyName: "rtSortHeader", publicName: "rtSortHeader", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.aria-sort": "ariaSort()" }, classAttribute: "rt-table-sort-header" }, ngImport: i0, template: "<ng-template #label>\n <ng-content />\n</ng-template>\n\n<ng-container rtBlock=\"rt-table-sort-header\">\n @if (sortable()) {\n <button\n rtElem=\"button\"\n qa-dataid=\"table-sort-header\"\n type=\"button\"\n [attr.data-column]=\"rtSortHeader()\"\n [attr.data-direction]=\"direction()\"\n [rtMod]=\"{ active: direction() !== null }\"\n (click)=\"onToggle()\">\n <ng-container [ngTemplateOutlet]=\"label\" />\n <rt-icon rtElem=\"icon\" size=\"xs\" [name]=\"icon()\" />\n </button>\n } @else {\n <ng-container [ngTemplateOutlet]=\"label\" />\n }\n</ng-container>\n", styles: ["@charset \"UTF-8\";.rt-table-sort-header__button{display:inline-flex;align-items:center;padding:0;border:none;background:none;color:inherit;cursor:pointer;font:inherit;gap:var(--rt-space-xs);text-align:inherit}.rt-table-sort-header__button:hover{color:var(--rt-color-text-primary)}.rt-table-sort-header__button:focus-visible{outline:2px solid var(--rt-color-action-primary);outline-offset:2px}.rt-table-sort-header__icon{flex:0 0 auto;color:var(--rt-color-text-muted)}.rt-table-sort-header__button--active{color:var(--rt-color-text-primary)}.rt-table-sort-header__button--active .rt-table-sort-header__icon{color:var(--rt-color-action-primary)}\n"], dependencies: [{ kind: "directive", type:
11225
11424
  // angular
11226
11425
  NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type:
11227
11426
  // rt-tools
11228
- ElemDirective, selector: "[rtElem]", inputs: ["rtMod"] }, { kind: "directive", type: ModDirective, selector: "[rtMod]" }, { kind: "component", type:
11427
+ BlockDirective, selector: "[rtBlock]", inputs: ["rtMod"] }, { kind: "directive", type: ElemDirective, selector: "[rtElem]", inputs: ["rtMod"] }, { kind: "directive", type: ModDirective, selector: "[rtMod]" }, { kind: "component", type:
11229
11428
  // components
11230
11429
  RtIconComponent, selector: "rt-icon", inputs: ["name", "size", "color", "rotate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
11231
11430
  }
@@ -11235,6 +11434,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
11235
11434
  // angular
11236
11435
  NgTemplateOutlet,
11237
11436
  // rt-tools
11437
+ BlockDirective,
11238
11438
  ElemDirective,
11239
11439
  ModDirective,
11240
11440
  // components
@@ -11242,7 +11442,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
11242
11442
  ], host: {
11243
11443
  class: BEM_BLOCK$b,
11244
11444
  '[attr.aria-sort]': 'ariaSort()',
11245
- }, template: "<ng-template #label>\n <ng-content />\n</ng-template>\n\n@if (sortable()) {\n <button\n rtElem=\"button\"\n qa-dataid=\"table-sort-header\"\n type=\"button\"\n [attr.data-column]=\"rtSortHeader()\"\n [attr.data-direction]=\"direction()\"\n [rtMod]=\"{ active: direction() !== null }\"\n (click)=\"onToggle()\">\n <ng-container [ngTemplateOutlet]=\"label\" />\n <rt-icon rtElem=\"icon\" size=\"xs\" [name]=\"icon()\" />\n </button>\n} @else {\n <ng-container [ngTemplateOutlet]=\"label\" />\n}\n", styles: ["@charset \"UTF-8\";.rt-table-sort-header__button{display:inline-flex;align-items:center;padding:0;border:none;background:none;color:inherit;cursor:pointer;font:inherit;gap:var(--rt-space-xs);text-align:inherit}.rt-table-sort-header__button:hover{color:var(--rt-color-text-primary)}.rt-table-sort-header__button:focus-visible{outline:2px solid var(--rt-color-action-primary);outline-offset:2px}.rt-table-sort-header__icon{flex:0 0 auto;color:var(--rt-color-text-muted)}.rt-table-sort-header__button--active{color:var(--rt-color-text-primary)}.rt-table-sort-header__button--active .rt-table-sort-header__icon{color:var(--rt-color-action-primary)}\n"] }]
11445
+ }, template: "<ng-template #label>\n <ng-content />\n</ng-template>\n\n<ng-container rtBlock=\"rt-table-sort-header\">\n @if (sortable()) {\n <button\n rtElem=\"button\"\n qa-dataid=\"table-sort-header\"\n type=\"button\"\n [attr.data-column]=\"rtSortHeader()\"\n [attr.data-direction]=\"direction()\"\n [rtMod]=\"{ active: direction() !== null }\"\n (click)=\"onToggle()\">\n <ng-container [ngTemplateOutlet]=\"label\" />\n <rt-icon rtElem=\"icon\" size=\"xs\" [name]=\"icon()\" />\n </button>\n } @else {\n <ng-container [ngTemplateOutlet]=\"label\" />\n }\n</ng-container>\n", styles: ["@charset \"UTF-8\";.rt-table-sort-header__button{display:inline-flex;align-items:center;padding:0;border:none;background:none;color:inherit;cursor:pointer;font:inherit;gap:var(--rt-space-xs);text-align:inherit}.rt-table-sort-header__button:hover{color:var(--rt-color-text-primary)}.rt-table-sort-header__button:focus-visible{outline:2px solid var(--rt-color-action-primary);outline-offset:2px}.rt-table-sort-header__icon{flex:0 0 auto;color:var(--rt-color-text-muted)}.rt-table-sort-header__button--active{color:var(--rt-color-text-primary)}.rt-table-sort-header__button--active .rt-table-sort-header__icon{color:var(--rt-color-action-primary)}\n"] }]
11246
11446
  }], propDecorators: { rtSortHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtSortHeader", required: true }] }] } });
11247
11447
 
11248
11448
  /**
@@ -11543,8 +11743,15 @@ class RtTabsControlDirective {
11543
11743
  constructor() {
11544
11744
  /** Шаблон контрола, выводимый в header-слоте. */
11545
11745
  this.templateRef = inject(TemplateRef);
11546
- /** Сторона размещения относительно полосы вкладок. */
11547
- this.side = input('right', { ...(ngDevMode ? { debugName: "side" } : /* istanbul ignore next */ {}), alias: 'rtTabsControl' });
11746
+ /**
11747
+ * Сторона размещения относительно полосы вкладок.
11748
+ *
11749
+ * Пустое значение приводится к правой стороне: `<ng-template rtTabsControl>` без значения
11750
+ * задаёт входу пустую строку, а не умолчание, и такой контрол не попадал ни в левый слот,
11751
+ * ни в правый — он пропадал из шапки вовсе.
11752
+ */
11753
+ this.side = input('right', { ...(ngDevMode ? { debugName: "side" } : /* istanbul ignore next */ {}), alias: 'rtTabsControl',
11754
+ transform: (value) => (value === '' ? 'right' : value) });
11548
11755
  }
11549
11756
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RtTabsControlDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
11550
11757
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: RtTabsControlDirective, isStandalone: true, selector: "[rtTabsControl]", inputs: { side: { classPropertyName: "side", publicName: "rtTabsControl", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }