@libs-ui/components-tabs 0.2.356-9 → 0.2.357-1

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,150 +1,524 @@
1
1
  # @libs-ui/components-tabs
2
2
 
3
- > Library Angular Component hiển thị Tabs, hỗ trợ Signals, Drag & Drop, Responsive "More" Menu và nhiều modes hiển thị.
3
+ > Angular Standalone Component hiển thị thanh Tabs với hỗ trợ đầy đủ Angular Signals, Responsive "More" Menu tự động, Drag & Drop và nhiều chế độ căn chỉnh.
4
4
 
5
5
  ## Giới thiệu
6
6
 
7
- `LibsUiComponentsTabsComponent` là một standalone Angular component để hiển thị danh sách các tabs. Nó được thiết kế để làm việc với Angular Signals, hỗ trợ nested reactivity cho từng tab item. Component này tự động xử responsive bằng cách nhóm các tabs không vừa vào menu "More", hỗ trợ kéo thả để sắp xếp, và cung cấp nhiều chế độ hiển thị khác nhau.
7
+ `LibsUiComponentsTabsComponent` là một standalone Angular component được xây dựng hoàn toàn trên Angular Signals. Component tự động tính toán ẩn các tab không vừa chiều ngang vào menu "More" (chế độ `left`), hỗ trợ kéo thả để sắp xếp lại thứ tự, cho phép kiểm soát chuyển tab bằng callback bất đồng bộ, và cung cấp FunctionsControl để điều khiển từ component cha qua `viewChild`.
8
8
 
9
- ### Tính năng
9
+ ## Tính năng
10
10
 
11
- - ✅ **Angular Signals**: Full support cho reactive updates.
12
- - ✅ **Responsive "More" Menu**: Tự động tính toán gom gọn các tabs không hiển thị hết (Mode 'left').
13
- - ✅ **Drag & Drop**: Hỗ trợ kéo thả để thay đổi vị trí tab.
14
- - ✅ **Multiple Modes**: 'left', 'center', 'space-between', 'center-has-line'.
15
- - ✅ **Rich Content**: Hỗ trợ Icon, Badge/Count, Close button trên từng tab.
16
- - ✅ **OnPush Change Detection**: Tối ưu hiệu năng.
11
+ - ✅ **Angular Signals**: Full support `items` là `WritableSignal<Array<WritableSignal<ITabsItem>>>`, phản ứng ngay khi signal bên trong thay đổi.
12
+ - ✅ **Responsive "More" Menu**: Chế độ `left` tự động ẩn các tab thừa vào popover "Xem thêm" khi container hẹp.
13
+ - ✅ **Calculator V2**: Thuật toán tính chiều rộng thế hệ mới dùng `ResizeObserver`, không flicker, không reorder DOM.
14
+ - ✅ **Drag & Drop**: Hỗ trợ kéo thả để thay đổi vị trí tab (tích hợp `@libs-ui/components-drag-drop`).
15
+ - ✅ **4 chế độ hiển thị**: `left`, `center`, `space-between`, `center-has-line`.
16
+ - ✅ **Rich Content**: Mỗi tab item hỗ trợ icon trái/phải, badge số lượng, red dot, ảnh avatar, nút action trái/phải.
17
+ - ✅ **Guard chuyển tab**: Input `checkCanChangeTabSelected` cho phép chặn hoặc xác nhận trước khi đổi tab (hỗ trợ async).
18
+ - ✅ **FunctionsControl**: Expose API `addTabsItem`, `selectedTabsItem`, `calculatorTabsItemsDisplay` ra ngoài qua `outFunctionsControl`.
19
+ - ✅ **OnPush Change Detection**: Tối ưu hiệu năng cho danh sách tab lớn.
17
20
 
18
21
  ## Khi nào sử dụng
19
22
 
20
- - Khi cần phân chia nội dung thành các modules hoặc views khác nhau.
21
- - Khi cần hiển thị danh sách các mục thể đóng mở (như trình duyệt web).
22
- - Khi không gian ngang bị giới hạn (sử dụng tính năng "More" menu tự động).
23
- - Khi cần chức năng kéo thả sắp xếp tabs.
23
+ - Phân chia nội dung thành nhiều view/module trong cùng một màn hình (trang chi tiết, dashboard).
24
+ - Thanh điều hướng ngang với số tab không cố định tính năng "More" sẽ gom phần thừa tự động.
25
+ - Tab thể thêm/xóa động (browser-like tabs) dùng `FunctionsControl.addTabsItem`.
26
+ - Cần quy trình dạng bước (step wizard) dùng `mode="center-has-line"` kết hợp `hasStep`.
27
+ - Danh sách tab cần sắp xếp lại bằng kéo thả.
24
28
 
25
29
  ## Cài đặt
26
30
 
27
31
  ```bash
28
- # npm
29
32
  npm install @libs-ui/components-tabs
30
-
31
- # yarn
32
- yarn add @libs-ui/components-tabs
33
33
  ```
34
34
 
35
35
  ## Import
36
36
 
37
37
  ```typescript
38
38
  import { LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';
39
+ import { ITabs, ITabsItem, ITabsFunctionControlEvent, ITabsItemEvent, ITabCssConfig, TYPE_TAB_MODE } from '@libs-ui/components-tabs';
39
40
 
40
41
  @Component({
41
42
  standalone: true,
42
43
  imports: [LibsUiComponentsTabsComponent],
43
- // ...
44
44
  })
45
45
  export class YourComponent {}
46
46
  ```
47
47
 
