@libs-ui/components-scroll-overlay 0.2.356-9 → 0.2.357-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,153 +1,386 @@
1
1
  # @libs-ui/components-scroll-overlay
2
2
 
3
- > Directive tạo thanh cuộn tùy chỉnh (custom scrollbar) thay thế thanh cuộn mặc định của trình duyệt.
3
+ > Directive tạo thanh cuộn tùy chỉnh (custom scrollbar) dạng overlay, thay thế thanh cuộn mặc định của trình duyệt với giao diện đồng nhất trên mọi hệ điều hành.
4
4
 
5
5
  ## Giới thiệu
6
6
 
7
- `LibsUiComponentsScrollOverlayDirective` là một standalone Angular directive giúp tạo ra thanh cuộn tùy chỉnh với giao diện đẹp mắt, hỗ trợ kéo thả, tự động ẩn hiệntùy biến màu sắc.
7
+ `LibsUiComponentsScrollOverlayDirective` là một Angular standalone directive giúp thay thế thanh cuộn mặc định của trình duyệt bằng một thanh cuộn tùy chỉnh hiển thị dạng overlay — không chiếm diện tích layout. Directive tự động bọc element host trong một `div` container, inject CSS global một lần duy nhất, và quản lý toàn bộ tương tác kéo thả, hover, resize qua RxJSrequestAnimationFrame throttling. Mỗi instance độc lập màu sắc thông qua CSS custom properties.
8
8
 
9
- ### Tính năng
9
+ ## Tính năng
10
10
 
11
- - Custom Scrollbar (thay thế native scrollbar)
12
- - Hỗ trợ cả chiều ngang (Horizontal) và chiều dọc (Vertical)
13
- - Tự động ẩn hiện khi hover
14
- - Hỗ trợ kéo thả (Drag & Drop) thanh cuộn
15
- - Tùy biến màu sắc, kích thước
16
- - Hỗ trợ Mobile/Touch devices (thông qua `getDragEventByElement`)
17
- - Resize Observer tự động cập nhật kích thước
11
+ - Thanh cuộn tùy chỉnh thay thế native scrollbar, giao diện đồng nhất trên Windows, macOS, Linux
12
+ - Hỗ trợ cả scroll dọc (Y) và scroll ngang (X) đồng thời
13
+ - Tự động ẩn/hiện khi hover vào container, giữ hiện khi đang kéo thả
14
+ - Kéo thả (drag & drop) thumb với xử lý mouse chính xác
15
+ - Click vào track để nhảy đến vị trí scroll tương ứng
16
+ - Tự động tính lại kích thước thumb khi nội dung thay đổi (interval poll mỗi 1s khi hover + dimension cache)
17
+ - Resize cửa sổ tự động cập nhật scrollbar
18
+ - Scroll handler throttle qua `requestAnimationFrame` — tránh layout thrashing
19
+ - Màu sắc per-instance qua CSS variables, không ghi đè lẫn nhau
20
+ - Phát 5 loại output event: `outScroll`, `outScrollX`, `outScrollY`, `outScrollTop`, `outScrollBottom`
21
+ - Hỗ trợ truyền element riêng để tính `scrollWidth`/`scrollHeight` (hữu ích khi content element khác scroll element)
18
22
 
19
23
  ## Khi nào sử dụng
20
24
 
21
- - Khi cần giao diện thanh cuộn đồng bộ trên các trình duyệt/hệ điều hành khác nhau.
22
- - Khi cần thanh cuộn nhỏ gọn, overlay lên nội dung mà không chiếm diện tích layout.
23
- - Khi cần tùy biến màu sắc thanh cuộn theo theme của ứng dụng.
25
+ - Khi cần giao diện thanh cuộn đồng bộ trên các trình duyệthệ điều hành khác nhau
26
+ - Khi cần thanh cuộn nhỏ gọn, overlay lên nội dung mà không chiếm diện tích layout
27
+ - Khi cần tùy biến màu sắc thanh cuộn theo theme của ứng dụng
28
+ - Khi cần lắng nghe sự kiện scroll lên đầu hoặc xuống cuối để trigger lazy loading
29
+ - Khi muốn ẩn hoàn toàn scrollbar nhưng vẫn cho phép scroll bằng mouse wheel hoặc touch
24
30
 
