@kanso-protocol/virtual-list 0.1.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,233 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, TemplateRef, Directive, EventEmitter, ChangeDetectorRef, NgZone, ContentChild, ViewChild, Output, Input, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { NgTemplateOutlet } from '@angular/common';
4
+
5
+ /**
6
+ * Marker directive — pairs the row template with `<kp-virtual-list>`.
7
+ *
8
+ * @example
9
+ * <kp-virtual-list ...>
10
+ * <ng-template kpVirtualRow let-item let-i="index">
11
+ * <div>row {{ i }}: {{ item.name }}</div>
12
+ * </ng-template>
13
+ * </kp-virtual-list>
14
+ */
15
+ class KpVirtualRowDirective {
16
+ template = inject((TemplateRef));
17
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualRowDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
18
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.7", type: KpVirtualRowDirective, isStandalone: true, selector: "[kpVirtualRow]", ngImport: i0 });
19
+ }
20
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualRowDirective, decorators: [{
21
+ type: Directive,
22
+ args: [{
23
+ selector: '[kpVirtualRow]',
24
+ standalone: true,
25
+ }]
26
+ }] });
27
+ /**
28
+ * Kanso Protocol — VirtualList
29
+ *
30
+ * Window-mode virtual scroller for **fixed-height rows**. Renders only the
31
+ * rows currently visible in the viewport (plus a configurable `[overscan]`
32
+ * buffer). Lets the consumer hand any item shape via a projected
33
+ * `<ng-template kpVirtualRow let-item let-i="index">`.
34
+ *
35
+ * Why fixed-height: keeps the math O(1) per scroll event (`scrollTop /
36
+ * itemHeight`), no measurement pass, no layout thrash. Variable-height
37
+ * support is on the roadmap (`KpVariableVirtualListComponent`); for now,
38
+ * size each row to a uniform height.
39
+ *
40
+ * Use this for tables / message lists / log views with thousands of rows.
41
+ * Below ~100 rows just render them — virtualization adds complexity for
42
+ * negligible gain.
43
+ *
44
+ * @example
45
+ * <kp-virtual-list
46
+ * [items]="rows"
47
+ * [itemHeight]="40"
48
+ * [viewportHeight]="480"
49
+ * [overscan]="6"
50
+ * (rangeChange)="onRange($event)"
51
+ * >
52
+ * <ng-template kpVirtualRow let-row let-i="index">
53
+ * <div class="row" [class.row--alt]="i % 2">{{ row.name }}</div>
54
+ * </ng-template>
55
+ * </kp-virtual-list>
56
+ */
57
+ class KpVirtualListComponent {
58
+ /** The full list. The component never iterates the whole thing — only the visible window. */
59
+ items = [];
60
+ /** Pixel height of one row. Required; must be uniform across rows. */
61
+ itemHeight = 40;
62
+ /** Pixel height of the scroll viewport. */
63
+ viewportHeight = 400;
64
+ /** Extra rows rendered above + below the visible window to soften scroll-flicker. */
65
+ overscan = 4;
66
+ /** Optional trackBy for the projected rows. Defaults to index — fine for stable lists. */
67
+ trackBy = null;
68
+ rangeChange = new EventEmitter();
69
+ viewportRef;
70
+ rowTemplate;
71
+ visibleStart = 0;
72
+ visibleEnd = 0;
73
+ cdr = inject(ChangeDetectorRef);
74
+ zone = inject(NgZone);
75
+ ngOnChanges(changes) {
76
+ if ('items' in changes || 'itemHeight' in changes || 'viewportHeight' in changes || 'overscan' in changes) {
77
+ // Synchronous recompute: ngOnChanges fires BEFORE template eval in the
78
+ // same CD pass, so visibleStart/End read by the template are already
79
+ // up-to-date — no ExpressionChangedAfterItHasBeenChecked.
80
+ this.recompute(/* fromInputChange */ true);
81
+ }
82
+ }
83
+ ngOnDestroy() {
84
+ /* HostListener auto-unbinds */
85
+ }
86
+ get totalHeight() {
87
+ return this.items.length * this.itemHeight;
88
+ }
89
+ get visibleItems() {
90
+ return this.items.slice(this.visibleStart, this.visibleEnd);
91
+ }
92
+ get windowTransform() {
93
+ return `translate3d(0, ${this.visibleStart * this.itemHeight}px, 0)`;
94
+ }
95
+ trackByOrIndex(index, item) {
96
+ if (this.trackBy)
97
+ return this.trackBy(this.visibleStart + index, item);
98
+ return this.visibleStart + index;
99
+ }
100
+ /** Imperatively scroll to a specific row index. */
101
+ scrollToIndex(index, position = 'start') {
102
+ const el = this.viewportRef?.nativeElement;
103
+ if (!el)
104
+ return;
105
+ const top = position === 'start'
106
+ ? index * this.itemHeight
107
+ : position === 'end'
108
+ ? index * this.itemHeight - this.viewportHeight + this.itemHeight
109
+ : index * this.itemHeight - this.viewportHeight / 2 + this.itemHeight / 2;
110
+ el.scrollTop = Math.max(0, Math.min(top, this.totalHeight - this.viewportHeight));
111
+ }
112
+ onScroll() {
113
+ this.recompute(/* fromInputChange */ false);
114
+ }
115
+ recompute(fromInputChange) {
116
+ const el = this.viewportRef?.nativeElement;
117
+ const scrollTop = el?.scrollTop ?? 0;
118
+ const total = this.items.length;
119
+ let start = 0;
120
+ let end = 0;
121
+ if (total > 0 && this.itemHeight > 0) {
122
+ const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight);
123
+ const firstVisible = Math.floor(scrollTop / this.itemHeight);
124
+ start = Math.max(0, firstVisible - this.overscan);
125
+ end = Math.min(total, firstVisible + visibleCount + this.overscan);
126
+ }
127
+ if (this.visibleStart === start && this.visibleEnd === end)
128
+ return;
129
+ this.visibleStart = start;
130
+ this.visibleEnd = end;
131
+ if (!fromInputChange) {
132
+ // Scroll-driven recompute happens outside CD; mark so the visible
133
+ // window re-renders.
134
+ this.cdr.markForCheck();
135
+ }
136
+ // Always defer the emit. From input changes: avoids reentrancy during CD.
137
+ // From scroll: lets consumers mutate state without re-entering the
138
+ // current event loop synchronously.
139
+ queueMicrotask(() => this.rangeChange.emit({ start, end }));
140
+ }
141
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
142
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: KpVirtualListComponent, isStandalone: true, selector: "kp-virtual-list", inputs: { items: "items", itemHeight: "itemHeight", viewportHeight: "viewportHeight", overscan: "overscan", trackBy: "trackBy" }, outputs: { rangeChange: "rangeChange" }, host: { attributes: { "role": "list" }, properties: { "attr.aria-rowcount": "items.length" } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: KpVirtualRowDirective, descendants: true }], viewQueries: [{ propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
143
+ <div
144
+ #viewport
145
+ class="kp-virtual-list__viewport"
146
+ [style.height.px]="viewportHeight"
147
+ (scroll)="onScroll()"
148
+ >
149
+ <div class="kp-virtual-list__spacer" [style.height.px]="totalHeight">
150
+ <div
151
+ class="kp-virtual-list__window"
152
+ [style.transform]="windowTransform"
153
+ >
154
+ @for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {
155
+ <div
156
+ class="kp-virtual-list__row"
157
+ role="listitem"
158
+ [attr.aria-rowindex]="visibleStart + i + 1"
159
+ [style.height.px]="itemHeight"
160
+ >
161
+ @if (rowTemplate) {
162
+ <ng-container
163
+ *ngTemplateOutlet="rowTemplate.template; context: { $implicit: item, index: visibleStart + i }"
164
+ />
165
+ }
166
+ </div>
167
+ }
168
+ </div>
169
+ </div>
170
+ </div>
171
+ `, isInline: true, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
172
+ }
173
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KpVirtualListComponent, decorators: [{
174
+ type: Component,
175
+ args: [{ selector: 'kp-virtual-list', imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
176
+ role: 'list',
177
+ '[attr.aria-rowcount]': 'items.length',
178
+ }, template: `
179
+ <div
180
+ #viewport
181
+ class="kp-virtual-list__viewport"
182
+ [style.height.px]="viewportHeight"
183
+ (scroll)="onScroll()"
184
+ >
185
+ <div class="kp-virtual-list__spacer" [style.height.px]="totalHeight">
186
+ <div
187
+ class="kp-virtual-list__window"
188
+ [style.transform]="windowTransform"
189
+ >
190
+ @for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {
191
+ <div
192
+ class="kp-virtual-list__row"
193
+ role="listitem"
194
+ [attr.aria-rowindex]="visibleStart + i + 1"
195
+ [style.height.px]="itemHeight"
196
+ >
197
+ @if (rowTemplate) {
198
+ <ng-container
199
+ *ngTemplateOutlet="rowTemplate.template; context: { $implicit: item, index: visibleStart + i }"
200
+ />
201
+ }
202
+ </div>
203
+ }
204
+ </div>
205
+ </div>
206
+ </div>
207
+ `, styles: [":host{display:block;width:100%;font-family:var(--kp-font-family-sans, \"Onest\", system-ui, sans-serif)}.kp-virtual-list__viewport{width:100%;overflow-y:auto;overflow-x:hidden;position:relative;contain:strict}.kp-virtual-list__spacer{position:relative;width:100%}.kp-virtual-list__window{position:absolute;inset-block-start:0;inset-inline:0;will-change:transform}.kp-virtual-list__row{box-sizing:border-box;width:100%}\n"] }]
208
+ }], propDecorators: { items: [{
209
+ type: Input
210
+ }], itemHeight: [{
211
+ type: Input
212
+ }], viewportHeight: [{
213
+ type: Input
214
+ }], overscan: [{
215
+ type: Input
216
+ }], trackBy: [{
217
+ type: Input
218
+ }], rangeChange: [{
219
+ type: Output
220
+ }], viewportRef: [{
221
+ type: ViewChild,
222
+ args: ['viewport', { static: true }]
223
+ }], rowTemplate: [{
224
+ type: ContentChild,
225
+ args: [KpVirtualRowDirective]
226
+ }] } });
227
+
228
+ /**
229
+ * Generated bundle index. Do not edit.
230
+ */
231
+
232
+ export { KpVirtualListComponent, KpVirtualRowDirective };
233
+ //# sourceMappingURL=kanso-protocol-virtual-list.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kanso-protocol-virtual-list.mjs","sources":["../../../../../packages/components/virtual-list/src/virtual-list.component.ts","../../../../../packages/components/virtual-list/src/kanso-protocol-virtual-list.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ContentChild,\n Directive,\n ElementRef,\n EventEmitter,\n HostListener,\n Input,\n NgZone,\n OnChanges,\n OnDestroy,\n Output,\n SimpleChanges,\n TemplateRef,\n ViewChild,\n inject,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\n\nexport interface KpVirtualRange {\n start: number;\n end: number;\n}\n\n/**\n * Marker directive — pairs the row template with `<kp-virtual-list>`.\n *\n * @example\n * <kp-virtual-list ...>\n * <ng-template kpVirtualRow let-item let-i=\"index\">\n * <div>row {{ i }}: {{ item.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Directive({\n selector: '[kpVirtualRow]',\n standalone: true,\n})\nexport class KpVirtualRowDirective<T = unknown> {\n readonly template = inject(TemplateRef<{ $implicit: T; index: number }>);\n}\n\n/**\n * Kanso Protocol — VirtualList\n *\n * Window-mode virtual scroller for **fixed-height rows**. Renders only the\n * rows currently visible in the viewport (plus a configurable `[overscan]`\n * buffer). Lets the consumer hand any item shape via a projected\n * `<ng-template kpVirtualRow let-item let-i=\"index\">`.\n *\n * Why fixed-height: keeps the math O(1) per scroll event (`scrollTop /\n * itemHeight`), no measurement pass, no layout thrash. Variable-height\n * support is on the roadmap (`KpVariableVirtualListComponent`); for now,\n * size each row to a uniform height.\n *\n * Use this for tables / message lists / log views with thousands of rows.\n * Below ~100 rows just render them — virtualization adds complexity for\n * negligible gain.\n *\n * @example\n * <kp-virtual-list\n * [items]=\"rows\"\n * [itemHeight]=\"40\"\n * [viewportHeight]=\"480\"\n * [overscan]=\"6\"\n * (rangeChange)=\"onRange($event)\"\n * >\n * <ng-template kpVirtualRow let-row let-i=\"index\">\n * <div class=\"row\" [class.row--alt]=\"i % 2\">{{ row.name }}</div>\n * </ng-template>\n * </kp-virtual-list>\n */\n@Component({\n selector: 'kp-virtual-list',\n imports: [NgTemplateOutlet],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n role: 'list',\n '[attr.aria-rowcount]': 'items.length',\n },\n template: `\n <div\n #viewport\n class=\"kp-virtual-list__viewport\"\n [style.height.px]=\"viewportHeight\"\n (scroll)=\"onScroll()\"\n >\n <div class=\"kp-virtual-list__spacer\" [style.height.px]=\"totalHeight\">\n <div\n class=\"kp-virtual-list__window\"\n [style.transform]=\"windowTransform\"\n >\n @for (item of visibleItems; track trackByOrIndex($index, item); let i = $index) {\n <div\n class=\"kp-virtual-list__row\"\n role=\"listitem\"\n [attr.aria-rowindex]=\"visibleStart + i + 1\"\n [style.height.px]=\"itemHeight\"\n >\n @if (rowTemplate) {\n <ng-container\n *ngTemplateOutlet=\"rowTemplate.template; context: { $implicit: item, index: visibleStart + i }\"\n />\n }\n </div>\n }\n </div>\n </div>\n </div>\n `,\n styles: [`\n :host {\n display: block;\n width: 100%;\n font-family: var(--kp-font-family-sans, 'Onest', system-ui, sans-serif);\n }\n .kp-virtual-list__viewport {\n width: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n position: relative;\n contain: strict;\n }\n .kp-virtual-list__spacer {\n position: relative;\n width: 100%;\n }\n .kp-virtual-list__window {\n position: absolute;\n inset-block-start: 0;\n inset-inline: 0;\n will-change: transform;\n }\n .kp-virtual-list__row {\n box-sizing: border-box;\n width: 100%;\n }\n `],\n})\nexport class KpVirtualListComponent<T = unknown> implements OnChanges, OnDestroy {\n /** The full list. The component never iterates the whole thing — only the visible window. */\n @Input() items: readonly T[] = [];\n\n /** Pixel height of one row. Required; must be uniform across rows. */\n @Input() itemHeight = 40;\n\n /** Pixel height of the scroll viewport. */\n @Input() viewportHeight = 400;\n\n /** Extra rows rendered above + below the visible window to soften scroll-flicker. */\n @Input() overscan = 4;\n\n /** Optional trackBy for the projected rows. Defaults to index — fine for stable lists. */\n @Input() trackBy: ((index: number, item: T) => unknown) | null = null;\n\n @Output() readonly rangeChange = new EventEmitter<KpVirtualRange>();\n\n @ViewChild('viewport', { static: true }) private viewportRef!: ElementRef<HTMLDivElement>;\n @ContentChild(KpVirtualRowDirective) rowTemplate?: KpVirtualRowDirective<T>;\n\n visibleStart = 0;\n visibleEnd = 0;\n\n private readonly cdr = inject(ChangeDetectorRef);\n private readonly zone = inject(NgZone);\n\n ngOnChanges(changes: SimpleChanges): void {\n if ('items' in changes || 'itemHeight' in changes || 'viewportHeight' in changes || 'overscan' in changes) {\n // Synchronous recompute: ngOnChanges fires BEFORE template eval in the\n // same CD pass, so visibleStart/End read by the template are already\n // up-to-date — no ExpressionChangedAfterItHasBeenChecked.\n this.recompute(/* fromInputChange */ true);\n }\n }\n\n ngOnDestroy(): void {\n /* HostListener auto-unbinds */\n }\n\n get totalHeight(): number {\n return this.items.length * this.itemHeight;\n }\n\n get visibleItems(): readonly T[] {\n return this.items.slice(this.visibleStart, this.visibleEnd);\n }\n\n get windowTransform(): string {\n return `translate3d(0, ${this.visibleStart * this.itemHeight}px, 0)`;\n }\n\n trackByOrIndex(index: number, item: T): unknown {\n if (this.trackBy) return this.trackBy(this.visibleStart + index, item);\n return this.visibleStart + index;\n }\n\n /** Imperatively scroll to a specific row index. */\n scrollToIndex(index: number, position: 'start' | 'center' | 'end' = 'start'): void {\n const el = this.viewportRef?.nativeElement;\n if (!el) return;\n const top =\n position === 'start'\n ? index * this.itemHeight\n : position === 'end'\n ? index * this.itemHeight - this.viewportHeight + this.itemHeight\n : index * this.itemHeight - this.viewportHeight / 2 + this.itemHeight / 2;\n el.scrollTop = Math.max(0, Math.min(top, this.totalHeight - this.viewportHeight));\n }\n\n onScroll(): void {\n this.recompute(/* fromInputChange */ false);\n }\n\n private recompute(fromInputChange: boolean): void {\n const el = this.viewportRef?.nativeElement;\n const scrollTop = el?.scrollTop ?? 0;\n const total = this.items.length;\n\n let start = 0;\n let end = 0;\n if (total > 0 && this.itemHeight > 0) {\n const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight);\n const firstVisible = Math.floor(scrollTop / this.itemHeight);\n start = Math.max(0, firstVisible - this.overscan);\n end = Math.min(total, firstVisible + visibleCount + this.overscan);\n }\n\n if (this.visibleStart === start && this.visibleEnd === end) return;\n\n this.visibleStart = start;\n this.visibleEnd = end;\n\n if (!fromInputChange) {\n // Scroll-driven recompute happens outside CD; mark so the visible\n // window re-renders.\n this.cdr.markForCheck();\n }\n // Always defer the emit. From input changes: avoids reentrancy during CD.\n // From scroll: lets consumers mutate state without re-entering the\n // current event loop synchronously.\n queueMicrotask(() => this.rangeChange.emit({ start, end }));\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@kanso-protocol/virtual-list",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "peerDependencies": {
6
+ "@angular/core": "^18.0.0",
7
+ "@angular/common": "^18.0.0",
8
+ "@kanso-protocol/core": "^0.0.1"
9
+ },
10
+ "description": "Kanso Protocol — virtual-list (component).",
11
+ "author": "GregNBlack",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/GregNBlack/kanso-protocol.git",
15
+ "directory": "packages/components/virtual-list"
16
+ },
17
+ "homepage": "https://gregnblack.github.io/kanso-protocol/?path=/docs/components-virtual-list--docs",
18
+ "bugs": "https://github.com/GregNBlack/kanso-protocol/issues",
19
+ "keywords": [
20
+ "design-system",
21
+ "angular",
22
+ "kanso",
23
+ "virtual-list",
24
+ "virtualization",
25
+ "windowing"
26
+ ],
27
+ "sideEffects": false,
28
+ "module": "fesm2022/kanso-protocol-virtual-list.mjs",
29
+ "typings": "types/kanso-protocol-virtual-list.d.ts",
30
+ "exports": {
31
+ "./package.json": {
32
+ "default": "./package.json"
33
+ },
34
+ ".": {
35
+ "types": "./types/kanso-protocol-virtual-list.d.ts",
36
+ "default": "./fesm2022/kanso-protocol-virtual-list.mjs"
37
+ }
38
+ },
39
+ "type": "module",
40
+ "dependencies": {
41
+ "tslib": "^2.3.0"
42
+ }
43
+ }
@@ -0,0 +1,86 @@
1
+ import * as i0 from '@angular/core';
2
+ import { OnChanges, OnDestroy, EventEmitter, TemplateRef, SimpleChanges } from '@angular/core';
3
+
4
+ interface KpVirtualRange {
5
+ start: number;
6
+ end: number;
7
+ }
8
+ /**
9
+ * Marker directive — pairs the row template with `<kp-virtual-list>`.
10
+ *
11
+ * @example
12
+ * <kp-virtual-list ...>
13
+ * <ng-template kpVirtualRow let-item let-i="index">
14
+ * <div>row {{ i }}: {{ item.name }}</div>
15
+ * </ng-template>
16
+ * </kp-virtual-list>
17
+ */
18
+ declare class KpVirtualRowDirective<T = unknown> {
19
+ readonly template: TemplateRef<any>;
20
+ static ɵfac: i0.ɵɵFactoryDeclaration<KpVirtualRowDirective<any>, never>;
21
+ static ɵdir: i0.ɵɵDirectiveDeclaration<KpVirtualRowDirective<any>, "[kpVirtualRow]", never, {}, {}, never, never, true, never>;
22
+ }
23
+ /**
24
+ * Kanso Protocol — VirtualList
25
+ *
26
+ * Window-mode virtual scroller for **fixed-height rows**. Renders only the
27
+ * rows currently visible in the viewport (plus a configurable `[overscan]`
28
+ * buffer). Lets the consumer hand any item shape via a projected
29
+ * `<ng-template kpVirtualRow let-item let-i="index">`.
30
+ *
31
+ * Why fixed-height: keeps the math O(1) per scroll event (`scrollTop /
32
+ * itemHeight`), no measurement pass, no layout thrash. Variable-height
33
+ * support is on the roadmap (`KpVariableVirtualListComponent`); for now,
34
+ * size each row to a uniform height.
35
+ *
36
+ * Use this for tables / message lists / log views with thousands of rows.
37
+ * Below ~100 rows just render them — virtualization adds complexity for
38
+ * negligible gain.
39
+ *
40
+ * @example
41
+ * <kp-virtual-list
42
+ * [items]="rows"
43
+ * [itemHeight]="40"
44
+ * [viewportHeight]="480"
45
+ * [overscan]="6"
46
+ * (rangeChange)="onRange($event)"
47
+ * >
48
+ * <ng-template kpVirtualRow let-row let-i="index">
49
+ * <div class="row" [class.row--alt]="i % 2">{{ row.name }}</div>
50
+ * </ng-template>
51
+ * </kp-virtual-list>
52
+ */
53
+ declare class KpVirtualListComponent<T = unknown> implements OnChanges, OnDestroy {
54
+ /** The full list. The component never iterates the whole thing — only the visible window. */
55
+ items: readonly T[];
56
+ /** Pixel height of one row. Required; must be uniform across rows. */
57
+ itemHeight: number;
58
+ /** Pixel height of the scroll viewport. */
59
+ viewportHeight: number;
60
+ /** Extra rows rendered above + below the visible window to soften scroll-flicker. */
61
+ overscan: number;
62
+ /** Optional trackBy for the projected rows. Defaults to index — fine for stable lists. */
63
+ trackBy: ((index: number, item: T) => unknown) | null;
64
+ readonly rangeChange: EventEmitter<KpVirtualRange>;
65
+ private viewportRef;
66
+ rowTemplate?: KpVirtualRowDirective<T>;
67
+ visibleStart: number;
68
+ visibleEnd: number;
69
+ private readonly cdr;
70
+ private readonly zone;
71
+ ngOnChanges(changes: SimpleChanges): void;
72
+ ngOnDestroy(): void;
73
+ get totalHeight(): number;
74
+ get visibleItems(): readonly T[];
75
+ get windowTransform(): string;
76
+ trackByOrIndex(index: number, item: T): unknown;
77
+ /** Imperatively scroll to a specific row index. */
78
+ scrollToIndex(index: number, position?: 'start' | 'center' | 'end'): void;
79
+ onScroll(): void;
80
+ private recompute;
81
+ static ɵfac: i0.ɵɵFactoryDeclaration<KpVirtualListComponent<any>, never>;
82
+ static ɵcmp: i0.ɵɵComponentDeclaration<KpVirtualListComponent<any>, "kp-virtual-list", never, { "items": { "alias": "items"; "required": false; }; "itemHeight": { "alias": "itemHeight"; "required": false; }; "viewportHeight": { "alias": "viewportHeight"; "required": false; }; "overscan": { "alias": "overscan"; "required": false; }; "trackBy": { "alias": "trackBy"; "required": false; }; }, { "rangeChange": "rangeChange"; }, ["rowTemplate"], never, true, never>;
83
+ }
84
+
85
+ export { KpVirtualListComponent, KpVirtualRowDirective };
86
+ export type { KpVirtualRange };