@rt-tools/ui-kit-v2 0.24.0 → 0.26.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rt-tools/ui-kit-v2",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Yauheni Krumin",
6
6
  "type": "module",
@@ -19,11 +19,15 @@
19
19
  "@angular/forms": "^22.0.0",
20
20
  "@angular/platform-browser": "^22.0.0",
21
21
  "@angular/router": "^22.0.0",
22
+ "pdfjs-dist": "^6.4.299",
22
23
  "quill": "^2.0.3",
23
24
  "rxjs": "^7.8.2",
24
25
  "typescript": "^6.0.0"
25
26
  },
26
27
  "peerDependenciesMeta": {
28
+ "pdfjs-dist": {
29
+ "optional": true
30
+ },
27
31
  "quill": {
28
32
  "optional": true
29
33
  }
@@ -313,6 +317,10 @@
313
317
  "types": "./types/rt-tools-ui-kit-v2-pagination.d.ts",
314
318
  "default": "./fesm2022/rt-tools-ui-kit-v2-pagination.mjs"
315
319
  },
320
+ "./pdf-viewer": {
321
+ "types": "./types/rt-tools-ui-kit-v2-pdf-viewer.d.ts",
322
+ "default": "./fesm2022/rt-tools-ui-kit-v2-pdf-viewer.mjs"
323
+ },
316
324
  "./photo-viewer": {
317
325
  "types": "./types/rt-tools-ui-kit-v2-photo-viewer.d.ts",
318
326
  "default": "./fesm2022/rt-tools-ui-kit-v2-photo-viewer.mjs"
@@ -41,7 +41,7 @@ tag/
41
41
  | списки выбора | `select`, `multiselect`, `autocomplete`, `filter-control`, `dynamic-selector` |
42
42
  | обёртка поля | `field`, `form-control` (основа) |
43
43
  | плавающие панели | `popover`, `tooltip`, `confirm-popover`, `menu`, `toast` |
44
- | окна и панели | `dialog`, `aside`, `aside-section`, `bottom-sheet`, `welcome-dialog`, `photo-viewer` |
44
+ | окна и панели | `dialog`, `aside`, `aside-section`, `bottom-sheet`, `welcome-dialog`, `photo-viewer`, `pdf-viewer` |
45
45
  | каркас страницы | `container`, `workspace`, `workspace-details`, `header`, `page-header`, `toolbar`, `section-nav` |
46
46
  | данные | `table`, `pagination`, `thread-list`, `detail-list`, `money-list`, `bar-list`, `timeline`, `night-grid`, `calendar` |
47
47
  | переписка | `chat`, `message`, `message-composer` |
@@ -0,0 +1,54 @@
1
+ # `rt-pdf-viewer`
2
+
3
+ Полноэкранный просмотр документа PDF. Открывается через [`RtDialogService`](../dialog/CONTEXT.md)
4
+ из отдельного входа `@rt-tools/ui-kit-v2/pdf-viewer`.
5
+
6
+ ```typescript
7
+ this.#dialog.open<RtPdfViewerComponent, IRtPdfViewer.Data, void>(RtPdfViewerComponent, {
8
+ data: { source: bytes, fileName: 'Договор.pdf' },
9
+ });
10
+ ```
11
+
12
+ Данные приходят токеном `RT_DIALOG_DATA`: `source` — байты (`Uint8Array`, `ArrayBuffer`) или
13
+ адрес, `fileName` — имя в полосе и имя скачанного файла.
14
+
15
+ ## Главное, что нужно знать
16
+
17
+ **Рисует `pdfjs-dist`, и он необязателен.** Пакет стоит в `peerDependenciesMeta` как optional и
18
+ грузится динамическим `import()` только при открытии окна. Вход отдельный и корнем пакета не
19
+ переотдаётся: из корня сборщик разрешал бы `import('pdfjs-dist')` каждому приложению, даже тому,
20
+ кто просмотрщик не ставит.
21
+
22
+ **Воркер приложение кладёт себе само.** Адрес по умолчанию — `assets/pdfjs/pdf.worker.min.mjs`
23
+ от `<base href>`, другой задаётся `provideRtPdfViewer({ workerSrc })`. Файл копируется из того же
24
+ `node_modules`: версии воркера и рисовальщика обязаны совпадать, и чужой адрес разошёлся бы с ними
25
+ на первом же обновлении.
26
+
27
+ **Рисуются только видимые страницы и по одной соседней.** Места под все страницы ставятся сразу,
28
+ по размерам из документа, — прокрутка не прыгает. Холсты дальше трёх страниц от видимых очищаются.
29
+ Холст рисуется в точках экрана (`devicePixelRatio`), а не в CSS-пикселях.
30
+
31
+ **Масштаб «по ширине» — режим, а не число.** При смене ширины окна страница подстраивается заново.
32
+ Узкое окно (до 768 включительно) и страница, которая в 100% не влезает, открываются «по ширине»,
33
+ остальные — в 100%. Ступени — 50…300%; «по ширине» ими не зажат: на телефоне это около 45%, и
34
+ «−» там уже выключен.
35
+
36
+ **Печать — картинками, а не кадром с файлом.** Каждая страница рисуется в картинку 150 точек на
37
+ дюйм и встаёт в лист в конце `<body>`; на время печати корень страницы получает признак
38
+ `rt-pdf-viewer-printing`, и печатается только лист. Кадр `<iframe>` с самим файлом отвергнут:
39
+ у мобильных браузеров встроенного показа PDF нет, а часть настольных печатает из такого кадра
40
+ пустую или только первую страницу. Картинки печатает любой браузер. Лист убирается по
41
+ `afterprint` и при закрытии окна.
42
+
43
+ **Байты уходят рисовальщику копией.** Рисовальщик забирает буфер себе в поток, и без копии
44
+ скачать файл после открытия было бы нечем.
45
+
46
+ ## Края
47
+
48
+ - Документ, который не открылся (битый файл, рисовальщик не загрузился, воркер не найден), даёт
49
+ отказ по центру с кнопкой «Скачать»: файл всё равно у человека.
50
+ - Скачивание адреса без загруженного документа отдаёт ссылку как есть — чужой домен может не
51
+ принять имя из `download`.
52
+ - Фокус после закрытия возвращается туда, где стоял при открытии: просмотрщик помнит его сам и
53
+ не зависит от `restoreFocus` в настройке открытия.
54
+ - Номер видимой страницы — та, что занимает больше высоты окна; в конце прокрутки — последняя.
@@ -0,0 +1,231 @@
1
+ @use '../../../styles/breakpoints' as bp;
2
+
3
+ @layer rt-kit.components {
4
+ /* rt-pdf-viewer — ViewEncapsulation.None: правила таргетируют BEM-класс блока.
5
+ `.rt-pdf-viewer` — сам хост (шаблон начинается с ng-container, класс ставит host). */
6
+
7
+ .rt-pdf-viewer {
8
+ // Документ читают на тёмном в обеих темах: белый лист на светлой подложке теряет край.
9
+ // Поэтому цвета взяты со ступеней серого, а не с назначений темы, и стоят своими
10
+ // свойствами — потребитель перекрывает их правилом той же цели.
11
+ --rt-pdf-viewer-bg: var(--rt-neutral-900);
12
+ --rt-pdf-viewer-bar-bg: color-mix(in srgb, var(--rt-neutral-900) 35%, var(--rt-neutral-950));
13
+ --rt-pdf-viewer-bar-height: var(--rt-control-height-2xl);
14
+ --rt-pdf-viewer-text-color: var(--rt-neutral-0);
15
+ --rt-pdf-viewer-muted-color: var(--rt-neutral-200);
16
+ --rt-pdf-viewer-page-bg: var(--rt-neutral-0);
17
+ --rt-pdf-viewer-page-shadow: var(--rt-shadow-md);
18
+ --rt-pdf-viewer-page-gap: var(--rt-space-4);
19
+ --rt-pdf-viewer-toolbar-bg: var(--rt-neutral-800);
20
+ --rt-pdf-viewer-toolbar-radius: var(--rt-radius-full);
21
+ --rt-pdf-viewer-divider-width: var(--rt-border-width-thin);
22
+ --rt-pdf-viewer-divider-color: color-mix(in srgb, var(--rt-neutral-0) 20%, transparent);
23
+ --rt-pdf-viewer-control-radius: var(--rt-radius-md);
24
+ --rt-pdf-viewer-control-bg: color-mix(in srgb, var(--rt-neutral-0) 10%, transparent);
25
+ --rt-pdf-viewer-control-bg-hover: color-mix(in srgb, var(--rt-neutral-0) 24%, transparent);
26
+
27
+ display: block;
28
+ width: 100vw;
29
+ height: 100vh;
30
+ height: 100dvh;
31
+ background: var(--rt-pdf-viewer-bg);
32
+ color: var(--rt-pdf-viewer-text-color);
33
+
34
+ // Обёртка существует ради ловушки фокуса: display: contents ломает ловушку
35
+ &__surface {
36
+ position: absolute;
37
+ display: flex;
38
+ flex-direction: column;
39
+ inset: 0;
40
+ }
41
+
42
+ &__bar {
43
+ display: flex;
44
+ height: var(--rt-pdf-viewer-bar-height);
45
+ flex: none;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ padding: 0 var(--rt-space-4);
49
+ background: var(--rt-pdf-viewer-bar-bg);
50
+ gap: var(--rt-space-4);
51
+ }
52
+
53
+ // Имя файла сжимается первым: кнопки справа не уходят за край ни при каком имени
54
+ &__title {
55
+ display: flex;
56
+ min-width: 0;
57
+ align-items: center;
58
+ gap: var(--rt-space-2);
59
+ }
60
+
61
+ &__file-icon {
62
+ flex: none;
63
+ color: var(--rt-pdf-viewer-text-color);
64
+ }
65
+
66
+ // Обрезанное имя целиком видно в подсказке: оно же стоит в title
67
+ &__file-name {
68
+ overflow: hidden;
69
+ min-width: 0;
70
+ font-size: var(--rt-text-sm);
71
+ text-overflow: ellipsis;
72
+ white-space: nowrap;
73
+ }
74
+
75
+ &__actions {
76
+ display: flex;
77
+ flex: none;
78
+ align-items: center;
79
+ gap: var(--rt-space-2);
80
+ }
81
+
82
+ // Кнопки полосы — тот же вид управляющего элемента поверх тёмного, что у просмотра фото
83
+ &__actions rt-icon-button {
84
+ --rt-icon-button-color: var(--rt-pdf-viewer-text-color);
85
+ --rt-icon-button-color-bg-hover: var(--rt-pdf-viewer-control-bg-hover);
86
+
87
+ border-radius: var(--rt-pdf-viewer-control-radius);
88
+ background: var(--rt-pdf-viewer-control-bg);
89
+ }
90
+
91
+ &__scroller {
92
+ position: relative;
93
+ overflow: auto;
94
+ flex: 1 1 auto;
95
+ overscroll-behavior: contain;
96
+
97
+ // Место под полосу прокрутки занято с самого начала: «по ширине» меряется до того, как
98
+ // страницы встанут, и полоса, появившись после, давала бы прокрутку вбок
99
+ scrollbar-gutter: stable;
100
+ }
101
+
102
+ // Столбец шире окна, когда страница увеличена: иначе левый край листа уходил бы за край
103
+ // прокрутки и до него нельзя было бы дотянуться
104
+ &__column {
105
+ display: flex;
106
+ width: max-content;
107
+ min-width: 100%;
108
+ box-sizing: border-box;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ padding: var(--rt-space-4) var(--rt-space-4) var(--rt-space-24);
112
+ gap: var(--rt-pdf-viewer-page-gap);
113
+ }
114
+
115
+ &__page {
116
+ flex: none;
117
+ background: var(--rt-pdf-viewer-page-bg);
118
+ box-shadow: var(--rt-pdf-viewer-page-shadow);
119
+ }
120
+
121
+ &__canvas {
122
+ display: block;
123
+ width: 100%;
124
+ height: 100%;
125
+ }
126
+
127
+ // Индикатор и отказ стоят по центру окна под полосой, поверх пустого столбца
128
+ &__center {
129
+ position: absolute;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ padding: var(--rt-space-4);
134
+ inset: var(--rt-pdf-viewer-bar-height) 0 0;
135
+ }
136
+
137
+ // Свойства заглушки объявлены на её же блоке — перекрываются правилом сильнее его
138
+ & &__error {
139
+ --rt-empty-state-title-color: var(--rt-pdf-viewer-text-color);
140
+
141
+ .rt-empty-state__description {
142
+ color: var(--rt-pdf-viewer-muted-color);
143
+ }
144
+ }
145
+
146
+ // Белая кнопка на тёмном: та же кнопка кита, перекрашенная своими свойствами
147
+ & &__error-download {
148
+ --rt-btn-bg: var(--rt-pdf-viewer-page-bg);
149
+ --rt-btn-bg-hover: var(--rt-neutral-100);
150
+ --rt-btn-bg-active: var(--rt-neutral-200);
151
+ --rt-btn-color: var(--rt-neutral-900);
152
+ }
153
+
154
+ &__toolbar {
155
+ position: absolute;
156
+ bottom: var(--rt-space-6);
157
+ left: 50%;
158
+ display: flex;
159
+ align-items: center;
160
+ padding: var(--rt-space-1) var(--rt-space-2);
161
+ border-radius: var(--rt-pdf-viewer-toolbar-radius);
162
+ background: var(--rt-pdf-viewer-toolbar-bg);
163
+ gap: var(--rt-space-1);
164
+ transform: translateX(-50%);
165
+ white-space: nowrap;
166
+ }
167
+
168
+ &__toolbar rt-icon-button {
169
+ --rt-icon-button-color: var(--rt-pdf-viewer-text-color);
170
+ --rt-icon-button-color-bg-hover: var(--rt-pdf-viewer-control-bg-hover);
171
+ }
172
+
173
+ &__counter,
174
+ &__zoom {
175
+ padding: 0 var(--rt-space-2);
176
+ font-size: var(--rt-text-sm);
177
+ font-variant-numeric: tabular-nums;
178
+ }
179
+
180
+ // Подпись масштаба не дёргает кнопки при смене «75%» на «100%»
181
+ &__zoom {
182
+ min-width: var(--rt-size-12);
183
+ text-align: center;
184
+ }
185
+
186
+ &__divider,
187
+ &__fit-divider {
188
+ width: var(--rt-pdf-viewer-divider-width);
189
+ height: var(--rt-size-5);
190
+ margin: 0 var(--rt-space-1);
191
+ background: var(--rt-pdf-viewer-divider-color);
192
+ }
193
+
194
+ & &__fit {
195
+ --rt-btn-color: var(--rt-pdf-viewer-text-color);
196
+ --rt-btn-bg-hover: var(--rt-pdf-viewer-control-bg-hover);
197
+ }
198
+
199
+ @media (width <= #{bp.$rt-bp-sm}) {
200
+ --rt-pdf-viewer-bar-height: var(--rt-control-height-xl);
201
+
202
+ &__fit,
203
+ &__fit-divider {
204
+ display: none;
205
+ }
206
+ }
207
+ }
208
+
209
+ /* Печатный лист: страницы картинками в конце <body>. Вне печати его не видно, а при печати
210
+ не видно ничего, кроме него, — ни полосы, ни плашки, ни страницы приложения под окном. */
211
+ .rt-pdf-viewer-print {
212
+ display: none;
213
+
214
+ &__page {
215
+ display: block;
216
+ width: 100%;
217
+ break-after: page;
218
+ break-inside: avoid;
219
+ }
220
+ }
221
+
222
+ @media print {
223
+ .rt-pdf-viewer-printing body > :not(.rt-pdf-viewer-print) {
224
+ display: none;
225
+ }
226
+
227
+ .rt-pdf-viewer-printing .rt-pdf-viewer-print {
228
+ display: block;
229
+ }
230
+ }
231
+ }
@@ -53,6 +53,9 @@
53
53
  «Delete» — `danger`, отдаёт `deleteThread` без `confirmMessage`, подтверждение на приложении.
54
54
  Подписи — `uiMoreActions`, `aiCopyThreadId`, `uiRemove`. Пока панель открыта, действия строки
55
55
  видны: правило `:has(.rt-menu--open)` в `rt-thread-list`.
56
+ - `--rt-ai-chat-content-max-width` (по умолчанию `100%`) становится боковым отступом `feed`,
57
+ `composer` и `disclaimer`: колонка стоит по центру беседы, полоса прокрутки ленты остаётся у её
58
+ края. `max-inline-size` на самой ленте увёл бы полосу прокрутки внутрь колонки.
56
59
  - Подсветка совпадений поиска по беседам — `splitSideMenuTitle`.
57
60
  - Строки-превью пустого списка бесед — `emptyPreviewIcons`, по умолчанию `sparkle`, `bot`, `sparkle`:
58
61
  беседы идут с ассистентом, а не с людьми. Вход уходит в `rt-thread-list` как есть.
@@ -6,6 +6,7 @@
6
6
  --rt-ai-chat-bubble-radius: var(--rt-radius-xl);
7
7
  --rt-ai-chat-column-border-width: var(--rt-border-width-thin);
8
8
  --rt-ai-chat-column-width: 280px;
9
+ --rt-ai-chat-content-max-width: 100%;
9
10
  --rt-ai-chat-match-bg: var(--rt-yellow-400);
10
11
  --rt-ai-chat-match-color: var(--rt-neutral-950);
11
12
  --rt-ai-chat-match-radius: var(--rt-radius-xs);
@@ -91,11 +92,12 @@
91
92
  display: flex;
92
93
  flex: 1 1 auto;
93
94
  flex-direction: column;
94
- padding: var(--rt-space-md);
95
95
  font-size: var(--rt-text-sm);
96
96
  gap: var(--rt-space-md);
97
97
  min-block-size: 0;
98
98
  overflow-y: auto;
99
+ padding-block: var(--rt-space-md);
100
+ padding-inline: max(var(--rt-space-md), calc((100% - var(--rt-ai-chat-content-max-width)) / 2));
99
101
  }