25
31
  ## Cài đặt
26
32
 
27
33
  ```bash
28
- # npm
29
34
  npm install @libs-ui/components-scroll-overlay
30
-
31
- # yarn
32
- yarn add @libs-ui/components-scroll-overlay
33
35
  ```
34
36
 
35
37
  ## Import
36
38
 
37
39
  ```typescript
38
40
  import { LibsUiComponentsScrollOverlayDirective } from '@libs-ui/components-scroll-overlay';
41
+ import { IScrollOverlayOptions } from '@libs-ui/components-scroll-overlay';
39
42
 
40
43
  @Component({
41
44
  standalone: true,
42
45
  imports: [LibsUiComponentsScrollOverlayDirective],
43
- // ...
44
46
  })
45
- export class YourComponent {}
47
+ export class MyComponent {}
48
+ ```
49
+
50
+ ## Ví dụ sử dụng
51
+
52
+ ### Ví dụ 1 — Scroll dọc cơ bản
53
+
54
+ ```html
55
+ <div
56
+ LibsUiComponentsScrollOverlayDirective
57
+ class="h-64 w-full overflow-y-auto p-4 border rounded bg-white"
58
+ >
59
+ @for (item of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; track item) {
60
+ <div class="p-3 mb-2 bg-gray-50 rounded">
61
+ Item {{ item }}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
62
+ </div>
63
+ }
64
+ </div>
46
65
  ```
47
66
 
48
- ## Ví dụ
67
+ ```typescript
68
+ import { LibsUiComponentsScrollOverlayDirective } from '@libs-ui/components-scroll-overlay';
49
69
 
50
- ### Basic
70
+ @Component({
71
+ standalone: true,
72
+ changeDetection: ChangeDetectionStrategy.OnPush,
73
+ imports: [LibsUiComponentsScrollOverlayDirective],
74
+ })
75
+ export class MyComponent {}
76
+ ```
51
77
 
52
- Thêm directive vào phần tử `overflow: scroll` hoặc `overflow: auto`.
78
+ ### dụ 2 Scroll ngang với nội dung rộng
53
79
 
54
80
  ```html
55
81
  <div
56
82
  LibsUiComponentsScrollOverlayDirective
57
- class="h-[300px] w-full overflow-y-auto">
58
- <!-- Content dài -->
83
+ class="w-full overflow-x-auto border rounded bg-white"
84
+ >
85
+ <div class="flex gap-4 w-[1200px] p-4">
86
+ @for (item of [1, 2, 3, 4, 5, 6, 7, 8]; track item) {
87
+ <div class="w-48 h-32 bg-blue-100 rounded flex-shrink-0 flex items-center justify-center">
88
+ Card {{ item }}
89
+ </div>
90
+ }
91
+ </div>
59
92
  </div>
60
93
  ```
61
94
 
62
- ### With Options
95
+ ### dụ 3 — Tùy chỉnh màu sắc và kích thước qua options
63
96
 
