vue-virtual-flow 0.0.3 → 0.0.5

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.vi.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Bộ component virtual scrolling nhỏ gọn, có type đầy đủ và hỗ trợ khả năng tiếp
6
6
  cận cho Vue 3. Thư viện hỗ trợ danh sách chiều cao cố định hoặc động, cuộn theo
7
- cửa sổ, giao diện chat, feed media ngắn và carousel ngang.
7
+ cửa sổ, grid cuộn theo cửa sổ, giao diện chat, feed media ngắn và carousel ngang.
8
8
 
9
9
  Chỉ các item cần thiết cho viewport hiện tại được mount, nhờ đó tập dữ liệu lớn
10
10
  vẫn phản hồi nhanh mà component không áp đặt giao diện của từng item.
@@ -18,9 +18,11 @@ vẫn phản hồi nhanh mà component không áp đặt giao diện của từn
18
18
  - [VirtualList và DynamicVirtualScroll](#virtuallist-và-dynamicvirtualscroll)
19
19
  - [VirtualScroll](#virtualscroll)
20
20
  - [WindowDynamicVirtualScroll](#windowdynamicvirtualscroll)
21
+ - [WindowGirdVirtualScroll](#windowgirdvirtualscroll)
21
22
  - [ChatVirtualScroll](#chatvirtualscroll)
22
23
  - [ShortMediaFeed](#shortmediafeed)
23
24
  - [VirtualCarousel](#virtualcarousel)
25
+ - [SkeletonLoader](#skeletonloader)
24
26
  - [Cách dùng chung](#cách-dùng-chung)
25
27
  - [Chiều cao viewport](#chiều-cao-viewport)
26
28
  - [Key ổn định cho item](#key-ổn-định-cho-item)
@@ -43,10 +45,19 @@ Import stylesheet của thư viện một lần tại entry point của ứng d
43
45
  import 'vue-virtual-flow/style.css'
44
46
  ```
45
47
 
46
- Package yêu cầu Vue `^3.4.0` và Node.js `>=18`.
48
+ | Dependency | Yêu cầu |
49
+ | --- | --- |
50
+ | Vue | `^3.4.0` |
51
+ | Node.js | `>=18` |
47
52
 
48
53
  ## Bắt đầu nhanh
49
54
 
55
+ | Cách đăng ký | Cách dùng |
56
+ | --- | --- |
57
+ | Import cục bộ | `import { VirtualList, SkeletonLoader } from 'vue-virtual-flow'` |
58
+ | Plugin global | `createApp(App).use(VueVirtualScroll)` |
59
+ | Style dùng chung | `import 'vue-virtual-flow/style.css'` |
60
+
50
61
  Import component cục bộ:
51
62
 
52
63
  ```vue
@@ -83,8 +94,8 @@ createApp(App).use(VueVirtualScroll).mount('#app')
83
94
  ```
84
95
 
85
96
  Plugin đăng ký `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
86
- `WindowDynamicVirtualScroll`, `ChatVirtualScroll`, `ShortMediaFeed` và
87
- `VirtualCarousel`.
97
+ `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll`, `ChatVirtualScroll`,
98
+ `ShortMediaFeed`, `VirtualCarousel` và `SkeletonLoader`.
88
99
 
89
100
  ## Chọn component
90
101
 
@@ -94,12 +105,27 @@ Plugin đăng ký `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`,
94
105
  | `DynamicVirtualScroll` | Giống `VirtualList` nhưng tên gọi thể hiện rõ chiều cao động. | Container của component | Động, tự đo |
95
106
  | `VirtualScroll` | Tất cả hàng có cùng chiều cao đã biết. | Container của component | Cố định |
96
107
  | `WindowDynamicVirtualScroll` | Muốn trang cuộn thay vì tạo một container cuộn lồng bên trong. | Cửa sổ trình duyệt | Động, tự đo |
108
+ | `WindowGirdVirtualScroll` | Grid card có chiều cao cố định và dùng trang để cuộn. | Cửa sổ trình duyệt | Chiều cao hàng cố định |
97
109
  | `ChatVirtualScroll` | Tin nhắn mới được thêm ở cuối và lịch sử cũ được thêm ở đầu. | Container của component | Động, tự đo |
98
110
  | `ShortMediaFeed` | Mỗi lần cần snap một item đầy viewport. | Container của component | Một viewport cho mỗi item |
99
111
  | `VirtualCarousel` | Cần hiển thị và snap nhiều slide theo chiều ngang. | Container của component | Tính từ chiều rộng container |
112
+ | `SkeletonLoader` | Cần hiển thị khối giữ chỗ khi đang tải nội dung. | Không có | Tùy chỉnh chiều rộng và chiều cao |
100
113
 
101
114
  ## Hướng dẫn từng component
102
115
 
116
+ Event được lắng nghe bằng cú pháp template của Vue; các method được gọi qua
117
+ ref của component. Các bảng bên dưới dùng tên ref là `list`, `chat`, `feed`
118
+ hoặc `carousel`.
119
+
120
+ | Thao tác | Cách dùng |
121
+ | --- | --- |
122
+ | Khai báo ref có type | `const list = ref<VirtualScrollExpose>()` |
123
+ | Gắn ref vào component | `<VirtualScroll ref="list" ... />` |
124
+ | Gọi sau khi mount trong script | `list.value?.scrollToIndex(10)` |
125
+ | Gọi từ event trong template | `@click="list?.scrollToIndex(10)"` |
126
+ | Chọn cách cuộn | Truyền `{ behavior: 'smooth' }` cho `scrollTo` / `scrollToIndex`, hoặc `'smooth'` cho `scrollToTop`, `scrollToBottom`, `next`, `previous`. |
127
+ | Căn vị trí item trong danh sách | `scrollToIndex(index, { align: 'start' })` / `'center'` / `'end'` |
128
+
103
129
  ### VirtualList và DynamicVirtualScroll
104
130
 
105
131
  `VirtualList` là alias của `DynamicVirtualScroll`. Hai tên này render cùng một
@@ -197,19 +223,19 @@ và thao tác nhảy tới item chưa được đo chính xác hơn.
197
223
 
198
224
  #### Events
199
225
 
200
- | Event | Payload | Thời điểm emit |
201
- | --- | --- | --- |
202
- | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. `endIndex` là mốc không bao gồm item tại index đó. |
203
- | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. |
204
- | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. |
226
+ | Event | Payload | Thời điểm emit | Cách dùng |
227
+ | --- | --- | --- | --- |
228
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. `endIndex` là mốc không bao gồm item tại index đó. | `@scroll="onScroll"` |
229
+ | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. | `@load-more="loadMore"` |
230
+ | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. | `@refresh="refreshFirstPage"` |
205
231
 
206
232
  #### API qua ref
207
233
 
208
- | Method | Mô tả |
209
- | --- | --- |
210
- | `scrollTo(position, options?)` | Cuộn tới offset pixel bên trong danh sách. |
211
- | `scrollToIndex(index, { align?, behavior? })` | Cuộn tới index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. |
212
- | `scrollToTop(behavior?)` | Cuộn về đầu danh sách. |
234
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
235
+ | --- | --- | --- |
236
+ | `scrollTo(position, options?)` | Cuộn tới offset pixel bên trong danh sách. | `list?.scrollTo(200, { behavior: 'smooth' })` |
237
+ | `scrollToIndex(index, options?)` | Cuộn tới index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
238
+ | `scrollToTop(behavior?)` | Cuộn về đầu danh sách. | `list?.scrollToTop()` |
213
239
 
214
240
  Smooth scroll ở khoảng cách xa tự chuyển thành nhảy tức thời vì các hàng chưa
215
241
  đo có thể làm thay đổi offset đích trong lúc cuộn.
@@ -283,13 +309,30 @@ thay đổi kích thước, hãy dùng `VirtualList`.
283
309
  | `refreshing` | `boolean` | `false` | Giữ indicator mở trong lúc refresh bất đồng bộ. |
284
310
  | `pullRefreshThreshold` | `number` | `64` | Khoảng kéo cần thiết để emit `refresh`, tính bằng pixel. |
285
311
 
286
- #### Slots, events và API qua ref
312
+ #### Slots
313
+
314
+ | Slot | Scope | Mô tả |
315
+ | --- | --- | --- |
316
+ | `default` | `{ item, index }` | Render từng hàng đang được mount. |
317
+ | `empty` | không có | Render khi `items` rỗng và `loading` là false. |
318
+ | `loading` | không có | Hàng loading; mặc định là “Loading more…”. |
319
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Indicator kéo để làm mới. `progress` được giới hạn từ `0` tới `1`. |
320
+
321
+ #### Events
322
+
323
+ | Event | Payload | Thời điểm emit | Cách dùng |
324
+ | --- | --- | --- | --- |
325
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. `endIndex` là mốc không bao gồm item tại index đó. | `@scroll="onScroll"` |
326
+ | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. | `@load-more="loadMore"` |
327
+ | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. | `@refresh="refreshFirstPage"` |
328
+
329
+ #### API qua ref
287
330
 
288
- - Slots: `default({ item, index })`, `empty`, `loading` và
289
- `refresh({ pullDistance, progress, refreshing })`.
290
- - Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more` và
291
- `refresh`.
292
- - Methods: `scrollTo`, `scrollToIndex` và `scrollToTop`.
331
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
332
+ | --- | --- | --- |
333
+ | `scrollTo(position, options?)` | Cuộn tới offset pixel bên trong danh sách. | `list?.scrollTo(200, { behavior: 'smooth' })` |
334
+ | `scrollToIndex(index, options?)` | Cuộn tới item có index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
335
+ | `scrollToTop(behavior?)` | Cuộn về đầu danh sách. | `list?.scrollToTop()` |
293
336
 
294
337
  ### WindowDynamicVirtualScroll
295
338
 
@@ -353,14 +396,111 @@ const list = ref<VirtualScrollExpose>()
353
396
  | `refreshing` | `boolean` | `false` | Giữ indicator mở trong lúc refresh bất đồng bộ. |
354
397
  | `pullRefreshThreshold` | `number` | `64` | Khoảng kéo cần thiết để emit `refresh`, tính bằng pixel. |
355
398
 
356
- #### Slots, events và API qua ref
399
+ #### Slots
400
+
401
+ | Slot | Scope | Mô tả |
402
+ | --- | --- | --- |
403
+ | `default` | `{ item, index }` | Render từng hàng đang được mount. |
404
+ | `empty` | không có | Render khi `items` rỗng và `loading` là false. |
405
+ | `loading` | không có | Hàng loading; mặc định là “Loading more…”. |
406
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Indicator kéo để làm mới. `progress` được giới hạn từ `0` tới `1`. |
407
+
408
+ #### Events
409
+
410
+ | Event | Payload | Thời điểm emit | Cách dùng |
411
+ | --- | --- | --- | --- |
412
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi cửa sổ cuộn. `scrollTop` tính tương đối từ đầu component. `endIndex` là mốc không bao gồm item tại index đó. | `@scroll="onScroll"` |
413
+ | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. | `@load-more="loadMore"` |
414
+ | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. | `@refresh="refreshFirstPage"` |
415
+
416
+ #### API qua ref
417
+
418
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
419
+ | --- | --- | --- |
420
+ | `scrollTo(position, options?)` | Cuộn cửa sổ tới offset pixel tương đối so với đầu danh sách. | `list?.scrollTo(200, { behavior: 'smooth' })` |
421
+ | `scrollToIndex(index, options?)` | Cuộn cửa sổ tới item có index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
422
+ | `scrollToTop(behavior?)` | Cuộn cửa sổ về đầu component. | `list?.scrollToTop()` |
423
+
424
+ ### WindowGirdVirtualScroll
425
+
426
+ Dùng `WindowGirdVirtualScroll` cho grid nhiều cột có card với chiều cao cố định
427
+ đã biết và dùng cửa sổ trình duyệt làm viewport. Component virtualize theo cả
428
+ hàng để các cột đang hiển thị luôn được mount cùng nhau.
429
+
430
+ #### Ví dụ
431
+
432
+ ```vue
433
+ <script setup lang="ts">
434
+ import { WindowGirdVirtualScroll } from 'vue-virtual-flow'
435
+
436
+ const products = Array.from({ length: 10_000 }, (_, id) => ({
437
+ id,
438
+ name: `Sản phẩm ${id + 1}`,
439
+ }))
440
+ </script>
441
+
442
+ <template>
443
+ <WindowGirdVirtualScroll
444
+ :items="products"
445
+ :item-size="240"
446
+ :columns="4"
447
+ :gap="16"
448
+ item-key="id"
449
+ aria-label="Grid sản phẩm"
450
+ >
451
+ <template #default="{ item, rowIndex, columnIndex }">
452
+ <ProductCard
453
+ :product="item"
454
+ :data-position="`${rowIndex}:${columnIndex}`"
455
+ />
456
+ </template>
457
+ </WindowGirdVirtualScroll>
458
+ </template>
459
+ ```
460
+
461
+ #### Props
462
+
463
+ | Prop | Kiểu | Mặc định | Mô tả |
464
+ | --- | --- | --- | --- |
465
+ | `items` | `readonly T[]` | bắt buộc | Dữ liệu truyền vào default slot. |
466
+ | `itemSize` | `number` | bắt buộc | Chiều cao cố định của mỗi hàng grid, tính bằng pixel. Nội dung vượt quá bị ẩn. |
467
+ | `columns` | `number` | `2` | Số cột trên mỗi hàng. |
468
+ | `gap` | `number` | `0` | Khoảng cách giữa các hàng và cột, tính bằng pixel. |
469
+ | `overscan` | `number` | `1` | Số hàng mount thêm trước và sau vùng đang hiển thị. |
470
+ | `itemKey` | `keyof T \| (item, index) => PropertyKey` | `id`, `key` hoặc index | Định danh ổn định cho mỗi item. |
471
+ | `ariaLabel` | `string` | `"Window virtual grid"` | Nhãn hỗ trợ khả năng tiếp cận cho grid. |
472
+ | `hasMore` | `boolean` | `false` | Cho biết vẫn còn trang dữ liệu tiếp theo. |
473
+ | `loading` | `boolean` | `false` | Ngăn request trùng và hiển thị hàng loading. |
474
+ | `loadingItemSize` | `number` | `itemSize` | Chiều cao dành cho hàng loading, tính bằng pixel. |
475
+ | `loadMoreThreshold` | `number` | `200` | Khoảng cách từ đáy cửa sổ tới cuối grid để emit `load-more`, tính bằng pixel. |
476
+ | `pullToRefresh` | `boolean` | `false` | Bật refresh khi trang đang ở `window.scrollY = 0`. |
477
+ | `refreshing` | `boolean` | `false` | Giữ indicator mở trong lúc refresh bất đồng bộ. |
478
+ | `pullRefreshThreshold` | `number` | `64` | Khoảng kéo cần thiết để emit `refresh`, tính bằng pixel. |
479
+
480
+ #### Slots
481
+
482
+ | Slot | Scope | Mô tả |
483
+ | --- | --- | --- |
484
+ | `default` | `{ item, index, rowIndex, columnIndex }` | Render từng ô đang được mount. |
485
+ | `empty` | không có | Render khi `items` rỗng và `loading` là false. |
486
+ | `loading` | không có | Hàng loading; mặc định là “Loading more…”. |
487
+ | `refresh` | `{ pullDistance, progress, refreshing }` | Indicator kéo để làm mới. `progress` được giới hạn từ `0` tới `1`. |
488
+
489
+ #### Events
490
+
491
+ | Event | Payload | Thời điểm emit | Cách dùng |
492
+ | --- | --- | --- | --- |
493
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi cửa sổ cuộn. `scrollTop` tính tương đối từ đầu component. `endIndex` là mốc không bao gồm item tại index đó. | `@scroll="onScroll"` |
494
+ | `load-more` | không có | Gần cuối danh sách khi `hasMore` là true và `loading` là false. | `@load-more="loadMore"` |
495
+ | `refresh` | không có | Khi kéo qua ngưỡng refresh rồi thả tay. | `@refresh="refreshFirstPage"` |
496
+
497
+ #### API qua ref
357
498
 
358
- - Slots: `default({ item, index })`, `empty`, `loading` và
359
- `refresh({ pullDistance, progress, refreshing })`.
360
- - Events: `scroll({ scrollTop, startIndex, endIndex })`, `load-more` và
361
- `refresh`. `scrollTop` được tính tương đối từ đầu component.
362
- - Methods: `scrollTo`, `scrollToIndex` và `scrollToTop`. Các method này cuộn
363
- cửa sổ trình duyệt; `scrollTo` nhận offset tương đối so với đầu danh sách.
499
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
500
+ | --- | --- | --- |
501
+ | `scrollTo(position, options?)` | Cuộn cửa sổ tới offset pixel tương đối so với đầu danh sách. | `list?.scrollTo(200, { behavior: 'smooth' })` |
502
+ | `scrollToIndex(index, options?)` | Cuộn cửa sổ tới hàng chứa item có index bắt đầu từ 0. `align` nhận `start`, `center` hoặc `end`. | `list?.scrollToIndex(10, { behavior: 'smooth' })` |
503
+ | `scrollToTop(behavior?)` | Cuộn cửa sổ về đầu component. | `list?.scrollToTop()` |
364
504
 
365
505
  ### ChatVirtualScroll
366
506
 
@@ -450,21 +590,21 @@ chiều cao được thêm phía trên viewport và khôi phục vị trí của
450
590
 
451
591
  #### Events
452
592
 
453
- | Event | Payload | Thời điểm emit |
454
- | --- | --- | --- |
455
- | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. |
456
- | `load-older` | không có | Gần đầu khi `hasOlder` là true và `loadingOlder` là false. |
457
- | `bottom-change` | `boolean` | Khi viewport đi vào hoặc rời ngưỡng ở cuối. |
593
+ | Event | Payload | Thời điểm emit | Cách dùng |
594
+ | --- | --- | --- | --- |
595
+ | `scroll` | `{ scrollTop, startIndex, endIndex }` | Khi container cuộn. | `@scroll="onScroll"` |
596
+ | `load-older` | không có | Gần đầu khi `hasOlder` là true và `loadingOlder` là false. | `@load-older="loadOlder"` |
597
+ | `bottom-change` | `boolean` | Khi viewport đi vào hoặc rời ngưỡng ở cuối. | `@bottom-change="onBottomChange"` |
458
598
 
459
599
  #### API qua ref
460
600
 
461
- | Member | Mô tả |
462
- | --- | --- |
463
- | `isAtBottom` | Boolean readonly cho biết người dùng có đang gần cuối hay không. |
464
- | `scrollTo(position, options?)` | Cuộn tới offset pixel. |
465
- | `scrollToIndex(index, { align?, behavior? })` | Cuộn tới index tin nhắn bắt đầu từ 0. |
466
- | `scrollToTop(behavior?)` | Cuộn tới tin nhắn cũ nhất đã tải. |
467
- | `scrollToBottom(behavior?)` | Cuộn tới tin nhắn mới nhất đã tải. |
601
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
602
+ | --- | --- | --- |
603
+ | `isAtBottom` | Boolean readonly cho biết người dùng có đang gần cuối hay không. | `chat?.isAtBottom` |
604
+ | `scrollTo(position, options?)` | Cuộn tới offset pixel. | `chat?.scrollTo(200, { behavior: 'smooth' })` |
605
+ | `scrollToIndex(index, options?)` | Cuộn tới index tin nhắn bắt đầu từ 0. | `chat?.scrollToIndex(10, { behavior: 'smooth' })` |
606
+ | `scrollToTop(behavior?)` | Cuộn tới tin nhắn cũ nhất đã tải. | `chat?.scrollToTop()` |
607
+ | `scrollToBottom(behavior?)` | Cuộn tới tin nhắn mới nhất đã tải. | `chat?.scrollToBottom('smooth')` |
468
608
 
469
609
  ### ShortMediaFeed
470
610
 
@@ -532,22 +672,39 @@ vậy hãy lưu playback state cần duy trì ở component cha.
532
672
  | `loading` | `boolean` | `false` | Ngăn request `load-more` bị lặp. |
533
673
  | `loadMoreThreshold` | `number` | `2` | Số item còn lại để kích hoạt `load-more`. |
534
674
 
535
- #### Slots và events
675
+ #### Slots
676
+
677
+ | Slot | Scope | Mô tả |
678
+ | --- | --- | --- |
679
+ | `default` | `{ item, index, active }` | Render item đang mount; `active` đánh dấu item đang snap. |
680
+ | `empty` | không có | Render khi `items` rỗng. |
681
+
682
+ #### Events
536
683
 
537
- | Loại | Tên | Payload/scope | Mô tả |
684
+ | Event | Payload | Thời điểm emit | Cách dùng |
538
685
  | --- | --- | --- | --- |
539
- | Slot | `default` | `{ item, index, active }` | Render item đang mount; `active` đánh dấu item đang snap. |
540
- | Slot | `empty` | không có | Render khi `items` rỗng. |
541
- | Event | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. |
542
- | Event | `change` | `{ index, item }` | Khi item active thay đổi. |
543
- | Event | `reach-start` | không có | Khi điều hướng chuyển tới item đầu. |
544
- | Event | `reach-end` | không có | Khi điều hướng chuyển tới item cuối. |
545
- | Event | `load-more` | không có | Khi số item còn lại đạt `loadMoreThreshold`. |
686
+ | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. | `v-model:active-index="activeIndex"` |
687
+ | `change` | `{ index, item }` | Khi item active thay đổi. | `@change="onChange"` |
688
+ | `reach-start` | không có | Khi điều hướng chuyển tới item đầu. | `@reach-start="onReachStart"` |
689
+ | `reach-end` | không có | Khi điều hướng chuyển tới item cuối. | `@reach-end="onReachEnd"` |
690
+ | `load-more` | không có | Khi số item còn lại đạt `loadMoreThreshold`. | `@load-more="loadMore"` |
691
+
692
+ #### API qua ref
693
+
694
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
695
+ | --- | --- | --- |
696
+ | `scrollToIndex(index, options?)` | Cuộn tới item có index bắt đầu từ 0, nhận `ScrollToOptions` chuẩn. | `feed?.scrollToIndex(10, { behavior: 'smooth' })` |
546
697
 
547
- #### API qua ref và bàn phím
698
+ #### Điều khiển bàn phím
548
699
 
549
- `scrollToIndex(index, options?)` cuộn tới item bằng `ScrollToOptions` tiêu
550
- chuẩn. Viewport hỗ trợ Arrow Up/Down, Page Up/Down, Home và End.
700
+ Các phím sau hoạt động khi viewport có focus:
701
+
702
+ | Phím | Thao tác |
703
+ | --- | --- |
704
+ | `ArrowDown`, `PageDown` | Chuyển tới item tiếp theo. |
705
+ | `ArrowUp`, `PageUp` | Chuyển tới item trước. |
706
+ | `Home` | Chuyển tới vị trí đầu. |
707
+ | `End` | Chuyển tới item cuối. |
551
708
 
552
709
  Với feed không giới hạn, hãy tải theo trang bằng `load-more` và chỉ giữ một cửa
553
710
  sổ dữ liệu hợp lý ở component cha. Virtualization giới hạn DOM và media element
@@ -630,31 +787,74 @@ thường tối đa năm slide được mount ở giữa collection.
630
787
  | `autoplayDelay` | `number` | `3000` | Thời gian chờ giữa các lần tự động chuyển, tính bằng mili giây; giá trị dưới `1` được giới hạn và giá trị không hữu hạn dùng mặc định. |
631
788
  | `autoplayLoop` | `boolean` | `true` | Quay lại slide đầu tiên khi đến cuối. |
632
789
  | `pauseOnHover` | `boolean` | `true` | Tạm dừng autoplay khi con trỏ nằm trên carousel. |
790
+ | `responsive` | `ResponsiveBreakpoint[]` | `undefined` | Ghi đè số slide hiển thị, bước cuộn và khoảng cách theo breakpoint chiều rộng container. |
791
+
792
+ #### Cấu hình responsive
793
+
794
+ Component chọn `breakpoint` lớn nhất không vượt quá chiều rộng container của
795
+ carousel. Nếu không có breakpoint phù hợp, component dùng các prop gốc.
796
+ Trường bị bỏ qua dùng prop gốc, không kế thừa từ breakpoint khác.
797
+
798
+ | Trường | Kiểu | Mặc định | Mô tả |
799
+ | --- | --- | --- | --- |
800
+ | `breakpoint` | `number` | bắt buộc | Chiều rộng tối thiểu của container, tính bằng pixel. |
801
+ | `slidesPerView` | `number` | `slidesPerView` | Số slide hiển thị tại breakpoint này. |
802
+ | `slidesToScroll` | `number` | `slidesToScroll` | Số slide di chuyển bởi `next()` và `previous()`. |
803
+ | `gap` | `number` | `gap` | Khoảng cách giữa các slide, tính bằng pixel. |
804
+
805
+ ```vue
806
+ <VirtualCarousel
807
+ :items="products"
808
+ :slides-per-view="1"
809
+ :responsive="[
810
+ { breakpoint: 640, slidesPerView: 2, gap: 12 },
811
+ { breakpoint: 1024, slidesPerView: 3, slidesToScroll: 2, gap: 16 },
812
+ ]"
813
+ >
814
+ <template #default="{ item }"><ProductCard :product="item" /></template>
815
+ </VirtualCarousel>
816
+ ```
817
+
818
+ #### Slots
633
819
 
634
- #### Slots và events
820
+ | Slot | Scope | Mô tả |
821
+ | --- | --- | --- |
822
+ | `default` | `{ item, index, active, visible }` | Render từng slide đang mount. |
823
+ | `empty` | không có | Render khi `items` rỗng. |
824
+
825
+ #### Events
635
826
 
636
- | Loại | Tên | Payload/scope | Mô tả |
827
+ | Event | Payload | Thời điểm emit | Cách dùng |
637
828
  | --- | --- | --- | --- |
638
- | Slot | `default` | `{ item, index, active, visible }` | Render từng slide đang mount. |
639
- | Slot | `empty` | không có | Render khi `items` rỗng. |
640
- | Event | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. |
641
- | Event | `change` | `{ index, item }` | Khi slide đầu tiên trong vùng nhìn thay đổi. |
642
- | Event | `reach-start` | không có | Khi điều hướng chuyển tới vị trí đầu. |
643
- | Event | `reach-end` | không có | Khi điều hướng chuyển tới vị trí đầu-slide hợp lệ cuối cùng. |
644
- | Event | `load-more` | không có | Khi số slide còn lại đạt `loadMoreThreshold`. |
829
+ | `update:activeIndex` | `number` | Hỗ trợ `v-model:active-index`. | `v-model:active-index="activeIndex"` |
830
+ | `change` | `{ index, item }` | Khi slide đầu tiên trong vùng nhìn thay đổi. | `@change="onChange"` |
831
+ | `reach-start` | không có | Khi điều hướng chuyển tới vị trí đầu. | `@reach-start="onReachStart"` |
832
+ | `reach-end` | không có | Khi điều hướng chuyển tới vị trí đầu-slide hợp lệ cuối cùng. | `@reach-end="onReachEnd"` |
833
+ | `load-more` | không có | Khi số slide còn lại đạt `loadMoreThreshold`. | `@load-more="loadMore"` |
834
+
835
+ #### API qua ref
645
836
 
646
- #### API qua ref và bàn phím
837
+ | Method / trạng thái | Mô tả | Ví dụ trong template |
838
+ | --- | --- | --- |
839
+ | `next(behavior?)` | Tiến thêm `slidesToScroll` slide; mặc định cuộn mượt. | `carousel?.next()` |
840
+ | `previous(behavior?)` | Lùi lại `slidesToScroll` slide. | `carousel?.previous()` |
841
+ | `scrollToIndex(index, options?)` | Đưa index thành slide đầu tiên trong vùng nhìn. | `carousel?.scrollToIndex(10, { behavior: 'smooth' })` |
842
+ | `startAutoplay()` | Bắt đầu hoặc khởi động lại timer khi `autoplay` là true và có item. | `carousel?.startAutoplay()` |
843
+ | `stopAutoplay()` | Xóa timer hiện tại; đặt `autoplay=false` nếu muốn giữ chế độ tự động ở trạng thái tắt. | `carousel?.stopAutoplay()` |
844
+
845
+ #### Điều khiển bàn phím
846
+
847
+ Các phím sau hoạt động khi viewport có focus:
647
848
 
648
- | Method | Mô tả |
849
+ | Phím | Thao tác |
649
850
  | --- | --- |
650
- | `next(behavior?)` | Tiến thêm `slidesToScroll` slide; mặc định cuộn mượt. |
651
- | `previous(behavior?)` | Lùi lại `slidesToScroll` slide. |
652
- | `scrollToIndex(index, options?)` | Đưa index thành slide đầu tiên trong vùng nhìn. |
653
- | `startAutoplay()` | Bắt đầu hoặc khởi động lại quá trình tự động chuyển slide. |
654
- | `stopAutoplay()` | Dừng quá trình tự động chuyển slide. |
851
+ | `ArrowRight`, `PageDown` | Tiến thêm `slidesToScroll` slide, tương đương `next()`. |
852
+ | `ArrowLeft`, `PageUp` | Lùi lại `slidesToScroll` slide, tương đương `previous()`. |
853
+ | `Home` | Chuyển tới vị trí đầu. |
854
+ | `End` | Chuyển tới vị trí đầu-slide hợp lệ cuối cùng. |
655
855
 
656
- Viewport hỗ trợ Arrow Left/Right, Page Up/Down, Home và End. Smooth scroll ở
657
- khoảng cách xa tự chuyển thành tức thời để không lộ khoảng trống virtualized.
856
+ Smooth scroll ở khoảng cách xa tự chuyển thành tức thời để không lộ khoảng
857
+ trống virtualized.
658
858
 
659
859
  #### Ví dụ autoplay
660
860
 
@@ -687,18 +887,134 @@ const products = ref(loadProducts())
687
887
  </template>
688
888
  ```
689
889
 
690
- Autoplay cũng tự bắt đầu khi collection `items` ban đầu rỗng nhận được dữ liệu.
691
- `stopAutoplay()` tạm dừng timer và `startAutoplay()` tiếp tục chạy khi prop
692
- `autoplay` vẫn được bật. Các thay đổi của `autoplayDelay`, `autoplayLoop` và
693
- `pauseOnHover` có hiệu lực ngay trong lúc component đang chạy.
890
+ | Thao tác / điều kiện | Kết quả |
891
+ | --- | --- |
892
+ | `autoplay=true` | Tự chạy, kể cả khi collection `items` ban đầu rỗng nhận được dữ liệu. |
893
+ | `stopAutoplay()` | Xóa timer hiện tại. Điều hướng hoặc sự kiện con trỏ sau đó có thể khởi động lại nếu `autoplay` vẫn là true. |
894
+ | `startAutoplay()` | Bắt đầu hoặc khởi động lại timer khi `autoplay` là true và có item. |
895
+ | `autoplay=false` | Tắt tự động chuyển slide cho tới khi prop được bật lại. |
896
+ | `autoplayLoop` | Khi tới cuối, true quay về đầu; false dừng timer. |
897
+ | `pauseOnHover` | Khi là true, rê chuột vào sẽ tạm dừng; rời chuột sẽ chạy tiếp nếu autoplay đang bật. |
898
+ | Đổi cấu hình autoplay | `autoplayDelay`, `autoplayLoop` và `pauseOnHover` có hiệu lực ngay khi component đang chạy. |
899
+
900
+ ### SkeletonLoader
901
+
902
+ `SkeletonLoader` hiển thị khối giữ chỗ với hiệu ứng shimmer. Có thể dùng độc lập
903
+ hoặc trong slot `loading` của danh sách. Component luôn hiển thị khi được mount;
904
+ dùng `v-if` để điều khiển khi dùng độc lập, hoặc để prop `loading` của danh sách
905
+ điều khiển slot.
906
+
907
+ #### Ví dụ
908
+
909
+ Import stylesheet của thư viện một lần như bên dưới hoặc tại entry point của
910
+ ứng dụng. Đặt `loading` thành false khi request dữ liệu hoàn tất.
911
+
912
+ ```vue
913
+ <script setup lang="ts">
914
+ import { ref } from 'vue'
915
+ import { SkeletonLoader } from 'vue-virtual-flow'
916
+ import 'vue-virtual-flow/style.css'
917
+
918
+ const loading = ref(true)
919
+ </script>
920
+
921
+ <template>
922
+ <SkeletonLoader
923
+ v-if="loading"
924
+ width="100%"
925
+ :height="120"
926
+ aria-label="Đang tải nội dung…"
927
+ />
928
+ <p v-else>Nội dung đã sẵn sàng.</p>
929
+ </template>
930
+ ```
931
+
932
+ #### Props
933
+
934
+ | Prop | Kiểu | Mặc định | Mô tả |
935
+ | --- | --- | --- | --- |
936
+ | `width` | `number \| string` | `'100%'` | Chiều rộng: số tính bằng pixel, chuỗi nhận độ dài CSS như `'60%'` hoặc `'12rem'`. |
937
+ | `height` | `number \| string` | `16` | Chiều cao: số tính bằng pixel, chuỗi nhận độ dài CSS. |
938
+ | `variant` | `'rectangle' \| 'rounded' \| 'circle'` | `'rounded'` | Hình dạng khối giữ chỗ. Đặt chiều rộng bằng chiều cao để tạo hình tròn. |
939
+ | `animated` | `boolean` | `true` | Bật hiệu ứng shimmer. |
940
+ | `ariaLabel` | `string` | `'Loading…'` | Nhãn cho trình đọc màn hình của khối có `role="status"`. |
941
+
942
+ ```vue
943
+ <SkeletonLoader variant="circle" :width="48" :height="48" />
944
+ <SkeletonLoader variant="rectangle" :height="180" />
945
+ <SkeletonLoader width="60%" :height="20" :animated="false" />
946
+ ```
947
+
948
+ Hiệu ứng tự dừng khi người dùng bật tùy chọn giảm chuyển động. Khi ghép nhiều
949
+ skeleton trang trí cùng một thông báo loading chung, truyền `aria-hidden="true"`
950
+ cho từng skeleton để tránh lặp nhãn với trình đọc màn hình.
951
+
952
+ #### Slots, events và API qua ref
953
+
954
+ | API | Hỗ trợ |
955
+ | --- | --- |
956
+ | Slots | Không có. Component render một khối giữ chỗ. |
957
+ | Events | Không có event tùy chỉnh. |
958
+ | Method qua ref | Không có. Điều khiển component bằng props và `v-if`. |
959
+
960
+ #### Thao tác
961
+
962
+ | Thao tác | Cách dùng |
963
+ | --- | --- |
964
+ | Hiện hoặc ẩn | `<SkeletonLoader v-if="loading" />` |
965
+ | Dùng hình tròn | `<SkeletonLoader variant="circle" :width="48" :height="48" />` |
966
+ | Tắt shimmer | `<SkeletonLoader :animated="false" />` |
967
+ | Đặt nhãn cho trình đọc màn hình | `aria-label="Đang tải…"` |
968
+ | Ẩn skeleton trang trí khỏi trình đọc màn hình | `aria-hidden="true"` |
969
+
970
+ #### Dùng trong slot loading
971
+
972
+ Import `SkeletonLoader` cùng với `VirtualList`. Với state `items` và `loading`
973
+ hiện có, thay thông báo loading bằng skeleton:
974
+
975
+ ```vue
976
+ <VirtualList :items="items" :loading="loading" :loading-item-size="64">
977
+ <template #default="{ item }"><FeedCard :item="item" /></template>
978
+ <template #loading>
979
+ <SkeletonLoader :height="40" aria-label="Đang tải trang tiếp theo…" />
980
+ </template>
981
+ </VirtualList>
982
+ ```
983
+
984
+ | Cấu hình | Cách dùng |
985
+ | --- | --- |
986
+ | Component hỗ trợ slot `loading` | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`, `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll`. |
987
+ | `loading` trên danh sách | Đặt thành true để hiển thị slot; false để ẩn. |
988
+ | `height` trên skeleton | Đặt chiều cao khối giữ chỗ, ví dụ `40` pixel. |
989
+ | `loadingItemSize` trên danh sách | Dành đủ chỗ cho skeleton và padding dọc của hàng. Ví dụ: `40px + 1.5rem = 64px` với cỡ chữ gốc 16px. |
990
+
991
+ #### Tùy chỉnh giao diện
992
+
993
+ Đặt các biến CSS sau trên skeleton hoặc phần tử cha:
994
+
995
+ | Biến CSS | Mặc định | Mô tả |
996
+ | --- | --- | --- |
997
+ | `--skeleton-background` | `#e2e8f0` | Màu nền. |
998
+ | `--skeleton-highlight` | `rgb(255 255 255 / 60%)` | Màu vệt sáng của hiệu ứng shimmer. |
999
+ | `--skeleton-radius` | `8px` | Bán kính bo góc cho variant `rounded`. |
1000
+
1001
+ ```vue
1002
+ <SkeletonLoader
1003
+ :height="80"
1004
+ style="--skeleton-background: #334155; --skeleton-highlight: #475569; --skeleton-radius: 12px"
1005
+ />
1006
+ ```
694
1007
 
695
1008
  ## Cách dùng chung
696
1009
 
697
1010
  ### Chiều cao viewport
698
1011
 
699
- Các component dùng container nhận một số hoặc chuỗi chiều cao CSS. Giá trị số
700
- được đổi thành pixel. `height="fill"` là cách viết ngắn của
701
- `height="100%"`:
1012
+ | Giá trị / component | Cách hoạt động |
1013
+ | --- | --- |
1014
+ | `:height="400"` | Giá trị số được đổi thành pixel. |
1015
+ | `height="60vh"` | Chuỗi chiều cao CSS được giữ nguyên. |
1016
+ | `height="fill"` / `height="100%"` | Dùng chiều cao phần tử cha. Thẻ cha cần có chiều cao xác định; bản development cảnh báo nếu viewport tính thành `0px`. |
1017
+ | `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | Dùng cửa sổ trình duyệt và không nhận prop `height`. |
702
1018
 
703
1019
  ```vue
704
1020
  <div class="list-container">
@@ -715,15 +1031,14 @@ Các component dùng container nhận một số hoặc chuỗi chiều cao CSS.
715
1031
  }
716
1032
  ```
717
1033
 
718
- Chiều cao phần trăm chỉ hoạt động khi thẻ cha có chiều cao xác định. Bản
719
- development cảnh báo nếu viewport được tính thành `0px`.
720
- `WindowDynamicVirtualScroll` dùng cửa sổ trình duyệt nên không có prop
721
- `height`.
722
-
723
1034
  ### Key ổn định cho item
724
1035
 
725
- Với object, component tự thử `item.id` rồi tới `item.key`. Item nguyên thủy dùng
726
- index làm fallback. Hãy truyền `itemKey` nếu định danh có tên khác:
1036
+ | Cấu hình / dữ liệu | Cách chọn key |
1037
+ | --- | --- |
1038
+ | `:item-key="(item, index) => …"` | Dùng giá trị hàm trả về. |
1039
+ | `item-key="uuid"` | Dùng `item.uuid` hợp lệ; nếu không có, thử `id`, `key`, rồi index. |
1040
+ | Object không truyền `itemKey` | Thử `item.id`, sau đó `item.key`, rồi index. |
1041
+ | Giá trị nguyên thủy không có hàm key | Dùng index làm fallback. |
727
1042
 
728
1043
  ```vue
729
1044
  <VirtualList :items="users" item-key="uuid" />
@@ -739,8 +1054,11 @@ lại dữ liệu. Key cũng gắn kết quả đo chiều cao với đúng item
739
1054
 
740
1055
  ### Tải thêm dữ liệu
741
1056
 
742
- Các component dạng list dùng ngưỡng khoảng cách pixel. `ShortMediaFeed` và
743
- `VirtualCarousel` dùng ngưỡng số item còn lại.
1057
+ | Component | `loadMoreThreshold` | Event |
1058
+ | --- | --- | --- |
1059
+ | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`, `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | Khoảng cách tới cuối danh sách, tính bằng pixel. | `load-more` |
1060
+ | `ShortMediaFeed` | Số item phía sau item active. | `load-more` |
1061
+ | `VirtualCarousel` | Số slide phía sau nhóm đang hiển thị. | `load-more` |
744
1062
 
745
1063
  ```vue
746
1064
  <script setup lang="ts">
@@ -772,14 +1090,20 @@ async function loadMore() {
772
1090
  </template>
773
1091
  ```
774
1092
 
775
- Event chỉ emit tối đa một lần với số lượng item hiện tại. Nối thêm item sẽ cho
776
- phép request tiếp theo. Giữ `loading` là true trong toàn bộ request và đặt
777
- `hasMore` thành false sau trang cuối.
1093
+ | Bước / điều kiện | Thao tác / kết quả |
1094
+ | --- | --- |
1095
+ | Nhận `load-more` | Chỉ bắt đầu request tiếp theo khi `loading` là false và `hasMore` là true. |
1096
+ | Request đang chạy | Giữ `loading=true` trong toàn bộ request. |
1097
+ | Nhận trang tiếp theo | Nối thêm item. Event thông thường chỉ emit một lần với số lượng item hiện tại; tăng số lượng cho phép request tiếp theo. |
1098
+ | Request hoàn tất | Đặt `loading=false`; dùng `finally` khi request có thể thất bại. |
1099
+ | Trang cuối | Đặt `hasMore=false` để ngừng request tiếp theo. |
778
1100
 
779
1101
  ### Kéo để làm mới
780
1102
 
781
- Pull-to-refresh có trên `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll`
782
- và `WindowDynamicVirtualScroll`:
1103
+ | Component | Vị trí bắt đầu thao tác |
1104
+ | --- | --- |
1105
+ | `VirtualList`, `DynamicVirtualScroll`, `VirtualScroll` | Đầu container của component. |
1106
+ | `WindowDynamicVirtualScroll`, `WindowGirdVirtualScroll` | `window.scrollY = 0` |
783
1107
 
784
1108
  ```vue
785
1109
  <VirtualList
@@ -796,21 +1120,51 @@ và `WindowDynamicVirtualScroll`:
796
1120
  </VirtualList>
797
1121
  ```
798
1122
 
799
- Thao tác một ngón chỉ bắt đầu khi đang ở đầu danh sách. Đặt `refreshing` thành
800
- true trong lúc chạy refresh bất đồng bộ, sau đó trả về false để đóng indicator.
1123
+ | Bước / trạng thái | Thao tác / kết quả |
1124
+ | --- | --- |
1125
+ | Bật thao tác | `pull-to-refresh` |
1126
+ | Kéo xuống bằng một ngón khi đang ở đầu | Cập nhật scope của slot `refresh`: `pullDistance`, `progress` và `refreshing`. |
1127
+ | Thả sau khi đạt `pullRefreshThreshold` | Emit `refresh` không có payload. |
1128
+ | Xử lý `refresh` | Đặt `refreshing=true` trong lúc tải lại dữ liệu để giữ indicator mở. |
1129
+ | Hoàn tất làm mới | Đặt `refreshing=false` để đóng indicator. |
801
1130
 
802
1131
  ## Các type được export
803
1132
 
1133
+ | Type | Mục đích |
1134
+ | --- | --- |
1135
+ | `ChatVirtualScrollExpose` | API ref và trạng thái `isAtBottom` của chat. |
1136
+ | `ChatVirtualScrollProps<T>` | Props của component chat. |
1137
+ | `DynamicVirtualScrollProps<T>` | Props của danh sách chiều cao động. |
1138
+ | `ItemKey<T>` | Tên thuộc tính hoặc hàm xác định key của item. |
1139
+ | `ResponsiveBreakpoint` | Breakpoint container và cấu hình ghi đè của carousel. |
1140
+ | `ScrollAlignment` | `'start'`, `'center'` hoặc `'end'` khi cuộn tới index trong danh sách. |
1141
+ | `ShortMediaFeedChangeEvent<T>` | Payload event `change` của feed: `{ index, item }`. |
1142
+ | `ShortMediaFeedExpose` | API ref của feed. |
1143
+ | `ShortMediaFeedProps<T>` | Props của feed. |
1144
+ | `SkeletonLoaderProps` | Kích thước, hình dạng, hiệu ứng và nhãn hỗ trợ tiếp cận của skeleton. |
1145
+ | `VirtualCarouselChangeEvent<T>` | Payload event `change` của carousel: `{ index, item }`. |
1146
+ | `VirtualCarouselExpose` | API ref điều hướng và autoplay của carousel. |
1147
+ | `VirtualCarouselProps<T>` | Props của carousel, gồm cấu hình responsive. |
1148
+ | `VirtualListExpose` | Alias của `VirtualScrollExpose`. |
1149
+ | `VirtualListProps<T>` | Alias của `DynamicVirtualScrollProps<T>`. |
1150
+ | `VirtualScrollEvent` | Payload scroll: `{ scrollTop, startIndex, endIndex }`; `endIndex` không bao gồm item tại index đó. |
1151
+ | `VirtualScrollExpose` | API ref chung cho danh sách cố định, động và danh sách/grid cuộn theo cửa sổ. |
1152
+ | `VirtualScrollProps<T>` | Props của danh sách chiều cao cố định. |
1153
+ | `WindowDynamicVirtualScrollProps<T>` | Props của danh sách chiều cao động cuộn theo cửa sổ. |
1154
+ | `WindowGirdVirtualScrollProps<T>` | Props của grid hàng cố định cuộn theo cửa sổ. |
1155
+
804
1156
  ```ts
805
1157
  import type {
806
1158
  ChatVirtualScrollExpose,
807
1159
  ChatVirtualScrollProps,
808
1160
  DynamicVirtualScrollProps,
809
1161
  ItemKey,
1162
+ ResponsiveBreakpoint,
810
1163
  ScrollAlignment,
811
1164
  ShortMediaFeedChangeEvent,
812
1165
  ShortMediaFeedExpose,
813
1166
  ShortMediaFeedProps,
1167
+ SkeletonLoaderProps,
814
1168
  VirtualCarouselChangeEvent,
815
1169
  VirtualCarouselExpose,
816
1170
  VirtualCarouselProps,
@@ -820,11 +1174,28 @@ import type {
820
1174
  VirtualScrollExpose,
821
1175
  VirtualScrollProps,
822
1176
  WindowDynamicVirtualScrollProps,
1177
+ WindowGirdVirtualScrollProps,
823
1178
  } from 'vue-virtual-flow'
824
1179
  ```
825
1180
 
826
1181
  ## Phát triển và phát hành
827
1182
 
1183
+ | Lệnh / lifecycle | Thao tác |
1184
+ | --- | --- |
1185
+ | `bun install` | Cài đặt dependency. |
1186
+ | `bunx playwright install chromium firefox webkit` | Cài trình duyệt cho smoke test. |
1187
+ | `bun run dev` | Chạy playground. |
1188
+ | `bun run typecheck` | Kiểm tra type TypeScript và Vue. |
1189
+ | `bun run test` | Chạy unit test. |
1190
+ | `bun run test:watch` | Chạy unit test ở chế độ theo dõi thay đổi. |
1191
+ | `bun run build` | Kiểm tra type và build package kèm khai báo type. |
1192
+ | `bun run typecheck:package` | Kiểm tra type phía dùng package ESM và CommonJS sau khi build. |
1193
+ | `bun run lint:package` | Kiểm tra exports và cách đóng gói type sau khi build. |
1194
+ | `bun run test:browser` | Chạy test trình duyệt bằng Playwright. |
1195
+ | `bun run check` | Chạy toàn bộ: unit test, build, type phía dùng package, lint package và test trình duyệt. |
1196
+ | `npm pack --dry-run` | Kiểm tra các file sẽ được publish. |
1197
+ | `prepublishOnly` | Tự chạy `bun run check` trước khi publish. |
1198
+
828
1199
  ```bash
829
1200
  bun install
830
1201
  bunx playwright install chromium firefox webkit