arilekh-report-viewer 1.0.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.
@@ -0,0 +1,1087 @@
1
+ import * as i0 from '@angular/core';
2
+ import { ViewChild, Input, ChangeDetectionStrategy, Component, Injectable, EventEmitter, Output, ViewChildren } from '@angular/core';
3
+ import * as i3 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import * as i2 from '@angular/forms';
6
+ import { FormsModule } from '@angular/forms';
7
+ import { BehaviorSubject, Subject, Observable } from 'rxjs';
8
+ import { tap, takeUntil, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
9
+ import * as i1 from '@angular/common/http';
10
+ import { HttpParams } from '@angular/common/http';
11
+
12
+ const PT_TO_PX$1 = 1.333; // 1pt = 1.333px at 96dpi
13
+ class PageRendererComponent {
14
+ page;
15
+ scale = PT_TO_PX$1;
16
+ pageEl;
17
+ Math = Math;
18
+ get widthPx() { return (this.page?.widthPt ?? 595) * this.scale; }
19
+ get heightPx() { return (this.page?.heightPt ?? 842) * this.scale; }
20
+ ngOnChanges(c) { }
21
+ // ── Style builders ────────────────────────────────────────────────────────
22
+ textStyle(el) {
23
+ const s = el.style;
24
+ const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
25
+ const vAlign = el.verticalAlign === 'middle' ? 'center'
26
+ : el.verticalAlign === 'bottom' ? 'flex-end'
27
+ : 'flex-start';
28
+ return {
29
+ position: 'absolute',
30
+ left: `${el.x * this.scale}px`,
31
+ top: `${el.y * this.scale}px`,
32
+ width: `${el.width * this.scale}px`,
33
+ height: `${el.height * this.scale}px`,
34
+ overflow: 'hidden',
35
+ fontSize: `${(s?.fontSize ?? 9) * this.scale}px`,
36
+ fontFamily: s?.fontFamily ?? 'inherit',
37
+ fontWeight: s?.bold ? 'bold' : 'normal',
38
+ fontStyle: s?.italic ? 'italic' : 'normal',
39
+ textDecoration: s?.underline ? 'underline' : 'none',
40
+ color: s?.foreColor ?? '#000',
41
+ background: s?.backColor ?? 'transparent',
42
+ textAlign: el.alignment ?? 'left',
43
+ display: 'flex',
44
+ flexDirection: 'column',
45
+ justifyContent: vAlign,
46
+ paddingLeft: `${(s?.paddingLeft ?? 0) * this.scale}px`,
47
+ paddingRight: `${(s?.paddingRight ?? 0) * this.scale}px`,
48
+ transform: rot,
49
+ transformOrigin: rot ? 'center center' : '',
50
+ boxSizing: 'border-box',
51
+ };
52
+ }
53
+ rectStyle(el) {
54
+ const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
55
+ return {
56
+ position: 'absolute',
57
+ left: `${el.x * this.scale}px`,
58
+ top: `${el.y * this.scale}px`,
59
+ width: `${el.width * this.scale}px`,
60
+ height: `${el.height * this.scale}px`,
61
+ background: el.fillColor ?? 'transparent',
62
+ border: el.strokeColor
63
+ ? `${(el.strokeWidth ?? 1) * this.scale}px solid ${el.strokeColor}`
64
+ : 'none',
65
+ borderRadius: el.borderRadius ? `${el.borderRadius}%` : '0',
66
+ transform: rot,
67
+ transformOrigin: rot ? 'center center' : '',
68
+ boxSizing: 'border-box',
69
+ };
70
+ }
71
+ lineSvgStyle(el) {
72
+ const x1 = el.x, y1 = el.y;
73
+ const x2 = el.x2 ?? el.x, y2 = el.y2 ?? el.y;
74
+ const left = Math.min(x1, x2) * this.scale;
75
+ const top = Math.min(y1, y2) * this.scale;
76
+ const width = Math.max(Math.abs(x2 - x1) * this.scale, (el.strokeWidth ?? 1) * this.scale + 2);
77
+ const height = Math.max(Math.abs(y2 - y1) * this.scale, (el.strokeWidth ?? 1) * this.scale + 2);
78
+ const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
79
+ return {
80
+ position: 'absolute',
81
+ left: `${left}px`, top: `${top}px`,
82
+ width: `${width}px`, height: `${height}px`,
83
+ transform: rot, transformOrigin: rot ? 'center center' : '',
84
+ };
85
+ }
86
+ lineX1(el) {
87
+ return (el.x * this.scale) - Math.min(el.x, el.x2 ?? el.x) * this.scale;
88
+ }
89
+ lineY1(el) {
90
+ return (el.y * this.scale) - Math.min(el.y, el.y2 ?? el.y) * this.scale;
91
+ }
92
+ lineX2(el) {
93
+ return ((el.x2 ?? el.x) * this.scale) - Math.min(el.x, el.x2 ?? el.x) * this.scale;
94
+ }
95
+ lineY2(el) {
96
+ return ((el.y2 ?? el.y) * this.scale) - Math.min(el.y, el.y2 ?? el.y) * this.scale;
97
+ }
98
+ ellipseSvgStyle(el) {
99
+ const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
100
+ return {
101
+ position: 'absolute',
102
+ left: `${el.x * this.scale}px`, top: `${el.y * this.scale}px`,
103
+ width: `${el.width * this.scale}px`, height: `${el.height * this.scale}px`,
104
+ transform: rot, transformOrigin: rot ? 'center center' : '',
105
+ };
106
+ }
107
+ imageStyle(el) {
108
+ const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
109
+ return {
110
+ position: 'absolute',
111
+ left: `${el.x * this.scale}px`,
112
+ top: `${el.y * this.scale}px`,
113
+ width: `${el.width * this.scale}px`,
114
+ height: `${el.height * this.scale}px`,
115
+ objectFit: el.stretch ?? 'contain',
116
+ transform: rot, transformOrigin: rot ? 'center center' : '',
117
+ };
118
+ }
119
+ openLink(url) {
120
+ window.open(url, '_blank', 'noopener');
121
+ }
122
+ /** Expose the page div for screenshot (called by parent). */
123
+ getPageElement() {
124
+ return this.pageEl?.nativeElement;
125
+ }
126
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: PageRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
127
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: PageRendererComponent, isStandalone: true, selector: "arv-page-renderer", inputs: { page: "page", scale: "scale" }, viewQueries: [{ propertyName: "pageEl", first: true, predicate: ["pageEl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
128
+ <div class="arv-page"
129
+ #pageEl
130
+ [style.width.px]="widthPx"
131
+ [style.height.px]="heightPx"
132
+ [attr.data-page]="page?.pageNumber">
133
+
134
+ @if (page) {
135
+ @for (el of page.elements; track $index) {
136
+ <!-- TEXT -->
137
+ @if (el.type === 'text') {
138
+ <div [style]="textStyle(el)"
139
+ [class.arv-hyperlink]="!!el.hyperlinkUrl"
140
+ (click)="el.hyperlinkUrl && openLink(el.hyperlinkUrl)">
141
+ {{ el.text }}
142
+ </div>
143
+ }
144
+ <!-- RECT -->
145
+ @else if (el.type === 'rect') {
146
+ <div [style]="rectStyle(el)"></div>
147
+ }
148
+ <!-- LINE -->
149
+ @else if (el.type === 'line') {
150
+ <svg [style]="lineSvgStyle(el)" xmlns="http://www.w3.org/2000/svg">
151
+ <line
152
+ [attr.x1]="lineX1(el)" [attr.y1]="lineY1(el)"
153
+ [attr.x2]="lineX2(el)" [attr.y2]="lineY2(el)"
154
+ [attr.stroke]="el.strokeColor || '#000'"
155
+ [attr.stroke-width]="(el.strokeWidth || 1) * scale" />
156
+ </svg>
157
+ }
158
+ <!-- ELLIPSE -->
159
+ @else if (el.type === 'ellipse') {
160
+ <svg [style]="ellipseSvgStyle(el)" xmlns="http://www.w3.org/2000/svg">
161
+ <ellipse
162
+ [attr.cx]="(el.width * scale / 2)"
163
+ [attr.cy]="(el.height * scale / 2)"
164
+ [attr.rx]="Math.max(0, el.width * scale / 2 - (el.strokeWidth||1)*scale/2)"
165
+ [attr.ry]="Math.max(0, el.height * scale / 2 - (el.strokeWidth||1)*scale/2)"
166
+ [attr.fill]="el.fillColor || 'none'"
167
+ [attr.stroke]="el.strokeColor || '#000'"
168
+ [attr.stroke-width]="(el.strokeWidth || 1) * scale" />
169
+ </svg>
170
+ }
171
+ <!-- IMAGE -->
172
+ @else if (el.type === 'image') {
173
+ <img [style]="imageStyle(el)"
174
+ [src]="el.src"
175
+ alt="" />
176
+ }
177
+ }
178
+ }
179
+ </div>
180
+ `, isInline: true, styles: [":host{display:block}.arv-page{position:relative;background:#fff;overflow:hidden;box-shadow:0 2px 12px #0000004d}.arv-hyperlink{cursor:pointer}.arv-hyperlink:hover{opacity:.8}svg{overflow:visible}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
181
+ }
182
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: PageRendererComponent, decorators: [{
183
+ type: Component,
184
+ args: [{ selector: 'arv-page-renderer', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
185
+ <div class="arv-page"
186
+ #pageEl
187
+ [style.width.px]="widthPx"
188
+ [style.height.px]="heightPx"
189
+ [attr.data-page]="page?.pageNumber">
190
+
191
+ @if (page) {
192
+ @for (el of page.elements; track $index) {
193
+ <!-- TEXT -->
194
+ @if (el.type === 'text') {
195
+ <div [style]="textStyle(el)"
196
+ [class.arv-hyperlink]="!!el.hyperlinkUrl"
197
+ (click)="el.hyperlinkUrl && openLink(el.hyperlinkUrl)">
198
+ {{ el.text }}
199
+ </div>
200
+ }
201
+ <!-- RECT -->
202
+ @else if (el.type === 'rect') {
203
+ <div [style]="rectStyle(el)"></div>
204
+ }
205
+ <!-- LINE -->
206
+ @else if (el.type === 'line') {
207
+ <svg [style]="lineSvgStyle(el)" xmlns="http://www.w3.org/2000/svg">
208
+ <line
209
+ [attr.x1]="lineX1(el)" [attr.y1]="lineY1(el)"
210
+ [attr.x2]="lineX2(el)" [attr.y2]="lineY2(el)"
211
+ [attr.stroke]="el.strokeColor || '#000'"
212
+ [attr.stroke-width]="(el.strokeWidth || 1) * scale" />
213
+ </svg>
214
+ }
215
+ <!-- ELLIPSE -->
216
+ @else if (el.type === 'ellipse') {
217
+ <svg [style]="ellipseSvgStyle(el)" xmlns="http://www.w3.org/2000/svg">
218
+ <ellipse
219
+ [attr.cx]="(el.width * scale / 2)"
220
+ [attr.cy]="(el.height * scale / 2)"
221
+ [attr.rx]="Math.max(0, el.width * scale / 2 - (el.strokeWidth||1)*scale/2)"
222
+ [attr.ry]="Math.max(0, el.height * scale / 2 - (el.strokeWidth||1)*scale/2)"
223
+ [attr.fill]="el.fillColor || 'none'"
224
+ [attr.stroke]="el.strokeColor || '#000'"
225
+ [attr.stroke-width]="(el.strokeWidth || 1) * scale" />
226
+ </svg>
227
+ }
228
+ <!-- IMAGE -->
229
+ @else if (el.type === 'image') {
230
+ <img [style]="imageStyle(el)"
231
+ [src]="el.src"
232
+ alt="" />
233
+ }
234
+ }
235
+ }
236
+ </div>
237
+ `, styles: [":host{display:block}.arv-page{position:relative;background:#fff;overflow:hidden;box-shadow:0 2px 12px #0000004d}.arv-hyperlink{cursor:pointer}.arv-hyperlink:hover{opacity:.8}svg{overflow:visible}\n"] }]
238
+ }], propDecorators: { page: [{
239
+ type: Input
240
+ }], scale: [{
241
+ type: Input
242
+ }], pageEl: [{
243
+ type: ViewChild,
244
+ args: ['pageEl']
245
+ }] } });
246
+
247
+ // ── Service ────────────────────────────────────────────────────────────────
248
+ class ReportViewerApiService {
249
+ http;
250
+ baseUrl = '';
251
+ // ── Observable state ───────────────────────────────────────────────────────
252
+ _session = new BehaviorSubject(null);
253
+ _loading = new BehaviorSubject(false);
254
+ _error = new BehaviorSubject(null);
255
+ _pageCache = new Map();
256
+ _navigateTo$ = new Subject();
257
+ session$ = this._session.asObservable();
258
+ loading$ = this._loading.asObservable();
259
+ error$ = this._error.asObservable();
260
+ navigateTo$ = this._navigateTo$.asObservable();
261
+ // ── Configuration ──────────────────────────────────────────────────────────
262
+ configure(apiBaseUrl) {
263
+ this.baseUrl = apiBaseUrl.replace(/\/$/, '');
264
+ }
265
+ get sessionId() {
266
+ return this._session.value?.sessionId ?? null;
267
+ }
268
+ get pageCount() {
269
+ return this._session.value?.pageCount ?? 0;
270
+ }
271
+ get pageWidthPt() {
272
+ return this._session.value?.pageWidthPt ?? 595;
273
+ }
274
+ get pageHeightPt() {
275
+ return this._session.value?.pageHeightPt ?? 842;
276
+ }
277
+ // ── Render ────────────────────────────────────────────────────────────────
278
+ constructor(http) {
279
+ this.http = http;
280
+ }
281
+ render(req) {
282
+ this._loading.next(true);
283
+ this._error.next(null);
284
+ this._pageCache.clear();
285
+ this._session.next(null);
286
+ return this.http
287
+ .post(`${this.baseUrl}/api/reports/render`, req)
288
+ .pipe(tap({
289
+ next: r => { this._session.next(r); this._loading.next(false); },
290
+ error: e => {
291
+ this._error.next(e?.error?.error ?? 'Render failed');
292
+ this._loading.next(false);
293
+ }
294
+ }));
295
+ }
296
+ // ── Page retrieval (cached) ────────────────────────────────────────────────
297
+ getPage(pageNumber) {
298
+ const id = this.sessionId;
299
+ if (!id)
300
+ throw new Error('No active session.');
301
+ // Check cache first
302
+ const cached = this._pageCache.get(pageNumber);
303
+ if (cached) {
304
+ return new Observable(o => { o.next(cached); o.complete(); });
305
+ }
306
+ return this.http
307
+ .get(`${this.baseUrl}/api/reports/${id}/pages/${pageNumber}`)
308
+ .pipe(tap(p => this._pageCache.set(pageNumber, p)));
309
+ }
310
+ getPageRange(from, to) {
311
+ const id = this.sessionId;
312
+ if (!id)
313
+ throw new Error('No active session.');
314
+ const params = new HttpParams()
315
+ .set('from', from.toString())
316
+ .set('to', to.toString());
317
+ return this.http.get(`${this.baseUrl}/api/reports/${id}/pages`, { params });
318
+ }
319
+ // ── Thumbnails ────────────────────────────────────────────────────────────
320
+ getThumbnails() {
321
+ const id = this.sessionId;
322
+ return this.http.get(`${this.baseUrl}/api/reports/${id}/thumbnails`);
323
+ }
324
+ // ── Search ────────────────────────────────────────────────────────────────
325
+ search(query) {
326
+ const id = this.sessionId;
327
+ const params = new HttpParams().set('q', query);
328
+ return this.http.get(`${this.baseUrl}/api/reports/${id}/search`, { params });
329
+ }
330
+ // ── Navigation helper ─────────────────────────────────────────────────────
331
+ navigateTo(page) {
332
+ this._navigateTo$.next(Math.max(1, Math.min(page, this.pageCount)));
333
+ }
334
+ // ── Cache management ──────────────────────────────────────────────────────
335
+ evictPage(pageNumber) {
336
+ this._pageCache.delete(pageNumber);
337
+ }
338
+ clearCache() {
339
+ this._pageCache.clear();
340
+ }
341
+ isPageCached(pageNumber) {
342
+ return this._pageCache.has(pageNumber);
343
+ }
344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerApiService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
345
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerApiService, providedIn: 'root' });
346
+ }
347
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerApiService, decorators: [{
348
+ type: Injectable,
349
+ args: [{ providedIn: 'root' }]
350
+ }], ctorParameters: () => [{ type: i1.HttpClient }] });
351
+
352
+ class LeftPanelComponent {
353
+ api;
354
+ cdr;
355
+ currentPage = 1;
356
+ pageSelected = new EventEmitter();
357
+ thumbnails;
358
+ searchQuery = '';
359
+ lastQuery = '';
360
+ searching = false;
361
+ searchResults = [];
362
+ destroy$ = new Subject();
363
+ search$ = new Subject();
364
+ constructor(api, cdr) {
365
+ this.api = api;
366
+ this.cdr = cdr;
367
+ }
368
+ ngOnInit() {
369
+ // Load thumbnails when session is ready
370
+ this.api.session$
371
+ .pipe(takeUntil(this.destroy$))
372
+ .subscribe(s => {
373
+ if (s)
374
+ this.loadThumbnails();
375
+ });
376
+ // Debounced search
377
+ this.search$.pipe(takeUntil(this.destroy$), debounceTime(400), distinctUntilChanged(), switchMap(q => {
378
+ if (!q.trim()) {
379
+ this.searchResults = [];
380
+ this.searching = false;
381
+ this.cdr.markForCheck();
382
+ return [];
383
+ }
384
+ this.searching = true;
385
+ this.cdr.markForCheck();
386
+ return this.api.search(q);
387
+ })).subscribe({
388
+ next: r => {
389
+ this.searchResults = r.results;
390
+ this.lastQuery = r.query;
391
+ this.searching = false;
392
+ this.cdr.markForCheck();
393
+ },
394
+ error: () => { this.searching = false; this.cdr.markForCheck(); }
395
+ });
396
+ }
397
+ ngOnDestroy() {
398
+ this.destroy$.next();
399
+ this.destroy$.complete();
400
+ }
401
+ loadThumbnails() {
402
+ this.api.getThumbnails()
403
+ .pipe(takeUntil(this.destroy$))
404
+ .subscribe(t => {
405
+ this.thumbnails = t;
406
+ this.cdr.markForCheck();
407
+ });
408
+ }
409
+ onSearchChange(q) {
410
+ this.search$.next(q);
411
+ }
412
+ goTo(page) {
413
+ this.pageSelected.emit(page);
414
+ }
415
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: LeftPanelComponent, deps: [{ token: ReportViewerApiService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
416
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: LeftPanelComponent, isStandalone: true, selector: "arv-left-panel", inputs: { currentPage: "currentPage" }, outputs: { pageSelected: "pageSelected" }, ngImport: i0, template: `
417
+ <div class="arv-left">
418
+
419
+ <!-- Header -->
420
+ <div class="arv-left__header">
421
+ <span class="arv-left__title">Pages</span>
422
+ <span class="arv-left__badge">{{ api.pageCount }}</span>
423
+ </div>
424
+
425
+ <!-- Search -->
426
+ <div class="arv-left__search">
427
+ <input class="arv-input"
428
+ placeholder="Search in report…"
429
+ [(ngModel)]="searchQuery"
430
+ (ngModelChange)="onSearchChange($event)" />
431
+ @if (searching) {
432
+ <span class="arv-spinner-sm"></span>
433
+ }
434
+ </div>
435
+
436
+ <!-- Search results -->
437
+ @if (searchResults.length > 0) {
438
+ <div class="arv-left__section-title">
439
+ {{ searchResults.length }} result(s) for "{{ lastQuery }}"
440
+ </div>
441
+ <div class="arv-left__list">
442
+ @for (hit of searchResults; track hit.pageNumber) {
443
+ <div class="arv-bm arv-bm--search"
444
+ [class.arv-bm--active]="currentPage === hit.pageNumber"
445
+ (click)="goTo(hit.pageNumber)">
446
+ <span class="arv-bm__icon">🔍</span>
447
+ <div class="arv-bm__col">
448
+ <span class="arv-bm__label">Page {{ hit.pageNumber }}</span>
449
+ <span class="arv-bm__snippet">{{ hit.snippet }}</span>
450
+ </div>
451
+ </div>
452
+ }
453
+ </div>
454
+ }
455
+
456
+ <!-- Page thumbnails -->
457
+ @if (searchResults.length === 0) {
458
+ <div class="arv-left__section-title">All Pages</div>
459
+ <div class="arv-left__list">
460
+ @if (thumbnails) {
461
+ @for (pg of thumbnails.pages; track pg.pageNumber) {
462
+ <div class="arv-bm arv-thumb"
463
+ [class.arv-bm--active]="currentPage === pg.pageNumber"
464
+ (click)="goTo(pg.pageNumber)">
465
+ <!-- Mini thumbnail placeholder -->
466
+ <div class="arv-thumb__box">
467
+ <span class="arv-thumb__num">{{ pg.pageNumber }}</span>
468
+ </div>
469
+ <div class="arv-bm__col">
470
+ <span class="arv-bm__label">Page {{ pg.pageNumber }}</span>
471
+ @if (pg.firstText) {
472
+ <span class="arv-bm__snippet">{{ pg.firstText | slice:0:40 }}</span>
473
+ }
474
+ </div>
475
+ </div>
476
+ }
477
+ } @else {
478
+ <div class="arv-left__loading">Loading…</div>
479
+ }
480
+ </div>
481
+ }
482
+
483
+ <!-- Footer info -->
484
+ <div class="arv-left__footer">
485
+ <span>{{ api.pageCount }} pages</span>
486
+ @if (api.pageCount > 0) {
487
+ <span>· Page {{ currentPage }}</span>
488
+ }
489
+ </div>
490
+ </div>
491
+ `, isInline: true, styles: [".arv-left{width:200px;flex-shrink:0;display:flex;flex-direction:column;background:var(--arv-surface, #2a2a3e);border-right:1px solid var(--arv-border, #3d3d5c);overflow:hidden;height:100%}.arv-left__header{display:flex;align-items:center;justify-content:space-between;padding:8px 10px 4px;font-weight:700;font-size:11px;color:var(--arv-text-muted, #888);text-transform:uppercase;flex-shrink:0}.arv-left__badge{font-size:9px;background:var(--arv-border, #3d3d5c);border-radius:8px;padding:1px 6px;color:var(--arv-text, #ccc)}.arv-left__search{display:flex;align-items:center;gap:4px;padding:4px 8px;flex-shrink:0}.arv-input{flex:1;padding:4px 8px;background:var(--arv-surface2, #313148);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:11px;outline:none;width:100%;box-sizing:border-box}.arv-left__section-title{font-size:9px;color:var(--arv-text-muted, #888);text-transform:uppercase;padding:4px 10px 2px;flex-shrink:0}.arv-left__list{flex:1;overflow-y:auto;min-height:0}.arv-left__loading{padding:12px;font-size:11px;color:var(--arv-text-muted, #888)}.arv-bm{display:flex;align-items:flex-start;gap:6px;padding:5px 10px;cursor:pointer;font-size:11px;border-radius:2px;margin:1px 4px}.arv-bm:hover{background:var(--arv-surface2, #313148)}.arv-bm--active{background:#5b8ef026;color:var(--arv-accent, #5b8ef0)}.arv-bm__col{display:flex;flex-direction:column;gap:2px;overflow:hidden}.arv-bm__label{font-size:11px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.arv-bm__snippet{font-size:9px;color:var(--arv-text-muted, #888);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.arv-bm__icon{flex-shrink:0;font-size:12px}.arv-thumb__box{width:30px;height:40px;background:#fff;border:1px solid var(--arv-border, #3d3d5c);border-radius:2px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.arv-thumb__num{font-size:8px;color:#999}.arv-left__footer{padding:6px 10px;font-size:10px;color:var(--arv-text-muted, #888);border-top:1px solid var(--arv-border, #3d3d5c);flex-shrink:0;display:flex;gap:4px}.arv-spinner-sm{width:14px;height:14px;border:2px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .7s linear infinite;flex-shrink:0}@keyframes arv-spin{to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i3.SlicePipe, name: "slice" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
492
+ }
493
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: LeftPanelComponent, decorators: [{
494
+ type: Component,
495
+ args: [{ selector: 'arv-left-panel', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
496
+ <div class="arv-left">
497
+
498
+ <!-- Header -->
499
+ <div class="arv-left__header">
500
+ <span class="arv-left__title">Pages</span>
501
+ <span class="arv-left__badge">{{ api.pageCount }}</span>
502
+ </div>
503
+
504
+ <!-- Search -->
505
+ <div class="arv-left__search">
506
+ <input class="arv-input"
507
+ placeholder="Search in report…"
508
+ [(ngModel)]="searchQuery"
509
+ (ngModelChange)="onSearchChange($event)" />
510
+ @if (searching) {
511
+ <span class="arv-spinner-sm"></span>
512
+ }
513
+ </div>
514
+
515
+ <!-- Search results -->
516
+ @if (searchResults.length > 0) {
517
+ <div class="arv-left__section-title">
518
+ {{ searchResults.length }} result(s) for "{{ lastQuery }}"
519
+ </div>
520
+ <div class="arv-left__list">
521
+ @for (hit of searchResults; track hit.pageNumber) {
522
+ <div class="arv-bm arv-bm--search"
523
+ [class.arv-bm--active]="currentPage === hit.pageNumber"
524
+ (click)="goTo(hit.pageNumber)">
525
+ <span class="arv-bm__icon">🔍</span>
526
+ <div class="arv-bm__col">
527
+ <span class="arv-bm__label">Page {{ hit.pageNumber }}</span>
528
+ <span class="arv-bm__snippet">{{ hit.snippet }}</span>
529
+ </div>
530
+ </div>
531
+ }
532
+ </div>
533
+ }
534
+
535
+ <!-- Page thumbnails -->
536
+ @if (searchResults.length === 0) {
537
+ <div class="arv-left__section-title">All Pages</div>
538
+ <div class="arv-left__list">
539
+ @if (thumbnails) {
540
+ @for (pg of thumbnails.pages; track pg.pageNumber) {
541
+ <div class="arv-bm arv-thumb"
542
+ [class.arv-bm--active]="currentPage === pg.pageNumber"
543
+ (click)="goTo(pg.pageNumber)">
544
+ <!-- Mini thumbnail placeholder -->
545
+ <div class="arv-thumb__box">
546
+ <span class="arv-thumb__num">{{ pg.pageNumber }}</span>
547
+ </div>
548
+ <div class="arv-bm__col">
549
+ <span class="arv-bm__label">Page {{ pg.pageNumber }}</span>
550
+ @if (pg.firstText) {
551
+ <span class="arv-bm__snippet">{{ pg.firstText | slice:0:40 }}</span>
552
+ }
553
+ </div>
554
+ </div>
555
+ }
556
+ } @else {
557
+ <div class="arv-left__loading">Loading…</div>
558
+ }
559
+ </div>
560
+ }
561
+
562
+ <!-- Footer info -->
563
+ <div class="arv-left__footer">
564
+ <span>{{ api.pageCount }} pages</span>
565
+ @if (api.pageCount > 0) {
566
+ <span>· Page {{ currentPage }}</span>
567
+ }
568
+ </div>
569
+ </div>
570
+ `, styles: [".arv-left{width:200px;flex-shrink:0;display:flex;flex-direction:column;background:var(--arv-surface, #2a2a3e);border-right:1px solid var(--arv-border, #3d3d5c);overflow:hidden;height:100%}.arv-left__header{display:flex;align-items:center;justify-content:space-between;padding:8px 10px 4px;font-weight:700;font-size:11px;color:var(--arv-text-muted, #888);text-transform:uppercase;flex-shrink:0}.arv-left__badge{font-size:9px;background:var(--arv-border, #3d3d5c);border-radius:8px;padding:1px 6px;color:var(--arv-text, #ccc)}.arv-left__search{display:flex;align-items:center;gap:4px;padding:4px 8px;flex-shrink:0}.arv-input{flex:1;padding:4px 8px;background:var(--arv-surface2, #313148);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:11px;outline:none;width:100%;box-sizing:border-box}.arv-left__section-title{font-size:9px;color:var(--arv-text-muted, #888);text-transform:uppercase;padding:4px 10px 2px;flex-shrink:0}.arv-left__list{flex:1;overflow-y:auto;min-height:0}.arv-left__loading{padding:12px;font-size:11px;color:var(--arv-text-muted, #888)}.arv-bm{display:flex;align-items:flex-start;gap:6px;padding:5px 10px;cursor:pointer;font-size:11px;border-radius:2px;margin:1px 4px}.arv-bm:hover{background:var(--arv-surface2, #313148)}.arv-bm--active{background:#5b8ef026;color:var(--arv-accent, #5b8ef0)}.arv-bm__col{display:flex;flex-direction:column;gap:2px;overflow:hidden}.arv-bm__label{font-size:11px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.arv-bm__snippet{font-size:9px;color:var(--arv-text-muted, #888);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.arv-bm__icon{flex-shrink:0;font-size:12px}.arv-thumb__box{width:30px;height:40px;background:#fff;border:1px solid var(--arv-border, #3d3d5c);border-radius:2px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.arv-thumb__num{font-size:8px;color:#999}.arv-left__footer{padding:6px 10px;font-size:10px;color:var(--arv-text-muted, #888);border-top:1px solid var(--arv-border, #3d3d5c);flex-shrink:0;display:flex;gap:4px}.arv-spinner-sm{width:14px;height:14px;border:2px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .7s linear infinite;flex-shrink:0}@keyframes arv-spin{to{transform:rotate(360deg)}}\n"] }]
571
+ }], ctorParameters: () => [{ type: ReportViewerApiService }, { type: i0.ChangeDetectorRef }], propDecorators: { currentPage: [{
572
+ type: Input
573
+ }], pageSelected: [{
574
+ type: Output
575
+ }] } });
576
+
577
+ const PT_TO_PX = 1.333;
578
+ const WINDOW_RADIUS = 5; // render ±5 pages around current
579
+ class ReportViewerComponent {
580
+ api;
581
+ cdr;
582
+ /** API base URL, e.g. 'http://localhost:5000' */
583
+ set apiBaseUrl(url) {
584
+ if (url)
585
+ this.api.configure(url);
586
+ }
587
+ /** Light or dark theme */
588
+ theme = 'dark';
589
+ /** If provided, render this report on init */
590
+ renderRequest;
591
+ scrollEl;
592
+ pageRenderers;
593
+ currentPage = 1;
594
+ scale = PT_TO_PX;
595
+ pageCache = new Map();
596
+ destroy$ = new Subject();
597
+ printPage; // undefined = all, number = single page
598
+ constructor(api, cdr) {
599
+ this.api = api;
600
+ this.cdr = cdr;
601
+ }
602
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
603
+ ngOnInit() {
604
+ // Subscribe to external navigation commands
605
+ this.api.navigateTo$
606
+ .pipe(takeUntil(this.destroy$))
607
+ .subscribe(p => this.navigateTo(p));
608
+ // Auto-render if input provided
609
+ if (this.renderRequest) {
610
+ this.api.render(this.renderRequest)
611
+ .pipe(takeUntil(this.destroy$))
612
+ .subscribe(() => { this.currentPage = 1; this.cdr.markForCheck(); });
613
+ }
614
+ }
615
+ ngAfterViewInit() { }
616
+ ngOnDestroy() {
617
+ this.destroy$.next();
618
+ this.destroy$.complete();
619
+ }
620
+ // ── Page numbers ───────────────────────────────────────────────────────────
621
+ get pageNumbers() {
622
+ const count = this.api.pageCount;
623
+ return count > 0 ? Array.from({ length: count }, (_, i) => i + 1) : [];
624
+ }
625
+ get pageWidthPx() { return this.api.pageWidthPt * this.scale; }
626
+ get pageHeightPx() { return this.api.pageHeightPt * this.scale; }
627
+ get pageWrapHeight() { return this.pageHeightPx + 32; }
628
+ get pageListWidth() { return this.pageWidthPx + 40; }
629
+ get zoomPercent() { return Math.round(this.scale / PT_TO_PX * 100); }
630
+ isInWindow(p) {
631
+ return Math.abs(p - this.currentPage) <= WINDOW_RADIUS;
632
+ }
633
+ getPage(p) {
634
+ if (!this.pageCache.has(p)) {
635
+ // Trigger async load
636
+ this.loadPage(p);
637
+ return undefined;
638
+ }
639
+ return this.pageCache.get(p);
640
+ }
641
+ loadPage(p) {
642
+ if (this.pageCache.has(p))
643
+ return;
644
+ this.api.getPage(p)
645
+ .pipe(takeUntil(this.destroy$))
646
+ .subscribe(page => {
647
+ this.pageCache.set(p, page);
648
+ this.cdr.markForCheck();
649
+ });
650
+ }
651
+ // Preload window pages whenever currentPage changes
652
+ preloadWindow() {
653
+ const from = Math.max(1, this.currentPage - WINDOW_RADIUS);
654
+ const to = Math.min(this.api.pageCount, this.currentPage + WINDOW_RADIUS);
655
+ for (let p = from; p <= to; p++)
656
+ this.loadPage(p);
657
+ }
658
+ // ── Navigation ─────────────────────────────────────────────────────────────
659
+ navigateTo(page) {
660
+ this.currentPage = Math.max(1, Math.min(page, this.api.pageCount));
661
+ this.preloadWindow();
662
+ this.cdr.markForCheck();
663
+ setTimeout(() => this.scrollToPage(this.currentPage), 50);
664
+ }
665
+ onPageInputChange(e) {
666
+ const v = parseInt(e.target.value, 10);
667
+ if (!isNaN(v))
668
+ this.navigateTo(v);
669
+ }
670
+ scrollToPage(p) {
671
+ const el = document.getElementById(`arv-pw-${p}`);
672
+ const scroll = this.scrollEl?.nativeElement;
673
+ if (el && scroll) {
674
+ scroll.scrollTo({ top: el.offsetTop - 8, behavior: 'smooth' });
675
+ }
676
+ }
677
+ onScroll() {
678
+ const scroll = this.scrollEl?.nativeElement;
679
+ if (!scroll)
680
+ return;
681
+ const list = scroll.querySelector('.arv-page-list');
682
+ if (!list)
683
+ return;
684
+ const mid = scroll.scrollTop + scroll.clientHeight / 2;
685
+ let best = 1, bestDist = Infinity;
686
+ for (const child of Array.from(list.children)) {
687
+ if (!child.id?.startsWith('arv-pw-'))
688
+ continue;
689
+ const p = parseInt(child.id.replace('arv-pw-', ''), 10);
690
+ const dist = Math.abs(child.offsetTop + child.offsetHeight / 2 - mid);
691
+ if (dist < bestDist) {
692
+ bestDist = dist;
693
+ best = p;
694
+ }
695
+ }
696
+ if (best !== this.currentPage) {
697
+ this.currentPage = best;
698
+ this.preloadWindow();
699
+ this.cdr.markForCheck();
700
+ }
701
+ }
702
+ // ── Zoom ───────────────────────────────────────────────────────────────────
703
+ zoomIn() { this.scale = Math.min(this.scale * 1.2, PT_TO_PX * 4); this.cdr.markForCheck(); }
704
+ zoomOut() { this.scale = Math.max(this.scale / 1.2, PT_TO_PX * 0.25); this.cdr.markForCheck(); }
705
+ zoomReset() { this.scale = PT_TO_PX; this.cdr.markForCheck(); }
706
+ // ── Refresh ────────────────────────────────────────────────────────────────
707
+ refresh() {
708
+ this.pageCache.clear();
709
+ this.api.clearCache();
710
+ this.preloadWindow();
711
+ this.cdr.markForCheck();
712
+ }
713
+ // ── Print ──────────────────────────────────────────────────────────────────
714
+ printAll() {
715
+ this.ensureAllPagesLoaded(() => window.print());
716
+ }
717
+ printCurrent() {
718
+ // Add temporary class to hide all other pages
719
+ const list = document.querySelector('.arv-page-list');
720
+ if (!list) {
721
+ window.print();
722
+ return;
723
+ }
724
+ Array.from(list.querySelectorAll('.arv-page-wrap')).forEach((el) => {
725
+ const pageEl = el;
726
+ const id = pageEl.id?.replace('arv-pw-', '');
727
+ pageEl.style.display = id === String(this.currentPage) ? '' : 'none';
728
+ });
729
+ window.print();
730
+ // Restore
731
+ Array.from(list.querySelectorAll('.arv-page-wrap'))
732
+ .forEach((el) => (el.style.display = ''));
733
+ }
734
+ ensureAllPagesLoaded(callback) {
735
+ // Pre-load remaining pages then print
736
+ const missing = [];
737
+ for (let p = 1; p <= this.api.pageCount; p++) {
738
+ if (!this.pageCache.has(p))
739
+ missing.push(p);
740
+ }
741
+ if (missing.length === 0) {
742
+ callback();
743
+ return;
744
+ }
745
+ let loaded = 0;
746
+ for (const p of missing) {
747
+ this.api.getPage(p).pipe(takeUntil(this.destroy$)).subscribe(page => {
748
+ this.pageCache.set(p, page);
749
+ loaded++;
750
+ this.cdr.markForCheck();
751
+ if (loaded === missing.length)
752
+ callback();
753
+ });
754
+ }
755
+ }
756
+ // ── Download page as image ─────────────────────────────────────────────────
757
+ async downloadPageImage() {
758
+ // Find the current page's renderer
759
+ const renderer = this.pageRenderers.find(r => {
760
+ const el = r.getPageElement();
761
+ return el?.getAttribute('data-page') === String(this.currentPage);
762
+ });
763
+ const pageEl = renderer?.getPageElement();
764
+ if (!pageEl) {
765
+ alert('Page not rendered yet. Please wait.');
766
+ return;
767
+ }
768
+ try {
769
+ // Use html2canvas if available, otherwise canvas screenshot
770
+ const h2c = window.html2canvas;
771
+ if (h2c) {
772
+ const canvas = await h2c(pageEl, { scale: 2, useCORS: true });
773
+ this.downloadCanvas(canvas, `page-${this.currentPage}.png`);
774
+ }
775
+ else {
776
+ // Fallback: DOM-to-canvas via browser print screenshot prompt
777
+ alert('html2canvas not loaded. Add <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> to your index.html for image download.');
778
+ }
779
+ }
780
+ catch (e) {
781
+ console.error('Screenshot failed', e);
782
+ }
783
+ }
784
+ downloadCanvas(canvas, filename) {
785
+ const a = document.createElement('a');
786
+ a.href = canvas.toDataURL('image/png');
787
+ a.download = filename;
788
+ document.body.appendChild(a);
789
+ a.click();
790
+ document.body.removeChild(a);
791
+ }
792
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerComponent, deps: [{ token: ReportViewerApiService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
793
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ReportViewerComponent, isStandalone: true, selector: "arv-report-viewer", inputs: { apiBaseUrl: "apiBaseUrl", theme: "theme", renderRequest: "renderRequest" }, viewQueries: [{ propertyName: "scrollEl", first: true, predicate: ["scrollEl"], descendants: true }, { propertyName: "pageRenderers", predicate: ["pageRenderers"], descendants: true }], ngImport: i0, template: `
794
+ <div class="arv-shell" [attr.data-theme]="theme">
795
+
796
+ <!-- ═══ TOOLBAR ════════════════════════════════════════════════ -->
797
+ <div class="arv-toolbar arv-no-print">
798
+
799
+ <span class="arv-toolbar__brand">📄 Report Viewer</span>
800
+
801
+ <!-- Navigation -->
802
+ <div class="arv-nav">
803
+ <button class="arv-btn arv-btn--icon" title="First" [disabled]="currentPage <= 1"
804
+ (click)="navigateTo(1)">⏮</button>
805
+ <button class="arv-btn arv-btn--icon" title="Previous" [disabled]="currentPage <= 1"
806
+ (click)="navigateTo(currentPage - 1)">◀</button>
807
+
808
+ <input class="arv-page-input" type="number"
809
+ [min]="1" [max]="api.pageCount"
810
+ [ngModel]="currentPage"
811
+ (change)="onPageInputChange($event)" />
812
+ <span class="arv-nav__of">/ {{ api.pageCount }}</span>
813
+
814
+ <button class="arv-btn arv-btn--icon" title="Next"
815
+ [disabled]="currentPage >= api.pageCount"
816
+ (click)="navigateTo(currentPage + 1)">▶</button>
817
+ <button class="arv-btn arv-btn--icon" title="Last"
818
+ [disabled]="currentPage >= api.pageCount"
819
+ (click)="navigateTo(api.pageCount)">⏭</button>
820
+ </div>
821
+
822
+ <!-- Zoom -->
823
+ <div class="arv-zoom">
824
+ <button class="arv-btn arv-btn--icon" title="Zoom out" (click)="zoomOut()">−</button>
825
+ <span class="arv-zoom__val">{{ zoomPercent }}%</span>
826
+ <button class="arv-btn arv-btn--icon" title="Zoom in" (click)="zoomIn()">+</button>
827
+ <button class="arv-btn arv-btn--sm" (click)="zoomReset()">1:1</button>
828
+ </div>
829
+
830
+ <!-- Refresh -->
831
+ <button class="arv-btn arv-btn--sm" title="Refresh" (click)="refresh()">🔄</button>
832
+
833
+ <!-- Print -->
834
+ <div class="arv-btn-group">
835
+ <button class="arv-btn arv-btn--sm" title="Print all pages" (click)="printAll()">🖨 Print All</button>
836
+ <button class="arv-btn arv-btn--sm" title="Print current page" (click)="printCurrent()">🖨 Page</button>
837
+ </div>
838
+
839
+ <!-- Download page as image -->
840
+ <button class="arv-btn arv-btn--sm" title="Download current page as image"
841
+ (click)="downloadPageImage()">⬇ Image</button>
842
+
843
+ <!-- Status -->
844
+ <div class="arv-status">
845
+ @if (api.loading$ | async) {
846
+ <span class="arv-spinner-sm"></span> Rendering…
847
+ } @else if (api.pageCount > 0) {
848
+ <span class="arv-status__ok">✓ {{ api.pageCount }} pages</span>
849
+ }
850
+ </div>
851
+ </div>
852
+
853
+ <!-- ═══ BODY ════════════════════════════════════════════════════ -->
854
+ <div class="arv-body">
855
+
856
+ <!-- Left panel -->
857
+ <arv-left-panel
858
+ class="arv-no-print"
859
+ [currentPage]="currentPage"
860
+ (pageSelected)="navigateTo($event)">
861
+ </arv-left-panel>
862
+
863
+ <!-- Virtual page list -->
864
+ <div class="arv-canvas-scroll"
865
+ #scrollEl
866
+ (scroll)="onScroll()">
867
+
868
+ @if (api.loading$ | async) {
869
+ <div class="arv-loading">
870
+ <div class="arv-spinner"></div>
871
+ <div>Rendering report…</div>
872
+ </div>
873
+ }
874
+
875
+ @else if ((api.session$ | async) === null) {
876
+ <div class="arv-empty">
877
+ No report loaded.
878
+ </div>
879
+ }
880
+
881
+ @else {
882
+ <div class="arv-page-list"
883
+ [style.width.px]="pageListWidth">
884
+
885
+ @for (p of pageNumbers; track p) {
886
+ <div class="arv-page-wrap"
887
+ [id]="'arv-pw-' + p"
888
+ [style.height.px]="pageWrapHeight">
889
+
890
+ <div class="arv-page-num-label arv-no-print">{{ p }}</div>
891
+
892
+ @if (isInWindow(p)) {
893
+ <!-- Rendered page -->
894
+ <arv-page-renderer
895
+ #pageRenderers
896
+ [page]="getPage(p)"
897
+ [scale]="scale"
898
+ [attr.data-page]="p">
899
+ </arv-page-renderer>
900
+ } @else {
901
+ <!-- Placeholder (correct height to keep scrollbar accurate) -->
902
+ <div class="arv-placeholder"
903
+ [style.width.px]="pageWidthPx"
904
+ [style.height.px]="pageHeightPx">
905
+ Page {{ p }}
906
+ </div>
907
+ }
908
+ </div>
909
+ }
910
+ </div>
911
+ }
912
+ </div>
913
+ </div>
914
+ </div>
915
+
916
+ <!-- ═══ PRINT STYLES ═══════════════════════════════════════════════ -->
917
+ <style id="arv-print-css">
918
+ @media print {
919
+ .arv-no-print { display: none !important; }
920
+ .arv-shell { height: auto !important; }
921
+ .arv-body { overflow: visible !important; }
922
+ .arv-canvas-scroll { overflow: visible !important; height: auto !important; }
923
+ .arv-page-wrap { page-break-after: always; }
924
+ .arv-page-wrap:last-child { page-break-after: avoid; }
925
+ }
926
+ </style>
927
+ `, isInline: true, styles: [":host{display:block;height:100%}.arv-shell{display:flex;flex-direction:column;height:100%;background:var(--arv-bg, #1e1e2e);color:var(--arv-text, #abb2bf);font-family:system-ui,-apple-system,sans-serif;font-size:12px}[data-theme=light]{--arv-bg: #f0f0f5;--arv-surface: #ffffff;--arv-surface2: #f5f5fa;--arv-border: #d0d0e0;--arv-accent: #3b6fd4;--arv-text: #333;--arv-text-muted: #888}.arv-toolbar{display:flex;align-items:center;gap:8px;padding:0 12px;height:44px;flex-shrink:0;background:var(--arv-surface, #2a2a3e);border-bottom:1px solid var(--arv-border, #3d3d5c);flex-wrap:wrap}.arv-toolbar__brand{font-weight:700;font-size:13px;color:var(--arv-text-strong, #eee);margin-right:8px}.arv-nav{display:flex;align-items:center;gap:4px}.arv-nav__of{font-size:11px;color:var(--arv-text-muted, #888)}.arv-page-input{width:52px;text-align:center;padding:2px 4px;background:var(--arv-surface, #2a2a3e);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:12px}.arv-zoom{display:flex;align-items:center;gap:4px}.arv-zoom__val{min-width:42px;text-align:center;font-size:11px;color:var(--arv-text-muted, #888)}.arv-btn{display:inline-flex;align-items:center;gap:3px;padding:3px 8px;background:var(--arv-surface2, #313148);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:11px;cursor:pointer;white-space:nowrap}.arv-btn:hover{background:var(--arv-border, #3d3d5c)}.arv-btn:disabled{opacity:.4;pointer-events:none}.arv-btn--icon{padding:3px 6px;font-size:13px}.arv-btn--sm{padding:2px 7px}.arv-btn-group{display:flex;gap:1px}.arv-btn-group .arv-btn:first-child{border-radius:4px 0 0 4px}.arv-btn-group .arv-btn:last-child{border-radius:0 4px 4px 0}.arv-status{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:11px;color:var(--arv-text-muted, #888)}.arv-status__ok{color:#4caf50}.arv-body{display:flex;flex:1;overflow:hidden;min-height:0}.arv-canvas-scroll{flex:1;overflow:auto;background:var(--arv-bg, #1e1e2e);padding:20px;display:flex;flex-direction:column;align-items:center}.arv-page-list{display:flex;flex-direction:column;align-items:center}.arv-page-wrap{position:relative;display:flex;flex-direction:column;align-items:center;padding-bottom:16px}.arv-page-num-label{font-size:10px;color:var(--arv-text-muted, #888);margin-bottom:4px;align-self:flex-start}.arv-placeholder{background:var(--arv-surface, #2a2a3e);border:1px dashed var(--arv-border, #3d3d5c);display:flex;align-items:center;justify-content:center;color:var(--arv-text-muted, #888);font-size:11px}.arv-loading{display:flex;flex-direction:column;align-items:center;gap:12px;padding:60px;color:var(--arv-text-muted, #888)}.arv-spinner{width:32px;height:32px;border:3px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .8s linear infinite}.arv-spinner-sm{display:inline-block;width:14px;height:14px;border:2px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .7s linear infinite}@keyframes arv-spin{to{transform:rotate(360deg)}}.arv-empty{padding:60px;text-align:center;color:var(--arv-text-muted, #888)}\n", "@media print{.arv-no-print{display:none!important}.arv-shell{height:auto!important}.arv-body{overflow:visible!important}.arv-canvas-scroll{overflow:visible!important;height:auto!important}.arv-page-wrap{page-break-after:always}.arv-page-wrap:last-child{page-break-after:avoid}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PageRendererComponent, selector: "arv-page-renderer", inputs: ["page", "scale"] }, { kind: "component", type: LeftPanelComponent, selector: "arv-left-panel", inputs: ["currentPage"], outputs: ["pageSelected"] }, { kind: "pipe", type: i3.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
928
+ }
929
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerComponent, decorators: [{
930
+ type: Component,
931
+ args: [{ selector: 'arv-report-viewer', standalone: true, imports: [CommonModule, FormsModule, PageRendererComponent, LeftPanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
932
+ <div class="arv-shell" [attr.data-theme]="theme">
933
+
934
+ <!-- ═══ TOOLBAR ════════════════════════════════════════════════ -->
935
+ <div class="arv-toolbar arv-no-print">
936
+
937
+ <span class="arv-toolbar__brand">📄 Report Viewer</span>
938
+
939
+ <!-- Navigation -->
940
+ <div class="arv-nav">
941
+ <button class="arv-btn arv-btn--icon" title="First" [disabled]="currentPage <= 1"
942
+ (click)="navigateTo(1)">⏮</button>
943
+ <button class="arv-btn arv-btn--icon" title="Previous" [disabled]="currentPage <= 1"
944
+ (click)="navigateTo(currentPage - 1)">◀</button>
945
+
946
+ <input class="arv-page-input" type="number"
947
+ [min]="1" [max]="api.pageCount"
948
+ [ngModel]="currentPage"
949
+ (change)="onPageInputChange($event)" />
950
+ <span class="arv-nav__of">/ {{ api.pageCount }}</span>
951
+
952
+ <button class="arv-btn arv-btn--icon" title="Next"
953
+ [disabled]="currentPage >= api.pageCount"
954
+ (click)="navigateTo(currentPage + 1)">▶</button>
955
+ <button class="arv-btn arv-btn--icon" title="Last"
956
+ [disabled]="currentPage >= api.pageCount"
957
+ (click)="navigateTo(api.pageCount)">⏭</button>
958
+ </div>
959
+
960
+ <!-- Zoom -->
961
+ <div class="arv-zoom">
962
+ <button class="arv-btn arv-btn--icon" title="Zoom out" (click)="zoomOut()">−</button>
963
+ <span class="arv-zoom__val">{{ zoomPercent }}%</span>
964
+ <button class="arv-btn arv-btn--icon" title="Zoom in" (click)="zoomIn()">+</button>
965
+ <button class="arv-btn arv-btn--sm" (click)="zoomReset()">1:1</button>
966
+ </div>
967
+
968
+ <!-- Refresh -->
969
+ <button class="arv-btn arv-btn--sm" title="Refresh" (click)="refresh()">🔄</button>
970
+
971
+ <!-- Print -->
972
+ <div class="arv-btn-group">
973
+ <button class="arv-btn arv-btn--sm" title="Print all pages" (click)="printAll()">🖨 Print All</button>
974
+ <button class="arv-btn arv-btn--sm" title="Print current page" (click)="printCurrent()">🖨 Page</button>
975
+ </div>
976
+
977
+ <!-- Download page as image -->
978
+ <button class="arv-btn arv-btn--sm" title="Download current page as image"
979
+ (click)="downloadPageImage()">⬇ Image</button>
980
+
981
+ <!-- Status -->
982
+ <div class="arv-status">
983
+ @if (api.loading$ | async) {
984
+ <span class="arv-spinner-sm"></span> Rendering…
985
+ } @else if (api.pageCount > 0) {
986
+ <span class="arv-status__ok">✓ {{ api.pageCount }} pages</span>
987
+ }
988
+ </div>
989
+ </div>
990
+
991
+ <!-- ═══ BODY ════════════════════════════════════════════════════ -->
992
+ <div class="arv-body">
993
+
994
+ <!-- Left panel -->
995
+ <arv-left-panel
996
+ class="arv-no-print"
997
+ [currentPage]="currentPage"
998
+ (pageSelected)="navigateTo($event)">
999
+ </arv-left-panel>
1000
+
1001
+ <!-- Virtual page list -->
1002
+ <div class="arv-canvas-scroll"
1003
+ #scrollEl
1004
+ (scroll)="onScroll()">
1005
+
1006
+ @if (api.loading$ | async) {
1007
+ <div class="arv-loading">
1008
+ <div class="arv-spinner"></div>
1009
+ <div>Rendering report…</div>
1010
+ </div>
1011
+ }
1012
+
1013
+ @else if ((api.session$ | async) === null) {
1014
+ <div class="arv-empty">
1015
+ No report loaded.
1016
+ </div>
1017
+ }
1018
+
1019
+ @else {
1020
+ <div class="arv-page-list"
1021
+ [style.width.px]="pageListWidth">
1022
+
1023
+ @for (p of pageNumbers; track p) {
1024
+ <div class="arv-page-wrap"
1025
+ [id]="'arv-pw-' + p"
1026
+ [style.height.px]="pageWrapHeight">
1027
+
1028
+ <div class="arv-page-num-label arv-no-print">{{ p }}</div>
1029
+
1030
+ @if (isInWindow(p)) {
1031
+ <!-- Rendered page -->
1032
+ <arv-page-renderer
1033
+ #pageRenderers
1034
+ [page]="getPage(p)"
1035
+ [scale]="scale"
1036
+ [attr.data-page]="p">
1037
+ </arv-page-renderer>
1038
+ } @else {
1039
+ <!-- Placeholder (correct height to keep scrollbar accurate) -->
1040
+ <div class="arv-placeholder"
1041
+ [style.width.px]="pageWidthPx"
1042
+ [style.height.px]="pageHeightPx">
1043
+ Page {{ p }}
1044
+ </div>
1045
+ }
1046
+ </div>
1047
+ }
1048
+ </div>
1049
+ }
1050
+ </div>
1051
+ </div>
1052
+ </div>
1053
+
1054
+ <!-- ═══ PRINT STYLES ═══════════════════════════════════════════════ -->
1055
+ <style id="arv-print-css">
1056
+ @media print {
1057
+ .arv-no-print { display: none !important; }
1058
+ .arv-shell { height: auto !important; }
1059
+ .arv-body { overflow: visible !important; }
1060
+ .arv-canvas-scroll { overflow: visible !important; height: auto !important; }
1061
+ .arv-page-wrap { page-break-after: always; }
1062
+ .arv-page-wrap:last-child { page-break-after: avoid; }
1063
+ }
1064
+ </style>
1065
+ `, styles: [":host{display:block;height:100%}.arv-shell{display:flex;flex-direction:column;height:100%;background:var(--arv-bg, #1e1e2e);color:var(--arv-text, #abb2bf);font-family:system-ui,-apple-system,sans-serif;font-size:12px}[data-theme=light]{--arv-bg: #f0f0f5;--arv-surface: #ffffff;--arv-surface2: #f5f5fa;--arv-border: #d0d0e0;--arv-accent: #3b6fd4;--arv-text: #333;--arv-text-muted: #888}.arv-toolbar{display:flex;align-items:center;gap:8px;padding:0 12px;height:44px;flex-shrink:0;background:var(--arv-surface, #2a2a3e);border-bottom:1px solid var(--arv-border, #3d3d5c);flex-wrap:wrap}.arv-toolbar__brand{font-weight:700;font-size:13px;color:var(--arv-text-strong, #eee);margin-right:8px}.arv-nav{display:flex;align-items:center;gap:4px}.arv-nav__of{font-size:11px;color:var(--arv-text-muted, #888)}.arv-page-input{width:52px;text-align:center;padding:2px 4px;background:var(--arv-surface, #2a2a3e);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:12px}.arv-zoom{display:flex;align-items:center;gap:4px}.arv-zoom__val{min-width:42px;text-align:center;font-size:11px;color:var(--arv-text-muted, #888)}.arv-btn{display:inline-flex;align-items:center;gap:3px;padding:3px 8px;background:var(--arv-surface2, #313148);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:11px;cursor:pointer;white-space:nowrap}.arv-btn:hover{background:var(--arv-border, #3d3d5c)}.arv-btn:disabled{opacity:.4;pointer-events:none}.arv-btn--icon{padding:3px 6px;font-size:13px}.arv-btn--sm{padding:2px 7px}.arv-btn-group{display:flex;gap:1px}.arv-btn-group .arv-btn:first-child{border-radius:4px 0 0 4px}.arv-btn-group .arv-btn:last-child{border-radius:0 4px 4px 0}.arv-status{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:11px;color:var(--arv-text-muted, #888)}.arv-status__ok{color:#4caf50}.arv-body{display:flex;flex:1;overflow:hidden;min-height:0}.arv-canvas-scroll{flex:1;overflow:auto;background:var(--arv-bg, #1e1e2e);padding:20px;display:flex;flex-direction:column;align-items:center}.arv-page-list{display:flex;flex-direction:column;align-items:center}.arv-page-wrap{position:relative;display:flex;flex-direction:column;align-items:center;padding-bottom:16px}.arv-page-num-label{font-size:10px;color:var(--arv-text-muted, #888);margin-bottom:4px;align-self:flex-start}.arv-placeholder{background:var(--arv-surface, #2a2a3e);border:1px dashed var(--arv-border, #3d3d5c);display:flex;align-items:center;justify-content:center;color:var(--arv-text-muted, #888);font-size:11px}.arv-loading{display:flex;flex-direction:column;align-items:center;gap:12px;padding:60px;color:var(--arv-text-muted, #888)}.arv-spinner{width:32px;height:32px;border:3px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .8s linear infinite}.arv-spinner-sm{display:inline-block;width:14px;height:14px;border:2px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .7s linear infinite}@keyframes arv-spin{to{transform:rotate(360deg)}}.arv-empty{padding:60px;text-align:center;color:var(--arv-text-muted, #888)}\n", "@media print{.arv-no-print{display:none!important}.arv-shell{height:auto!important}.arv-body{overflow:visible!important}.arv-canvas-scroll{overflow:visible!important;height:auto!important}.arv-page-wrap{page-break-after:always}.arv-page-wrap:last-child{page-break-after:avoid}}\n"] }]
1066
+ }], ctorParameters: () => [{ type: ReportViewerApiService }, { type: i0.ChangeDetectorRef }], propDecorators: { apiBaseUrl: [{
1067
+ type: Input
1068
+ }], theme: [{
1069
+ type: Input
1070
+ }], renderRequest: [{
1071
+ type: Input
1072
+ }], scrollEl: [{
1073
+ type: ViewChild,
1074
+ args: ['scrollEl']
1075
+ }], pageRenderers: [{
1076
+ type: ViewChildren,
1077
+ args: ['pageRenderers']
1078
+ }] } });
1079
+
1080
+ // Public API for the report-viewer Angular library
1081
+
1082
+ /**
1083
+ * Generated bundle index. Do not edit.
1084
+ */
1085
+
1086
+ export { LeftPanelComponent, PageRendererComponent, ReportViewerApiService, ReportViewerComponent };
1087
+ //# sourceMappingURL=arilekh-report-viewer.mjs.map