100
102
 
101
103
  &__spinner {
@@ -171,16 +173,18 @@
171
173
  &__composer {
172
174
  box-sizing: border-box;
173
175
  flex: none;
174
- padding: var(--rt-space-sm) var(--rt-space-md) 0;
176
+ padding-block: var(--rt-space-sm) 0;
177
+ padding-inline: max(var(--rt-space-md), calc((100% - var(--rt-ai-chat-content-max-width)) / 2));
175
178
  }
176
179
 
177
180
  &__disclaimer {
178
181
  flex: none;
179
- padding: var(--rt-space-xs) var(--rt-space-md) var(--rt-space-sm);
180
182
  margin: 0;
181
183
  color: var(--rt-color-text-muted);
182
184
  font-size: var(--rt-text-xs);
183
185
  line-height: var(--rt-leading-snug);
186
+ padding-block: var(--rt-space-xs) var(--rt-space-sm);
187
+ padding-inline: max(var(--rt-space-md), calc((100% - var(--rt-ai-chat-content-max-width)) / 2));
184
188
  text-align: center;
185
189
  }
186
190
 
@@ -539,6 +539,15 @@ export declare const RT_KIT_LABELS_EN: {
539
539
  readonly fieldErrorMinLength: "Value is too short";
540
540
  readonly fieldErrorPattern: "Invalid format";
541
541
  readonly fieldErrorRequired: "Required field";
542
+ readonly pdfViewerClose: "Close document";
543
+ readonly pdfViewerDownload: "Download";
544
+ readonly pdfViewerErrorText: "You can download the file and open it on your computer.";
545
+ readonly pdfViewerErrorTitle: "Couldn't show the document";
546
+ readonly pdfViewerFitWidth: "Fit to width";
547
+ readonly pdfViewerPageOf: "Page {{page}} of {{total}}";
548
+ readonly pdfViewerPrint: "Print";
549
+ readonly pdfViewerZoomIn: "Zoom in";
550
+ readonly pdfViewerZoomOut: "Zoom out";
542
551
  readonly photoViewerClose: "Close viewer";
543
552
  readonly photoViewerNext: "Next photo";
544
553
  readonly photoViewerPrev: "Previous photo";
@@ -0,0 +1,243 @@
1
+ import * as i0 from "@angular/core";
2
+ import { ElementRef, InjectionToken, Provider, Signal, WritableSignal } from "@angular/core";
3
+ import { TRtKitLabelMap } from "@rt-tools/ui-kit-v2/core";
4
+ /**
5
+ * Контракты полноэкранного просмотрщика документа PDF. Один корневой неймспейс с префиксом `I`.
6
+ */
7
+ export declare namespace IRtPdfViewer {
8
+ /**
9
+ * Откуда взять документ: байты или адрес. Байты просмотрщик не отдаёт рисовальщику как есть —
10
+ * тот забирает буфер себе в поток, и скачать файл после этого было бы нечем. Отдаётся копия.
11
+ */
12
+ type Source = Uint8Array | ArrayBuffer | string;
13
+ /**
14
+ * Данные открытия: документ и имя файла.
15
+ *
16
+ * Имя показывается в полосе сверху и становится именем скачанного файла: адрес документа
17
+ * часто бывает подписанной ссылкой, и имени в нём нет.
18
+ */
19
+ interface Data {
20
+ readonly source: Source;
21
+ readonly fileName: string;
22
+ }
23
+ /** Что сейчас на месте страниц: индикатор, сами страницы или отказ. */
24
+ type Status = 'loading' | 'ready' | 'error';
25
+ /**
26
+ * Масштаб. «По ширине» хранится как режим, а не как число: окно меняет ширину, и страница
27
+ * обязана подстроиться под новую, а не остаться на числе, посчитанном для прежней.
28
+ */
29
+ type Zoom = {
30
+ readonly kind: 'fit';
31
+ } | {
32
+ readonly kind: 'percent';
33
+ readonly percent: number;
34
+ };
35
+ /** Размер страницы в пунктах документа — в единицах, в которых его пишет сам PDF. */
36
+ interface PageSize {
37
+ readonly width: number;
38
+ readonly height: number;
39
+ }
40
+ /** Размер места под страницу на экране, в CSS-пикселях. */
41
+ interface PageBox {
42
+ readonly index: number;
43
+ readonly width: number;
44
+ readonly height: number;
45
+ }
46
+ /**
47
+ * Настройки рисовальщика. Даёт их приложение через `provideRtPdfViewer()`: только оно знает,
48
+ * куда при сборке положен файл воркера.
49
+ */
50
+ interface Config {
51
+ /**
52
+ * Адрес файла воркера `pdf.worker.min.mjs`. Относительный адрес считается от `<base href>`
53
+ * страницы. Версия воркера обязана совпадать с версией `pdfjs-dist`, поэтому файл копируется
54
+ * из того же `node_modules`, а не берётся с чужого адреса.
55
+ */
56
+ readonly workerSrc: string;
57
+ /** Как загрузить рисовальщик. Умолчание — динамический `import('pdfjs-dist')`. */
58
+ readonly load: () => Promise<PdfJs>;
59
+ /** Каталог `cmaps/` пакета — нужен документам с азиатскими шрифтами без вложенных карт. */
60
+ readonly cMapUrl?: string;
61
+ /** Каталог `standard_fonts/` пакета — нужен документам на стандартных шрифтах без вложения. */
62
+ readonly standardFontDataUrl?: string;
63
+ /** Каталог `wasm/` пакета — нужен документам со снимками в JPEG 2000. */
64
+ readonly wasmUrl?: string;
65
+ }
66
+ /**
67
+ * Та часть `pdfjs-dist`, которой пользуется просмотрщик. Описана своими типами, а не взята из
68
+ * пакета: тогда типы кита не требуют пакета у того, кто просмотрщик не ставит, а в тесте
69
+ * рисовальщик подменяется объектом из нескольких функций.
70
+ */
71
+ interface PdfJs {
72
+ readonly GlobalWorkerOptions: {
73
+ workerSrc: string;
74
+ };
75
+ getDocument(params: DocumentParams): LoadingTask;
76
+ }
77
+ interface DocumentParams {
78
+ readonly url?: string;
79
+ readonly data?: Uint8Array;
80
+ readonly cMapUrl?: string;
81
+ readonly cMapPacked?: boolean;
82
+ readonly standardFontDataUrl?: string;
83
+ readonly wasmUrl?: string;
84
+ }
85
+ interface LoadingTask {
86
+ readonly promise: Promise<PdfDocument>;
87
+ destroy(): Promise<void>;
88
+ }
89
+ interface PdfDocument {
90
+ readonly numPages: number;
91
+ getPage(pageNumber: number): Promise<PdfPage>;
92
+ getData(): Promise<Uint8Array>;
93
+ }
94
+ interface PdfPage {
95
+ getViewport(params: {
96
+ scale: number;
97
+ }): Viewport;
98
+ render(params: RenderParams): RenderTask;
99
+ }
100
+ interface Viewport {
101
+ readonly width: number;
102
+ readonly height: number;
103
+ }
104
+ interface RenderParams {
105
+ readonly canvas: HTMLCanvasElement;
106
+ readonly viewport: Viewport;
107
+ readonly intent?: string;
108
+ }
109
+ interface RenderTask {
110
+ readonly promise: Promise<void>;
111
+ cancel(): void;
112
+ }
113
+ /** Печатный лист: разметка страниц картинками и уборка за ней. */
114
+ interface PrintSheet {
115
+ dispose(): void;
116
+ }
117
+ }
118
+ /**
119
+ * Полноэкранный просмотр документа PDF поверх `RtDialogService`.
120
+ *
121
+ * Страницы рисует `pdfjs-dist` — необязательная зависимость кита. Он грузится динамическим импортом
122
+ * только при открытии окна, поэтому вход `@rt-tools/ui-kit-v2/pdf-viewer` не утяжеляет бандл, пока
123
+ * окно не открыто. Рисуются только видимые страницы и их соседи, с плотностью точек экрана.
124
+ *
125
+ * `Esc`, блокировку прокрутки фона и затемнение даёт сам диалог; фокус после закрытия
126
+ * просмотрщик возвращает туда, откуда его открыли.
127
+ */
128
+ export declare class RtPdfViewerComponent {
129
+ #private;
130
+ protected readonly t: Signal<TRtKitLabelMap>;
131
+ protected readonly scroller: Signal<ElementRef<HTMLElement> | undefined>;
132
+ protected readonly column: Signal<ElementRef<HTMLElement> | undefined>;
133
+ protected readonly pageNodes: Signal<readonly ElementRef<HTMLElement>[]>;
134
+ protected readonly canvases: Signal<readonly ElementRef<HTMLCanvasElement>[]>;
135
+ protected readonly fileName: string;
136
+ protected readonly status: WritableSignal<IRtPdfViewer.Status>;
137
+ protected readonly printing: WritableSignal<boolean>;
138
+ protected readonly zoom: WritableSignal<IRtPdfViewer.Zoom>;
139
+ /** Масштаб на экране в процентах — и для «по ширине», который хранится режимом */
140
+ protected readonly percent: Signal<number>;
141
+ protected readonly isFit: Signal<boolean>;
142
+ protected readonly zoomLabel: Signal<string>;
143
+ protected readonly canZoomIn: Signal<boolean>;
144
+ protected readonly canZoomOut: Signal<boolean>;
145
+ protected readonly boxes: Signal<IRtPdfViewer.PageBox[]>;
146
+ /** Видимая сейчас страница, с нуля */
147
+ protected readonly currentPage: WritableSignal<number>;
148
+ protected readonly counter: Signal<string>;
149
+ protected readonly pageOfLabel: Signal<string>;
150
+ constructor();
151
+ close(): void;
152
+ /** Esc закрывает только окно: окно под ним, например боковая панель, остаётся открытым */
153
+ protected onEscape(event: Event): void;
154
+ protected zoomIn(): void;
155
+ protected zoomOut(): void;
156
+ protected fitWidth(): void;
157
+ protected onScroll(): void;
158
+ protected onResize(): void;
159
+ /** Байты уходят как пришли, а адрес — байтами документа, если он уже загружен */
160
+ protected download(): Promise<void>;
161
+ /**
162
+ * Печать документа целиком, без полосы и плашки: страницы рисуются картинками в отдельный лист,
163
+ * и на время печати страница прячет всё, кроме него. Лист убирается после печати.
164
+ */
165
+ protected print(): Promise<void>;
166
+ static ɵfac: i0.ɵɵFactoryDeclaration<RtPdfViewerComponent, never>;
167
+ static ɵcmp: i0.ɵɵComponentDeclaration<RtPdfViewerComponent, "rt-pdf-viewer", never, {}, {}, never, never, true, never>;
168
+ }
169
+ /**
170
+ * Чистая логика просмотрщика документа: шаги масштаба, начальный масштаб, номер видимой страницы и
171
+ * то, какие страницы стоит держать нарисованными.
172
+ */
173
+ /** Ступени масштаба в процентах. Кнопки «−» и «+» ходят только по ним. */
174
+ export declare const RT_PDF_ZOOM_STEPS: readonly number[];
175
+ export declare const RT_PDF_ZOOM_MIN: number;
176
+ export declare const RT_PDF_ZOOM_MAX: number;
177
+ /**
178
+ * Сколько CSS-пикселей в пункте документа. Пункт — 1/72 дюйма, CSS-пиксель — 1/96: при 100%
179
+ * страница A4 на экране той же ширины, что на бумаге.
180
+ */
181
+ export declare const RT_PDF_CSS_UNITS: number;
182
+ /** Ширина окна, до которой документ открывается «по ширине» при любом размере страницы. */
183
+ export declare const RT_PDF_NARROW_WIDTH: number;
184
+ /**
185
+ * Следующая ступень масштаба. Шаг идёт от того, что на экране сейчас: из «по ширине» в 83% кнопка
186
+ * «+» ведёт на 100%, а «−» — на 75%, а не на ступень, соседнюю с последней выбранной руками.
187
+ */
188
+ export declare function nextZoomStep(current: number, direction: 1 | -1): number;
189
+ /** Есть ли куда увеличивать: на верхней границе кнопка «+» выключена. */
190
+ export declare function canZoomIn(current: number): boolean;
191
+ /** Есть ли куда уменьшать: на нижней границе кнопка «−» выключена. */
192
+ export declare function canZoomOut(current: number): boolean;
193
+ /**
194
+ * Масштаб «по ширине» в процентах: самая широкая страница занимает всё место столбца.
195
+ *
196
+ * Границами ступеней он не зажат. На телефоне страница A4 «по ширине» — около 45%, и зажать её до
197
+ * 50% значило бы вернуть горизонтальную прокрутку, ради которой режим и существует.
198
+ */
199
+ export declare function fitWidthPercent(availableWidth: number, pages: readonly IRtPdfViewer.PageSize[]): number;
200
+ /**
201
+ * С какого масштаба открыть документ. Узкий экран и страница, которая в 100% не влезает, —
202
+ * «по ширине»: иначе человек сначала видит обрезанный край листа. Остальные — в 100%.
203
+ */
204
+ export declare function initialZoom(viewportWidth: number, availableWidth: number, pages: readonly IRtPdfViewer.PageSize[]): IRtPdfViewer.Zoom;
205
+ /** Места под страницы на экране: от размера в пунктах и масштаба, в целых пикселях. */
206
+ export declare function pageBoxes(pages: readonly IRtPdfViewer.PageSize[], percent: number): IRtPdfViewer.PageBox[];
207
+ /**
208
+ * Номер страницы, которая видна сейчас, — с нуля. Это та, что занимает больше всего высоты окна;
209
+ * при прокрутке до конца — последняя, даже если она короче соседки: иначе счётчик никогда не
210
+ * показал бы «M / M» у документа с маленькой последней страницей. Документ, который виден целиком
211
+ * без прокрутки, стоит на первой: до конца его никто не прокручивал.
212
+ */
213
+ export declare function visiblePageIndex(tops: readonly number[], heights: readonly number[], scrollTop: number, viewportHeight: number): number;
214
+ /**
215
+ * Страницы, которые стоит нарисовать: видимые и по `overscan` соседних с каждой стороны. Документ
216
+ * в сотню страниц разом не рисуется — каждая страница это холст в мегабайты памяти.
217
+ */
218
+ export declare function pagesToRender(tops: readonly number[], heights: readonly number[], scrollTop: number, viewportHeight: number, overscan?: number): number[];
219
+ /**
220
+ * Куда прокрутить после смены масштаба, чтобы на экране осталось то же место той же страницы.
221
+ * Без этого увеличение уводило бы человека на несколько страниц вниз от того, что он читал.
222
+ */
223
+ export declare function anchoredScrollTop(oldTop: number, oldHeight: number, oldScrollTop: number, newTop: number, newHeight: number): number;
224
+ /** Подпись масштаба для плашки в процентах: «83%», а не «83.47%». */
225
+ export declare function zoomPercentLabel(percent: number): string;
226
+ /**
227
+ * Адрес воркера по умолчанию. Относительный: считается от `<base href>`, поэтому приложение под
228
+ * своим префиксом не правит его руками. Файл туда кладёт сборка приложения — см. обзор компонента.
229
+ */
230
+ export declare const RT_PDF_VIEWER_WORKER_SRC: string;
231
+ /** Настройки рисовальщика просмотрщика. Без провайдера — умолчания выше. */
232
+ export declare const RT_PDF_VIEWER_CONFIG: InjectionToken<IRtPdfViewer.Config>;
233
+ /**
234
+ * Настраивает просмотрщик: адрес воркера и каталоги данных `pdfjs-dist`. Незаданное берётся из
235
+ * умолчаний.
236
+ *
237
+ * ```typescript
238
+ * export const appConfig: ApplicationConfig = {
239
+ * providers: [provideRtPdfViewer({ workerSrc: 'static/pdf.worker.min.mjs' })],
240
+ * };
241
+ * ```
242
+ */
243
+ export declare function provideRtPdfViewer(config: Partial<IRtPdfViewer.Config>): Provider;