64
97
  ```html
65
98
  <div
66
99
  LibsUiComponentsScrollOverlayDirective
67
- [options]="{
68
- scrollbarColor: '#e5e7eb',
69
- scrollThumbColor: '#9ca3af',
100
+ [options]="scrollOptions"
101
+ class="h-80 w-full overflow-auto border rounded bg-white p-4"
102
+ >
103
+ <div class="w-[900px]">
104
+ <p>Nội dung có thể scroll cả dọc lẫn ngang...</p>
105
+ @for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) {
106
+ <div class="p-3 mb-2 bg-gray-50 rounded">Dòng {{ i }}</div>
107
+ }
108
+ </div>
109
+ </div>
110
+ ```
111
+
112
+ ```typescript
113
+ import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
114
+ import { LibsUiComponentsScrollOverlayDirective, IScrollOverlayOptions } from '@libs-ui/components-scroll-overlay';
115
+
116
+ @Component({
117
+ standalone: true,
118
+ changeDetection: ChangeDetectionStrategy.OnPush,
119
+ imports: [LibsUiComponentsScrollOverlayDirective],
120
+ })
121
+ export class MyComponent {
122
+ protected readonly scrollOptions: IScrollOverlayOptions = {
70
123
  scrollbarWidth: 8,
71
- scrollbarPadding: 0
72
- }"
73
- class="h-[300px] w-full overflow-y-auto">
74
- <!-- Content -->
124
+ scrollbarPadding: 2,
125
+ scrollbarColor: '#f1f5f9',
126
+ scrollbarHoverColor: '#e2e8f0',
127
+ scrollThumbColor: '#94a3b8',
128
+ scrollThumbHoverColor: '#64748b',
129
+ scrollX: 'scroll',
130
+ scrollY: 'scroll',
131
+ };
132
+ }
133
+ ```
134
+
135
+ ### Ví dụ 4 — Lắng nghe sự kiện scroll để lazy load
136
+
137
+ ```html
138
+ <div
139
+ LibsUiComponentsScrollOverlayDirective
140
+ class="h-96 w-full overflow-y-auto border rounded"
141
+ (outScrollBottom)="handlerScrollBottom($event)"
142
+ (outScrollTop)="handlerScrollTop($event)"
143
+ (outScrollY)="handlerScrollY($event)"
144
+ >
145
+ @for (item of items(); track item.id) {
146
+ <div class="p-4 border-b">{{ item.name }}</div>
147
+ }
148
+ @if (loading()) {
149
+ <div class="p-4 text-center text-gray-400">Đang tải thêm...</div>
150
+ }
75
151
  </div>
76
152
  ```
77
153
 
78
- ## API
154
+ ```typescript
155
+ import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
156
+ import { LibsUiComponentsScrollOverlayDirective } from '@libs-ui/components-scroll-overlay';
157
+ import { ItemApiService } from './item-api.service';
79
158
 
80
- ### LibsUiComponentsScrollOverlayDirective
159
+ @Component({
160
+ standalone: true,
161
+ changeDetection: ChangeDetectionStrategy.OnPush,
162
+ imports: [LibsUiComponentsScrollOverlayDirective],
163
+ })
164
+ export class MyListComponent {
165
+ private readonly apiService = inject(ItemApiService);
166
+
167
+ protected items = signal<{ id: string; name: string }[]>([]);
168
+ protected loading = signal(false);
169
+
170
+ protected handlerScrollBottom(event: Event): void {
171
+ event.stopPropagation();
172
+ if (this.loading()) return;
173
+ this.loadMore();
174
+ }
175
+
176
+ protected handlerScrollTop(event: Event): void {
177
+ event.stopPropagation();
178
+ // xử lý khi scroll lên đầu
179
+ }
180
+
181
+ protected handlerScrollY(event: Event): void {
182
+ event.stopPropagation();
183
+ // xử lý mỗi lần scroll dọc
184
+ }
185
+
186
+ private loadMore(): void {
187
+ this.loading.set(true);
188
+ // gọi API tải thêm dữ liệu
189
+ }
190
+ }
191
+ ```
81
192
 
82
- Selector: `[LibsUiComponentsScrollOverlayDirective]`
193
+ ### Ví dụ 5 — Dynamic content với signal (tự cập nhật scrollbar)
83
194
 
84
- #### Inputs
195
+ ```html
196
+ <div class="flex gap-2 mb-3">
197
+ <button (click)="handlerAddItem()">+ Thêm item</button>
198
+ <button (click)="handlerRemoveItem()">- Xóa item</button>
199
+ </div>
85
200
 
86
- | Property | Type | Default | Description |
87
- | ----------------------- | ----------------------- | -------------- | ------------------------------------------------------------- |
88
- | `[options]` | `IScrollOverlayOptions` | `{}` | Cấu hình cho thanh cuộn (màu sắc, kích thước, behavior). |
89
- | `[classContainer]` | `string` | `''` | Class CSS thêm vào container bao ngoài (do directive tạo ra). |
90
- | `[elementScroll]` | `HTMLElement` | `Host Element` | Element cần scroll (mặc định element gắn directive). |
91
- | `[elementCheckScrollX]` | `HTMLElement` | `undefined` | Element để tính toán scroll width (nếu khác element scroll). |
92
- | `[elementCheckScrollY]` | `HTMLElement` | `undefined` | Element để tính toán scroll height (nếu khác element scroll). |
93
- | `[debugMode]` | `boolean` | `false` | Bật chế độ debug log. |
94
- | `[ignoreInit]` | `boolean` | `false` | Nếu true, directive sẽ không khởi tạo scrollbar. |
201
+ <div
202
+ LibsUiComponentsScrollOverlayDirective
203
+ class="h-64 w-full overflow-y-auto border rounded bg-gray-50 p-3"
204
+ >
205
+ @for (item of items(); track item) {
206
+ <div class="p-3 mb-2 bg-white rounded shadow-sm">{{ item }}</div>
207
+ }
208
+ </div>
209
+ ```
95
210
 
