ngx-com 0.0.1 → 0.0.4
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/fesm2022/ngx-com-components-avatar.mjs +772 -0
- package/fesm2022/ngx-com-components-avatar.mjs.map +1 -0
- package/fesm2022/ngx-com-components-badge.mjs +138 -0
- package/fesm2022/ngx-com-components-badge.mjs.map +1 -0
- package/fesm2022/ngx-com-components-button.mjs +146 -0
- package/fesm2022/ngx-com-components-button.mjs.map +1 -0
- package/fesm2022/ngx-com-components-calendar.mjs +5046 -0
- package/fesm2022/ngx-com-components-calendar.mjs.map +1 -0
- package/fesm2022/ngx-com-components-card.mjs +590 -0
- package/fesm2022/ngx-com-components-card.mjs.map +1 -0
- package/fesm2022/ngx-com-components-checkbox.mjs +344 -0
- package/fesm2022/ngx-com-components-checkbox.mjs.map +1 -0
- package/fesm2022/ngx-com-components-collapsible.mjs +612 -0
- package/fesm2022/ngx-com-components-collapsible.mjs.map +1 -0
- package/fesm2022/ngx-com-components-confirm.mjs +562 -0
- package/fesm2022/ngx-com-components-confirm.mjs.map +1 -0
- package/fesm2022/ngx-com-components-dropdown-testing.mjs +255 -0
- package/fesm2022/ngx-com-components-dropdown-testing.mjs.map +1 -0
- package/fesm2022/ngx-com-components-dropdown.mjs +2692 -0
- package/fesm2022/ngx-com-components-dropdown.mjs.map +1 -0
- package/fesm2022/ngx-com-components-empty-state.mjs +382 -0
- package/fesm2022/ngx-com-components-empty-state.mjs.map +1 -0
- package/fesm2022/ngx-com-components-form-field.mjs +924 -0
- package/fesm2022/ngx-com-components-form-field.mjs.map +1 -0
- package/fesm2022/ngx-com-components-icon.mjs +183 -0
- package/fesm2022/ngx-com-components-icon.mjs.map +1 -0
- package/fesm2022/ngx-com-components-item.mjs +578 -0
- package/fesm2022/ngx-com-components-item.mjs.map +1 -0
- package/fesm2022/ngx-com-components-menu.mjs +1200 -0
- package/fesm2022/ngx-com-components-menu.mjs.map +1 -0
- package/fesm2022/ngx-com-components-paginator.mjs +823 -0
- package/fesm2022/ngx-com-components-paginator.mjs.map +1 -0
- package/fesm2022/ngx-com-components-popover.mjs +901 -0
- package/fesm2022/ngx-com-components-popover.mjs.map +1 -0
- package/fesm2022/ngx-com-components-radio.mjs +621 -0
- package/fesm2022/ngx-com-components-radio.mjs.map +1 -0
- package/fesm2022/ngx-com-components-segmented-control.mjs +538 -0
- package/fesm2022/ngx-com-components-segmented-control.mjs.map +1 -0
- package/fesm2022/ngx-com-components-sort.mjs +368 -0
- package/fesm2022/ngx-com-components-sort.mjs.map +1 -0
- package/fesm2022/ngx-com-components-spinner.mjs +189 -0
- package/fesm2022/ngx-com-components-spinner.mjs.map +1 -0
- package/fesm2022/ngx-com-components-tabs.mjs +1522 -0
- package/fesm2022/ngx-com-components-tabs.mjs.map +1 -0
- package/fesm2022/ngx-com-components-tooltip.mjs +625 -0
- package/fesm2022/ngx-com-components-tooltip.mjs.map +1 -0
- package/fesm2022/ngx-com-components.mjs +17 -0
- package/fesm2022/ngx-com-components.mjs.map +1 -0
- package/fesm2022/ngx-com-tokens.mjs +12 -0
- package/fesm2022/ngx-com-tokens.mjs.map +1 -0
- package/fesm2022/ngx-com-utils.mjs +601 -0
- package/fesm2022/ngx-com-utils.mjs.map +1 -0
- package/fesm2022/ngx-com.mjs +9 -23
- package/fesm2022/ngx-com.mjs.map +1 -1
- package/package.json +105 -1
- package/types/ngx-com-components-avatar.d.ts +409 -0
- package/types/ngx-com-components-badge.d.ts +97 -0
- package/types/ngx-com-components-button.d.ts +69 -0
- package/types/ngx-com-components-calendar.d.ts +1665 -0
- package/types/ngx-com-components-card.d.ts +373 -0
- package/types/ngx-com-components-checkbox.d.ts +116 -0
- package/types/ngx-com-components-collapsible.d.ts +379 -0
- package/types/ngx-com-components-confirm.d.ts +160 -0
- package/types/ngx-com-components-dropdown-testing.d.ts +116 -0
- package/types/ngx-com-components-dropdown.d.ts +938 -0
- package/types/ngx-com-components-empty-state.d.ts +269 -0
- package/types/ngx-com-components-form-field.d.ts +531 -0
- package/types/ngx-com-components-icon.d.ts +94 -0
- package/types/ngx-com-components-item.d.ts +336 -0
- package/types/ngx-com-components-menu.d.ts +479 -0
- package/types/ngx-com-components-paginator.d.ts +265 -0
- package/types/ngx-com-components-popover.d.ts +309 -0
- package/types/ngx-com-components-radio.d.ts +258 -0
- package/types/ngx-com-components-segmented-control.d.ts +274 -0
- package/types/ngx-com-components-sort.d.ts +133 -0
- package/types/ngx-com-components-spinner.d.ts +120 -0
- package/types/ngx-com-components-tabs.d.ts +396 -0
- package/types/ngx-com-components-tooltip.d.ts +200 -0
- package/types/ngx-com-components.d.ts +12 -0
- package/types/ngx-com-tokens.d.ts +7 -0
- package/types/ngx-com-utils.d.ts +424 -0
- package/types/ngx-com.d.ts +10 -7
|
@@ -0,0 +1,1522 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { inject, TemplateRef, Directive, input, booleanAttribute, output, contentChild, viewChild, signal, computed, ChangeDetectionStrategy, Component, DestroyRef, viewChildren, afterNextRender, effect, model, contentChildren, ElementRef } from '@angular/core';
|
|
4
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
5
|
+
import { FocusKeyManager } from '@angular/cdk/a11y';
|
|
6
|
+
import { mergeClasses } from 'ngx-com/utils';
|
|
7
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
8
|
+
import { RouterLinkActive } from '@angular/router';
|
|
9
|
+
import { startWith } from 'rxjs';
|
|
10
|
+
|
|
11
|
+
// ─── Tab Button / Link ───
|
|
12
|
+
const tabItemVariants = cva([
|
|
13
|
+
'relative inline-flex items-center justify-center gap-2',
|
|
14
|
+
'whitespace-nowrap font-medium select-none',
|
|
15
|
+
'transition-colors duration-150',
|
|
16
|
+
'disabled:bg-disabled disabled:text-disabled-foreground disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
17
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
|
|
18
|
+
], {
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
underline: 'bg-transparent border-b-2 border-transparent rounded-none',
|
|
22
|
+
pill: 'rounded-pill',
|
|
23
|
+
outline: 'border border-transparent rounded-tab',
|
|
24
|
+
solid: 'rounded-tab',
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
sm: 'h-8 px-3 text-xs gap-1.5',
|
|
28
|
+
md: 'h-10 px-4 text-sm gap-2',
|
|
29
|
+
lg: 'h-12 px-5 text-base gap-2.5',
|
|
30
|
+
},
|
|
31
|
+
color: {
|
|
32
|
+
primary: '',
|
|
33
|
+
accent: '',
|
|
34
|
+
muted: '',
|
|
35
|
+
},
|
|
36
|
+
active: {
|
|
37
|
+
true: '',
|
|
38
|
+
false: '',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
compoundVariants: [
|
|
42
|
+
// ─── Underline ───
|
|
43
|
+
// Active
|
|
44
|
+
{
|
|
45
|
+
variant: 'underline',
|
|
46
|
+
color: 'primary',
|
|
47
|
+
active: true,
|
|
48
|
+
class: 'border-b-primary text-primary',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
variant: 'underline',
|
|
52
|
+
color: 'accent',
|
|
53
|
+
active: true,
|
|
54
|
+
class: 'border-b-accent text-accent',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
variant: 'underline',
|
|
58
|
+
color: 'muted',
|
|
59
|
+
active: true,
|
|
60
|
+
class: 'border-b-foreground text-foreground',
|
|
61
|
+
},
|
|
62
|
+
// Inactive
|
|
63
|
+
{
|
|
64
|
+
variant: 'underline',
|
|
65
|
+
active: false,
|
|
66
|
+
class: 'text-muted-foreground hover:text-foreground hover:border-b-border',
|
|
67
|
+
},
|
|
68
|
+
// ─── Pill ───
|
|
69
|
+
{
|
|
70
|
+
variant: 'pill',
|
|
71
|
+
color: 'primary',
|
|
72
|
+
active: true,
|
|
73
|
+
class: 'bg-primary text-primary-foreground',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
variant: 'pill',
|
|
77
|
+
color: 'accent',
|
|
78
|
+
active: true,
|
|
79
|
+
class: 'bg-accent text-accent-foreground',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
variant: 'pill',
|
|
83
|
+
color: 'muted',
|
|
84
|
+
active: true,
|
|
85
|
+
class: 'bg-muted text-foreground',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
variant: 'pill',
|
|
89
|
+
active: false,
|
|
90
|
+
class: 'text-muted-foreground hover:text-foreground hover:bg-muted',
|
|
91
|
+
},
|
|
92
|
+
// ─── Outline ───
|
|
93
|
+
{
|
|
94
|
+
variant: 'outline',
|
|
95
|
+
color: 'primary',
|
|
96
|
+
active: true,
|
|
97
|
+
class: 'border-primary text-primary bg-primary-subtle',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
variant: 'outline',
|
|
101
|
+
color: 'accent',
|
|
102
|
+
active: true,
|
|
103
|
+
class: 'border-accent text-accent bg-accent-subtle',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
variant: 'outline',
|
|
107
|
+
color: 'muted',
|
|
108
|
+
active: true,
|
|
109
|
+
class: 'border-border text-foreground bg-muted',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
variant: 'outline',
|
|
113
|
+
active: false,
|
|
114
|
+
class: 'text-muted-foreground hover:text-foreground hover:border-border',
|
|
115
|
+
},
|
|
116
|
+
// ─── Solid ───
|
|
117
|
+
{
|
|
118
|
+
variant: 'solid',
|
|
119
|
+
color: 'primary',
|
|
120
|
+
active: true,
|
|
121
|
+
class: 'bg-primary text-primary-foreground shadow-sm',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
variant: 'solid',
|
|
125
|
+
color: 'accent',
|
|
126
|
+
active: true,
|
|
127
|
+
class: 'bg-accent text-accent-foreground shadow-sm',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
variant: 'solid',
|
|
131
|
+
color: 'muted',
|
|
132
|
+
active: true,
|
|
133
|
+
class: 'bg-muted text-foreground shadow-sm',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
variant: 'solid',
|
|
137
|
+
active: false,
|
|
138
|
+
class: 'text-muted-foreground hover:text-foreground hover:bg-muted',
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
defaultVariants: {
|
|
142
|
+
variant: 'underline',
|
|
143
|
+
size: 'md',
|
|
144
|
+
color: 'primary',
|
|
145
|
+
active: false,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
// ─── Tab Header / Nav Bar Container ───
|
|
149
|
+
const tabHeaderVariants = cva('relative flex', {
|
|
150
|
+
variants: {
|
|
151
|
+
alignment: {
|
|
152
|
+
start: 'justify-start',
|
|
153
|
+
center: 'justify-center',
|
|
154
|
+
end: 'justify-end',
|
|
155
|
+
stretch: '[&>*]:flex-1',
|
|
156
|
+
},
|
|
157
|
+
variant: {
|
|
158
|
+
underline: 'border-b border-border',
|
|
159
|
+
pill: 'gap-1 p-1 bg-muted rounded-tab-list',
|
|
160
|
+
outline: 'gap-1',
|
|
161
|
+
solid: 'gap-1 p-1 bg-muted rounded-tab-list',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
defaultVariants: {
|
|
165
|
+
alignment: 'start',
|
|
166
|
+
variant: 'underline',
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
// ─── Scroll Button ───
|
|
170
|
+
const tabScrollButtonVariants = cva([
|
|
171
|
+
'absolute top-0 z-10 flex items-center justify-center',
|
|
172
|
+
'h-full w-8',
|
|
173
|
+
'text-muted-foreground hover:text-foreground',
|
|
174
|
+
'transition-opacity duration-150',
|
|
175
|
+
'focus-visible:outline-none',
|
|
176
|
+
], {
|
|
177
|
+
variants: {
|
|
178
|
+
direction: {
|
|
179
|
+
left: 'left-0 bg-gradient-to-r from-background to-transparent',
|
|
180
|
+
right: 'right-0 bg-gradient-to-l from-background to-transparent',
|
|
181
|
+
},
|
|
182
|
+
variant: {
|
|
183
|
+
underline: '',
|
|
184
|
+
pill: 'from-muted',
|
|
185
|
+
outline: '',
|
|
186
|
+
solid: 'from-muted',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
defaultVariants: {
|
|
190
|
+
direction: 'left',
|
|
191
|
+
variant: 'underline',
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
// ─── Close Button on Closable Tabs ───
|
|
195
|
+
const tabCloseButtonVariants = cva([
|
|
196
|
+
'inline-flex items-center justify-center rounded-interactive-sm',
|
|
197
|
+
'text-current opacity-60 hover:opacity-100',
|
|
198
|
+
'transition-opacity duration-100',
|
|
199
|
+
'focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring',
|
|
200
|
+
], {
|
|
201
|
+
variants: {
|
|
202
|
+
size: {
|
|
203
|
+
sm: 'h-3.5 w-3.5',
|
|
204
|
+
md: 'h-4 w-4',
|
|
205
|
+
lg: 'h-5 w-5',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
defaultVariants: { size: 'md' },
|
|
209
|
+
});
|
|
210
|
+
// ─── Tab Panel Container ───
|
|
211
|
+
const tabPanelVariants = cva('focus-visible:outline-none', {
|
|
212
|
+
variants: {
|
|
213
|
+
animated: {
|
|
214
|
+
true: 'animate-fade-in',
|
|
215
|
+
false: '',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
defaultVariants: {
|
|
219
|
+
animated: true,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Marker directive for custom tab label templates.
|
|
225
|
+
*
|
|
226
|
+
* Provides a custom label template for rich tab headers (icons, badges, counters).
|
|
227
|
+
* Applied to an `<ng-template>` inside `<ui-tab>`.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```html
|
|
231
|
+
* <com-tab>
|
|
232
|
+
* <ng-template comTabLabel>
|
|
233
|
+
* <svg class="w-4 h-4">...</svg>
|
|
234
|
+
* <span>Settings</span>
|
|
235
|
+
* <span class="bg-warn text-warn-foreground text-xs rounded-pill px-1.5">3</span>
|
|
236
|
+
* </ng-template>
|
|
237
|
+
* <p>Tab content here.</p>
|
|
238
|
+
* </com-tab>
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
class TabLabelDirective {
|
|
242
|
+
templateRef = inject(TemplateRef);
|
|
243
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabLabelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
247
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: TabLabelDirective, isStandalone: true, selector: "ng-template[comTabLabel]", ngImport: i0 });
|
|
248
|
+
}
|
|
249
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabLabelDirective, decorators: [{
|
|
250
|
+
type: Directive,
|
|
251
|
+
args: [{
|
|
252
|
+
selector: 'ng-template[comTabLabel]',
|
|
253
|
+
}]
|
|
254
|
+
}] });
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Marker directive for lazy tab content rendering.
|
|
258
|
+
*
|
|
259
|
+
* Content wrapped in `<ng-template comTabContent>` is only instantiated
|
|
260
|
+
* when the tab becomes active for the first time.
|
|
261
|
+
*
|
|
262
|
+
* @example Lazy loaded content
|
|
263
|
+
* ```html
|
|
264
|
+
* <com-tab label="Analytics">
|
|
265
|
+
* <ng-template comTabContent>
|
|
266
|
+
* <app-analytics-dashboard />
|
|
267
|
+
* </ng-template>
|
|
268
|
+
* </com-tab>
|
|
269
|
+
* ```
|
|
270
|
+
*
|
|
271
|
+
* @example Combined with @defer
|
|
272
|
+
* ```html
|
|
273
|
+
* <com-tab label="Reports">
|
|
274
|
+
* <ng-template comTabContent>
|
|
275
|
+
* @defer {
|
|
276
|
+
* <app-report-builder />
|
|
277
|
+
* } @loading {
|
|
278
|
+
* <p>Loading reports...</p>
|
|
279
|
+
* }
|
|
280
|
+
* </ng-template>
|
|
281
|
+
* </com-tab>
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
class TabContentDirective {
|
|
285
|
+
templateRef = inject(TemplateRef);
|
|
286
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
290
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: TabContentDirective, isStandalone: true, selector: "ng-template[comTabContent]", ngImport: i0 });
|
|
291
|
+
}
|
|
292
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabContentDirective, decorators: [{
|
|
293
|
+
type: Directive,
|
|
294
|
+
args: [{
|
|
295
|
+
selector: 'ng-template[comTabContent]',
|
|
296
|
+
}]
|
|
297
|
+
}] });
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Individual tab definition component.
|
|
301
|
+
*
|
|
302
|
+
* This is a **definition component** — it doesn't render anything itself.
|
|
303
|
+
* It provides a label and content template to the parent `TabGroupComponent`.
|
|
304
|
+
*
|
|
305
|
+
* @example Basic usage
|
|
306
|
+
* ```html
|
|
307
|
+
* <com-tab label="Overview">
|
|
308
|
+
* <p>Overview content.</p>
|
|
309
|
+
* </com-tab>
|
|
310
|
+
* ```
|
|
311
|
+
*
|
|
312
|
+
* @example Custom label with icon
|
|
313
|
+
* ```html
|
|
314
|
+
* <com-tab>
|
|
315
|
+
* <ng-template comTabLabel>
|
|
316
|
+
* <svg class="w-4 h-4"><!-- icon --></svg>
|
|
317
|
+
* <span>Settings</span>
|
|
318
|
+
* </ng-template>
|
|
319
|
+
* <p>Settings content.</p>
|
|
320
|
+
* </com-tab>
|
|
321
|
+
* ```
|
|
322
|
+
*
|
|
323
|
+
* @example Lazy loaded content
|
|
324
|
+
* ```html
|
|
325
|
+
* <com-tab label="Analytics">
|
|
326
|
+
* <ng-template comTabContent>
|
|
327
|
+
* <app-heavy-dashboard />
|
|
328
|
+
* </ng-template>
|
|
329
|
+
* </com-tab>
|
|
330
|
+
* ```
|
|
331
|
+
*
|
|
332
|
+
* @example Closable tab
|
|
333
|
+
* ```html
|
|
334
|
+
* <com-tab label="Document" [closable]="true" (closed)="onClose()">
|
|
335
|
+
* <p>Document content.</p>
|
|
336
|
+
* </com-tab>
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
class TabComponent {
|
|
340
|
+
// ─── Inputs ───
|
|
341
|
+
/** Plain text label; ignored if `[comTabLabel]` template is provided. */
|
|
342
|
+
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
343
|
+
/** Prevents selection when true. */
|
|
344
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : {}), transform: booleanAttribute });
|
|
345
|
+
/** Shows a close/remove button on the tab. */
|
|
346
|
+
closable = input(false, { ...(ngDevMode ? { debugName: "closable" } : {}), transform: booleanAttribute });
|
|
347
|
+
// ─── Outputs ───
|
|
348
|
+
/** Emitted when the close button is clicked. */
|
|
349
|
+
closed = output();
|
|
350
|
+
// ─── Template References ───
|
|
351
|
+
/** Custom label template (queried from content). */
|
|
352
|
+
customLabel = contentChild(TabLabelDirective, ...(ngDevMode ? [{ debugName: "customLabel" }] : []));
|
|
353
|
+
/** Lazy content template (queried from content). */
|
|
354
|
+
lazyContent = contentChild(TabContentDirective, ...(ngDevMode ? [{ debugName: "lazyContent" }] : []));
|
|
355
|
+
/** Implicit content template from ng-content. */
|
|
356
|
+
implicitContent = viewChild('implicitContent', ...(ngDevMode ? [{ debugName: "implicitContent" }] : []));
|
|
357
|
+
// ─── State (set by parent TabGroupComponent) ───
|
|
358
|
+
/** Whether this tab is currently active. Set by TabGroupComponent. */
|
|
359
|
+
isActive = signal(false, ...(ngDevMode ? [{ debugName: "isActive" }] : []));
|
|
360
|
+
/** Whether this tab content has been rendered at least once. */
|
|
361
|
+
hasBeenActivated = signal(false, ...(ngDevMode ? [{ debugName: "hasBeenActivated" }] : []));
|
|
362
|
+
// ─── Computed ───
|
|
363
|
+
/**
|
|
364
|
+
* Returns the label template if provided, otherwise null.
|
|
365
|
+
* Parent uses this to decide between string label or template.
|
|
366
|
+
*/
|
|
367
|
+
labelTemplate = computed(() => this.customLabel()?.templateRef ?? null, ...(ngDevMode ? [{ debugName: "labelTemplate" }] : []));
|
|
368
|
+
/**
|
|
369
|
+
* Returns the content template: lazy template if present, else implicit content.
|
|
370
|
+
*/
|
|
371
|
+
contentTemplate = computed(() => this.lazyContent()?.templateRef ?? this.implicitContent(), ...(ngDevMode ? [{ debugName: "contentTemplate" }] : []));
|
|
372
|
+
/**
|
|
373
|
+
* Whether this tab uses lazy loading.
|
|
374
|
+
*/
|
|
375
|
+
isLazy = computed(() => !!this.lazyContent(), ...(ngDevMode ? [{ debugName: "isLazy" }] : []));
|
|
376
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
377
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.0", type: TabComponent, isStandalone: true, selector: "com-tab", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closable: { classPropertyName: "closable", publicName: "closable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, queries: [{ propertyName: "customLabel", first: true, predicate: TabLabelDirective, descendants: true, isSignal: true }, { propertyName: "lazyContent", first: true, predicate: TabContentDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "implicitContent", first: true, predicate: ["implicitContent"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
378
|
+
<ng-template #implicitContent>
|
|
379
|
+
<ng-content />
|
|
380
|
+
</ng-template>
|
|
381
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
382
|
+
}
|
|
383
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabComponent, decorators: [{
|
|
384
|
+
type: Component,
|
|
385
|
+
args: [{
|
|
386
|
+
selector: 'com-tab',
|
|
387
|
+
template: `
|
|
388
|
+
<ng-template #implicitContent>
|
|
389
|
+
<ng-content />
|
|
390
|
+
</ng-template>
|
|
391
|
+
`,
|
|
392
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
393
|
+
}]
|
|
394
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], closable: [{ type: i0.Input, args: [{ isSignal: true, alias: "closable", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], customLabel: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TabLabelDirective), { isSignal: true }] }], lazyContent: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TabContentDirective), { isSignal: true }] }], implicitContent: [{ type: i0.ViewChild, args: ['implicitContent', { isSignal: true }] }] } });
|
|
395
|
+
|
|
396
|
+
let tabIdCounter = 0;
|
|
397
|
+
/**
|
|
398
|
+
* Generates a unique ID for tab components.
|
|
399
|
+
*/
|
|
400
|
+
function generateTabId() {
|
|
401
|
+
return `com-tab-${++tabIdCounter}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Internal scrollable tab header component.
|
|
406
|
+
*
|
|
407
|
+
* Handles overflow detection, scroll buttons, keyboard navigation,
|
|
408
|
+
* and the active indicator (for underline variant).
|
|
409
|
+
*
|
|
410
|
+
* @internal Not exported in public API.
|
|
411
|
+
*/
|
|
412
|
+
class TabHeaderComponent {
|
|
413
|
+
destroyRef = inject(DestroyRef);
|
|
414
|
+
// ─── Inputs ───
|
|
415
|
+
tabs = input.required(...(ngDevMode ? [{ debugName: "tabs" }] : []));
|
|
416
|
+
selectedIndex = input.required(...(ngDevMode ? [{ debugName: "selectedIndex" }] : []));
|
|
417
|
+
variant = input('underline', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
418
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
419
|
+
color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : []));
|
|
420
|
+
alignment = input('start', ...(ngDevMode ? [{ debugName: "alignment" }] : []));
|
|
421
|
+
baseId = input.required(...(ngDevMode ? [{ debugName: "baseId" }] : []));
|
|
422
|
+
// ─── Outputs ───
|
|
423
|
+
tabSelected = output();
|
|
424
|
+
tabFocused = output();
|
|
425
|
+
tabClosed = output();
|
|
426
|
+
// ─── View Children ───
|
|
427
|
+
scrollContainer = viewChild('scrollContainer', ...(ngDevMode ? [{ debugName: "scrollContainer" }] : []));
|
|
428
|
+
tabButtons = viewChildren('tabButton', ...(ngDevMode ? [{ debugName: "tabButtons" }] : []));
|
|
429
|
+
// ─── State ───
|
|
430
|
+
scrollLeftValue = signal(0, ...(ngDevMode ? [{ debugName: "scrollLeftValue" }] : []));
|
|
431
|
+
containerWidth = signal(0, ...(ngDevMode ? [{ debugName: "containerWidth" }] : []));
|
|
432
|
+
scrollWidth = signal(0, ...(ngDevMode ? [{ debugName: "scrollWidth" }] : []));
|
|
433
|
+
indicatorLeft = signal(0, ...(ngDevMode ? [{ debugName: "indicatorLeft" }] : []));
|
|
434
|
+
indicatorWidth = signal(0, ...(ngDevMode ? [{ debugName: "indicatorWidth" }] : []));
|
|
435
|
+
keyManager = null;
|
|
436
|
+
resizeObserver = null;
|
|
437
|
+
// ─── Computed ───
|
|
438
|
+
hasOverflow = computed(() => this.scrollWidth() > this.containerWidth(), ...(ngDevMode ? [{ debugName: "hasOverflow" }] : []));
|
|
439
|
+
showScrollLeft = computed(() => this.hasOverflow() && this.scrollLeftValue() > 0, ...(ngDevMode ? [{ debugName: "showScrollLeft" }] : []));
|
|
440
|
+
showScrollRight = computed(() => {
|
|
441
|
+
const remaining = this.scrollWidth() - this.containerWidth() - this.scrollLeftValue();
|
|
442
|
+
return this.hasOverflow() && remaining > 1;
|
|
443
|
+
}, ...(ngDevMode ? [{ debugName: "showScrollRight" }] : []));
|
|
444
|
+
headerClasses = computed(() => mergeClasses(tabHeaderVariants({
|
|
445
|
+
alignment: this.alignment(),
|
|
446
|
+
variant: this.variant(),
|
|
447
|
+
}), 'relative'), ...(ngDevMode ? [{ debugName: "headerClasses" }] : []));
|
|
448
|
+
scrollLeftClasses = computed(() => tabScrollButtonVariants({
|
|
449
|
+
direction: 'left',
|
|
450
|
+
variant: this.variant(),
|
|
451
|
+
}), ...(ngDevMode ? [{ debugName: "scrollLeftClasses" }] : []));
|
|
452
|
+
scrollRightClasses = computed(() => tabScrollButtonVariants({
|
|
453
|
+
direction: 'right',
|
|
454
|
+
variant: this.variant(),
|
|
455
|
+
}), ...(ngDevMode ? [{ debugName: "scrollRightClasses" }] : []));
|
|
456
|
+
closeButtonClasses = computed(() => tabCloseButtonVariants({ size: this.size() }), ...(ngDevMode ? [{ debugName: "closeButtonClasses" }] : []));
|
|
457
|
+
indicatorColorClass = computed(() => {
|
|
458
|
+
const colorMap = {
|
|
459
|
+
primary: 'text-primary',
|
|
460
|
+
accent: 'text-accent',
|
|
461
|
+
muted: 'text-foreground',
|
|
462
|
+
};
|
|
463
|
+
return colorMap[this.color()];
|
|
464
|
+
}, ...(ngDevMode ? [{ debugName: "indicatorColorClass" }] : []));
|
|
465
|
+
constructor() {
|
|
466
|
+
// Setup resize observer after render
|
|
467
|
+
afterNextRender(() => {
|
|
468
|
+
this.setupResizeObserver();
|
|
469
|
+
this.updateScrollState();
|
|
470
|
+
this.updateIndicator();
|
|
471
|
+
this.setupKeyManager();
|
|
472
|
+
});
|
|
473
|
+
// Update indicator when selected index changes
|
|
474
|
+
effect(() => {
|
|
475
|
+
this.selectedIndex();
|
|
476
|
+
this.updateIndicator();
|
|
477
|
+
});
|
|
478
|
+
// Cleanup on destroy
|
|
479
|
+
this.destroyRef.onDestroy(() => {
|
|
480
|
+
this.resizeObserver?.disconnect();
|
|
481
|
+
this.keyManager?.destroy();
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
// ─── Public Methods ───
|
|
485
|
+
getTabId(index) {
|
|
486
|
+
return `${this.baseId()}-tab-${index}`;
|
|
487
|
+
}
|
|
488
|
+
getPanelId(index) {
|
|
489
|
+
return `${this.baseId()}-panel-${index}`;
|
|
490
|
+
}
|
|
491
|
+
getTabClasses(index) {
|
|
492
|
+
return tabItemVariants({
|
|
493
|
+
variant: this.variant(),
|
|
494
|
+
size: this.size(),
|
|
495
|
+
color: this.color(),
|
|
496
|
+
active: this.selectedIndex() === index,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
selectTab(index) {
|
|
500
|
+
const tab = this.tabs()[index];
|
|
501
|
+
if (tab && !tab.disabled()) {
|
|
502
|
+
this.tabSelected.emit(index);
|
|
503
|
+
this.scrollTabIntoView(index);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
scrollLeft() {
|
|
507
|
+
const container = this.scrollContainer()?.nativeElement;
|
|
508
|
+
if (container) {
|
|
509
|
+
const scrollAmount = container.clientWidth * 0.75;
|
|
510
|
+
container.scrollTo({
|
|
511
|
+
left: container.scrollLeft - scrollAmount,
|
|
512
|
+
behavior: 'smooth',
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
scrollRight() {
|
|
517
|
+
const container = this.scrollContainer()?.nativeElement;
|
|
518
|
+
if (container) {
|
|
519
|
+
const scrollAmount = container.clientWidth * 0.75;
|
|
520
|
+
container.scrollTo({
|
|
521
|
+
left: container.scrollLeft + scrollAmount,
|
|
522
|
+
behavior: 'smooth',
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// ─── Event Handlers ───
|
|
527
|
+
onScroll() {
|
|
528
|
+
this.updateScrollState();
|
|
529
|
+
}
|
|
530
|
+
onTabFocus(index) {
|
|
531
|
+
this.tabFocused.emit(index);
|
|
532
|
+
this.scrollTabIntoView(index);
|
|
533
|
+
}
|
|
534
|
+
closeTab(event, index) {
|
|
535
|
+
event.stopPropagation();
|
|
536
|
+
this.tabClosed.emit(index);
|
|
537
|
+
}
|
|
538
|
+
onKeydown(event) {
|
|
539
|
+
if (!this.keyManager)
|
|
540
|
+
return;
|
|
541
|
+
switch (event.key) {
|
|
542
|
+
case 'ArrowLeft':
|
|
543
|
+
this.keyManager.setPreviousItemActive();
|
|
544
|
+
event.preventDefault();
|
|
545
|
+
break;
|
|
546
|
+
case 'ArrowRight':
|
|
547
|
+
this.keyManager.setNextItemActive();
|
|
548
|
+
event.preventDefault();
|
|
549
|
+
break;
|
|
550
|
+
case 'Home':
|
|
551
|
+
this.keyManager.setFirstItemActive();
|
|
552
|
+
event.preventDefault();
|
|
553
|
+
break;
|
|
554
|
+
case 'End':
|
|
555
|
+
this.keyManager.setLastItemActive();
|
|
556
|
+
event.preventDefault();
|
|
557
|
+
break;
|
|
558
|
+
case 'Enter':
|
|
559
|
+
case ' ':
|
|
560
|
+
const activeIndex = this.keyManager.activeItemIndex;
|
|
561
|
+
if (activeIndex !== null && activeIndex >= 0) {
|
|
562
|
+
this.selectTab(activeIndex);
|
|
563
|
+
}
|
|
564
|
+
event.preventDefault();
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// ─── Private Methods ───
|
|
569
|
+
setupResizeObserver() {
|
|
570
|
+
const container = this.scrollContainer()?.nativeElement;
|
|
571
|
+
if (!container)
|
|
572
|
+
return;
|
|
573
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
574
|
+
this.updateScrollState();
|
|
575
|
+
this.updateIndicator();
|
|
576
|
+
});
|
|
577
|
+
this.resizeObserver.observe(container);
|
|
578
|
+
}
|
|
579
|
+
updateScrollState() {
|
|
580
|
+
const container = this.scrollContainer()?.nativeElement;
|
|
581
|
+
if (!container)
|
|
582
|
+
return;
|
|
583
|
+
this.scrollLeftValue.set(container.scrollLeft);
|
|
584
|
+
this.containerWidth.set(container.clientWidth);
|
|
585
|
+
this.scrollWidth.set(container.scrollWidth);
|
|
586
|
+
}
|
|
587
|
+
updateIndicator() {
|
|
588
|
+
const buttons = this.tabButtons();
|
|
589
|
+
const index = this.selectedIndex();
|
|
590
|
+
if (buttons.length === 0 || index < 0 || index >= buttons.length) {
|
|
591
|
+
this.indicatorLeft.set(0);
|
|
592
|
+
this.indicatorWidth.set(0);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const button = buttons[index]?.nativeElement;
|
|
596
|
+
if (button) {
|
|
597
|
+
this.indicatorLeft.set(button.offsetLeft);
|
|
598
|
+
this.indicatorWidth.set(button.offsetWidth);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
scrollTabIntoView(index) {
|
|
602
|
+
const container = this.scrollContainer()?.nativeElement;
|
|
603
|
+
const buttons = this.tabButtons();
|
|
604
|
+
if (!container || index < 0 || index >= buttons.length)
|
|
605
|
+
return;
|
|
606
|
+
const button = buttons[index]?.nativeElement;
|
|
607
|
+
if (!button)
|
|
608
|
+
return;
|
|
609
|
+
const containerRect = container.getBoundingClientRect();
|
|
610
|
+
const buttonRect = button.getBoundingClientRect();
|
|
611
|
+
if (buttonRect.left < containerRect.left) {
|
|
612
|
+
container.scrollTo({
|
|
613
|
+
left: container.scrollLeft - (containerRect.left - buttonRect.left) - 16,
|
|
614
|
+
behavior: 'smooth',
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
else if (buttonRect.right > containerRect.right) {
|
|
618
|
+
container.scrollTo({
|
|
619
|
+
left: container.scrollLeft + (buttonRect.right - containerRect.right) + 16,
|
|
620
|
+
behavior: 'smooth',
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
setupKeyManager() {
|
|
625
|
+
const items = this.createKeyManagerItems();
|
|
626
|
+
this.keyManager = new FocusKeyManager(items)
|
|
627
|
+
.withHorizontalOrientation('ltr')
|
|
628
|
+
.withWrap()
|
|
629
|
+
.skipPredicate(item => item.disabled);
|
|
630
|
+
this.keyManager.setActiveItem(this.selectedIndex());
|
|
631
|
+
}
|
|
632
|
+
createKeyManagerItems() {
|
|
633
|
+
const buttons = this.tabButtons();
|
|
634
|
+
const tabs = this.tabs();
|
|
635
|
+
return buttons.map((buttonRef, index) => ({
|
|
636
|
+
focus: () => buttonRef.nativeElement.focus(),
|
|
637
|
+
disabled: tabs[index]?.disabled() ?? false,
|
|
638
|
+
}));
|
|
639
|
+
}
|
|
640
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
641
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: TabHeaderComponent, isStandalone: true, selector: "com-tab-header", inputs: { tabs: { classPropertyName: "tabs", publicName: "tabs", isSignal: true, isRequired: true, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: true, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, baseId: { classPropertyName: "baseId", publicName: "baseId", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { tabSelected: "tabSelected", tabFocused: "tabFocused", tabClosed: "tabClosed" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }, { propertyName: "tabButtons", predicate: ["tabButton"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
642
|
+
<!-- Scroll button left -->
|
|
643
|
+
@if (showScrollLeft()) {
|
|
644
|
+
<button
|
|
645
|
+
type="button"
|
|
646
|
+
[class]="scrollLeftClasses()"
|
|
647
|
+
(click)="scrollLeft()"
|
|
648
|
+
aria-hidden="true"
|
|
649
|
+
tabindex="-1"
|
|
650
|
+
>
|
|
651
|
+
<svg
|
|
652
|
+
class="h-4 w-4"
|
|
653
|
+
viewBox="0 0 24 24"
|
|
654
|
+
fill="none"
|
|
655
|
+
stroke="currentColor"
|
|
656
|
+
stroke-width="2"
|
|
657
|
+
stroke-linecap="round"
|
|
658
|
+
stroke-linejoin="round"
|
|
659
|
+
>
|
|
660
|
+
<polyline points="15 18 9 12 15 6" />
|
|
661
|
+
</svg>
|
|
662
|
+
</button>
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
<!-- Tab list -->
|
|
666
|
+
<div
|
|
667
|
+
#scrollContainer
|
|
668
|
+
class="flex overflow-x-auto scrollbar-none"
|
|
669
|
+
[class]="headerClasses()"
|
|
670
|
+
role="tablist"
|
|
671
|
+
[attr.aria-orientation]="'horizontal'"
|
|
672
|
+
(scroll)="onScroll()"
|
|
673
|
+
(keydown)="onKeydown($event)"
|
|
674
|
+
>
|
|
675
|
+
@for (tab of tabs(); track $index; let i = $index) {
|
|
676
|
+
<button
|
|
677
|
+
#tabButton
|
|
678
|
+
type="button"
|
|
679
|
+
role="tab"
|
|
680
|
+
[id]="getTabId(i)"
|
|
681
|
+
[class]="getTabClasses(i)"
|
|
682
|
+
[attr.aria-selected]="selectedIndex() === i"
|
|
683
|
+
[attr.aria-controls]="getPanelId(i)"
|
|
684
|
+
[attr.aria-disabled]="tab.disabled() || null"
|
|
685
|
+
[attr.data-state]="selectedIndex() === i ? 'active' : 'inactive'"
|
|
686
|
+
[disabled]="tab.disabled()"
|
|
687
|
+
[tabindex]="selectedIndex() === i ? 0 : -1"
|
|
688
|
+
(click)="selectTab(i)"
|
|
689
|
+
(focus)="onTabFocus(i)"
|
|
690
|
+
>
|
|
691
|
+
@if (tab.labelTemplate()) {
|
|
692
|
+
<ng-container [ngTemplateOutlet]="tab.labelTemplate()!" />
|
|
693
|
+
} @else {
|
|
694
|
+
{{ tab.label() }}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
@if (tab.closable()) {
|
|
698
|
+
<span
|
|
699
|
+
role="button"
|
|
700
|
+
[class]="closeButtonClasses()"
|
|
701
|
+
(click)="closeTab($event, i)"
|
|
702
|
+
[attr.aria-label]="'Close ' + tab.label()"
|
|
703
|
+
>
|
|
704
|
+
<svg
|
|
705
|
+
viewBox="0 0 24 24"
|
|
706
|
+
fill="none"
|
|
707
|
+
stroke="currentColor"
|
|
708
|
+
stroke-width="2"
|
|
709
|
+
stroke-linecap="round"
|
|
710
|
+
stroke-linejoin="round"
|
|
711
|
+
class="h-full w-full"
|
|
712
|
+
>
|
|
713
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
714
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
715
|
+
</svg>
|
|
716
|
+
</span>
|
|
717
|
+
}
|
|
718
|
+
</button>
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
<!-- Active indicator (underline variant only) -->
|
|
722
|
+
@if (variant() === 'underline') {
|
|
723
|
+
<div
|
|
724
|
+
class="absolute bottom-0 h-0.5 bg-current transition-all duration-200 ease-out"
|
|
725
|
+
[class]="indicatorColorClass()"
|
|
726
|
+
[style.left.px]="indicatorLeft()"
|
|
727
|
+
[style.width.px]="indicatorWidth()"
|
|
728
|
+
></div>
|
|
729
|
+
}
|
|
730
|
+
</div>
|
|
731
|
+
|
|
732
|
+
<!-- Scroll button right -->
|
|
733
|
+
@if (showScrollRight()) {
|
|
734
|
+
<button
|
|
735
|
+
type="button"
|
|
736
|
+
[class]="scrollRightClasses()"
|
|
737
|
+
(click)="scrollRight()"
|
|
738
|
+
aria-hidden="true"
|
|
739
|
+
tabindex="-1"
|
|
740
|
+
>
|
|
741
|
+
<svg
|
|
742
|
+
class="h-4 w-4"
|
|
743
|
+
viewBox="0 0 24 24"
|
|
744
|
+
fill="none"
|
|
745
|
+
stroke="currentColor"
|
|
746
|
+
stroke-width="2"
|
|
747
|
+
stroke-linecap="round"
|
|
748
|
+
stroke-linejoin="round"
|
|
749
|
+
>
|
|
750
|
+
<polyline points="9 18 15 12 9 6" />
|
|
751
|
+
</svg>
|
|
752
|
+
</button>
|
|
753
|
+
}
|
|
754
|
+
`, isInline: true, styles: [":host{display:block;position:relative}.scrollbar-none{scrollbar-width:none;-ms-overflow-style:none}.scrollbar-none::-webkit-scrollbar{display:none}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
755
|
+
}
|
|
756
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabHeaderComponent, decorators: [{
|
|
757
|
+
type: Component,
|
|
758
|
+
args: [{ selector: 'com-tab-header', template: `
|
|
759
|
+
<!-- Scroll button left -->
|
|
760
|
+
@if (showScrollLeft()) {
|
|
761
|
+
<button
|
|
762
|
+
type="button"
|
|
763
|
+
[class]="scrollLeftClasses()"
|
|
764
|
+
(click)="scrollLeft()"
|
|
765
|
+
aria-hidden="true"
|
|
766
|
+
tabindex="-1"
|
|
767
|
+
>
|
|
768
|
+
<svg
|
|
769
|
+
class="h-4 w-4"
|
|
770
|
+
viewBox="0 0 24 24"
|
|
771
|
+
fill="none"
|
|
772
|
+
stroke="currentColor"
|
|
773
|
+
stroke-width="2"
|
|
774
|
+
stroke-linecap="round"
|
|
775
|
+
stroke-linejoin="round"
|
|
776
|
+
>
|
|
777
|
+
<polyline points="15 18 9 12 15 6" />
|
|
778
|
+
</svg>
|
|
779
|
+
</button>
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
<!-- Tab list -->
|
|
783
|
+
<div
|
|
784
|
+
#scrollContainer
|
|
785
|
+
class="flex overflow-x-auto scrollbar-none"
|
|
786
|
+
[class]="headerClasses()"
|
|
787
|
+
role="tablist"
|
|
788
|
+
[attr.aria-orientation]="'horizontal'"
|
|
789
|
+
(scroll)="onScroll()"
|
|
790
|
+
(keydown)="onKeydown($event)"
|
|
791
|
+
>
|
|
792
|
+
@for (tab of tabs(); track $index; let i = $index) {
|
|
793
|
+
<button
|
|
794
|
+
#tabButton
|
|
795
|
+
type="button"
|
|
796
|
+
role="tab"
|
|
797
|
+
[id]="getTabId(i)"
|
|
798
|
+
[class]="getTabClasses(i)"
|
|
799
|
+
[attr.aria-selected]="selectedIndex() === i"
|
|
800
|
+
[attr.aria-controls]="getPanelId(i)"
|
|
801
|
+
[attr.aria-disabled]="tab.disabled() || null"
|
|
802
|
+
[attr.data-state]="selectedIndex() === i ? 'active' : 'inactive'"
|
|
803
|
+
[disabled]="tab.disabled()"
|
|
804
|
+
[tabindex]="selectedIndex() === i ? 0 : -1"
|
|
805
|
+
(click)="selectTab(i)"
|
|
806
|
+
(focus)="onTabFocus(i)"
|
|
807
|
+
>
|
|
808
|
+
@if (tab.labelTemplate()) {
|
|
809
|
+
<ng-container [ngTemplateOutlet]="tab.labelTemplate()!" />
|
|
810
|
+
} @else {
|
|
811
|
+
{{ tab.label() }}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
@if (tab.closable()) {
|
|
815
|
+
<span
|
|
816
|
+
role="button"
|
|
817
|
+
[class]="closeButtonClasses()"
|
|
818
|
+
(click)="closeTab($event, i)"
|
|
819
|
+
[attr.aria-label]="'Close ' + tab.label()"
|
|
820
|
+
>
|
|
821
|
+
<svg
|
|
822
|
+
viewBox="0 0 24 24"
|
|
823
|
+
fill="none"
|
|
824
|
+
stroke="currentColor"
|
|
825
|
+
stroke-width="2"
|
|
826
|
+
stroke-linecap="round"
|
|
827
|
+
stroke-linejoin="round"
|
|
828
|
+
class="h-full w-full"
|
|
829
|
+
>
|
|
830
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
831
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
832
|
+
</svg>
|
|
833
|
+
</span>
|
|
834
|
+
}
|
|
835
|
+
</button>
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
<!-- Active indicator (underline variant only) -->
|
|
839
|
+
@if (variant() === 'underline') {
|
|
840
|
+
<div
|
|
841
|
+
class="absolute bottom-0 h-0.5 bg-current transition-all duration-200 ease-out"
|
|
842
|
+
[class]="indicatorColorClass()"
|
|
843
|
+
[style.left.px]="indicatorLeft()"
|
|
844
|
+
[style.width.px]="indicatorWidth()"
|
|
845
|
+
></div>
|
|
846
|
+
}
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
<!-- Scroll button right -->
|
|
850
|
+
@if (showScrollRight()) {
|
|
851
|
+
<button
|
|
852
|
+
type="button"
|
|
853
|
+
[class]="scrollRightClasses()"
|
|
854
|
+
(click)="scrollRight()"
|
|
855
|
+
aria-hidden="true"
|
|
856
|
+
tabindex="-1"
|
|
857
|
+
>
|
|
858
|
+
<svg
|
|
859
|
+
class="h-4 w-4"
|
|
860
|
+
viewBox="0 0 24 24"
|
|
861
|
+
fill="none"
|
|
862
|
+
stroke="currentColor"
|
|
863
|
+
stroke-width="2"
|
|
864
|
+
stroke-linecap="round"
|
|
865
|
+
stroke-linejoin="round"
|
|
866
|
+
>
|
|
867
|
+
<polyline points="9 18 15 12 9 6" />
|
|
868
|
+
</svg>
|
|
869
|
+
</button>
|
|
870
|
+
}
|
|
871
|
+
`, imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative}.scrollbar-none{scrollbar-width:none;-ms-overflow-style:none}.scrollbar-none::-webkit-scrollbar{display:none}\n"] }]
|
|
872
|
+
}], ctorParameters: () => [], propDecorators: { tabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabs", required: true }] }], selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], baseId: [{ type: i0.Input, args: [{ isSignal: true, alias: "baseId", required: true }] }], tabSelected: [{ type: i0.Output, args: ["tabSelected"] }], tabFocused: [{ type: i0.Output, args: ["tabFocused"] }], tabClosed: [{ type: i0.Output, args: ["tabClosed"] }], scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], tabButtons: [{ type: i0.ViewChildren, args: ['tabButton', { isSignal: true }] }] } });
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Tab group component — orchestrates tab state and renders header + panels.
|
|
876
|
+
*
|
|
877
|
+
* @tokens `--color-primary`, `--color-primary-foreground`, `--color-accent`, `--color-accent-foreground`,
|
|
878
|
+
* `--color-muted`, `--color-muted-foreground`, `--color-border`, `--color-ring`,
|
|
879
|
+
* `--color-disabled`, `--color-disabled-foreground`
|
|
880
|
+
*
|
|
881
|
+
* @example Basic usage
|
|
882
|
+
* ```html
|
|
883
|
+
* <com-tab-group>
|
|
884
|
+
* <com-tab label="Overview">
|
|
885
|
+
* <p>Overview content.</p>
|
|
886
|
+
* </com-tab>
|
|
887
|
+
* <com-tab label="Settings">
|
|
888
|
+
* <p>Settings content.</p>
|
|
889
|
+
* </com-tab>
|
|
890
|
+
* </com-tab-group>
|
|
891
|
+
* ```
|
|
892
|
+
*
|
|
893
|
+
* @example With variants
|
|
894
|
+
* ```html
|
|
895
|
+
* <com-tab-group variant="pill" color="accent">
|
|
896
|
+
* <com-tab label="Tab 1"><p>Pill style.</p></com-tab>
|
|
897
|
+
* <com-tab label="Tab 2"><p>Content.</p></com-tab>
|
|
898
|
+
* </com-tab-group>
|
|
899
|
+
* ```
|
|
900
|
+
*
|
|
901
|
+
* @example Two-way binding
|
|
902
|
+
* ```html
|
|
903
|
+
* <com-tab-group [(selectedIndex)]="currentTab">
|
|
904
|
+
* <com-tab label="One"><p>First.</p></com-tab>
|
|
905
|
+
* <com-tab label="Two"><p>Second.</p></com-tab>
|
|
906
|
+
* </com-tab-group>
|
|
907
|
+
* ```
|
|
908
|
+
*
|
|
909
|
+
* @example Lazy loaded content
|
|
910
|
+
* ```html
|
|
911
|
+
* <com-tab-group>
|
|
912
|
+
* <com-tab label="Summary"><p>Loads immediately.</p></com-tab>
|
|
913
|
+
* <com-tab label="Analytics">
|
|
914
|
+
* <ng-template comTabContent>
|
|
915
|
+
* <app-analytics-dashboard />
|
|
916
|
+
* </ng-template>
|
|
917
|
+
* </com-tab>
|
|
918
|
+
* </com-tab-group>
|
|
919
|
+
* ```
|
|
920
|
+
*/
|
|
921
|
+
class TabGroupComponent {
|
|
922
|
+
/** Unique ID for this tab group instance. */
|
|
923
|
+
baseId = generateTabId();
|
|
924
|
+
// ─── Inputs ───
|
|
925
|
+
/** Visual treatment of tab buttons. */
|
|
926
|
+
variant = input('underline', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
927
|
+
/** Controls tab button padding and font size. */
|
|
928
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
929
|
+
/** Active tab color. */
|
|
930
|
+
color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : []));
|
|
931
|
+
/** Tab alignment within the header. */
|
|
932
|
+
alignment = input('start', ...(ngDevMode ? [{ debugName: "alignment" }] : []));
|
|
933
|
+
/** Two-way bindable selected tab index. */
|
|
934
|
+
selectedIndex = model(0, ...(ngDevMode ? [{ debugName: "selectedIndex" }] : []));
|
|
935
|
+
/** Enable/disable panel transition animation. */
|
|
936
|
+
animationEnabled = input(true, { ...(ngDevMode ? { debugName: "animationEnabled" } : {}), transform: booleanAttribute });
|
|
937
|
+
/** When true, keeps inactive tab DOM alive (hidden); when false, destroys inactive tab content. */
|
|
938
|
+
preserveContent = input(false, { ...(ngDevMode ? { debugName: "preserveContent" } : {}), transform: booleanAttribute });
|
|
939
|
+
// ─── Outputs ───
|
|
940
|
+
/** Emits when the selected tab changes with index and tab reference. */
|
|
941
|
+
selectedTabChange = output();
|
|
942
|
+
/** Emits the index of the focused (not yet selected) tab for keyboard navigation feedback. */
|
|
943
|
+
focusChange = output();
|
|
944
|
+
// ─── Content Children ───
|
|
945
|
+
/** All TabComponent children. */
|
|
946
|
+
tabs = contentChildren(TabComponent, ...(ngDevMode ? [{ debugName: "tabs" }] : []));
|
|
947
|
+
// ─── Computed ───
|
|
948
|
+
/** The currently active tab. */
|
|
949
|
+
activeTab = computed(() => this.tabs()[this.selectedIndex()], ...(ngDevMode ? [{ debugName: "activeTab" }] : []));
|
|
950
|
+
/** ID of the active tab button. */
|
|
951
|
+
activeTabId = computed(() => this.getTabId(this.selectedIndex()), ...(ngDevMode ? [{ debugName: "activeTabId" }] : []));
|
|
952
|
+
/** ID of the active panel. */
|
|
953
|
+
activePanelId = computed(() => this.getPanelId(this.selectedIndex()), ...(ngDevMode ? [{ debugName: "activePanelId" }] : []));
|
|
954
|
+
/** Classes for panel container. */
|
|
955
|
+
panelClasses = computed(() => tabPanelVariants({ animated: this.animationEnabled() }), ...(ngDevMode ? [{ debugName: "panelClasses" }] : []));
|
|
956
|
+
constructor() {
|
|
957
|
+
// Update isActive state on tabs when selection changes
|
|
958
|
+
effect(() => {
|
|
959
|
+
const currentIndex = this.selectedIndex();
|
|
960
|
+
const allTabs = this.tabs();
|
|
961
|
+
allTabs.forEach((tab, index) => {
|
|
962
|
+
const isActive = index === currentIndex;
|
|
963
|
+
tab.isActive.set(isActive);
|
|
964
|
+
// Mark as activated for lazy loading
|
|
965
|
+
if (isActive && !tab.hasBeenActivated()) {
|
|
966
|
+
tab.hasBeenActivated.set(true);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
// ─── Public Methods ───
|
|
972
|
+
getTabId(index) {
|
|
973
|
+
return `${this.baseId}-tab-${index}`;
|
|
974
|
+
}
|
|
975
|
+
getPanelId(index) {
|
|
976
|
+
return `${this.baseId}-panel-${index}`;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Determines whether a tab's content should be rendered.
|
|
980
|
+
* For lazy tabs, content is only rendered after first activation.
|
|
981
|
+
*/
|
|
982
|
+
shouldRenderTab(tab, index) {
|
|
983
|
+
// Non-lazy tabs always render their content
|
|
984
|
+
if (!tab.isLazy()) {
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
// Lazy tabs render only if they've been activated at least once
|
|
988
|
+
return tab.hasBeenActivated();
|
|
989
|
+
}
|
|
990
|
+
// ─── Event Handlers ───
|
|
991
|
+
onTabSelected(index) {
|
|
992
|
+
const tab = this.tabs()[index];
|
|
993
|
+
if (tab && !tab.disabled()) {
|
|
994
|
+
this.selectedIndex.set(index);
|
|
995
|
+
this.selectedTabChange.emit({ index, tab });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
onTabFocused(index) {
|
|
999
|
+
this.focusChange.emit(index);
|
|
1000
|
+
}
|
|
1001
|
+
onTabClosed(index) {
|
|
1002
|
+
const tab = this.tabs()[index];
|
|
1003
|
+
if (tab) {
|
|
1004
|
+
tab.closed.emit();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1008
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: TabGroupComponent, isStandalone: true, selector: "com-tab-group", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, animationEnabled: { classPropertyName: "animationEnabled", publicName: "animationEnabled", isSignal: true, isRequired: false, transformFunction: null }, preserveContent: { classPropertyName: "preserveContent", publicName: "preserveContent", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", selectedTabChange: "selectedTabChange", focusChange: "focusChange" }, host: { classAttribute: "com-tab-group" }, queries: [{ propertyName: "tabs", predicate: TabComponent, isSignal: true }], ngImport: i0, template: `
|
|
1009
|
+
<com-tab-header
|
|
1010
|
+
[tabs]="tabs()"
|
|
1011
|
+
[selectedIndex]="selectedIndex()"
|
|
1012
|
+
[variant]="variant()"
|
|
1013
|
+
[size]="size()"
|
|
1014
|
+
[color]="color()"
|
|
1015
|
+
[alignment]="alignment()"
|
|
1016
|
+
[baseId]="baseId"
|
|
1017
|
+
(tabSelected)="onTabSelected($event)"
|
|
1018
|
+
(tabFocused)="onTabFocused($event)"
|
|
1019
|
+
(tabClosed)="onTabClosed($event)"
|
|
1020
|
+
/>
|
|
1021
|
+
|
|
1022
|
+
<div
|
|
1023
|
+
class="mt-2"
|
|
1024
|
+
role="tabpanel"
|
|
1025
|
+
[id]="activePanelId()"
|
|
1026
|
+
[attr.aria-labelledby]="activeTabId()"
|
|
1027
|
+
tabindex="0"
|
|
1028
|
+
>
|
|
1029
|
+
@if (preserveContent()) {
|
|
1030
|
+
@for (tab of tabs(); track $index; let i = $index) {
|
|
1031
|
+
<div
|
|
1032
|
+
[hidden]="selectedIndex() !== i"
|
|
1033
|
+
[class]="panelClasses()"
|
|
1034
|
+
role="tabpanel"
|
|
1035
|
+
[id]="getPanelId(i)"
|
|
1036
|
+
[attr.aria-labelledby]="getTabId(i)"
|
|
1037
|
+
>
|
|
1038
|
+
@if (shouldRenderTab(tab, i)) {
|
|
1039
|
+
<ng-container [ngTemplateOutlet]="tab.contentTemplate()!" />
|
|
1040
|
+
}
|
|
1041
|
+
</div>
|
|
1042
|
+
}
|
|
1043
|
+
} @else {
|
|
1044
|
+
@if (activeTab(); as tab) {
|
|
1045
|
+
<div [class]="panelClasses()">
|
|
1046
|
+
@if (shouldRenderTab(tab, selectedIndex())) {
|
|
1047
|
+
<ng-container [ngTemplateOutlet]="tab.contentTemplate()!" />
|
|
1048
|
+
}
|
|
1049
|
+
</div>
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
</div>
|
|
1053
|
+
`, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "component", type: TabHeaderComponent, selector: "com-tab-header", inputs: ["tabs", "selectedIndex", "variant", "size", "color", "alignment", "baseId"], outputs: ["tabSelected", "tabFocused", "tabClosed"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1054
|
+
}
|
|
1055
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabGroupComponent, decorators: [{
|
|
1056
|
+
type: Component,
|
|
1057
|
+
args: [{ selector: 'com-tab-group', template: `
|
|
1058
|
+
<com-tab-header
|
|
1059
|
+
[tabs]="tabs()"
|
|
1060
|
+
[selectedIndex]="selectedIndex()"
|
|
1061
|
+
[variant]="variant()"
|
|
1062
|
+
[size]="size()"
|
|
1063
|
+
[color]="color()"
|
|
1064
|
+
[alignment]="alignment()"
|
|
1065
|
+
[baseId]="baseId"
|
|
1066
|
+
(tabSelected)="onTabSelected($event)"
|
|
1067
|
+
(tabFocused)="onTabFocused($event)"
|
|
1068
|
+
(tabClosed)="onTabClosed($event)"
|
|
1069
|
+
/>
|
|
1070
|
+
|
|
1071
|
+
<div
|
|
1072
|
+
class="mt-2"
|
|
1073
|
+
role="tabpanel"
|
|
1074
|
+
[id]="activePanelId()"
|
|
1075
|
+
[attr.aria-labelledby]="activeTabId()"
|
|
1076
|
+
tabindex="0"
|
|
1077
|
+
>
|
|
1078
|
+
@if (preserveContent()) {
|
|
1079
|
+
@for (tab of tabs(); track $index; let i = $index) {
|
|
1080
|
+
<div
|
|
1081
|
+
[hidden]="selectedIndex() !== i"
|
|
1082
|
+
[class]="panelClasses()"
|
|
1083
|
+
role="tabpanel"
|
|
1084
|
+
[id]="getPanelId(i)"
|
|
1085
|
+
[attr.aria-labelledby]="getTabId(i)"
|
|
1086
|
+
>
|
|
1087
|
+
@if (shouldRenderTab(tab, i)) {
|
|
1088
|
+
<ng-container [ngTemplateOutlet]="tab.contentTemplate()!" />
|
|
1089
|
+
}
|
|
1090
|
+
</div>
|
|
1091
|
+
}
|
|
1092
|
+
} @else {
|
|
1093
|
+
@if (activeTab(); as tab) {
|
|
1094
|
+
<div [class]="panelClasses()">
|
|
1095
|
+
@if (shouldRenderTab(tab, selectedIndex())) {
|
|
1096
|
+
<ng-container [ngTemplateOutlet]="tab.contentTemplate()!" />
|
|
1097
|
+
}
|
|
1098
|
+
</div>
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
</div>
|
|
1102
|
+
`, imports: [TabHeaderComponent, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1103
|
+
class: 'com-tab-group',
|
|
1104
|
+
}, styles: [":host{display:block}\n"] }]
|
|
1105
|
+
}], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: false }] }, { type: i0.Output, args: ["selectedIndexChange"] }], animationEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "animationEnabled", required: false }] }], preserveContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "preserveContent", required: false }] }], selectedTabChange: [{ type: i0.Output, args: ["selectedTabChange"] }], focusChange: [{ type: i0.Output, args: ["focusChange"] }], tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TabComponent), { isSignal: true }] }] } });
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Tab link directive for route-driven navigation tabs.
|
|
1109
|
+
*
|
|
1110
|
+
* Applied to anchor or button elements inside `com-tab-nav-bar`.
|
|
1111
|
+
* Automatically detects active state from `routerLinkActive` if present.
|
|
1112
|
+
*
|
|
1113
|
+
* @example Basic usage with router
|
|
1114
|
+
* ```html
|
|
1115
|
+
* <nav com-tab-nav-bar>
|
|
1116
|
+
* <a comTabLink routerLink="overview" routerLinkActive>Overview</a>
|
|
1117
|
+
* <a comTabLink routerLink="settings" routerLinkActive>Settings</a>
|
|
1118
|
+
* </nav>
|
|
1119
|
+
* ```
|
|
1120
|
+
*
|
|
1121
|
+
* @example Manual active state control
|
|
1122
|
+
* ```html
|
|
1123
|
+
* <a comTabLink [active]="isOverviewActive">Overview</a>
|
|
1124
|
+
* ```
|
|
1125
|
+
*
|
|
1126
|
+
* @example Disabled link
|
|
1127
|
+
* ```html
|
|
1128
|
+
* <a comTabLink [disabled]="true">Coming Soon</a>
|
|
1129
|
+
* ```
|
|
1130
|
+
*/
|
|
1131
|
+
class TabLinkDirective {
|
|
1132
|
+
routerLinkActive = inject(RouterLinkActive, { optional: true, self: true });
|
|
1133
|
+
elementRef = inject(ElementRef);
|
|
1134
|
+
// ─── Inputs ───
|
|
1135
|
+
/** Manual active state control. */
|
|
1136
|
+
active = input(false, { ...(ngDevMode ? { debugName: "active" } : {}), transform: booleanAttribute });
|
|
1137
|
+
/** Prevents interaction when true. */
|
|
1138
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : {}), transform: booleanAttribute });
|
|
1139
|
+
/** Visual variant (inherited from parent nav bar or set directly). */
|
|
1140
|
+
variant = input('underline', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
1141
|
+
/** Size (inherited from parent nav bar or set directly). */
|
|
1142
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
1143
|
+
/** Color (inherited from parent nav bar or set directly). */
|
|
1144
|
+
color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : []));
|
|
1145
|
+
/** Additional CSS classes. */
|
|
1146
|
+
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
|
|
1147
|
+
// ─── Private ───
|
|
1148
|
+
/**
|
|
1149
|
+
* Reactive signal from RouterLinkActive.isActiveChange.
|
|
1150
|
+
* Converts the EventEmitter to a signal for proper reactivity.
|
|
1151
|
+
*/
|
|
1152
|
+
routerLinkActiveState = this.routerLinkActive
|
|
1153
|
+
? toSignal(this.routerLinkActive.isActiveChange.pipe(startWith(this.routerLinkActive.isActive)), { initialValue: this.routerLinkActive.isActive })
|
|
1154
|
+
: null;
|
|
1155
|
+
// ─── Computed ───
|
|
1156
|
+
/**
|
|
1157
|
+
* Resolved active state — uses routerLinkActive if available, otherwise input.
|
|
1158
|
+
*/
|
|
1159
|
+
isActive = computed(() => {
|
|
1160
|
+
if (this.routerLinkActiveState) {
|
|
1161
|
+
return this.routerLinkActiveState();
|
|
1162
|
+
}
|
|
1163
|
+
return this.active();
|
|
1164
|
+
}, ...(ngDevMode ? [{ debugName: "isActive" }] : []));
|
|
1165
|
+
/** Computed host class from CVA + consumer overrides. */
|
|
1166
|
+
computedClass = computed(() => mergeClasses(tabItemVariants({
|
|
1167
|
+
variant: this.variant(),
|
|
1168
|
+
size: this.size(),
|
|
1169
|
+
color: this.color(),
|
|
1170
|
+
active: this.isActive(),
|
|
1171
|
+
}), this.disabled() && 'pointer-events-none', this.userClass()), ...(ngDevMode ? [{ debugName: "computedClass" }] : []));
|
|
1172
|
+
/** Focus this tab link element. */
|
|
1173
|
+
focus() {
|
|
1174
|
+
this.elementRef.nativeElement.focus();
|
|
1175
|
+
}
|
|
1176
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabLinkDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1177
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: TabLinkDirective, isStandalone: true, selector: "a[comTabLink], button[comTabLink]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tab" }, properties: { "class": "computedClass()", "attr.aria-selected": "isActive()", "attr.aria-disabled": "disabled() || null", "attr.data-state": "isActive() ? \"active\" : \"inactive\"", "tabindex": "isActive() ? 0 : -1" } }, ngImport: i0 });
|
|
1178
|
+
}
|
|
1179
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabLinkDirective, decorators: [{
|
|
1180
|
+
type: Directive,
|
|
1181
|
+
args: [{
|
|
1182
|
+
selector: 'a[comTabLink], button[comTabLink]',
|
|
1183
|
+
host: {
|
|
1184
|
+
role: 'tab',
|
|
1185
|
+
'[class]': 'computedClass()',
|
|
1186
|
+
'[attr.aria-selected]': 'isActive()',
|
|
1187
|
+
'[attr.aria-disabled]': 'disabled() || null',
|
|
1188
|
+
'[attr.data-state]': 'isActive() ? "active" : "inactive"',
|
|
1189
|
+
'[tabindex]': 'isActive() ? 0 : -1',
|
|
1190
|
+
},
|
|
1191
|
+
}]
|
|
1192
|
+
}], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Tab navigation bar component for route-driven tabs.
|
|
1196
|
+
*
|
|
1197
|
+
* Renders a styled, scrollable row of links that map to routes.
|
|
1198
|
+
* Content is handled by `<router-outlet>`.
|
|
1199
|
+
*
|
|
1200
|
+
* @tokens `--color-primary`, `--color-accent`, `--color-muted`, `--color-muted-foreground`,
|
|
1201
|
+
* `--color-border`, `--color-ring`, `--color-disabled`, `--color-disabled-foreground`
|
|
1202
|
+
*
|
|
1203
|
+
* @example Basic usage
|
|
1204
|
+
* ```html
|
|
1205
|
+
* <nav com-tab-nav-bar>
|
|
1206
|
+
* <a comTabLink routerLink="overview" routerLinkActive>Overview</a>
|
|
1207
|
+
* <a comTabLink routerLink="settings" routerLinkActive>Settings</a>
|
|
1208
|
+
* </nav>
|
|
1209
|
+
* <router-outlet />
|
|
1210
|
+
* ```
|
|
1211
|
+
*
|
|
1212
|
+
* @example With variants
|
|
1213
|
+
* ```html
|
|
1214
|
+
* <nav com-tab-nav-bar variant="pill" color="accent" size="sm">
|
|
1215
|
+
* <a comTabLink routerLink="grid" routerLinkActive>Grid</a>
|
|
1216
|
+
* <a comTabLink routerLink="list" routerLinkActive>List</a>
|
|
1217
|
+
* </nav>
|
|
1218
|
+
* ```
|
|
1219
|
+
*/
|
|
1220
|
+
class TabNavBarComponent {
|
|
1221
|
+
destroyRef = inject(DestroyRef);
|
|
1222
|
+
/** Unique ID for this nav bar instance. */
|
|
1223
|
+
baseId = generateTabId();
|
|
1224
|
+
// ─── Inputs ───
|
|
1225
|
+
/** Visual treatment of tab links. */
|
|
1226
|
+
variant = input('underline', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
1227
|
+
/** Controls tab link padding and font size. */
|
|
1228
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
1229
|
+
/** Active tab color. */
|
|
1230
|
+
color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : []));
|
|
1231
|
+
/** Tab alignment within the bar. */
|
|
1232
|
+
alignment = input('start', ...(ngDevMode ? [{ debugName: "alignment" }] : []));
|
|
1233
|
+
// ─── Content Children ───
|
|
1234
|
+
/** All TabLinkDirective children. */
|
|
1235
|
+
tabLinks = contentChildren(TabLinkDirective, ...(ngDevMode ? [{ debugName: "tabLinks" }] : []));
|
|
1236
|
+
// ─── View Children ───
|
|
1237
|
+
scrollContainer = viewChild('scrollContainer', ...(ngDevMode ? [{ debugName: "scrollContainer" }] : []));
|
|
1238
|
+
// ─── State ───
|
|
1239
|
+
scrollLeftValue = signal(0, ...(ngDevMode ? [{ debugName: "scrollLeftValue" }] : []));
|
|
1240
|
+
containerWidth = signal(0, ...(ngDevMode ? [{ debugName: "containerWidth" }] : []));
|
|
1241
|
+
scrollWidth = signal(0, ...(ngDevMode ? [{ debugName: "scrollWidth" }] : []));
|
|
1242
|
+
indicatorLeft = signal(0, ...(ngDevMode ? [{ debugName: "indicatorLeft" }] : []));
|
|
1243
|
+
indicatorWidth = signal(0, ...(ngDevMode ? [{ debugName: "indicatorWidth" }] : []));
|
|
1244
|
+
keyManager = null;
|
|
1245
|
+
resizeObserver = null;
|
|
1246
|
+
// ─── Computed ───
|
|
1247
|
+
/** The currently active link. */
|
|
1248
|
+
activeLink = computed(() => this.tabLinks().find(link => link.isActive()), ...(ngDevMode ? [{ debugName: "activeLink" }] : []));
|
|
1249
|
+
hasOverflow = computed(() => this.scrollWidth() > this.containerWidth(), ...(ngDevMode ? [{ debugName: "hasOverflow" }] : []));
|
|
1250
|
+
showScrollLeft = computed(() => this.hasOverflow() && this.scrollLeftValue() > 0, ...(ngDevMode ? [{ debugName: "showScrollLeft" }] : []));
|
|
1251
|
+
showScrollRight = computed(() => {
|
|
1252
|
+
const remaining = this.scrollWidth() - this.containerWidth() - this.scrollLeftValue();
|
|
1253
|
+
return this.hasOverflow() && remaining > 1;
|
|
1254
|
+
}, ...(ngDevMode ? [{ debugName: "showScrollRight" }] : []));
|
|
1255
|
+
headerClasses = computed(() => mergeClasses(tabHeaderVariants({
|
|
1256
|
+
alignment: this.alignment(),
|
|
1257
|
+
variant: this.variant(),
|
|
1258
|
+
}), 'relative'), ...(ngDevMode ? [{ debugName: "headerClasses" }] : []));
|
|
1259
|
+
scrollLeftClasses = computed(() => tabScrollButtonVariants({
|
|
1260
|
+
direction: 'left',
|
|
1261
|
+
variant: this.variant(),
|
|
1262
|
+
}), ...(ngDevMode ? [{ debugName: "scrollLeftClasses" }] : []));
|
|
1263
|
+
scrollRightClasses = computed(() => tabScrollButtonVariants({
|
|
1264
|
+
direction: 'right',
|
|
1265
|
+
variant: this.variant(),
|
|
1266
|
+
}), ...(ngDevMode ? [{ debugName: "scrollRightClasses" }] : []));
|
|
1267
|
+
indicatorColorClass = computed(() => {
|
|
1268
|
+
const colorMap = {
|
|
1269
|
+
primary: 'text-primary',
|
|
1270
|
+
accent: 'text-accent',
|
|
1271
|
+
muted: 'text-foreground',
|
|
1272
|
+
};
|
|
1273
|
+
return colorMap[this.color()];
|
|
1274
|
+
}, ...(ngDevMode ? [{ debugName: "indicatorColorClass" }] : []));
|
|
1275
|
+
constructor() {
|
|
1276
|
+
// Setup resize observer after render
|
|
1277
|
+
afterNextRender(() => {
|
|
1278
|
+
this.setupResizeObserver();
|
|
1279
|
+
this.updateScrollState();
|
|
1280
|
+
this.setupKeyManager();
|
|
1281
|
+
});
|
|
1282
|
+
// Cleanup on destroy
|
|
1283
|
+
this.destroyRef.onDestroy(() => {
|
|
1284
|
+
this.resizeObserver?.disconnect();
|
|
1285
|
+
this.keyManager?.destroy();
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
// ─── Public Methods ───
|
|
1289
|
+
scrollLeft() {
|
|
1290
|
+
const container = this.scrollContainer()?.nativeElement;
|
|
1291
|
+
if (container) {
|
|
1292
|
+
const scrollAmount = container.clientWidth * 0.75;
|
|
1293
|
+
container.scrollTo({
|
|
1294
|
+
left: container.scrollLeft - scrollAmount,
|
|
1295
|
+
behavior: 'smooth',
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
scrollRight() {
|
|
1300
|
+
const container = this.scrollContainer()?.nativeElement;
|
|
1301
|
+
if (container) {
|
|
1302
|
+
const scrollAmount = container.clientWidth * 0.75;
|
|
1303
|
+
container.scrollTo({
|
|
1304
|
+
left: container.scrollLeft + scrollAmount,
|
|
1305
|
+
behavior: 'smooth',
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
// ─── Event Handlers ───
|
|
1310
|
+
onScroll() {
|
|
1311
|
+
this.updateScrollState();
|
|
1312
|
+
}
|
|
1313
|
+
onKeydown(event) {
|
|
1314
|
+
if (!this.keyManager)
|
|
1315
|
+
return;
|
|
1316
|
+
switch (event.key) {
|
|
1317
|
+
case 'ArrowLeft':
|
|
1318
|
+
this.keyManager.setPreviousItemActive();
|
|
1319
|
+
event.preventDefault();
|
|
1320
|
+
break;
|
|
1321
|
+
case 'ArrowRight':
|
|
1322
|
+
this.keyManager.setNextItemActive();
|
|
1323
|
+
event.preventDefault();
|
|
1324
|
+
break;
|
|
1325
|
+
case 'Home':
|
|
1326
|
+
this.keyManager.setFirstItemActive();
|
|
1327
|
+
event.preventDefault();
|
|
1328
|
+
break;
|
|
1329
|
+
case 'End':
|
|
1330
|
+
this.keyManager.setLastItemActive();
|
|
1331
|
+
event.preventDefault();
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
// ─── Private Methods ───
|
|
1336
|
+
setupResizeObserver() {
|
|
1337
|
+
const container = this.scrollContainer()?.nativeElement;
|
|
1338
|
+
if (!container)
|
|
1339
|
+
return;
|
|
1340
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
1341
|
+
this.updateScrollState();
|
|
1342
|
+
});
|
|
1343
|
+
this.resizeObserver.observe(container);
|
|
1344
|
+
}
|
|
1345
|
+
updateScrollState() {
|
|
1346
|
+
const container = this.scrollContainer()?.nativeElement;
|
|
1347
|
+
if (!container)
|
|
1348
|
+
return;
|
|
1349
|
+
this.scrollLeftValue.set(container.scrollLeft);
|
|
1350
|
+
this.containerWidth.set(container.clientWidth);
|
|
1351
|
+
this.scrollWidth.set(container.scrollWidth);
|
|
1352
|
+
}
|
|
1353
|
+
setupKeyManager() {
|
|
1354
|
+
const items = this.createKeyManagerItems();
|
|
1355
|
+
this.keyManager = new FocusKeyManager(items)
|
|
1356
|
+
.withHorizontalOrientation('ltr')
|
|
1357
|
+
.withWrap()
|
|
1358
|
+
.skipPredicate(item => item.disabled);
|
|
1359
|
+
}
|
|
1360
|
+
createKeyManagerItems() {
|
|
1361
|
+
return this.tabLinks().map(link => ({
|
|
1362
|
+
focus: () => link.focus(),
|
|
1363
|
+
disabled: link.disabled(),
|
|
1364
|
+
}));
|
|
1365
|
+
}
|
|
1366
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabNavBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1367
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: TabNavBarComponent, isStandalone: true, selector: "com-tab-nav-bar, nav[com-tab-nav-bar]", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "com-tab-nav-bar" }, queries: [{ propertyName: "tabLinks", predicate: TabLinkDirective, isSignal: true }], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1368
|
+
<!-- Scroll button left -->
|
|
1369
|
+
@if (showScrollLeft()) {
|
|
1370
|
+
<button
|
|
1371
|
+
type="button"
|
|
1372
|
+
[class]="scrollLeftClasses()"
|
|
1373
|
+
(click)="scrollLeft()"
|
|
1374
|
+
aria-hidden="true"
|
|
1375
|
+
tabindex="-1"
|
|
1376
|
+
>
|
|
1377
|
+
<svg
|
|
1378
|
+
class="h-4 w-4"
|
|
1379
|
+
viewBox="0 0 24 24"
|
|
1380
|
+
fill="none"
|
|
1381
|
+
stroke="currentColor"
|
|
1382
|
+
stroke-width="2"
|
|
1383
|
+
stroke-linecap="round"
|
|
1384
|
+
stroke-linejoin="round"
|
|
1385
|
+
>
|
|
1386
|
+
<polyline points="15 18 9 12 15 6" />
|
|
1387
|
+
</svg>
|
|
1388
|
+
</button>
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
<!-- Tab link container -->
|
|
1392
|
+
<div
|
|
1393
|
+
#scrollContainer
|
|
1394
|
+
class="flex overflow-x-auto scrollbar-none"
|
|
1395
|
+
[class]="headerClasses()"
|
|
1396
|
+
role="tablist"
|
|
1397
|
+
[attr.aria-orientation]="'horizontal'"
|
|
1398
|
+
(scroll)="onScroll()"
|
|
1399
|
+
(keydown)="onKeydown($event)"
|
|
1400
|
+
>
|
|
1401
|
+
<ng-content />
|
|
1402
|
+
|
|
1403
|
+
<!-- Active indicator (underline variant only) -->
|
|
1404
|
+
@if (variant() === 'underline' && activeLink()) {
|
|
1405
|
+
<div
|
|
1406
|
+
class="absolute bottom-0 h-0.5 bg-current transition-all duration-200 ease-out"
|
|
1407
|
+
[class]="indicatorColorClass()"
|
|
1408
|
+
[style.left.px]="indicatorLeft()"
|
|
1409
|
+
[style.width.px]="indicatorWidth()"
|
|
1410
|
+
></div>
|
|
1411
|
+
}
|
|
1412
|
+
</div>
|
|
1413
|
+
|
|
1414
|
+
<!-- Scroll button right -->
|
|
1415
|
+
@if (showScrollRight()) {
|
|
1416
|
+
<button
|
|
1417
|
+
type="button"
|
|
1418
|
+
[class]="scrollRightClasses()"
|
|
1419
|
+
(click)="scrollRight()"
|
|
1420
|
+
aria-hidden="true"
|
|
1421
|
+
tabindex="-1"
|
|
1422
|
+
>
|
|
1423
|
+
<svg
|
|
1424
|
+
class="h-4 w-4"
|
|
1425
|
+
viewBox="0 0 24 24"
|
|
1426
|
+
fill="none"
|
|
1427
|
+
stroke="currentColor"
|
|
1428
|
+
stroke-width="2"
|
|
1429
|
+
stroke-linecap="round"
|
|
1430
|
+
stroke-linejoin="round"
|
|
1431
|
+
>
|
|
1432
|
+
<polyline points="9 18 15 12 9 6" />
|
|
1433
|
+
</svg>
|
|
1434
|
+
</button>
|
|
1435
|
+
}
|
|
1436
|
+
`, isInline: true, styles: [":host{display:block;position:relative}.scrollbar-none{scrollbar-width:none;-ms-overflow-style:none}.scrollbar-none::-webkit-scrollbar{display:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1437
|
+
}
|
|
1438
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TabNavBarComponent, decorators: [{
|
|
1439
|
+
type: Component,
|
|
1440
|
+
args: [{ selector: 'com-tab-nav-bar, nav[com-tab-nav-bar]', template: `
|
|
1441
|
+
<!-- Scroll button left -->
|
|
1442
|
+
@if (showScrollLeft()) {
|
|
1443
|
+
<button
|
|
1444
|
+
type="button"
|
|
1445
|
+
[class]="scrollLeftClasses()"
|
|
1446
|
+
(click)="scrollLeft()"
|
|
1447
|
+
aria-hidden="true"
|
|
1448
|
+
tabindex="-1"
|
|
1449
|
+
>
|
|
1450
|
+
<svg
|
|
1451
|
+
class="h-4 w-4"
|
|
1452
|
+
viewBox="0 0 24 24"
|
|
1453
|
+
fill="none"
|
|
1454
|
+
stroke="currentColor"
|
|
1455
|
+
stroke-width="2"
|
|
1456
|
+
stroke-linecap="round"
|
|
1457
|
+
stroke-linejoin="round"
|
|
1458
|
+
>
|
|
1459
|
+
<polyline points="15 18 9 12 15 6" />
|
|
1460
|
+
</svg>
|
|
1461
|
+
</button>
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
<!-- Tab link container -->
|
|
1465
|
+
<div
|
|
1466
|
+
#scrollContainer
|
|
1467
|
+
class="flex overflow-x-auto scrollbar-none"
|
|
1468
|
+
[class]="headerClasses()"
|
|
1469
|
+
role="tablist"
|
|
1470
|
+
[attr.aria-orientation]="'horizontal'"
|
|
1471
|
+
(scroll)="onScroll()"
|
|
1472
|
+
(keydown)="onKeydown($event)"
|
|
1473
|
+
>
|
|
1474
|
+
<ng-content />
|
|
1475
|
+
|
|
1476
|
+
<!-- Active indicator (underline variant only) -->
|
|
1477
|
+
@if (variant() === 'underline' && activeLink()) {
|
|
1478
|
+
<div
|
|
1479
|
+
class="absolute bottom-0 h-0.5 bg-current transition-all duration-200 ease-out"
|
|
1480
|
+
[class]="indicatorColorClass()"
|
|
1481
|
+
[style.left.px]="indicatorLeft()"
|
|
1482
|
+
[style.width.px]="indicatorWidth()"
|
|
1483
|
+
></div>
|
|
1484
|
+
}
|
|
1485
|
+
</div>
|
|
1486
|
+
|
|
1487
|
+
<!-- Scroll button right -->
|
|
1488
|
+
@if (showScrollRight()) {
|
|
1489
|
+
<button
|
|
1490
|
+
type="button"
|
|
1491
|
+
[class]="scrollRightClasses()"
|
|
1492
|
+
(click)="scrollRight()"
|
|
1493
|
+
aria-hidden="true"
|
|
1494
|
+
tabindex="-1"
|
|
1495
|
+
>
|
|
1496
|
+
<svg
|
|
1497
|
+
class="h-4 w-4"
|
|
1498
|
+
viewBox="0 0 24 24"
|
|
1499
|
+
fill="none"
|
|
1500
|
+
stroke="currentColor"
|
|
1501
|
+
stroke-width="2"
|
|
1502
|
+
stroke-linecap="round"
|
|
1503
|
+
stroke-linejoin="round"
|
|
1504
|
+
>
|
|
1505
|
+
<polyline points="9 18 15 12 9 6" />
|
|
1506
|
+
</svg>
|
|
1507
|
+
</button>
|
|
1508
|
+
}
|
|
1509
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1510
|
+
class: 'com-tab-nav-bar',
|
|
1511
|
+
}, styles: [":host{display:block;position:relative}.scrollbar-none{scrollbar-width:none;-ms-overflow-style:none}.scrollbar-none::-webkit-scrollbar{display:none}\n"] }]
|
|
1512
|
+
}], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], tabLinks: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TabLinkDirective), { isSignal: true }] }], scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }] } });
|
|
1513
|
+
|
|
1514
|
+
// Public API for the tabs component
|
|
1515
|
+
// Variants (for advanced customization)
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Generated bundle index. Do not edit.
|
|
1519
|
+
*/
|
|
1520
|
+
|
|
1521
|
+
export { TabComponent, TabContentDirective, TabGroupComponent, TabLabelDirective, TabLinkDirective, TabNavBarComponent, tabCloseButtonVariants, tabHeaderVariants, tabItemVariants, tabPanelVariants, tabScrollButtonVariants };
|
|
1522
|
+
//# sourceMappingURL=ngx-com-components-tabs.mjs.map
|