48
- ## Ví dụ
48
+ ## Ví dụ sử dụng
49
+
50
+ ### Ví dụ 1 — Basic (tabs căn trái, responsive tự động)
51
+
52
+ ```typescript
53
+ import { signal, WritableSignal } from '@angular/core';
54
+ import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';
55
+
56
+ @Component({
57
+ standalone: true,
58
+ changeDetection: ChangeDetectionStrategy.OnPush,
59
+ imports: [LibsUiComponentsTabsComponent],
60
+ template: `
61
+ <libs_ui-components-tabs
62
+ [tabs]="tabsConfig"
63
+ [(keySelected)]="selectedKey"
64
+ (outKeySelected)="handlerKeySelected($event)"
65
+ />
66
+ <p>Tab đang chọn: {{ selectedKey() }}</p>
67
+ `,
68
+ })
69
+ export class BasicTabsComponent {
70
+ protected selectedKey = signal<string>('overview');
71
+
72
+ protected tabsConfig: ITabs = {
73
+ items: signal<Array<WritableSignal<ITabsItem>>>([
74
+ signal({ key: 'overview', label: 'Tổng quan' }),
75
+ signal({ key: 'detail', label: 'Chi tiết' }),
76
+ signal({ key: 'history', label: 'Lịch sử' }),
77
+ signal({ key: 'setting', label: 'Cài đặt', disable: true }),
78
+ ]),
79
+ };
80
+
81
+ protected handlerKeySelected(key: string): void {
82
+ // event.stopPropagation() không cần ở đây vì outKeySelected là OutputEmitterRef
83
+ console.log('Tab đã chọn:', key);
84
+ }
85
+ }
86
+ ```
87
+
88
+ ### Ví dụ 2 — Center mode với icon và badge
89
+
90
+ ```typescript
91
+ import { signal, WritableSignal } from '@angular/core';
92
+ import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';
93
+
94
+ @Component({
95
+ standalone: true,
96
+ changeDetection: ChangeDetectionStrategy.OnPush,
97
+ imports: [LibsUiComponentsTabsComponent],
98
+ template: `
99
+ <libs_ui-components-tabs
100
+ [tabs]="tabsConfig"
101
+ [(keySelected)]="selectedKey"
102
+ mode="center"
103
+ />
104
+ `,
105
+ })
106
+ export class CenterTabsComponent {
107
+ protected selectedKey = signal<string>('messages');
108
+
109
+ protected tabsConfig: ITabs = {
110
+ hasCount: true,
111
+ items: signal<Array<WritableSignal<ITabsItem>>>([
112
+ signal({
113
+ key: 'messages',
114
+ label: 'Tin nhắn',
115
+ iconLeft: 'libs-ui-icon-mail',
116
+ count: 5,
117
+ classCircle: 'bg-red-500 text-white',
118
+ }),
119
+ signal({
120
+ key: 'notifications',
121
+ label: 'Thông báo',
122
+ iconLeft: 'libs-ui-icon-bell',
123
+ count: 99,
124
+ modeCount: 'x+',
125
+ maxCount: 9,
126
+ classCircle: 'bg-blue-500 text-white',
127
+ }),
128
+ signal({
129
+ key: 'profile',
130
+ label: 'Hồ sơ',
131
+ iconRight: 'libs-ui-icon-user',
132
+ hasRedDot: true,
133
+ }),
134
+ ]),
135
+ };
136
+ }
137
+ ```
138
+
139
+ ### Ví dụ 3 — Drag & Drop với Calculator V2
49
140
 
50
- ### Basic
141
+ ```typescript
142
+ import { signal, WritableSignal } from '@angular/core';
143
+ import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';
51
144
 
52
- ```html
53
- <libs_ui-components-tabs
54
- [tabs]="tabsConfig"
55
- [(keySelected)]="selectedKey" />
145
+ @Component({
146
+ standalone: true,
147
+ changeDetection: ChangeDetectionStrategy.OnPush,
148
+ imports: [LibsUiComponentsTabsComponent],
149
+ template: `
150
+ <libs_ui-components-tabs
151
+ [tabs]="tabsConfig"
152
+ [(keySelected)]="selectedKey"
153
+ [allowDragDropPosition]="true"
154
+ [useCalculatorV2]="true"
155
+ (outDragTabChange)="handlerDragChange()"
156
+ />
157
+ `,
158
+ })
159
+ export class DragTabsComponent {
160
+ protected selectedKey = signal<string>('tab1');
161
+
162
+ protected tabsConfig: ITabs = {
163
+ items: signal<Array<WritableSignal<ITabsItem>>>([
164
+ signal({ key: 'tab1', label: 'Mục 1' }),
165
+ signal({ key: 'tab2', label: 'Mục 2' }),
166
+ signal({ key: 'tab3', label: 'Mục 3' }),
167
+ signal({ key: 'tab4', label: 'Mục 4' }),
168
+ ]),
169
+ };
170
+
171
+ protected handlerDragChange(): void {
172
+ console.log('Thứ tự tab đã thay đổi');
173
+ }
174
+ }
56
175
  ```
57
176
 
177
+ ### Ví dụ 4 — Guard chuyển tab (async confirmation)
178
+
58
179
  ```typescript
59
- // Trong component class
60
180
  import { signal, WritableSignal } from '@angular/core';