96
- #### Outputs
211
+ ```typescript
212
+ import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
213
+ import { LibsUiComponentsScrollOverlayDirective } from '@libs-ui/components-scroll-overlay';
214
+
215
+ @Component({
216
+ standalone: true,
217
+ changeDetection: ChangeDetectionStrategy.OnPush,
218
+ imports: [LibsUiComponentsScrollOverlayDirective],
219
+ })
220
+ export class DynamicListComponent {
221
+ protected items = signal<string[]>([
222
+ 'Thiết kế giao diện người dùng',
223
+ 'Viết unit test cho module auth',
224
+ 'Review Pull Request #124',
225
+ 'Cập nhật tài liệu API',
226
+ ]);
227
+
228
+ protected handlerAddItem(): void {
229
+ this.items.update((list) => [...list, `Item mới ${list.length + 1}`]);
230
+ // Sau khi thêm item, hover vào vùng scroll để scrollbar tự cập nhật (~1s)
231
+ }
232
+
233
+ protected handlerRemoveItem(): void {
234
+ if (this.items().length <= 1) return;
235
+ this.items.update((list) => list.slice(0, -1));
236
+ }
237
+ }
238
+ ```
239
+
240
+ ### Ví dụ 6 — Ẩn scrollbar hoàn toàn nhưng vẫn scroll được
241
+
242
+ ```html
243
+ <!-- scrollXOpacity0: true → scrollbar ngang không bao giờ hiện, nhưng vẫn scroll được bằng touch/trackpad -->
244
+ <div
245
+ LibsUiComponentsScrollOverlayDirective
246
+ [options]="hiddenScrollbarOptions"
247
+ class="w-full overflow-x-auto"
248
+ >
249
+ <div class="flex gap-4 w-[2000px] p-4">
250
+ @for (item of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; track item) {
251
+ <div class="w-40 h-24 bg-indigo-100 rounded flex-shrink-0 flex items-center justify-center">
252
+ Card {{ item }}
253
+ </div>
254
+ }
255
+ </div>
256
+ </div>
257
+ ```
97
258
 
