ngx-kit 6.2.0 → 7.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.
Files changed (71) hide show
  1. package/README.md +3 -3
  2. package/angle-selector/README.md +34 -34
  3. package/box-shadow/README.md +33 -33
  4. package/color-picker/README.md +52 -49
  5. package/data-table/README.md +120 -113
  6. package/date-picker/README.md +74 -69
  7. package/dialog/README.md +92 -92
  8. package/drag-resize/README.md +55 -0
  9. package/drawer-menu/README.md +125 -77
  10. package/dropzone/README.md +422 -422
  11. package/fesm2022/ngx-kit-angle-selector.mjs +2 -2
  12. package/fesm2022/ngx-kit-angle-selector.mjs.map +1 -1
  13. package/fesm2022/ngx-kit-box-shadow.mjs +2 -2
  14. package/fesm2022/ngx-kit-box-shadow.mjs.map +1 -1
  15. package/fesm2022/ngx-kit-color-picker.mjs +265 -73
  16. package/fesm2022/ngx-kit-color-picker.mjs.map +1 -1
  17. package/fesm2022/ngx-kit-data-table.mjs +2 -2
  18. package/fesm2022/ngx-kit-data-table.mjs.map +1 -1
  19. package/fesm2022/ngx-kit-date-picker.mjs +11 -11
  20. package/fesm2022/ngx-kit-date-picker.mjs.map +1 -1
  21. package/fesm2022/ngx-kit-dialog.mjs +3 -3
  22. package/fesm2022/ngx-kit-dialog.mjs.map +1 -1
  23. package/fesm2022/ngx-kit-drag-resize.mjs +1664 -0
  24. package/fesm2022/ngx-kit-drag-resize.mjs.map +1 -0
  25. package/fesm2022/ngx-kit-drawer-menu.mjs +424 -158
  26. package/fesm2022/ngx-kit-drawer-menu.mjs.map +1 -1
  27. package/fesm2022/ngx-kit-dropzone.mjs +2 -2
  28. package/fesm2022/ngx-kit-dropzone.mjs.map +1 -1
  29. package/fesm2022/ngx-kit-form-field.mjs +3 -5
  30. package/fesm2022/ngx-kit-form-field.mjs.map +1 -1
  31. package/fesm2022/ngx-kit-gallery.mjs +2 -2
  32. package/fesm2022/ngx-kit-gallery.mjs.map +1 -1
  33. package/fesm2022/ngx-kit-gradient-picker.mjs.map +1 -1
  34. package/fesm2022/ngx-kit-grid-layout.mjs +927 -0
  35. package/fesm2022/ngx-kit-grid-layout.mjs.map +1 -0
  36. package/fesm2022/ngx-kit-image-editor.mjs +2 -2
  37. package/fesm2022/ngx-kit-image-editor.mjs.map +1 -1
  38. package/fesm2022/ngx-kit-menu.mjs +8 -8
  39. package/fesm2022/ngx-kit-menu.mjs.map +1 -1
  40. package/fesm2022/ngx-kit-message.mjs +2 -2
  41. package/fesm2022/ngx-kit-message.mjs.map +1 -1
  42. package/fesm2022/ngx-kit-notify.mjs +2 -2
  43. package/fesm2022/ngx-kit-notify.mjs.map +1 -1
  44. package/fesm2022/ngx-kit-shared.mjs +54 -3
  45. package/fesm2022/ngx-kit-shared.mjs.map +1 -1
  46. package/fesm2022/ngx-kit-time-picker.mjs +2 -2
  47. package/fesm2022/ngx-kit-time-picker.mjs.map +1 -1
  48. package/fesm2022/ngx-kit-virtual-scroll.mjs +392 -0
  49. package/fesm2022/ngx-kit-virtual-scroll.mjs.map +1 -0
  50. package/fesm2022/ngx-kit-webflow.mjs +709 -0
  51. package/fesm2022/ngx-kit-webflow.mjs.map +1 -0
  52. package/gallery/README.md +71 -71
  53. package/gradient-picker/README.md +41 -41
  54. package/grid-layout/README.md +64 -0
  55. package/image-editor/README.md +12 -12
  56. package/menu/README.md +85 -85
  57. package/message/README.md +70 -70
  58. package/notify/README.md +56 -58
  59. package/package.json +17 -1
  60. package/shared/README.md +117 -117
  61. package/time-picker/README.md +89 -95
  62. package/types/ngx-kit-color-picker.d.ts +72 -16
  63. package/types/ngx-kit-date-picker.d.ts +2 -1
  64. package/types/ngx-kit-drag-resize.d.ts +368 -0
  65. package/types/ngx-kit-drawer-menu.d.ts +148 -88
  66. package/types/ngx-kit-grid-layout.d.ts +310 -0
  67. package/types/ngx-kit-image-editor.d.ts +1 -1
  68. package/types/ngx-kit-shared.d.ts +12 -1
  69. package/types/ngx-kit-virtual-scroll.d.ts +179 -0
  70. package/types/ngx-kit-webflow.d.ts +286 -0
  71. package/virtual-scroll/README.md +199 -0