61
- import { ITabs, ITabsItem } from '@libs-ui/components-tabs';
181
+ import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';
182
+
183
+ @Component({
184
+ standalone: true,
185
+ changeDetection: ChangeDetectionStrategy.OnPush,
186
+ imports: [LibsUiComponentsTabsComponent],
187
+ template: `
188
+ <libs_ui-components-tabs
189
+ [tabs]="tabsConfig"
190
+ [(keySelected)]="selectedKey"
191
+ [checkCanChangeTabSelected]="checkCanChange"
192
+ />
193
+ `,
194
+ })
195
+ export class GuardedTabsComponent {
196
+ protected selectedKey = signal<string>('tab1');
197
+ protected hasUnsavedChanges = signal<boolean>(true);
198
+
199
+ protected tabsConfig: ITabs = {
200
+ items: signal<Array<WritableSignal<ITabsItem>>>([
201
+ signal({ key: 'tab1', label: 'Form nhập liệu' }),
202
+ signal({ key: 'tab2', label: 'Xem trước' }),
203
+ ]),
204
+ };
205
+
206
+ protected checkCanChange = async (): Promise<boolean> => {
207
+ if (!this.hasUnsavedChanges()) return true;
208
+ return confirm('Bạn có thay đổi chưa lưu. Tiếp tục chuyển tab?');
209
+ };
210
+ }
211
+ ```
62
212
 
63
- // 1. Tạo các items dưới dạng signal
64
- const item1 = signal<ITabsItem>({ key: 'tab1', label: 'Tab 1' });
65
- const item2 = signal<ITabsItem>({ key: 'tab2', label: 'Tab 2' });
213
+ ### dụ 5 FunctionsControl để thêm tab động
66
214
 
67
- // 2. Tạo config tabs
68
- protected tabsConfig: ITabs = {
69
- items: signal<Array<WritableSignal<ITabsItem>>>([item1, item2])
70
- };
215
+ ```typescript
216
+ import { signal, WritableSignal } from '@angular/core';
217
+ import { ITabs, ITabsItem, ITabsFunctionControlEvent, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';
71
218
 
72
- protected selectedKey = signal<string>('tab1');
219
+ @Component({
220
+ standalone: true,
221
+ changeDetection: ChangeDetectionStrategy.OnPush,
222
+ imports: [LibsUiComponentsTabsComponent],
223
+ template: `
224
+ <libs_ui-components-tabs
225
+ [tabs]="tabsConfig"
226
+ [(keySelected)]="selectedKey"
227
+ [useEffectUpdateItems]="true"
228
+ (outFunctionsControl)="handlerFunctionsControl($event)"
229
+ />
230
+ <button (click)="addNewTab()">Thêm tab</button>
231
+ `,
232
+ })
233
+ export class DynamicTabsComponent {
234
+ protected selectedKey = signal<string>('tab1');
235
+ private functionsControl: ITabsFunctionControlEvent | undefined;
236
+
237
+ protected tabsConfig: ITabs = {
238
+ allowRemove: true,
239
+ items: signal<Array<WritableSignal<ITabsItem>>>([
240
+ signal({ key: 'tab1', label: 'Tab đầu tiên' }),
241
+ ]),
242
+ };
243
+
244
+ protected handlerFunctionsControl(fc: ITabsFunctionControlEvent): void {
245
+ this.functionsControl = fc;
246
+ }
247
+
248
+ protected addNewTab(): void {
249
+ const key = `tab_${Date.now()}`;
250
+ const newItem = signal<ITabsItem>({ key, label: `Tab ${key.slice(-4)}` });
251
+ this.functionsControl?.addTabsItem(newItem, true);
252
+ }
253
+ }
73
254
  ```
74
255
 
75
- ## API
76
-
77
- ### libs_ui-components-tabs
78
-
79
- #### Inputs
80
-
81
- | Property | Type | Default | Description |
82
- | ----------------------------- | ------------------------------------------------------------ | ------------ | ------------------------------------------------------- |
83
- | `[tabs]` | `ITabs` | **Required** | Cấu hình chính cho tabs, chứa danh sách items (Signal). |
84
- | `[(keySelected)]` | `string` (Model) | **Required** | Key của tab đang được chọn (Two-way binding). |
85
- | `[mode]` | `'left' \| 'center' \| 'space-between' \| 'center-has-line'` | `'left'` | Chế độ hiển thị của tabs. |
86
- | `[fieldKey]` | `string` | `'key'` | Tên trường dùng làm key định danh trong item object. |
87
- | `[fieldLabel]` | `string` | `'label'` | Tên trường hiển thị label trong item object. |
88
- | `[disable]` | `boolean` | `false` | Vô hiệu hóa toàn bộ tabs. |
89
- | `[heightTabItem]` | `number` | `40` | Chiều cao của tab bar (px). |
90
- | `[allowDragDropPosition]` | `boolean` | `false` | Cho phép kéo thả vị trí các tab. |
91
- | `[size]` | `'langer' \| 'medium'` | `'medium'` | Kích thước tab. |
92
- | `[disableLabel]` | `boolean` | `false` | Vô hiệu hóa label. |
93
- | `[ignoreCalculatorTab]` | `boolean` | `false` | Bỏ qua việc tính toán hiển thị responsive (menu More). |
94
- | `[zIndex]` | `number` | `undefined` | Z-index cho component. |
95
- | `[configCss]` | `ITabCssConfig` (Model) | `undefined` | Cấu hình CSS tùy chỉnh cho các mode. |
96
- | `[popoverShowMoreTabItem]` | `IPopover` | `undefined` | Cấu hình popover cho menu "Xem thêm". |
97
- | `[checkCanChangeTabSelected]` | `() => boolean \| Promise<boolean>` | `undefined` | Callback kiểm tra trước khi chuyển tab. |
98
-
99
- #### Outputs
100
-
101
- | Property | Type | Description |
102
- | ----------------------- | --------------------------- | ---------------------------------------------------- |
103
- | `(outKeySelected)` | `string` | Emit key của tab vừa được chọn. |
104
- | `(outDragTabChange)` | `void` | Emit sau khi người dùng kéo thả thay đổi vị trí tab. |
105
- | `(outDisplayMoreItem)` | `boolean` | Emit khi trạng thái hiển thị menu "More" thay đổi. |
106
- | `(outFunctionsControl)` | `ITabsFunctionControlEvent` | Emit các hàm điều khiển tabs từ bên ngoài. |
107
- | `(outAction)` | `ITabsItemEvent` | Emit các action khác (ví dụ: click nút close). |
256
+ ### Ví dụ 6 — Step mode (quy trình dạng bước)
257
+
258
+ ```typescript
259
+ import { signal, WritableSignal } from '@angular/core';
260
+ import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';
261
+
262
+ @Component({
263
+ standalone: true,
264
+ changeDetection: ChangeDetectionStrategy.OnPush,
265
+ imports: [LibsUiComponentsTabsComponent],
266
+ template: `
267
+ <libs_ui-components-tabs
268
+ [tabs]="tabsConfig"
269
+ [(keySelected)]="selectedKey"
270
+ mode="center-has-line"
271
+ [ignoreCalculatorTab]="true"
272
+ />
273
+ `,
274
+ })
275
+ export class StepTabsComponent {
276
+ protected selectedKey = signal<string>('step1');
277
+
278
+ protected tabsConfig: ITabs = {
279
+ hasStep: true,
280
+ stepCompleted: 1,
281
+ items: signal<Array<WritableSignal<ITabsItem>>>([
282
+ signal({ key: 'step1', label: 'Thông tin cơ bản' }),
283
+ signal({ key: 'step2', label: 'Xác minh' }),
284
+ signal({ key: 'step3', label: 'Hoàn tất' }),
285
+ ]),
286
+ };
287
+ }
288
+ ```
289
+
290
+ ## @Input()
291
+
292
+ | Input | Type | Default | Mô tả | Ví dụ |
293
+ |---|---|---|---|---|
294
+ | `tabs` | `ITabs` | **Required** | Cấu hình chính chứa danh sách items dạng nested Signal. | `[tabs]="tabsConfig"` |
295
+ | `keySelected` | `string` (model) | **Required** | Key của tab đang chọn, hỗ trợ two-way binding. | `[(keySelected)]="selectedKey"` |
296
+ | `mode` | `TYPE_TAB_MODE` | `'left'` | Chế độ căn chỉnh: `'left'`, `'center'`, `'space-between'`, `'center-has-line'`. | `mode="center"` |
297
+ | `fieldKey` | `string` | `'key'` | Tên trường làm định danh duy nhất trong object item. | `[fieldKey]="'id'"` |
298
+ | `fieldLabel` | `string` | `'label'` | Tên trường hiển thị nhãn tab (hỗ trợ i18n key). | `[fieldLabel]="'name'"` |
299
+ | `disable` | `boolean` | `undefined` | Vô hiệu hóa toàn bộ component tabs. | `[disable]="true"` |
300
+ | `disableLabel` | `boolean` | `undefined` | Ẩn nhãn văn bản trên tab item. | `[disableLabel]="true"` |
301
+ | `heightTabItem` | `number` | `40` | Chiều cao của thanh tab header tính bằng px. | `[heightTabItem]="48"` |
302
+ | `ignoreCalculatorTab` | `boolean` | `false` | Bỏ qua tính toán responsive — dùng khi biết trước số tab vừa vặn. | `[ignoreCalculatorTab]="true"` |
303
+ | `size` | `'langer' \| 'medium'` | `'medium'` | Kích thước tổng thể của component. | `size="langer"` |
304
+ | `allowDragDropPosition` | `boolean` | `undefined` | Cho phép kéo thả thay đổi thứ tự tab. | `[allowDragDropPosition]="true"` |
305
+ | `zIndex` | `number` | `undefined` | Z-index áp dụng cho popover More và overlay. | `[zIndex]="100"` |
306
+ | `configCss` | `ITabCssConfig` (model) | `undefined` | Override CSS padding/margin cho các mode. Tự động set nếu không truyền. | `[(configCss)]="cssConfig"` |
307
+ | `popoverShowMoreTabItem` | `IPopover` | `undefined` | Cấu hình vị trí và style cho popover menu "Xem thêm". | `[popoverShowMoreTabItem]="popoverCfg"` |
308
+ | `checkCanChangeTabSelected` | `() => boolean \| Promise<boolean>` | `undefined` | Callback gác cổng trước khi chuyển tab. Trả về `false` để hủy. | `[checkCanChangeTabSelected]="checkFn"` |
309
+ | `useEffectUpdateItems` | `boolean` | `false` | Dùng `effect()` để tự động cập nhật danh sách hiển thị khi signal items thay đổi từ bên ngoài. Bật khi tab list là động. | `[useEffectUpdateItems]="true"` |
310
+ | `useCalculatorV2` | `boolean` | `false` | Bật thuật toán V2 dùng `ResizeObserver` — không flicker, không reorder DOM. Khuyến nghị cho tab mới. | `[useCalculatorV2]="true"` |
311
+
312
+ ## @Output()
313
+
314
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
315
+ |---|---|---|---|---|
316
+ | `(outKeySelected)` | `string` | Emit key của tab vừa được chọn. | `handlerKeySelected(key: string): void { /* key là tab key */ }` | `(outKeySelected)="handlerKeySelected($event)"` |
317
+ | `(outFunctionsControl)` | `ITabsFunctionControlEvent` | Emit object chứa API điều khiển tabs: `addTabsItem`, `selectedTabsItem`, `calculatorTabsItemsDisplay`. | `handlerFunctionsControl(fc: ITabsFunctionControlEvent): void { this.tabsFc = fc; }` | `(outFunctionsControl)="handlerFunctionsControl($event)"` |
318
+ | `(outDragTabChange)` | `void` | Emit sau khi người dùng kéo thả hoàn tất, thứ tự items đã cập nhật. | `handlerDragChange(): void { /* đọc lại tabs().items() */ }` | `(outDragTabChange)="handlerDragChange()"` |
319
+ | `(outDisplayMoreItem)` | `boolean` | Emit `true` khi có tab bị ẩn vào menu More, `false` khi tất cả tab vừa. | `handlerDisplayMore(visible: boolean): void { this.hasMore.set(visible); }` | `(outDisplayMoreItem)="handlerDisplayMore($event)"` |
320
+ | `(outAction)` | `ITabsItemEvent` | Emit khi người dùng click vào action item (nút remove, configButtonLeft/Right). `key` là `'remove'` hoặc key action tùy chỉnh. | `handlerAction(event: ITabsItemEvent): void { if (event.key === 'remove') this.removeTab(event.item); }` | `(outAction)="handlerAction($event)"` |
108
321
 
109
322
  ## Types & Interfaces
110
323
 
111
324
  ```typescript
325
+ import {
326
+ ITabs,
327
+ ITabsItem,
328
+ ITabCssConfig,
329
+ ITabsFunctionControlEvent,
330
+ ITabsItemEvent,
331
+ TYPE_TAB_MODE,
332
+ } from '@libs-ui/components-tabs';
333
+ ```
334
+
335
+ ```typescript
336
+ /** Cấu hình tổng thể cho component tabs */
112
337
  export interface ITabs {
338
+ /** Danh sách các tab item dạng nested Signal — BẮT BUỘC */
113
339
  items: WritableSignal<Array<WritableSignal<ITabsItem>>>;
340
+
341
+ /** Hiển thị ảnh avatar bên trái nhãn tab */
114
342
  hasImage?: boolean;
343
+
344
+ /** Hiển thị badge số đếm bên phải nhãn tab */
115
345
  hasCount?: boolean;
346
+
347
+ /** Hiển thị nút xóa (remove) trên mỗi tab */
116
348
  allowRemove?: boolean;
349
+
350
+ /** Cấu hình nút xóa (IButton) */
351
+ configButtonRemove?: IButton;
352
+
353
+ /** Chế độ step wizard — hiển thị số thứ tự trên mỗi tab */
117
354
  hasStep?: boolean;
118
- // ... configuration options
355
+
356
+ /** Số bước đã hoàn thành — dùng với hasStep */
357
+ stepCompleted?: number;
358
+
359
+ /** Hiển thị nền cho step đã hoàn thành */
360
+ stepHasBackGround?: boolean;
361
+
362
+ /** Bỏ qua nền cho tab đang selected trong step mode */
363
+ ignoreSelectedBackgroundStep?: boolean;
364
+
365
+ /** Ẩn đường kẻ dưới trên tab header */
366
+ ignoreShowLineBottomInTab?: boolean;
367
+
368
+ /** Class CSS tùy chỉnh cho phần header */
369
+ classIncludeHeader?: string;
370
+
371
+ /** Class CSS cho vùng center của header */
372
+ classIncludeHeaderCenter?: string;
373
+
374
+ /** Class CSS cho vùng right của header */
375
+ classIncludeHeaderRight?: string;
376
+
377
+ /** Class CSS áp dụng lên mỗi tab item */
378
+ classIncludeItem?: string;
379
+
380
+ /** Class CSS áp dụng lên tab item đang active */
381
+ classIncludeActiveItem?: string;
382
+
383
+ /** Giới hạn chiều rộng tối đa (px) của nhãn tab */
384
+ maxWidthTextLabelItem?: number;
385
+
386
+ /** Cấu hình action ở góc phải header (popover với danh sách) */
387
+ actionRightConfig?: WritableSignal<{
388
+ getListViewConfig: TYPE_FUNCTION<WritableSignal<IListConfigItem>>;
389
+ config?: WritableSignal<IPopoverOverlay>;
390
+ onlyShowWhenHoverItemActive?: boolean;
391
+ classInclude?: string;
392
+ customView?: () => Observable<string>;
393
+ }>;
394
+
395
+ /** Bỏ qua margin-left cho nút "Xem thêm" */
396
+ viewMoreIgnoreMarginLeft?: boolean;
119
397
  }
120
398
 
399
+ /** Cấu hình cho từng tab item */
121
400
  export interface ITabsItem {
401
+ /** Định danh duy nhất của tab */
122
402
  key?: string;
403
+
404
+ /** Class CSS bổ sung cho tab item */
405
+ classInclude?: string;
406
+
407
+ /** Vô hiệu hóa tab item này */
408
+ disable?: boolean;
409
+
410
+ /** Nhãn hiển thị (hỗ trợ i18n key) */
123
411
  label?: string;
412
+
413
+ /** Class CSS cho nhãn */
414
+ classLabel?: string;
415
+
416
+ /** Icon class bên trái nhãn (vd: 'libs-ui-icon-mail') */
124
417
  iconLeft?: string;
418
+
419
+ /** Icon class bên phải nhãn */
420
+ iconRight?: string;
421
+
422
+ /** Hiển thị red dot trên tab */
423
+ hasRedDot?: boolean;
424
+
425
+ /** Số đếm hiển thị dạng badge */
125
426
  count?: number;
126
- disable?: boolean;
127
- // ... item properties
427
+
428
+ /** Chế độ hiển thị badge: 'x' | '0x' | 'x+' */
429
+ modeCount?: TYPE_BADGE_MODE;
430
+
431
+ /** Số tối đa hiển thị trước khi thêm '+' */
432
+ maxCount?: number;
433
+
434
+ /** Class CSS cho vòng tròn badge */
435
+ classCircle?: string;
436
+
437
+ /** URL ảnh avatar bên trái */
438
+ linkImage?: string;
439
+
440
+ /** URL ảnh fallback khi ảnh chính lỗi */
441
+ linkImageError?: string;
442
+
443
+ /** Trạng thái invalid — đổi màu sang đỏ trong step mode */
444
+ invalid?: boolean;
445
+
446
+ /** Nút action bên phải tab item */
447
+ configButtonRight?: IButton;
448
+
449
+ /** Nút action bên trái tab item */
450
+ configButtonLeft?: IButton;
451
+
452
+ /** Chiều rộng đã đo được — do component tự tính, không set thủ công */
453
+ specificWidth?: number;
454
+
455
+ /** Trạng thái hiển thị — do component tự quản lý */
456
+ specificDisplay?: boolean;
457
+
458
+ /** Thứ tự sắp xếp — do component tự quản lý */
459
+ order?: number;
460
+
461
+ /** Cho phép thêm thuộc tính tuỳ chỉnh */
462
+ [param: string]: any;
463
+ }
464
+
465
+ /** Override CSS theo từng mode */
466
+ export interface ITabCssConfig {
467
+ /** Class áp dụng cho tab đầu tiên */
468
+ first: string;
469
+ /** Class áp dụng cho các tab còn lại */
470
+ other: string;
471
+ /** Class áp dụng cho header wrapper */
472
+ header?: string;
473
+ /** Class áp dụng cho phần center của header */
474
+ headerCenter?: string;
475
+ }
476
+
477
+ /** API điều khiển tabs từ bên ngoài (nhận qua outFunctionsControl) */
478
+ export interface ITabsFunctionControlEvent {
479
+ /** Thêm tab item mới vào danh sách */
480
+ addTabsItem: (
481
+ item: WritableSignal<ITabsItem>,
482
+ selected?: boolean, // true = chuyển sang tab mới ngay
483
+ addFirst?: boolean, // true = thêm vào đầu danh sách
484
+ indexAdd?: number // vị trí cụ thể để chèn vào
485
+ ) => Promise<void>;
486
+
487
+ /** Tính toán lại các tab cần hiển thị (dùng sau khi resize thủ công) */
488
+ calculatorTabsItemsDisplay: () => Promise<void>;
489
+
490
+ /** Chuyển sang tab theo key */
491
+ selectedTabsItem: (
492
+ key: string,
493
+ resetDisable?: boolean // true = bật lại tab dù đang disable
494
+ ) => Promise<void>;
495
+ }
496
+
497
+ /** Dữ liệu emit từ outAction */
498
+ export interface ITabsItemEvent {
499
+ /** Key của action: 'remove' hoặc key action từ configButtonLeft/Right */
500
+ key: string;
501
+ /** Data của tab item liên quan */
502
+ item: ITabsItem;
128
503
  }
504
+
505
+ /** Chế độ hiển thị của component */
506
+ export type TYPE_TAB_MODE = 'left' | 'center' | 'space-between' | 'center-has-line';
129
507
  ```
130
508
 
131
- ## Styling
509
+ ## Lưu ý quan trọng
510
+
511
+ ⚠️ **Nested Signals bắt buộc**: `tabs.items` PHẢI là `WritableSignal<Array<WritableSignal<ITabsItem>>>`. Không truyền plain array — component sẽ không phản ứng khi thêm/xóa item.
512
+
513
+ ⚠️ **useEffectUpdateItems khi items thay đổi động**: Khi danh sách tab được thay đổi từ bên ngoài component (vd: sau khi nhận dữ liệu từ API hoặc Modal), BẮT BUỘC bật `[useEffectUpdateItems]="true"` để component tự cập nhật danh sách hiển thị.
132
514
 
133
- ### CSS Classes
515
+ ⚠️ **Calculator V2 khuyến nghị cho code mới**: `[useCalculatorV2]="true"` dùng `ResizeObserver` thay vì `MutationObserver` + `setTimeout` — không flicker, không reorder DOM, an toàn với `ChangeDetectionStrategy.OnPush`. Mặc định `false` để giữ backward compatible với code cũ.
134
516
 
135
- | Class | Description |
136
- | --------------------- | ------------------------------------ |
137
- | `.libs-ui-tab` | Container chính của tabs component. |
138
- | `.libs-ui-tab-header` | Header chứa danh sách các tab items. |
517
+ ⚠️ **mode center-has-line cho step wizard**: Kết hợp với `tabs.hasStep = true` và `tabs.stepCompleted` để hiển thị chỉ báo bước hoàn thành. Nên dùng với `[ignoreCalculatorTab]="true"` khi số bước cố định.
139
518
 
140
- ## Công nghệ
519
+ ⚠️ **allowRemove cần xử lý outAction**: Khi bật `tabs.allowRemove`, lắng nghe `(outAction)` để xử lý sự kiện `key === 'remove'`. Component chỉ emit event, không tự xóa item khỏi danh sách.
141
520
 
142
- | Technology | Version | Purpose |
143
- | --------------- | ------- | ---------------- |
144
- | Angular | 18+ | Framework |
145
- | Angular Signals | - | State management |
146
- | TailwindCSS | 3.x | Styling |
147
- | OnPush | - | Change Detection |
521
+ ⚠️ **fieldKey fieldLabel cho data custom**: Khi object item dùng tên trường khác `key`/`label` (vd: từ API trả về `id`/`name`), phải truyền `[fieldKey]="'id'"` và `[fieldLabel]="'name'"` để component đọc đúng trường.
148
522
 
149
523
  ## Demo
150
524
 
@@ -154,12 +528,16 @@ npx nx serve core-ui
154
528
 
155
529
  Truy cập: `http://localhost:4500/tabs`
156
530
 
531
+ Bao gồm các ví dụ: Basic responsive, Center mode, Rich content (icon + badge), Drag & Drop, và tích hợp Modal V2 để cấu hình danh sách tab động.
532
+
157
533
  ## Unit Tests
158
534
 
159
535
  ```bash
160
536
  npx nx test components-tabs
161
537
  ```
162
538
 
163
- ## License
539
+ Chạy test cho file cụ thể:
164
540
 
165
- MIT
541
+ ```bash
542
+ npx nx test components-tabs --testFile=libs-ui/components/tabs/src/tabs.component.spec.ts
543
+ ```
@@ -1,2 +1,2 @@
1
1
  export {};
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGFiLmludGVyZmFjZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL2xpYnMtdWkvY29tcG9uZW50cy90YWJzL3NyYy9pbnRlcmZhY2VzL3RhYi5pbnRlcmZhY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IFdyaXRhYmxlU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XG5pbXBvcnQgeyBUWVBFX0JBREdFX01PREUgfSBmcm9tICdAbGlicy11aS9jb21wb25lbnRzLWJhZGdlJztcbmltcG9ydCB7IElCdXR0b24gfSBmcm9tICdAbGlicy11aS9jb21wb25lbnRzLWJ1dHRvbnMtYnV0dG9uJztcbmltcG9ydCB7IElMaXN0Q29uZmlnSXRlbSB9IGZyb20gJ0BsaWJzLXVpL2NvbXBvbmVudHMtbGlzdCc7XG5pbXBvcnQgeyBJUG9wb3Zlck92ZXJsYXkgfSBmcm9tICdAbGlicy11aS9jb21wb25lbnRzLXBvcG92ZXInO1xuaW1wb3J0IHsgVFlQRV9GVU5DVElPTiB9IGZyb20gJ0BsaWJzLXVpL2ludGVyZmFjZXMtdHlwZXMnO1xuaW1wb3J0IHsgT2JzZXJ2YWJsZSB9IGZyb20gJ3J4anMnO1xuXG5leHBvcnQgaW50ZXJmYWNlIElUYWJzIHtcbiAgaGFzSW1hZ2U/OiBib29sZWFuO1xuICBoYXNDb3VudD86IGJvb2xlYW47XG4gIGFsbG93UmVtb3ZlPzogYm9vbGVhbjtcbiAgY29uZmlnQnV0dG9uUmVtb3ZlPzogSUJ1dHRvbjtcbiAgaGFzU3RlcD86IGJvb2xlYW47XG4gIHN0ZXBDb21wbGV0ZWQ/OiBudW1iZXI7XG4gIHN0ZXBIYXNCYWNrR3JvdW5kPzogYm9vbGVhbjtcbiAgaWdub3JlU2VsZWN0ZWRCYWNrZ3JvdW5kU3RlcD86IGJvb2xlYW47XG4gIGlnbm9yZVNob3dMaW5lQm90dG9tSW5UYWI/OiBib29sZWFuO1xuICBjbGFzc0luY2x1ZGVIZWFkZXI/OiBzdHJpbmc7XG4gIGNsYXNzSW5jbHVkZUhlYWRlckNlbnRlcj86IHN0cmluZztcbiAgY2xhc3NJbmNsdWRlSGVhZGVyUmlnaHQ/OiBzdHJpbmc7XG4gIGNsYXNzSW5jbHVkZUl0ZW0/OiBzdHJpbmc7XG4gIGNsYXNzSW5jbHVkZUFjdGl2ZUl0ZW0/OiBzdHJpbmc7XG4gIG1heFdpZHRoVGV4dExhYmVsSXRlbT86IG51bWJlcjtcbiAgYWN0aW9uUmlnaHRDb25maWc/OiBXcml0YWJsZVNpZ25hbDx7XG4gICAgZ2V0TGlzdFZpZXdDb25maWc6IFRZUEVfRlVOQ1RJT048V3JpdGFibGVTaWduYWw8SUxpc3RDb25maWdJdGVtPj47XG4gICAgY29uZmlnPzogV3JpdGFibGVTaWduYWw8SVBvcG92ZXJPdmVybGF5PjtcbiAgICBvbmx5U2hvd1doZW5Ib3Zlckl0ZW1BY3RpdmU/OiBib29sZWFuO1xuICAgIGNsYXNzSW5jbHVkZT86IHN0cmluZztcbiAgICBjdXN0b21WaWV3PzogKCkgPT4gT2JzZXJ2YWJsZTxzdHJpbmc+O1xuICB9PjtcbiAgdmlld01vcmVJZ25vcmVNYXJnaW5MZWZ0PzogYm9vbGVhbjtcbiAgaXRlbXM6IFdyaXRhYmxlU2lnbmFsPEFycmF5PFdyaXRhYmxlU2lnbmFsPElUYWJzSXRlbT4+Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBJVGFic0l0ZW0ge1xuICBrZXk/OiBzdHJpbmc7XG4gIGNsYXNzSW5jbHVkZT86IHN0cmluZztcbiAgZGlzYWJsZT86IGJvb2xlYW47XG4gIGNyZWF0ZWRfYnlfc3lzdGVtPzogYm9vbGVhbjtcbiAgY3JlYXRlZF9ieT86IHN0cmluZztcbiAgaXNfZGVmYXVsdD86IGJvb2xlYW47XG4gIGlzX3Bpbj86IGJvb2xlYW47XG4gIG9yZGVyPzogbnVtYmVyO1xuICBsaW5rSW1hZ2U/OiBzdHJpbmc7XG4gIGxpbmtJbWFnZUVycm9yPzogc3RyaW5nO1xuICBjbGFzc0ltYWdlSW5jbHVkZT86IHN0cmluZztcbiAgcG9wb3ZlckltYWdlPzogSVBvcG92ZXJPdmVybGF5O1xuICBpY29uTGVmdD86IHN0cmluZztcbiAgcG9wb3Zlckljb25MZWZ0PzogSVBvcG92ZXJPdmVybGF5O1xuICBsYWJlbD86IHN0cmluZztcbiAgY2xhc3NMYWJlbD86IHN0cmluZztcbiAgcG9wb3Zlcj86IElQb3BvdmVyT3ZlcmxheTtcbiAgaWNvblJpZ2h0Pzogc3RyaW5nO1xuICBwb3BvdmVySWNvblJpZ2h0PzogSVBvcG92ZXJPdmVybGF5O1xuICBjb3VudD86IG51bWJlcjtcbiAgbW9kZUNvdW50PzogVFlQRV9CQURHRV9NT0RFO1xuICBtYXhDb3VudD86IG51bWJlcjtcbiAgY2xhc3NDaXJjbGU/OiBzdHJpbmc7XG4gIHNwZWNpZmljV2lkdGg/OiBudW1iZXI7XG4gIHNwZWNpZmljRGlzcGxheT86IGJvb2xlYW47XG4gIGNsYXNzU3RlcD86IHN0cmluZztcbiAgY29uZmlnQnV0dG9uUmlnaHQ/OiBJQnV0dG9uO1xuICBjb25maWdCdXR0b25MZWZ0PzogSUJ1dHRvbjtcbiAgaGFzUmVkRG90PzogYm9vbGVhbjtcbiAgaW52YWxpZD86IGJvb2xlYW47XG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBAdHlwZXNjcmlwdC1lc2xpbnQvbm8tZXhwbGljaXQtYW55XG4gIFtwYXJhbTogc3RyaW5nXTogYW55O1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIElUYWJDc3NDb25maWcge1xuICBmaXJzdDogc3RyaW5nO1xuICBvdGhlcjogc3RyaW5nO1xuICBoZWFkZXI/OiBzdHJpbmc7XG4gIGhlYWRlckNlbnRlcj86IHN0cmluZztcbn1cblxuZXhwb3J0IHR5cGUgVFlQRV9UQUJfTU9ERSA9ICdsZWZ0JyB8ICdjZW50ZXInIHwgJ3NwYWNlLWJldHdlZW4nIHwgJ2NlbnRlci1oYXMtbGluZSc7XG5cbmV4cG9ydCBpbnRlcmZhY2UgSVRhYnNGdW5jdGlvbkNvbnRyb2xFdmVudCB7XG4gIGFkZFRhYnNJdGVtOiAoaXRlbTogV3JpdGFibGVTaWduYWw8SVRhYnNJdGVtPiwgc2VsZWN0ZWQ/OiBib29sZWFuLCBhZGRGaXJzdD86IGJvb2xlYW4pID0+IFByb21pc2U8dm9pZD47XG4gIGNhbGN1bGF0b3JUYWJzSXRlbXNEaXNwbGF5OiAoKSA9PiBQcm9taXNlPHZvaWQ+O1xuICBzZWxlY3RlZFRhYnNJdGVtOiAoa2V5OiBzdHJpbmcsIHJlc2V0RGlzYWJsZT86IGJvb2xlYW4pID0+IFByb21pc2U8dm9pZD47XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgSVRhYnNJdGVtRXZlbnQge1xuICBrZXk6IHN0cmluZztcbiAgaXRlbTogSVRhYnNJdGVtO1xufVxuIl19
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGFiLmludGVyZmFjZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL2xpYnMtdWkvY29tcG9uZW50cy90YWJzL3NyYy9pbnRlcmZhY2VzL3RhYi5pbnRlcmZhY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IFdyaXRhYmxlU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XG5pbXBvcnQgeyBUWVBFX0JBREdFX01PREUgfSBmcm9tICdAbGlicy11aS9jb21wb25lbnRzLWJhZGdlJztcbmltcG9ydCB7IElCdXR0b24gfSBmcm9tICdAbGlicy11aS9jb21wb25lbnRzLWJ1dHRvbnMtYnV0dG9uJztcbmltcG9ydCB7IElMaXN0Q29uZmlnSXRlbSB9IGZyb20gJ0BsaWJzLXVpL2NvbXBvbmVudHMtbGlzdCc7XG5pbXBvcnQgeyBJUG9wb3Zlck92ZXJsYXkgfSBmcm9tICdAbGlicy11aS9jb21wb25lbnRzLXBvcG92ZXInO1xuaW1wb3J0IHsgVFlQRV9GVU5DVElPTiB9IGZyb20gJ0BsaWJzLXVpL2ludGVyZmFjZXMtdHlwZXMnO1xuaW1wb3J0IHsgT2JzZXJ2YWJsZSB9IGZyb20gJ3J4anMnO1xuXG5leHBvcnQgaW50ZXJmYWNlIElUYWJzIHtcbiAgaGFzSW1hZ2U/OiBib29sZWFuO1xuICBoYXNDb3VudD86IGJvb2xlYW47XG4gIGFsbG93UmVtb3ZlPzogYm9vbGVhbjtcbiAgY29uZmlnQnV0dG9uUmVtb3ZlPzogSUJ1dHRvbjtcbiAgaGFzU3RlcD86IGJvb2xlYW47XG4gIHN0ZXBDb21wbGV0ZWQ/OiBudW1iZXI7XG4gIHN0ZXBIYXNCYWNrR3JvdW5kPzogYm9vbGVhbjtcbiAgaWdub3JlU2VsZWN0ZWRCYWNrZ3JvdW5kU3RlcD86IGJvb2xlYW47XG4gIGlnbm9yZVNob3dMaW5lQm90dG9tSW5UYWI/OiBib29sZWFuO1xuICBjbGFzc0luY2x1ZGVIZWFkZXI/OiBzdHJpbmc7XG4gIGNsYXNzSW5jbHVkZUhlYWRlckNlbnRlcj86IHN0cmluZztcbiAgY2xhc3NJbmNsdWRlSGVhZGVyUmlnaHQ/OiBzdHJpbmc7XG4gIGNsYXNzSW5jbHVkZUl0ZW0/OiBzdHJpbmc7XG4gIGNsYXNzSW5jbHVkZUFjdGl2ZUl0ZW0/OiBzdHJpbmc7XG4gIG1heFdpZHRoVGV4dExhYmVsSXRlbT86IG51bWJlcjtcbiAgYWN0aW9uUmlnaHRDb25maWc/OiBXcml0YWJsZVNpZ25hbDx7XG4gICAgZ2V0TGlzdFZpZXdDb25maWc6IFRZUEVfRlVOQ1RJT048V3JpdGFibGVTaWduYWw8SUxpc3RDb25maWdJdGVtPj47XG4gICAgY29uZmlnPzogV3JpdGFibGVTaWduYWw8SVBvcG92ZXJPdmVybGF5PjtcbiAgICBvbmx5U2hvd1doZW5Ib3Zlckl0ZW1BY3RpdmU/OiBib29sZWFuO1xuICAgIGNsYXNzSW5jbHVkZT86IHN0cmluZztcbiAgICBjdXN0b21WaWV3PzogKCkgPT4gT2JzZXJ2YWJsZTxzdHJpbmc+O1xuICB9PjtcbiAgdmlld01vcmVJZ25vcmVNYXJnaW5MZWZ0PzogYm9vbGVhbjtcbiAgaXRlbXM6IFdyaXRhYmxlU2lnbmFsPEFycmF5PFdyaXRhYmxlU2lnbmFsPElUYWJzSXRlbT4+Pjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBJVGFic0l0ZW0ge1xuICBrZXk/OiBzdHJpbmc7XG4gIGNsYXNzSW5jbHVkZT86IHN0cmluZztcbiAgZGlzYWJsZT86IGJvb2xlYW47XG4gIGNyZWF0ZWRfYnlfc3lzdGVtPzogYm9vbGVhbjtcbiAgY3JlYXRlZF9ieT86IHN0cmluZztcbiAgaXNfZGVmYXVsdD86IGJvb2xlYW47XG4gIGlzX3Bpbj86IGJvb2xlYW47XG4gIG9yZGVyPzogbnVtYmVyO1xuICBsaW5rSW1hZ2U/OiBzdHJpbmc7XG4gIGxpbmtJbWFnZUVycm9yPzogc3RyaW5nO1xuICBjbGFzc0ltYWdlSW5jbHVkZT86IHN0cmluZztcbiAgcG9wb3ZlckltYWdlPzogSVBvcG92ZXJPdmVybGF5O1xuICBpY29uTGVmdD86IHN0cmluZztcbiAgcG9wb3Zlckljb25MZWZ0PzogSVBvcG92ZXJPdmVybGF5O1xuICBsYWJlbD86IHN0cmluZztcbiAgY2xhc3NMYWJlbD86IHN0cmluZztcbiAgcG9wb3Zlcj86IElQb3BvdmVyT3ZlcmxheTtcbiAgaWNvblJpZ2h0QmVoaW5kQmFkZ2U/OiBib29sZWFuO1xuICBpY29uUmlnaHQ/OiBzdHJpbmc7XG4gIHBvcG92ZXJJY29uUmlnaHQ/OiBJUG9wb3Zlck92ZXJsYXk7XG4gIGNvdW50PzogbnVtYmVyO1xuICBtb2RlQ291bnQ/OiBUWVBFX0JBREdFX01PREU7XG4gIG1heENvdW50PzogbnVtYmVyO1xuICBjbGFzc0NpcmNsZT86IHN0cmluZztcbiAgc3BlY2lmaWNXaWR0aD86IG51bWJlcjtcbiAgc3BlY2lmaWNEaXNwbGF5PzogYm9vbGVhbjtcbiAgY2xhc3NTdGVwPzogc3RyaW5nO1xuICBjb25maWdCdXR0b25SaWdodD86IElCdXR0b247XG4gIGNvbmZpZ0J1dHRvbkxlZnQ/OiBJQnV0dG9uO1xuICBoYXNSZWREb3Q/OiBib29sZWFuO1xuICBpbnZhbGlkPzogYm9vbGVhbjtcbiAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIEB0eXBlc2NyaXB0LWVzbGludC9uby1leHBsaWNpdC1hbnlcbiAgW3BhcmFtOiBzdHJpbmddOiBhbnk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgSVRhYkNzc0NvbmZpZyB7XG4gIGZpcnN0OiBzdHJpbmc7XG4gIG90aGVyOiBzdHJpbmc7XG4gIGhlYWRlcj86IHN0cmluZztcbiAgaGVhZGVyQ2VudGVyPzogc3RyaW5nO1xufVxuXG5leHBvcnQgdHlwZSBUWVBFX1RBQl9NT0RFID0gJ2xlZnQnIHwgJ2NlbnRlcicgfCAnc3BhY2UtYmV0d2VlbicgfCAnY2VudGVyLWhhcy1saW5lJztcblxuZXhwb3J0IGludGVyZmFjZSBJVGFic0Z1bmN0aW9uQ29udHJvbEV2ZW50IHtcbiAgYWRkVGFic0l0ZW06IChpdGVtOiBXcml0YWJsZVNpZ25hbDxJVGFic0l0ZW0+LCBzZWxlY3RlZD86IGJvb2xlYW4sIGFkZEZpcnN0PzogYm9vbGVhbiwgaW5kZXhBZGQ/OiBudW1iZXIpID0+IFByb21pc2U8dm9pZD47XG4gIGNhbGN1bGF0b3JUYWJzSXRlbXNEaXNwbGF5OiAoKSA9PiBQcm9taXNlPHZvaWQ+O1xuICBzZWxlY3RlZFRhYnNJdGVtOiAoa2V5OiBzdHJpbmcsIHJlc2V0RGlzYWJsZT86IGJvb2xlYW4pID0+IFByb21pc2U8dm9pZD47XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgSVRhYnNJdGVtRXZlbnQge1xuICBrZXk6IHN0cmluZztcbiAgaXRlbTogSVRhYnNJdGVtO1xufVxuIl19