98
- | Property | Type | Description |
99
- | ------------------- | ------- | ---------------------------------------- |
100
- | `(outScroll)` | `Event` | Emit khi scroll event xảy ra. |
101
- | `(outScrollX)` | `Event` | Emit khi scroll chiều ngang thay đổi. |
102
- | `(outScrollY)` | `Event` | Emit khi scroll chiều dọc thay đổi. |
103
- | `(outScrollTop)` | `Event` | Emit khi scroll lên đầu (scrollTop = 0). |
104
- | `(outScrollBottom)` | `Event` | Emit khi scroll xuống cuối. |
259
+ ```typescript
260
+ import { Component, ChangeDetectionStrategy } from '@angular/core';
261
+ import { LibsUiComponentsScrollOverlayDirective, IScrollOverlayOptions } from '@libs-ui/components-scroll-overlay';
262
+
263
+ @Component({
264
+ standalone: true,
265
+ changeDetection: ChangeDetectionStrategy.OnPush,
266
+ imports: [LibsUiComponentsScrollOverlayDirective],
267
+ })
268
+ export class HiddenScrollbarComponent {
269
+ protected readonly hiddenScrollbarOptions: IScrollOverlayOptions = {
270
+ scrollX: 'scroll',
271
+ scrollY: 'hidden',
272
+ scrollXOpacity0: true,
273
+ };
274
+ }
275
+ ```
276
+
277
+ ## @Input()
278
+
279
+ | Input | Type | Default | Mô tả | Ví dụ |
280
+ |---|---|---|---|---|
281
+ | `[options]` | `IScrollOverlayOptions` | `{}` | Cấu hình toàn bộ scrollbar: màu sắc, kích thước, chế độ cuộn và các flags | `[options]="{ scrollbarWidth: 8, scrollThumbColor: '#94a3b8' }"` |
282
+ | `[classContainer]` | `string` | `''` | Class CSS bổ sung vào `div` container bao ngoài mà directive tạo ra | `[classContainer]="'shadow-lg rounded-xl'"` |
283
+ | `[elementScroll]` | `HTMLElement` | Host element | Override element cần scroll thay vì dùng host element mặc định | `[elementScroll]="innerScrollRef"` |
284
+ | `[elementCheckScrollX]` | `HTMLElement` | `undefined` | Element dùng để tính `scrollWidth` — dùng khi content element khác với scroll element | `[elementCheckScrollX]="contentRef"` |
285
+ | `[elementCheckScrollY]` | `HTMLElement` | `undefined` | Element dùng để tính `scrollHeight` — dùng khi content element khác với scroll element | `[elementCheckScrollY]="contentRef"` |
286
+ | `[debugMode]` | `boolean` | `false` | Bật chế độ debug: ghi log ra console khi có thay đổi kích thước scrollbar | `[debugMode]="true"` |
287
+ | `[ignoreInit]` | `boolean` | `false` | Nếu `true`, directive bỏ qua toàn bộ khởi tạo scrollbar — dùng cho lazy init có điều kiện | `[ignoreInit]="!isVisible()"` |
288
+
289
+ ## @Output()
290
+
291
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
292
+ |---|---|---|---|---|
293
+ | `(outScroll)` | `Event` | Emit mỗi khi có sự kiện scroll (cả X lẫn Y) | `handlerScroll(event: Event): void { event.stopPropagation(); ... }` | `(outScroll)="handlerScroll($event)"` |
294
+ | `(outScrollBottom)` | `Event` | Emit khi scroll xuống đến cuối nội dung (tolerance 3px) | `handlerScrollBottom(event: Event): void { event.stopPropagation(); this.loadMore(); }` | `(outScrollBottom)="handlerScrollBottom($event)"` |
295
+ | `(outScrollTop)` | `Event` | Emit khi scroll về đầu (scrollTop === 0) | `handlerScrollTop(event: Event): void { event.stopPropagation(); ... }` | `(outScrollTop)="handlerScrollTop($event)"` |
296
+ | `(outScrollX)` | `Event` | Emit khi cuộn ngang (chỉ khi scrollLeft thực sự thay đổi) | `handlerScrollX(event: Event): void { event.stopPropagation(); ... }` | `(outScrollX)="handlerScrollX($event)"` |
297
+ | `(outScrollY)` | `Event` | Emit khi cuộn dọc (chỉ khi scrollTop thực sự thay đổi) | `handlerScrollY(event: Event): void { event.stopPropagation(); ... }` | `(outScrollY)="handlerScrollY($event)"` |
105
298
 
106
299
  ## Types & Interfaces
107
300
 
108
301
  ```typescript
109
- export type TYPE_SCROLL_DIRECTION = 'X' | 'Y';
110
- export type TYPE_SCROLL_OVERFLOW = 'hidden' | 'scroll';
302
+ import { IScrollOverlayOptions, TYPE_SCROLL_DIRECTION, TYPE_SCROLL_OVERFLOW } from '@libs-ui/components-scroll-overlay';
303
+ ```
111
304
 