@@ -0,0 +1,286 @@
1
+ import * as i0 from '@angular/core';
2
+ import { OnDestroy, EventEmitter, OnInit } from '@angular/core';
3
+ import * as ngx_kit_webflow from 'ngx-kit/webflow';
4
+
5
+ /**
6
+ * ngx-flowbuilder — مدل‌های پایه
7
+ */
8
+ type FlowAxis = 'row' | 'column';
9
+ /** موقعیت درج نسبت به آیتمی که ماوس روی آن است */
10
+ type DropZone = 'before' | 'after' | 'above' | 'below' | 'inside';
11
+ interface DropResult {
12
+ targetId: string;
13
+ zone: DropZone;
14
+ }
15
+ interface Rect {
16
+ top: number;
17
+ left: number;
18
+ width: number;
19
+ height: number;
20
+ right: number;
21
+ bottom: number;
22
+ }
23
+ interface IndicatorState {
24
+ visible: boolean;
25
+ /** خط باریک: 'h' یعنی خط افقی (جدا کردن ردیف‌ها)، 'v' یعنی خط عمودی (جدا کردن کنار هم) */
26
+ orientation: 'h' | 'v' | null;
27
+ x: number;
28
+ y: number;
29
+ length: number;
30
+ /** حالت nest: به‌جای خط، یک کادر دور کل container هایلایت می‌شود */
31
+ highlightRect: Rect | null;
32
+ }
33
+ interface DragStartEvent {
34
+ id: string;
35
+ pointerId: number;
36
+ originRect: Rect;
37
+ }
38
+ interface DragMoveEvent {
39
+ id: string;
40
+ clientX: number;
41
+ clientY: number;
42
+ deltaX: number;
43
+ deltaY: number;
44
+ }
45
+ interface DragEndEvent {
46
+ id: string;
47
+ drop: DropResult | null;
48
+ }
49
+ type ResizeHandlePosition = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
50
+ interface ResizeMoveEvent {
51
+ id: string;
52
+ handle: ResizeHandlePosition;
53
+ width: number;
54
+ height: number;
55
+ /** فقط برای هندل‌های شمال/غرب که مبدأ عنصر هم جابجا می‌شود */
56
+ offsetX: number;
57
+ offsetY: number;
58
+ }
59
+
60
+ /**
61
+ * ngxDraggable
62
+ * -------------------------------------------------------------
63
+ * تنها دایرکتیو مسئول درگ. کل منطق حرکت با CSS transform (translate3d)
64
+ * روی خودِ عنصر درگ‌شونده انجام می‌شود — نه با جابجایی واقعی در DOM لیست.
65
+ * جابجاییِ واقعیِ داده فقط یک‌بار، در لحظهٔ drop، از طریق خروجی
66
+ * (flowDrop) اتفاق می‌افتد و آن‌وقت مصرف‌کننده (کامپوننت کانتینر) با
67
+ * *ngFor + trackBy مدل را آپدیت می‌کند.
68
+ *
69
+ * دلیل مقیاس‌پذیری با هزاران آیتم:
70
+ * - در حین pointermove هیچ آیتم دیگری لمس/محاسبه نمی‌شود.
71
+ * - listener مربوط به تشخیص هدف روی کانتینر (event delegation) است،
72
+ * نه روی تک‌تک آیتم‌ها (این بخش در ContainerDropzoneDirective است).
73
+ * - آپدیت transform با requestAnimationFrame throttle می‌شود تا از
74
+ * چندبار layout/paint در هر فریم جلوگیری شود.
75
+ */
76
+ declare class DraggableDirective implements OnDestroy {
77
+ private readonly el;
78
+ private readonly renderer;
79
+ private readonly zone;
80
+ private readonly dragDrop;
81
+ private readonly dropPos;
82
+ /** شناسهٔ یکتای بلاک — برای شناسایی هنگام درگ لازم است */
83
+ ngxDraggableId: string;
84
+ /** غیرفعال کردن موقت درگ (مثلاً حین ادیت متن داخل بلاک) */
85
+ ngxDraggableDisabled: boolean;
86
+ /** هندلِ اختصاصی؛ اگر ندهی، کل عنصر هندل می‌شود */
87
+ ngxDragHandleSelector: string | null;
88
+ flowDragStart: EventEmitter<DragStartEvent>;
89
+ flowDragEnd: EventEmitter<DragEndEvent>;
90
+ readonly dragging: i0.WritableSignal<boolean>;
91
+ get draggingClass(): boolean;
92
+ get touchAction(): string;
93
+ private startX;
94
+ private startY;
95
+ private originLeft;
96
+ private originTop;
97
+ private rafId;
98
+ private pendingTransform;
99
+ private cleanupFns;
100
+ constructor();
101
+ private onPointerDown;
102
+ private onPointerMove;
103
+ private onPointerUp;
104
+ ngOnDestroy(): void;
105
+ static ɵfac: i0.ɵɵFactoryDeclaration<DraggableDirective, never>;
106
+ static ɵdir: i0.ɵɵDirectiveDeclaration<DraggableDirective, "[ngxDraggable]", ["ngxDraggable"], { "ngxDraggableId": { "alias": "ngxDraggableId"; "required": true; }; "ngxDraggableDisabled": { "alias": "ngxDraggableDisabled"; "required": false; }; "ngxDragHandleSelector": { "alias": "ngxDragHandleSelector"; "required": false; }; }, { "flowDragStart": "flowDragStart"; "flowDragEnd": "flowDragEnd"; }, never, never, true, never>;
107
+ }
108
+
109
+ /**
110
+ * ngxResizable
111
+ * -------------------------------------------------------------
112
+ * تنها دایرکتیو مسئول ریسایز. مستقل از DraggableDirective.
113
+ * روی عنصر اصلی قرار می‌گیرد و به همراه ۸ هندل (که خودت در تمپلیت با
114
+ * class مشخص می‌کنی، مثلاً <span class="rz-handle" data-handle="se">)
115
+ * کار می‌کند.
116
+ *
117
+ * فقط CSS width/height/left/top آپدیت می‌شود (از طریق خروجی flowResize)
118
+ * و مصرف‌کننده تصمیم می‌گیرد این مقادیر را چطور به مدل/استایل بایند کند —
119
+ * دایرکتیو خودش مستقیماً مدل داده را تغییر نمی‌دهد.
120
+ */
121
+ declare class ResizableDirective implements OnDestroy {
122
+ private readonly el;
123
+ private readonly renderer;
124
+ private readonly zone;
125
+ ngxResizableId: string;
126
+ ngxResizableMinWidth: number;
127
+ ngxResizableMinHeight: number;
128
+ ngxResizableDisabled: boolean;
129
+ /** انتخاب‌گر هندل‌ها؛ باید attribute یا data-handle داشته باشند */
130
+ ngxResizeHandleSelector: string;
131
+ flowResizeStart: EventEmitter<{
132
+ id: string;
133
+ handle: ResizeHandlePosition;
134
+ }>;
135
+ flowResizeMove: EventEmitter<ResizeMoveEvent>;
136
+ flowResizeEnd: EventEmitter<ResizeMoveEvent>;
137
+ readonly resizing: i0.WritableSignal<boolean>;
138
+ private startX;
139
+ private startY;
140
+ private startW;
141
+ private startH;
142
+ private handle;
143
+ private rafId;
144
+ private pending;
145
+ private unlisten;
146
+ constructor();
147
+ private onDown;
148
+ private onMove;
149
+ private onUp;
150
+ ngOnDestroy(): void;
151
+ static ɵfac: i0.ɵɵFactoryDeclaration<ResizableDirective, never>;
152
+ static ɵdir: i0.ɵɵDirectiveDeclaration<ResizableDirective, "[ngxResizable]", ["ngxResizable"], { "ngxResizableId": { "alias": "ngxResizableId"; "required": true; }; "ngxResizableMinWidth": { "alias": "ngxResizableMinWidth"; "required": false; }; "ngxResizableMinHeight": { "alias": "ngxResizableMinHeight"; "required": false; }; "ngxResizableDisabled": { "alias": "ngxResizableDisabled"; "required": false; }; "ngxResizeHandleSelector": { "alias": "ngxResizeHandleSelector"; "required": false; }; }, { "flowResizeStart": "flowResizeStart"; "flowResizeMove": "flowResizeMove"; "flowResizeEnd": "flowResizeEnd"; }, never, never, true, never>;
153
+ }
154
+
155
+ /**
156
+ * ngxDropzoneContainer
157
+ * -------------------------------------------------------------
158
+ * روی هر کانتینری که فرزندانش قابل‌درگ‌اند‌دراپ هستند قرار می‌گیرد
159
+ * (چه ریشهٔ صفحه، چه یک بلاک تو‌در‌تو).
160
+ *
161
+ * فقط یک pointermove-listener سراسری (روی document، حین یک درگ فعال)
162
+ * دارد و با event.target.closest('[data-flow-id]') هدف را پیدا می‌کند.
163
+ * این یعنی صرف‌نظر از اینکه چند هزار فرزند وجود دارد، هزینهٔ هر حرکتِ
164
+ * ماوس ثابت (O(1)) است — نه O(n).
165
+ *
166
+ * هر فرزند مستقیمِ قابل‌درگ باید attribute زیر را داشته باشد
167
+ * (به‌صورت خودکار توسط DraggableDirective/تمپلیت ست می‌شود):
168
+ * [attr.data-flow-id]="item.id"
169
+ * [attr.data-flow-container]="isContainer ? '' : null"
170
+ */
171
+ declare class DropzoneContainerDirective implements OnInit, OnDestroy {
172
+ private readonly el;
173
+ private readonly zone;
174
+ private readonly renderer;
175
+ private readonly dragDrop;
176
+ private readonly dropPos;
177
+ /** جهت چیدمان فرزندان مستقیم این کانتینر */
178
+ ngxDropzoneAxis: FlowAxis;
179
+ /** آیا خودِ این کانتینر می‌تواند میزبان nest شدن باشد */
180
+ ngxDropzoneNestable: boolean;
181
+ private unlistenMove;
182
+ private unlistenLeave;
183
+ ngOnInit(): void;
184
+ private handleMove;
185
+ ngOnDestroy(): void;
186
+ static ɵfac: i0.ɵɵFactoryDeclaration<DropzoneContainerDirective, never>;
187
+ static ɵdir: i0.ɵɵDirectiveDeclaration<DropzoneContainerDirective, "[ngxDropzoneContainer]", ["ngxDropzoneContainer"], { "ngxDropzoneAxis": { "alias": "ngxDropzoneAxis"; "required": false; }; "ngxDropzoneNestable": { "alias": "ngxDropzoneNestable"; "required": false; }; }, {}, never, never, true, never>;
188
+ }
189
+
190
+ /**
191
+ * FlowDropIndicatorComponent
192
+ * -------------------------------------------------------------
193
+ * یک‌بار در ریشهٔ صفحه‌ساز قرار می‌گیرد (position: fixed).
194
+ * تنها المنتی است که در طول درگ مدام آپدیت می‌شود؛ بقیهٔ آیتم‌های
195
+ * لیست دست‌نخورده باقی می‌مانند. کاملاً با computed از signal سرویس
196
+ * تغذیه می‌شود، بدون هیچ منطق اضافه در کامپوننت.
197
+ */
198
+ declare class FlowDropIndicatorComponent {
199
+ private readonly dropPos;
200
+ readonly indicator: i0.Signal<ngx_kit_webflow.IndicatorState>;
201
+ static ɵfac: i0.ɵɵFactoryDeclaration<FlowDropIndicatorComponent, never>;
202
+ static ɵcmp: i0.ɵɵComponentDeclaration<FlowDropIndicatorComponent, "flow-drop-indicator", never, {}, {}, never, never, true, never>;
203
+ }
204
+
205
+ /**
206
+ * DragDropService
207
+ * -------------------------------------------------------------
208
+ * وضعیت سراسریِ «چه چیزی الان در حال درگ است» را نگه می‌دارد.
209
+ * فقط signal، بدون هیچ Subject/Observable اضافه — سبک و قابل‌پیش‌بینی.
210
+ */
211
+ declare class DragDropService {
212
+ /** id آیتمی که هم‌اکنون درگ می‌شود، یا null */
213
+ readonly activeId: i0.WritableSignal<string | null>;
214
+ /** آخرین نتیجهٔ محاسبه‌شدهٔ drop توسط DropPositionService */
215
+ readonly pendingDrop: i0.WritableSignal<DropResult | null>;
216
+ startDrag(id: string): void;
217
+ setPendingDrop(result: DropResult | null): void;
218
+ endDrag(): DropResult | null;
219
+ get isDragging(): boolean;
220
+ static ɵfac: i0.ɵɵFactoryDeclaration<DragDropService, never>;
221
+ static ɵprov: i0.ɵɵInjectableDeclaration<DragDropService>;
222
+ }
223
+
224
+ declare class DropPositionService {
225
+ private readonly _indicator;
226
+ /** استیت فقط‌خواندنیِ اندیکاتور برای بایند در تمپلیت */
227
+ readonly indicator: i0.Signal<IndicatorState>;
228
+ private lastTargetId;
229
+ private lastZone;
230
+ /**
231
+ * فراخوانی در هر pointermove حین درگ.
232
+ * targetEl باید همان element.closest('[data-flow-id]') باشد که در
233
+ * دایرکتیو کانتینر با event delegation پیدا شده — نه یک لوپ روی آیتم‌ها.
234
+ */
235
+ evaluate(params: {
236
+ targetEl: HTMLElement;
237
+ targetId: string;
238
+ isContainer: boolean;
239
+ parentAxis: 'row' | 'column';
240
+ clientX: number;
241
+ clientY: number;
242
+ }): {
243
+ targetId: string;
244
+ zone: DropZone;
245
+ };
246
+ /** رسم خط/کادر راهنما - فقط نوشتن به یک signal، بدون لمس بقیهٔ DOM */
247
+ private paintIndicator;
248
+ clear(): void;
249
+ get current(): {
250
+ targetId: string;
251
+ zone: DropZone;
252
+ } | null;
253
+ static ɵfac: i0.ɵɵFactoryDeclaration<DropPositionService, never>;
254
+ static ɵprov: i0.ɵɵInjectableDeclaration<DropPositionService>;
255
+ }
256
+
257
+ interface FlowNode {
258
+ id: string;
259
+ type: string;
260
+ axis: 'row' | 'column';
261
+ isContainer: boolean;
262
+ props?: Record<string, unknown>;
263
+ children: FlowNode[];
264
+ }
265
+ /**
266
+ * FlowTreeService
267
+ * -------------------------------------------------------------
268
+ * مدل درختیِ صفحه (دقیقاً همان چیزی که Webflow "Element Panel" اسمش
269
+ * می‌گذارد). تغییر مدل فقط در لحظهٔ drop رخ می‌دهد (نه در هر
270
+ * pointermove) — به همین دلیل حتی با هزاران بلاک، re-render فقط
271
+ * یک‌بار در پایان درگ اتفاق می‌افتد.
272
+ *
273
+ * پیاده‌سازی immutable-ish: فقط مسیر تغییر‌یافته کپی می‌شود، نه کل
274
+ * درخت (structural sharing) تا با درخت‌های بزرگ هم CPU/GC اذیت نشود.
275
+ */
276
+ declare class FlowTreeService {
277
+ private readonly _root;
278
+ readonly root: i0.Signal<FlowNode>;
279
+ setRoot(node: FlowNode): void;
280
+ applyDrop(draggedId: string, drop: DropResult): void;
281
+ static ɵfac: i0.ɵɵFactoryDeclaration<FlowTreeService, never>;
282
+ static ɵprov: i0.ɵɵInjectableDeclaration<FlowTreeService>;
283
+ }
284
+
285
+ export { DragDropService, DraggableDirective, DropPositionService, DropzoneContainerDirective, FlowDropIndicatorComponent, FlowTreeService, ResizableDirective };
286
+ export type { DragEndEvent, DragMoveEvent, DragStartEvent, DropResult, DropZone, FlowAxis, FlowNode, IndicatorState, Rect, ResizeHandlePosition, ResizeMoveEvent };
@@ -0,0 +1,199 @@
1
+ # @ngx-kit/virtual-scroll
2
+
3
+ A zoneless, signal-based virtual scroll viewport for Angular. No `@angular/material`
4
+ or `@angular/cdk` dependency, no `NgZone`, no `ngOnChanges`, no `@Input() itemSize`,
5
+ no `@Input() orientation` — everything that can be derived from the real DOM is
6
+ derived from the real DOM.
7
+
8
+ ## Why this rewrite
9
+
10
+ | Old design | This design |
11
+ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
12
+ | `NgZone` injected, `zone.run` everywhere | No `NgZone` at all — works natively under `provideZonelessChangeDetection()` |
13
+ | `ngOnChanges` + `SimpleChanges` | `input()` signals + `computed()` / `effect()` |
14
+ | `@Input() itemSize` (broke as soon as your item had margin/border/box-shadow) | Real item size is **measured** from `getBoundingClientRect()` deltas between actually rendered siblings, so margins/borders/shadows are always accounted for |
15
+ | `@Input() orientation` | Axis is **auto-detected** from the real layout of your own item markup |
16
+ | Custom `*ngxVirtualFor` structural directive | Plain native `@for` over a `visibleItems()` signal — you keep full control of `track`, `@empty`, etc. |
17
+
18
+ ## Install & import
19
+
20
+ ```ts
21
+ import { NgxVirtualScrollViewport } from '@ngx-kit/virtual-scroll';
22
+
23
+ @Component({
24
+ standalone: true,
25
+ imports: [NgxVirtualScrollViewport],
26
+ ...
27
+ })
28
+ ```
29
+
30
+ ## Basic usage — vertical list (auto-detected)
31
+
32
+ ```html
33
+ <ngx-virtual-scroll-viewport #vs="ngxVirtualScrollViewport" [items]="items" style="height: 400px">
34
+ @for (item of vs.visibleItems(); track $index; let i = $index) {
35
+ <div class="row">{{ vs.baseIndex() + i | number }} - {{ item.name }}</div>
36
+ }
37
+ </ngx-virtual-scroll-viewport>
38
+ ```
39
+
40
+ This is exactly the case that used to break: `.row` can have `margin`, `border`,
41
+ `box-shadow`, `padding` — anything. The viewport measures the real distance between
42
+ two consecutively rendered rows and uses that, not a number you have to keep in sync
43
+ with your CSS.
44
+
45
+ ```css
46
+ .row {
47
+ height: 48px;
48
+ border: 1px rgb(166 166 166 / 51%) solid;
49
+ box-shadow: 0px 0px 20px rgb(122 122 122 / 20%);
50
+ border-radius: 5px;
51
+ display: flex;
52
+ align-items: center;
53
+ margin: 10px;
54
+ padding: 15px;
55
+ }
56
+ ```
57
+
58
+ ## Horizontal lists & wrapping grids (auto-detected)
59
+
60
+ The axis is inferred purely from how your own items lay out:
61
+
62
+ - Items stack top-to-bottom (default block flow) → **vertical** list.
63
+ - Items lay out left-to-right in a single line (e.g. `display: inline-flex` on the
64
+ item, or a `flex-wrap: nowrap` row) → **horizontal** list.
65
+ - Items lay out left-to-right and _wrap_ to a new line (e.g. cards with
66
+ `display: flex; flex-wrap: wrap`) → treated as a **vertical** list of "lines",
67
+ where each line holds N items. This is how mixed horizontal + vertical layouts
68
+ (a card grid that scrolls vertically) are supported without a separate 2D data
69
+ model — the cross axis (row width) lays out and scrolls natively, only the line
70
+ axis is virtualized.
71
+
72
+ You don't configure any of this — you only make sure your `.item` CSS actually lays
73
+ out the way you want, and the viewport detects it.
74
+
75
+ ## Mixed content (e.g. wide, virtualized table)
76
+
77
+ Both axes of the scroll container are always natively scrollable
78
+ (`overflow: auto` on both). Only the detected axis is virtualized; the cross axis
79
+ (for example, a table wider than its container) scrolls with the browser's native
80
+ scrollbar, so wide tables with virtualized rows work out of the box:
81
+
82
+ ```html
83
+ <div class="table-shell">
84
+ <table><thead>...</thead></table>
85
+ <!-- kept outside, sticky/fixed header -->
86
+
87
+ <ngx-virtual-scroll-viewport #vs="ngxVirtualScrollViewport" [items]="rows" style="height: 480px">
88
+ <table>
89
+ <tbody>
90
+ @for (row of vs.visibleItems(); track row.id; let i = $index) {
91
+ <tr>
92
+ <td>{{ vs.baseIndex() + i }}</td>
93
+ ...
94
+ </tr>
95
+ }
96
+ </tbody>
97
+ </table>
98
+ </ngx-virtual-scroll-viewport>
99
+ </div>
100
+ ```
101
+
102
+ ## Very large data sets (1M+ rows)
103
+
104
+ Browsers clamp any element's real CSS `height`/`width` to a hard maximum
105
+ (~33.5M px in Chromium, far less in older WebKit/Gecko). Once a naive
106
+ "spacer" element crosses that ceiling, the browser silently clamps it,
107
+ `scrollHeight` stops growing, and everything past that point becomes
108
+ unreachable — the classic "scroll gets stuck around ~500k rows" symptom.
109
+
110
+ This viewport handles it automatically: the spacer's real DOM size is capped
111
+ at `maxAxisSizePx` (6,000,000px by default — safely under every known
112
+ browser limit), and the DOM scrollbar operates in a _compressed_ coordinate
113
+ space via `scaleFactor()`. All range/index math is done against the real,
114
+ uncompressed content size (`naturalTotalSize()`), so scrolling the
115
+ (compressed) native scrollbar all the way to the end still lands you exactly
116
+ on the last item, no matter how many millions of rows you have. The only
117
+ user-visible trade-off for truly massive lists is that the scrollbar thumb's
118
+ motion is no longer perfectly 1:1 with "one row per pixel" — an unavoidable
119
+ consequence of representing an arbitrarily large data set inside a bounded
120
+ native scrollbar, and the same trade-off every large-scale virtual scroll
121
+ implementation makes.
122
+
123
+ You don't need to configure anything for this — it only engages once
124
+ `naturalTotalSize()` actually exceeds `maxAxisSizePx`, and is a no-op
125
+ (`scaleFactor() === 1`) for the vast majority of real-world lists.
126
+
127
+ ## API
128
+
129
+ ### Inputs
130
+
131
+ | Input | Type | Default | Notes |
132
+ | --------------- | --------------------------- | ----------- | ---------------------------------------------------------------------------- |
133
+ | `items` | `readonly T[]` | `[]` | Full data set |
134
+ | `minBufferPx` | `number` | `150` | Buffer left before recomputing the range |
135
+ | `maxBufferPx` | `number` | `300` | Extra px rendered outside the viewport |
136
+ | `measureFn` | `NgxVirtualScrollMeasureFn` | `undefined` | Advanced override for the built-in layout probe |
137
+ | `maxAxisSizePx` | `number` | `6_000_000` | Safe ceiling for the real DOM scroll size — see "Very large data sets" below |
138
+
139
+ ### Outputs
140
+
141
+ | Output | Type |
142
+ | --------------------- | ----------------------------------------- |
143
+ | `scrolledIndexChange` | `OutputEmitterRef<number>` |
144
+ | `rangeChange` | `OutputEmitterRef<NgxVirtualScrollRange>` |
145
+
146
+ ### Public signals (via `#vs="ngxVirtualScrollViewport"`)
147
+
148
+ | Signal | Type | Meaning |
149
+ | -------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
150
+ | `visibleItems()` | `readonly T[]` | Slice of `items()` to render right now |
151
+ | `range()` | `{ start, end }` | Currently rendered line range |
152
+ | `baseIndex()` | `number` | Absolute index of `visibleItems()[0]` |
153
+ | `axis()` | `'vertical' \| 'horizontal'` | Auto-detected scroll axis |
154
+ | `crossCount()` | `number` | Items per line (1 for a plain list) |
155
+ | `lineSize()` | `number` | Measured px size of one line |
156
+ | `isMeasured()` | `boolean` | Whether the initial layout probe has completed |
157
+ | `isScrolling()` | `boolean` | True while an active scroll gesture is in progress |
158
+ | `naturalTotalSize()` | `number` | Real, uncompressed total size along the scroll axis |
159
+ | `totalSize()` | `number` | Size actually applied to the spacer (capped at `maxAxisSizePx`) |
160
+ | `scaleFactor()` | `number` | `totalSize() / naturalTotalSize()`; `1` unless the data set is extremely large |
161
+
162
+ ### Methods
163
+
164
+ | Method | Description |
165
+ | ------------------------------------- | ----------------------------------------------------------------------------------------- |
166
+ | `scrollToIndex(index, behavior?)` | Scroll to a given item index |
167
+ | `scrollToOffset(offsetPx, behavior?)` | Scroll to a given pixel offset |
168
+ | `checkViewportSize()` | Force a re-measure (rarely needed — a `ResizeObserver` already handles container resizes) |
169
+
170
+ ## Performance notes
171
+
172
+ - Fully `ChangeDetectionStrategy.OnPush` and zoneless-compatible — no `NgZone`
173
+ import anywhere; signal writes are what schedule change detection.
174
+ - Scroll handling is `requestAnimationFrame`-throttled and the listener is
175
+ registered with `{ passive: true }`.
176
+ - The range is only recomputed once the scroll offset gets within
177
+ `minBufferPx` of the current range's edge — not on every scroll event.
178
+ - Layout is only ever **read** (`getBoundingClientRect`) inside an
179
+ `afterRenderEffect({ read: ... })` block, so DOM measurement never
180
+ interleaves with — and never forces — a layout/style write.
181
+ - `will-change: transform` is only applied to the content wrapper while
182
+ `isScrolling()` is true, and removed ~150ms after the last scroll event, to
183
+ avoid holding a compositor layer alive for the component's entire lifetime.
184
+ - `contain: strict` / `contain: layout style` isolate the viewport's layout
185
+ and paint from the rest of the page.
186
+
187
+ ## How auto-measurement works
188
+
189
+ On first render, up to 24 items are rendered "in the open" (un-transformed) purely
190
+ to be measured — see `measure-layout.ts`. Their real `getBoundingClientRect()`
191
+ deltas are used to infer axis, items-per-line, and line size in one pass, all pure
192
+ functions with no Angular dependency (so they are trivially unit-testable). Once
193
+ measured, the real virtualized range replaces the probe range and rendering
194
+ proceeds normally. If your rows can change size at runtime (e.g. responsive
195
+ breakpoints), a `ResizeObserver` on the scroll container triggers `checkViewportSize()`
196
+ automatically to re-fit the viewport dimension; if the _line size itself_ changes at
197
+ runtime you can force a full re-measure by re-creating the component (e.g. keyed with
198
+ `@if`/`*ngIf` + a changing key), which is the same limitation `cdk-virtual-scroll-viewport`
199
+ has with its own `FixedSizeVirtualScrollStrategy`.