305
+ ```typescript
112
306
  export interface IScrollOverlayOptions {
113
- ignoreTransparentScrollBarColorDefault?: boolean; // Nếu true, dùng 'auto' thay vì 'transparent' cho scrollbar-color
114
- scrollbarWidth?: number; // Độ rộng của track scrollbar (default: 10)
115
- scrollbarColor?: string; // Màu nền của track
116
- scrollbarHoverColor?: string; // Màu nền của track khi hover
117
- scrollThumbColor?: string; // Màu của thanh cuộn (thumb)
118
- scrollThumbHoverColor?: string; // Màu của thanh cuộn khi hover
119
- scrollbarPadding?: number; // Padding cho thumb (default: 2)
120
- scrollX?: TYPE_SCROLL_OVERFLOW; // 'scroll' hoặc 'hidden' cho chiều ngang
121
- scrollXOpacity0?: boolean; // Nếu true, thanh cuộn ngang vĩnh viễn ẩn (opacity 0) nhưng vẫn scroll được? (Check logic: visible but opacity 0)
122
- scrollY?: TYPE_SCROLL_OVERFLOW; // 'scroll' hoặc 'hidden' cho chiều dọc
123
- scrollYOpacity0?: boolean; // Tương tự cho chiều dọc
307
+ // Kích thước
308
+ scrollbarWidth?: number; // Độ rộng của track scrollbar tính bằng px (default: 10)
309
+ scrollbarPadding?: number; // Khoảng cách padding của thumb so với cạnh track (default: 2)
310
+
311
+ // Màu sắc
312
+ scrollbarColor?: string; // Màu nền của track khi không hover (default: transparent)
313
+ scrollbarHoverColor?: string; // Màu nền của track khi hover (default: '#CDD0D640')
314
+ scrollThumbColor?: string; // Màu của thumb scrollbar (default: '#CDD0D6')
315
+ scrollThumbHoverColor?: string; // Màu của thumb khi hover (default: '#9CA2AD')
316
+
317
+ // Chế độ cuộn
318
+ scrollX?: TYPE_SCROLL_OVERFLOW; // 'scroll' | 'hidden' — kiểm soát overflow-x (default: 'scroll')
319
+ scrollY?: TYPE_SCROLL_OVERFLOW; // 'scroll' | 'hidden' — kiểm soát overflow-y (default: 'scroll')
320
+
321
+ // Flags hành vi
322
+ scrollXOpacity0?: boolean; // Giữ scrollbar ngang luôn ẩn (opacity 0) dù hover — vẫn scroll được
323
+ scrollYOpacity0?: boolean; // Giữ scrollbar dọc luôn ẩn (opacity 0) dù hover — vẫn scroll được
324
+ ignoreTransparentScrollBarColorDefault?: boolean; // Không ép native scrollbar thành trong suốt — dùng 'auto' thay vì 'transparent transparent'
124
325
  }
326
+
327
+ export type TYPE_SCROLL_DIRECTION = 'X' | 'Y';
328
+ export type TYPE_SCROLL_OVERFLOW = 'hidden' | 'scroll';
125
329
  ```
126
330
 
127
- ## Styling
331
+ ## Cấu trúc DOM được tạo ra
128
332
 
129
- Directive tự động inject styles vào thẻ `<style id="id-style-tag-custom-scroll-overlay">` trong `<head>`.
130
- Cấu trúc DOM được tạo ra:
333
+ Khi directive khởi tạo, tự động wrap host element như sau:
131
334
 
132
335
  ```html
133
- <div class="lib-ui-scroll-overlay-container ...">
134
- <!-- Host Element -->
135
- <div class="scrollbar-track scrollbar-track-X">
136
- <div class="scrollbar-thumb scrollbar-thumb-X"></div>
336
+ <!-- Trước khi áp dụng directive -->
337
+ <div class="h-64 overflow-y-auto" LibsUiComponentsScrollOverlayDirective>
338
+ <!-- nội dung -->
339
+ </div>
340
+
341
+ <!-- Sau khi directive khởi tạo -->
342
+ <div class="libs-ui-scroll-overlay-container relative min-h-0 min-w-0 h-64">
343
+ <!-- Host element (đã thêm class libs-ui-scroll-overlay-element) -->
344
+ <div class="h-64 overflow-y-auto libs-ui-scroll-overlay-element" style="scrollbar-width: none; overflow-x: scroll; overflow-y: scroll;">
345
+ <!-- nội dung -->
346
+ </div>
347
+ <!-- Track và thumb cho scroll X -->
348
+ <div class="scrollbar-track scrollbar-track-X" style="height: 10px; display: none;">
349
+ <div class="scrollbar-thumb scrollbar-thumb-X" style="width: 60px; left: 0;"></div>
137
350
  </div>
138
- <div class="scrollbar-track scrollbar-track-Y">
139
- <div class="scrollbar-thumb scrollbar-thumb-Y"></div>
351
+ <!-- Track và thumb cho scroll Y -->
352
+ <div class="scrollbar-track scrollbar-track-Y" style="width: 10px; display: none;">
353
+ <div class="scrollbar-thumb scrollbar-thumb-Y" style="height: 60px; top: 0;"></div>
140
354
  </div>
141
355
  </div>
142
356
  ```
143
357
 
144
- ## Công nghệ
358
+ ## CSS Custom Properties
359
+
360
+ Mỗi instance directive tự inject CSS variables lên container div để không ảnh hưởng lẫn nhau:
361
+
362
+ | CSS Variable | Tương ứng với | Giá trị mặc định |
363
+ |---|---|---|
364
+ | `--libs-ui-sb-track-color` | `scrollbarColor` | `transparent` |
365
+ | `--libs-ui-sb-track-hover-color` | `scrollbarHoverColor` | `#CDD0D640` |
366
+ | `--libs-ui-sb-thumb-color` | `scrollThumbColor` | `#CDD0D6` |
367
+ | `--libs-ui-sb-thumb-hover-color` | `scrollThumbHoverColor` | `#9CA2AD` |
368
+ | `--libs-ui-sb-padding` | `scrollbarPadding` | `2px` |
369
+ | `--libs-ui-sb-padding-double` | `scrollbarPadding * 2` | `4px` |
370
+
371
+ ## Lưu ý quan trọng
372
+
373
+ ⚠️ **DOM Wrapping**: Directive tự động bọc host element trong một `div.libs-ui-scroll-overlay-container`. CSS selectors dựa vào cấu trúc cha-con (ví dụ: `.parent > div`) có thể bị ảnh hưởng. Kiểm tra kỹ layout sau khi áp dụng directive.
374
+
375
+ ⚠️ **Class kích thước tự động kế thừa**: Directive tự copy một số class kích thước từ host element (`w-full`, `w-screen`, `h-full`, `h-screen`, `h-[Npx]`, `w-[Npx]`, `min-h-*`, `min-w-*`, `shrink-0`) sang container. Nếu layout bị sai, kiểm tra xem class có được copy đúng không.
376
+
377
+ ⚠️ **Dynamic content cần hover**: Directive dùng `interval(1000)` khi hover để poll `scrollHeight`/`scrollWidth` và so sánh với cache dimension. Sau khi thêm/xóa nội dung hoặc thay đổi chiều cao element, người dùng cần hover vào vùng scroll để scrollbar cập nhật trong vòng ~1s. Đây là thiết kế chủ ý nhằm tránh dùng `MutationObserver({ attributes, subtree })` gây fire 50-200+ lần/giây khi hover.
378
+
379
+ ⚠️ **position: relative**: Container `div` được tạo ra luôn có `position: relative`. Nếu layout cần `position: static` hoặc layout cụ thể, cần dùng `[classContainer]` để điều chỉnh.
380
+
381
+ ⚠️ **scrollX/scrollY: 'hidden'**: Khi đặt cả hai thành `'hidden'`, host element sẽ có class `overflow-hidden`, ngăn mọi scroll. Chỉ dùng khi thực sự muốn block scroll hoàn toàn.
145
382
 
146
- | Technology | Version | Purpose |
147
- | ---------- | ------- | ------------------- |
148
- | Angular | 18+ | Framework |
149
- | RxJS | 7.x | Event Handling |
150
- | DOM API | - | Scroll Manipulation |
383
+ ⚠️ **Không dùng với `overflow: visible`**: Directive yêu cầu host element có thể scroll (overflow có giá trị). Nếu element không có nội dung tràn, scrollbar sẽ không hiển thị (ẩn tự động theo `display: none`).
151
384
 
152
385
  ## Demo
153
386