@styloviz/tabs 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 StyloViz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @styloviz/tabs
2
+
3
+ > 6 visual styles (underline, pills, boxed, lifted, border-card, unstyled), closable and scrollable tabs, vertical layout, keyboard navigation and lazy panels.
4
+
5
+ Part of the **StyloViz UI Kit** — a premium Angular 21 + Tailwind CSS 4 dashboard component library. Standalone, `OnPush`, strongly typed, dark-mode ready.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @styloviz/core @styloviz/tabs
11
+ ```
12
+
13
+ Requires `@angular/core` and `@angular/common` >= 21. `@styloviz/core` is a peer dependency shared by every component. Prefer everything at once? `@styloviz/all` installs the whole free tier in one command.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { SvTabsComponent } from '@styloviz/tabs';
19
+
20
+ @Component({
21
+ standalone: true,
22
+ imports: [SvTabsComponent],
23
+ template: `
24
+ <sv-tabs [tabs]="tabs">
25
+ <ng-template svTabPanel="overview">…</ng-template>
26
+ </sv-tabs>
27
+ `,
28
+ })
29
+ export class DemoComponent {}
30
+ ```
31
+
32
+ ## Inputs
33
+
34
+ | Input | Type | Default | Description |
35
+ | --- | --- | --- | --- |
36
+ | `tabs` | `TabItem[]` | `[]` | Tab definitions in display order. |
37
+ | `activeId` | `string (two-way)` | `''` | ID of the currently active tab. Use `[(activeId)]` for two-way binding. |
38
+ | `tabsStyle` | `TabsStyle` | `'underline'` | Visual style variant. |
39
+ | `size` | `TabsSize` | `'md'` | Size of the tab text and icons. |
40
+ | `layout` | `TabsLayout` | `'horizontal'` | Layout direction. |
41
+ | `fullWidth` | `boolean` | `false` | Whether tab buttons stretch to fill all available width. Only applicable to the `'horizontal'` layout. |
42
+ | `scrollable` | `boolean` | `false` | Allows the tab list to scroll horizontally instead of wrapping/overflowing when there are more tabs than fit the available width. |
43
+ | `customClass` | `string` | `''` | Additional CSS classes on the root wrapper. |
44
+
45
+ ## Outputs
46
+
47
+ | Output | Type | Description |
48
+ | --- | --- | --- |
49
+ | `tabClose` | `TabItem` | Emitted when a tab's close (×) button is clicked. The tab is NOT removed automatically — remove it from `tabs` yourself. |
50
+
51
+ ## Documentation
52
+
53
+ Full API reference and live demos: https://styloviz.dev/docs/tabs
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,339 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, inject, TemplateRef, Directive, model, output, contentChildren, signal, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { NgClass, NgTemplateOutlet } from '@angular/common';
4
+
5
+ // ─── Tab Panel Directive ──────────────────────────────────────────────────────
6
+ /**
7
+ * TabPanelDirective — marks a `<ng-template>` as content for a specific tab.
8
+ *
9
+ * Usage:
10
+ * ```html
11
+ * <sv-tabs [tabs]="tabs" [(activeId)]="active">
12
+ * <ng-template svTabPanel="settings">Settings content…</ng-template>
13
+ * <ng-template svTabPanel="profile">Profile content…</ng-template>
14
+ * </sv-tabs>
15
+ * ```
16
+ */
17
+ class SvTabPanelDirective {
18
+ /** Must match a `TabItem.id`. */
19
+ tabId = input.required({ ...(ngDevMode ? { debugName: "tabId" } : /* istanbul ignore next */ {}), alias: 'svTabPanel' });
20
+ templateRef = inject((TemplateRef));
21
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvTabPanelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
22
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", type: SvTabPanelDirective, isStandalone: true, selector: "[svTabPanel]", inputs: { tabId: { classPropertyName: "tabId", publicName: "svTabPanel", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
23
+ }
24
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvTabPanelDirective, decorators: [{
25
+ type: Directive,
26
+ args: [{
27
+ selector: '[svTabPanel]',
28
+ standalone: true,
29
+ }]
30
+ }], propDecorators: { tabId: [{ type: i0.Input, args: [{ isSignal: true, alias: "svTabPanel", required: true }] }] } });
31
+ // ─── Component ───────────────────────────────────────────────────────────────
32
+ /**
33
+ * Tabs — A horizontal (or vertical) tab switcher with content projection.
34
+ *
35
+ * Features:
36
+ * - 6 visual styles: underline · pills · boxed · lifted · border-card · unstyled
37
+ * - `unstyled` strips all decorative classes for fully headless theming — pairs with the
38
+ * `data-active`/`data-disabled` attributes exposed on every tab button for custom CSS
39
+ * - `border-card` wraps the tab row and panel in one cohesive bordered card
40
+ * - Horizontal and vertical layout modes
41
+ * - Keyboard navigation: ← → (horizontal) / ↑ ↓ (vertical), Home, End
42
+ * - Optional icon, badge, unread dot, and closable (×) button per tab
43
+ * - Optional horizontal scrolling for tab lists that overflow their container
44
+ * - Disabled tab support
45
+ * - Lazy rendering — tab panel content is only inserted into the DOM after
46
+ * first activation (prevents layout shift on initial paint)
47
+ * - Two-way `activeId` binding via `model()`
48
+ * - Content projection via `svTabPanel` directive on `<ng-template>` tags
49
+ * - Full dark-mode support
50
+ * - Accessible: `role="tablist"`, `role="tab"`, `role="tabpanel"`,
51
+ * `aria-selected`, `aria-controls`, `aria-disabled`
52
+ *
53
+ * **Content projection:**
54
+ * ```html
55
+ * <sv-tabs [tabs]="myTabs" [(activeId)]="activeTab">
56
+ * <ng-template svTabPanel="general">General settings…</ng-template>
57
+ * <ng-template svTabPanel="security">Security settings…</ng-template>
58
+ * </sv-tabs>
59
+ * ```
60
+ *
61
+ * **No-projection (external panel control):**
62
+ * Omit `<ng-template>` children and react to `(activeIdChange)` instead.
63
+ */
64
+ class SvTabsComponent {
65
+ // ── Inputs ────────────────────────────────────────────────────────────────
66
+ /** Tab definitions in display order. */
67
+ tabs = input([], ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
68
+ /** ID of the currently active tab. Use `[(activeId)]` for two-way binding. */
69
+ activeId = model('', ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
70
+ /** Visual style variant. @default 'underline' */
71
+ tabsStyle = input('underline', ...(ngDevMode ? [{ debugName: "tabsStyle" }] : /* istanbul ignore next */ []));
72
+ /** Size of the tab text and icons. @default 'md' */
73
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
74
+ /** Layout direction. @default 'horizontal' */
75
+ layout = input('horizontal', ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
76
+ /**
77
+ * Whether tab buttons stretch to fill all available width.
78
+ * Only applicable to the `'horizontal'` layout. @default false
79
+ */
80
+ fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth" }] : /* istanbul ignore next */ []));
81
+ /**
82
+ * Allows the tab list to scroll horizontally instead of wrapping/overflowing
83
+ * when there are more tabs than fit the available width. @default false
84
+ */
85
+ scrollable = input(false, ...(ngDevMode ? [{ debugName: "scrollable" }] : /* istanbul ignore next */ []));
86
+ /** Additional CSS classes on the root wrapper. */
87
+ customClass = input('', ...(ngDevMode ? [{ debugName: "customClass" }] : /* istanbul ignore next */ []));
88
+ // ── Outputs ───────────────────────────────────────────────────────────────
89
+ /** Emitted when a tab's close (×) button is clicked. The tab is NOT removed automatically — remove it from `tabs` yourself. */
90
+ tabClose = output();
91
+ // ── Content children (panel templates) ───────────────────────────────────
92
+ panels = contentChildren(SvTabPanelDirective, ...(ngDevMode ? [{ debugName: "panels" }] : /* istanbul ignore next */ []));
93
+ /** Look up the TemplateRef for a given tab ID. Returns null when no panel registered. */
94
+ panelFor(tabId) {
95
+ return this.panels().find(p => p.tabId() === tabId)?.templateRef ?? null;
96
+ }
97
+ // ── Internal ──────────────────────────────────────────────────────────────
98
+ /**
99
+ * Tracks which tab IDs have been activated at least once.
100
+ * Used for lazy-rendering: a panel is only projected into the DOM
101
+ * after the user first visits that tab.
102
+ */
103
+ activatedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "activatedIds" }] : /* istanbul ignore next */ []));
104
+ // ── Computed ──────────────────────────────────────────────────────────────
105
+ /** Resolved active ID — falls back to first enabled tab when empty. */
106
+ resolvedActiveId = computed(() => {
107
+ const id = this.activeId();
108
+ if (id)
109
+ return id;
110
+ return this.tabs().find(t => !t.disabled)?.id ?? '';
111
+ }, ...(ngDevMode ? [{ debugName: "resolvedActiveId" }] : /* istanbul ignore next */ []));
112
+ navigableTabs = computed(() => this.tabs().filter(t => !t.disabled), ...(ngDevMode ? [{ debugName: "navigableTabs" }] : /* istanbul ignore next */ []));
113
+ // ── Size maps ─────────────────────────────────────────────────────────────
114
+ tabTextClass = computed(() => ({
115
+ sm: 'text-xs',
116
+ md: 'text-sm',
117
+ lg: 'text-base',
118
+ })[this.size()], ...(ngDevMode ? [{ debugName: "tabTextClass" }] : /* istanbul ignore next */ []));
119
+ tabPadClass = computed(() => {
120
+ const s = this.size();
121
+ const v = this.tabsStyle();
122
+ if (this.layout() === 'vertical') {
123
+ return { sm: 'px-3 py-1.5', md: 'px-4 py-2', lg: 'px-5 py-2.5' }[s];
124
+ }
125
+ if (v === 'boxed' || v === 'lifted' || v === 'border-card') {
126
+ return { sm: 'px-3 py-1.5', md: 'px-4 py-2', lg: 'px-5 py-2.5' }[s];
127
+ }
128
+ return { sm: 'px-2.5 py-1.5', md: 'px-4 py-2.5', lg: 'px-5 py-3' }[s];
129
+ }, ...(ngDevMode ? [{ debugName: "tabPadClass" }] : /* istanbul ignore next */ []));
130
+ iconClass = computed(() => ({
131
+ sm: 'h-3.5 w-3.5',
132
+ md: 'h-4 w-4',
133
+ lg: 'h-5 w-5',
134
+ })[this.size()], ...(ngDevMode ? [{ debugName: "iconClass" }] : /* istanbul ignore next */ []));
135
+ // ── Root + list classes ───────────────────────────────────────────────────
136
+ rootClasses = computed(() => {
137
+ const base = this.layout() === 'vertical'
138
+ ? 'flex gap-4'
139
+ : 'flex flex-col';
140
+ const cardWrap = this.tabsStyle() === 'border-card'
141
+ ? 'overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900'
142
+ : '';
143
+ return [base, cardWrap, this.customClass()].filter(Boolean).join(' ');
144
+ }, ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
145
+ listClasses = computed(() => {
146
+ const v = this.tabsStyle();
147
+ const isV = this.layout() === 'vertical';
148
+ // Horizontal tab lists always allow horizontal scroll: `overflow-x-auto`
149
+ // shows no scrollbar while the tabs fit, and scrolls instead of clipping /
150
+ // forcing page overflow once they don't (e.g. many tabs on a phone). The
151
+ // `scrollable` input still controls whether tabs keep their natural width
152
+ // (`shrink-0`, below) or shrink to fit first.
153
+ const scroll = !isV ? 'overflow-x-auto' : '';
154
+ if (v === 'unstyled') {
155
+ return [
156
+ isV ? 'flex flex-col gap-1 shrink-0' : 'flex gap-1',
157
+ this.fullWidth() && !isV ? 'w-full' : '',
158
+ scroll,
159
+ ].filter(Boolean).join(' ');
160
+ }
161
+ if (v === 'border-card') {
162
+ return [
163
+ isV
164
+ ? 'flex flex-col gap-0 shrink-0 border-r border-gray-200 bg-gray-50/60 p-2 dark:border-gray-800 dark:bg-gray-800/30'
165
+ : 'flex gap-0 border-b border-gray-200 bg-gray-50/60 px-2 pt-2 dark:border-gray-800 dark:bg-gray-800/30',
166
+ this.fullWidth() && !isV ? 'w-full' : '',
167
+ scroll,
168
+ ].filter(Boolean).join(' ');
169
+ }
170
+ if (v === 'boxed') {
171
+ return [
172
+ isV ? 'flex flex-col gap-1 shrink-0' : 'flex gap-1',
173
+ 'rounded-xl bg-gray-100 p-1 dark:bg-gray-800/60',
174
+ this.fullWidth() && !isV ? 'w-full' : '',
175
+ scroll,
176
+ ].filter(Boolean).join(' ');
177
+ }
178
+ if (v === 'lifted') {
179
+ return [
180
+ isV ? 'flex flex-col gap-0 shrink-0 border-r border-gray-200/80 dark:border-gray-800/60 pr-0'
181
+ : 'flex gap-0 border-b border-gray-200/80 dark:border-gray-800/60',
182
+ this.fullWidth() && !isV ? 'w-full' : '',
183
+ scroll,
184
+ ].filter(Boolean).join(' ');
185
+ }
186
+ if (v === 'pills') {
187
+ return [
188
+ isV ? 'flex flex-col gap-1 shrink-0' : 'flex gap-1',
189
+ this.fullWidth() && !isV ? 'w-full' : '',
190
+ scroll,
191
+ ].filter(Boolean).join(' ');
192
+ }
193
+ // underline (default)
194
+ return [
195
+ isV ? 'flex flex-col gap-0 shrink-0 border-r border-gray-200/80 dark:border-gray-800/60'
196
+ : 'flex gap-0 border-b border-gray-200/80 dark:border-gray-800/60',
197
+ this.fullWidth() && !isV ? 'w-full' : '',
198
+ scroll,
199
+ ].filter(Boolean).join(' ');
200
+ }, ...(ngDevMode ? [{ debugName: "listClasses" }] : /* istanbul ignore next */ []));
201
+ // ── Per-tab classes ───────────────────────────────────────────────────────
202
+ tabClasses(tab) {
203
+ const isActive = tab.id === this.resolvedActiveId();
204
+ const isDisabled = !!tab.disabled;
205
+ const v = this.tabsStyle();
206
+ const isV = this.layout() === 'vertical';
207
+ const fw = this.fullWidth() && !isV;
208
+ const base = [
209
+ 'inline-flex items-center gap-2 font-medium transition-colors duration-150',
210
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-1',
211
+ this.tabPadClass(),
212
+ this.tabTextClass(),
213
+ isDisabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer',
214
+ fw ? 'flex-1 justify-center' : '',
215
+ this.scrollable() && !isV ? 'shrink-0' : '',
216
+ ].filter(Boolean);
217
+ if (v === 'unstyled') {
218
+ // No decorative classes — consumers theme via [data-active]/[data-disabled]
219
+ // hooks (see template) and their own CSS/customClass.
220
+ }
221
+ else if (v === 'border-card') {
222
+ if (isV) {
223
+ base.push('-mr-px rounded-l-lg border border-r-0 border-transparent');
224
+ base.push(isActive
225
+ ? 'border-gray-200 bg-white font-semibold text-gray-900 dark:border-gray-800 dark:bg-gray-900 dark:text-white'
226
+ : isDisabled ? 'text-gray-400' : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200');
227
+ }
228
+ else {
229
+ base.push('-mb-px rounded-t-lg border border-b-0 border-transparent');
230
+ base.push(isActive
231
+ ? 'border-gray-200 bg-white text-gray-900 dark:border-gray-800 dark:bg-gray-900 dark:text-white'
232
+ : isDisabled ? 'text-gray-400' : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200');
233
+ }
234
+ }
235
+ else if (v === 'pills') {
236
+ base.push('rounded-lg');
237
+ base.push(isActive
238
+ ? 'bg-primary-600 text-white shadow-sm dark:bg-primary-600'
239
+ : isDisabled ? 'text-gray-400' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200');
240
+ }
241
+ else if (v === 'boxed') {
242
+ base.push('rounded-lg');
243
+ base.push(isActive
244
+ ? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700/80 dark:text-white'
245
+ : isDisabled ? 'text-gray-400' : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200');
246
+ }
247
+ else if (v === 'lifted') {
248
+ if (isV) {
249
+ base.push('border-r-2 -mr-px');
250
+ base.push(isActive
251
+ ? 'border-primary-600 text-gray-900 font-semibold dark:text-white dark:border-primary-500 bg-primary-50/50 dark:bg-primary-950/20'
252
+ : isDisabled ? 'border-transparent text-gray-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600');
253
+ }
254
+ else {
255
+ base.push('rounded-t-lg border border-b-0 -mb-px');
256
+ base.push(isActive
257
+ ? 'border-gray-200/80 bg-white text-gray-900 dark:border-gray-700/60 dark:bg-gray-900 dark:text-white'
258
+ : isDisabled ? 'border-transparent text-gray-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-200 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-700');
259
+ }
260
+ }
261
+ else {
262
+ // underline
263
+ if (isV) {
264
+ base.push('border-r-2 -mr-px');
265
+ base.push(isActive
266
+ ? 'border-primary-600 text-gray-900 font-semibold dark:border-primary-500 dark:text-white'
267
+ : isDisabled ? 'border-transparent text-gray-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600');
268
+ }
269
+ else {
270
+ base.push('border-b-2 -mb-px');
271
+ base.push(isActive
272
+ ? 'border-primary-600 text-gray-900 font-semibold dark:border-primary-500 dark:text-white'
273
+ : isDisabled ? 'border-transparent text-gray-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600');
274
+ }
275
+ }
276
+ return base.join(' ');
277
+ }
278
+ // ── Panel visibility ──────────────────────────────────────────────────────
279
+ isPanelVisible(tab) {
280
+ return tab.id === this.resolvedActiveId();
281
+ }
282
+ /** Returns true once the tab has been activated (for lazy rendering). */
283
+ isPanelMounted(tab) {
284
+ return this.activatedIds().has(tab.id) || tab.id === this.resolvedActiveId();
285
+ }
286
+ // ── Event handlers ────────────────────────────────────────────────────────
287
+ selectTab(tab) {
288
+ if (tab.disabled || tab.id === this.resolvedActiveId())
289
+ return;
290
+ this.activatedIds.update(s => new Set([...s, tab.id]));
291
+ this.activeId.set(tab.id);
292
+ }
293
+ /** Close (×) button handler — emits `tabClose`; the consumer owns removing the tab. */
294
+ onCloseTab(event, tab) {
295
+ event.stopPropagation();
296
+ this.tabClose.emit(tab);
297
+ }
298
+ onTabKeydown(event, tab) {
299
+ const navigable = this.navigableTabs();
300
+ const idx = navigable.findIndex(t => t.id === tab.id);
301
+ const isV = this.layout() === 'vertical';
302
+ let next;
303
+ if (isV) {
304
+ if (event.key === 'ArrowDown')
305
+ next = navigable[idx + 1];
306
+ if (event.key === 'ArrowUp')
307
+ next = navigable[idx - 1];
308
+ }
309
+ else {
310
+ if (event.key === 'ArrowRight')
311
+ next = navigable[idx + 1];
312
+ if (event.key === 'ArrowLeft')
313
+ next = navigable[idx - 1];
314
+ }
315
+ if (event.key === 'Home')
316
+ next = navigable[0];
317
+ if (event.key === 'End')
318
+ next = navigable[navigable.length - 1];
319
+ if (next) {
320
+ event.preventDefault();
321
+ this.selectTab(next);
322
+ const el = document.getElementById(`tab-${next.id}`);
323
+ el?.focus();
324
+ }
325
+ }
326
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
327
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SvTabsComponent, isStandalone: true, selector: "sv-tabs", inputs: { tabs: { classPropertyName: "tabs", publicName: "tabs", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, tabsStyle: { classPropertyName: "tabsStyle", publicName: "tabsStyle", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, scrollable: { classPropertyName: "scrollable", publicName: "scrollable", isSignal: true, isRequired: false, transformFunction: null }, customClass: { classPropertyName: "customClass", publicName: "customClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeId: "activeIdChange", tabClose: "tabClose" }, queries: [{ propertyName: "panels", predicate: SvTabPanelDirective, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"rootClasses()\">\n\n <!-- \u2500\u2500 Tab list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n role=\"tablist\"\n [ngClass]=\"listClasses()\"\n [attr.aria-orientation]=\"layout() === 'vertical' ? 'vertical' : 'horizontal'\"\n >\n @for (tab of tabs(); track tab.id) {\n <!--\n Wrapper carries the visual treatment (padding, color, background, border) so a\n `closable` tab's \u00D7 button can sit as a real sibling <button> instead of being\n nested inside the tab's own <button> \u2014 nesting interactive controls inside a\n <button> is invalid HTML and gets silently mis-parsed by browsers.\n -->\n <span\n [attr.data-active]=\"tab.id === resolvedActiveId() ? '' : null\"\n [attr.data-disabled]=\"tab.disabled ? '' : null\"\n [ngClass]=\"tabClasses(tab)\"\n >\n <button\n [id]=\"'tab-' + tab.id\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"tab.id === resolvedActiveId()\"\n [attr.aria-controls]=\"'panel-' + tab.id\"\n [attr.aria-disabled]=\"tab.disabled || null\"\n [attr.tabindex]=\"tab.id === resolvedActiveId() ? 0 : -1\"\n class=\"inline-flex items-center gap-2 rounded-[inherit] bg-transparent\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n focus-visible:ring-offset-1\"\n (click)=\"selectTab(tab)\"\n (keydown)=\"onTabKeydown($event, tab)\"\n >\n <!-- Icon -->\n @if (tab.iconPath) {\n <svg\n [ngClass]=\"iconClass()\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path [attr.d]=\"tab.iconPath\" />\n </svg>\n }\n\n {{ tab.label }}\n\n <!-- Unread/status dot -->\n @if (tab.dot) {\n <span class=\"h-1.5 w-1.5 shrink-0 rounded-full bg-red-500\" aria-hidden=\"true\"></span>\n <span class=\"sr-only\">(unread)</span>\n }\n\n <!-- Badge -->\n @if (tab.badge) {\n <span\n class=\"inline-flex items-center justify-center rounded-full\n bg-gray-200 px-1.5 py-0.5 text-[10px] font-semibold\n leading-none text-gray-700 dark:bg-gray-700 dark:text-gray-200\"\n [class.bg-white]=\"tab.id === resolvedActiveId() && tabsStyle() === 'pills'\"\n [class.text-primary-600]=\"tab.id === resolvedActiveId() && tabsStyle() === 'pills'\"\n >\n {{ tab.badge }}\n </span>\n }\n </button>\n\n <!-- Close button \u2014 a real sibling <button>, not nested inside the tab button -->\n @if (tab.closable) {\n <button\n type=\"button\"\n class=\"-mr-1 ml-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded\n text-current/60 transition-colors hover:bg-black/10 hover:text-current\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n dark:hover:bg-white/10\"\n [attr.aria-label]=\"'Close ' + tab.label\"\n (click)=\"onCloseTab($event, tab)\"\n >\n <svg class=\"h-3 w-3\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n </span>\n }\n </div>\n\n <!-- \u2500\u2500 Tab panels \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"flex-1 min-w-0\" [ngClass]=\"tabsStyle() === 'border-card' ? 'p-4' : ''\">\n @for (tab of tabs(); track tab.id) {\n @if (isPanelMounted(tab)) {\n <div\n [id]=\"'panel-' + tab.id\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"'tab-' + tab.id\"\n [attr.tabindex]=\"isPanelVisible(tab) ? 0 : null\"\n [ngClass]=\"isPanelVisible(tab) ? 'block focus:outline-none' : 'hidden'\"\n >\n @if (panelFor(tab.id); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n }\n </div>\n }\n }\n </div>\n\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
328
+ }
329
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvTabsComponent, decorators: [{
330
+ type: Component,
331
+ args: [{ selector: 'sv-tabs', standalone: true, imports: [NgClass, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [ngClass]=\"rootClasses()\">\n\n <!-- \u2500\u2500 Tab list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n role=\"tablist\"\n [ngClass]=\"listClasses()\"\n [attr.aria-orientation]=\"layout() === 'vertical' ? 'vertical' : 'horizontal'\"\n >\n @for (tab of tabs(); track tab.id) {\n <!--\n Wrapper carries the visual treatment (padding, color, background, border) so a\n `closable` tab's \u00D7 button can sit as a real sibling <button> instead of being\n nested inside the tab's own <button> \u2014 nesting interactive controls inside a\n <button> is invalid HTML and gets silently mis-parsed by browsers.\n -->\n <span\n [attr.data-active]=\"tab.id === resolvedActiveId() ? '' : null\"\n [attr.data-disabled]=\"tab.disabled ? '' : null\"\n [ngClass]=\"tabClasses(tab)\"\n >\n <button\n [id]=\"'tab-' + tab.id\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"tab.id === resolvedActiveId()\"\n [attr.aria-controls]=\"'panel-' + tab.id\"\n [attr.aria-disabled]=\"tab.disabled || null\"\n [attr.tabindex]=\"tab.id === resolvedActiveId() ? 0 : -1\"\n class=\"inline-flex items-center gap-2 rounded-[inherit] bg-transparent\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n focus-visible:ring-offset-1\"\n (click)=\"selectTab(tab)\"\n (keydown)=\"onTabKeydown($event, tab)\"\n >\n <!-- Icon -->\n @if (tab.iconPath) {\n <svg\n [ngClass]=\"iconClass()\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path [attr.d]=\"tab.iconPath\" />\n </svg>\n }\n\n {{ tab.label }}\n\n <!-- Unread/status dot -->\n @if (tab.dot) {\n <span class=\"h-1.5 w-1.5 shrink-0 rounded-full bg-red-500\" aria-hidden=\"true\"></span>\n <span class=\"sr-only\">(unread)</span>\n }\n\n <!-- Badge -->\n @if (tab.badge) {\n <span\n class=\"inline-flex items-center justify-center rounded-full\n bg-gray-200 px-1.5 py-0.5 text-[10px] font-semibold\n leading-none text-gray-700 dark:bg-gray-700 dark:text-gray-200\"\n [class.bg-white]=\"tab.id === resolvedActiveId() && tabsStyle() === 'pills'\"\n [class.text-primary-600]=\"tab.id === resolvedActiveId() && tabsStyle() === 'pills'\"\n >\n {{ tab.badge }}\n </span>\n }\n </button>\n\n <!-- Close button \u2014 a real sibling <button>, not nested inside the tab button -->\n @if (tab.closable) {\n <button\n type=\"button\"\n class=\"-mr-1 ml-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded\n text-current/60 transition-colors hover:bg-black/10 hover:text-current\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n dark:hover:bg-white/10\"\n [attr.aria-label]=\"'Close ' + tab.label\"\n (click)=\"onCloseTab($event, tab)\"\n >\n <svg class=\"h-3 w-3\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n </span>\n }\n </div>\n\n <!-- \u2500\u2500 Tab panels \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"flex-1 min-w-0\" [ngClass]=\"tabsStyle() === 'border-card' ? 'p-4' : ''\">\n @for (tab of tabs(); track tab.id) {\n @if (isPanelMounted(tab)) {\n <div\n [id]=\"'panel-' + tab.id\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"'tab-' + tab.id\"\n [attr.tabindex]=\"isPanelVisible(tab) ? 0 : null\"\n [ngClass]=\"isPanelVisible(tab) ? 'block focus:outline-none' : 'hidden'\"\n >\n @if (panelFor(tab.id); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n }\n </div>\n }\n }\n </div>\n\n</div>\n" }]
332
+ }], propDecorators: { tabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabs", required: false }] }], activeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeId", required: false }] }, { type: i0.Output, args: ["activeIdChange"] }], tabsStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabsStyle", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], scrollable: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollable", required: false }] }], customClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "customClass", required: false }] }], tabClose: [{ type: i0.Output, args: ["tabClose"] }], panels: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SvTabPanelDirective), { isSignal: true }] }] } });
333
+
334
+ /**
335
+ * Generated bundle index. Do not edit.
336
+ */
337
+
338
+ export { SvTabPanelDirective, SvTabsComponent };
339
+ //# sourceMappingURL=styloviz-tabs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styloviz-tabs.mjs","sources":["../../../../projects/tabs/src/lib/tabs.component.ts","../../../../projects/tabs/src/lib/tabs.component.html","../../../../projects/tabs/src/styloviz-tabs.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n contentChildren,\n Directive,\n inject,\n input,\n model,\n output,\n signal,\n TemplateRef,\n} from '@angular/core';\nimport { NgClass, NgTemplateOutlet } from '@angular/common';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type TabsStyle = 'underline' | 'pills' | 'boxed' | 'lifted' | 'border-card' | 'unstyled';\nexport type TabsSize = 'sm' | 'md' | 'lg';\nexport type TabsLayout = 'horizontal' | 'vertical';\n\n/** A single tab definition. */\nexport interface TabItem {\n /** Unique key — used as the active-tab identifier. */\n id: string;\n /** Display label. */\n label: string;\n /** Optional SVG `<path d=\"...\">` shown before the label. */\n iconPath?: string;\n /** Suppress rendering this tab's content until it is first activated. @default false */\n lazy?: boolean;\n /** Prevent the user from switching to this tab. @default false */\n disabled?: boolean;\n /** Small badge text (e.g. count). */\n badge?: string;\n /** Small unread/status dot rendered next to the label (independent of `badge`). @default false */\n dot?: boolean;\n /** Shows a close (×) button on the tab and emits `tabClose` when clicked. @default false */\n closable?: boolean;\n}\n\n// ─── Tab Panel Directive ──────────────────────────────────────────────────────\n\n/**\n * TabPanelDirective — marks a `<ng-template>` as content for a specific tab.\n *\n * Usage:\n * ```html\n * <sv-tabs [tabs]=\"tabs\" [(activeId)]=\"active\">\n * <ng-template svTabPanel=\"settings\">Settings content…</ng-template>\n * <ng-template svTabPanel=\"profile\">Profile content…</ng-template>\n * </sv-tabs>\n * ```\n */\n@Directive({\n selector: '[svTabPanel]',\n standalone: true,\n})\nexport class SvTabPanelDirective {\n /** Must match a `TabItem.id`. */\n tabId = input.required<string>({ alias: 'svTabPanel' });\n\n readonly templateRef = inject(TemplateRef<unknown>);\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\n/**\n * Tabs — A horizontal (or vertical) tab switcher with content projection.\n *\n * Features:\n * - 6 visual styles: underline · pills · boxed · lifted · border-card · unstyled\n * - `unstyled` strips all decorative classes for fully headless theming — pairs with the\n * `data-active`/`data-disabled` attributes exposed on every tab button for custom CSS\n * - `border-card` wraps the tab row and panel in one cohesive bordered card\n * - Horizontal and vertical layout modes\n * - Keyboard navigation: ← → (horizontal) / ↑ ↓ (vertical), Home, End\n * - Optional icon, badge, unread dot, and closable (×) button per tab\n * - Optional horizontal scrolling for tab lists that overflow their container\n * - Disabled tab support\n * - Lazy rendering — tab panel content is only inserted into the DOM after\n * first activation (prevents layout shift on initial paint)\n * - Two-way `activeId` binding via `model()`\n * - Content projection via `svTabPanel` directive on `<ng-template>` tags\n * - Full dark-mode support\n * - Accessible: `role=\"tablist\"`, `role=\"tab\"`, `role=\"tabpanel\"`,\n * `aria-selected`, `aria-controls`, `aria-disabled`\n *\n * **Content projection:**\n * ```html\n * <sv-tabs [tabs]=\"myTabs\" [(activeId)]=\"activeTab\">\n * <ng-template svTabPanel=\"general\">General settings…</ng-template>\n * <ng-template svTabPanel=\"security\">Security settings…</ng-template>\n * </sv-tabs>\n * ```\n *\n * **No-projection (external panel control):**\n * Omit `<ng-template>` children and react to `(activeIdChange)` instead.\n */\n@Component({\n selector: 'sv-tabs',\n standalone: true,\n imports: [NgClass, NgTemplateOutlet],\n templateUrl: './tabs.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SvTabsComponent {\n // ── Inputs ────────────────────────────────────────────────────────────────\n\n /** Tab definitions in display order. */\n tabs = input<TabItem[]>([]);\n\n /** ID of the currently active tab. Use `[(activeId)]` for two-way binding. */\n activeId = model<string>('');\n\n /** Visual style variant. @default 'underline' */\n tabsStyle = input<TabsStyle>('underline');\n\n /** Size of the tab text and icons. @default 'md' */\n size = input<TabsSize>('md');\n\n /** Layout direction. @default 'horizontal' */\n layout = input<TabsLayout>('horizontal');\n\n /**\n * Whether tab buttons stretch to fill all available width.\n * Only applicable to the `'horizontal'` layout. @default false\n */\n fullWidth = input<boolean>(false);\n\n /**\n * Allows the tab list to scroll horizontally instead of wrapping/overflowing\n * when there are more tabs than fit the available width. @default false\n */\n scrollable = input<boolean>(false);\n\n /** Additional CSS classes on the root wrapper. */\n customClass = input<string>('');\n\n // ── Outputs ───────────────────────────────────────────────────────────────\n\n /** Emitted when a tab's close (×) button is clicked. The tab is NOT removed automatically — remove it from `tabs` yourself. */\n tabClose = output<TabItem>();\n\n // ── Content children (panel templates) ───────────────────────────────────\n\n private panels = contentChildren(SvTabPanelDirective);\n\n /** Look up the TemplateRef for a given tab ID. Returns null when no panel registered. */\n panelFor(tabId: string): TemplateRef<unknown> | null {\n return this.panels().find(p => p.tabId() === tabId)?.templateRef ?? null;\n }\n\n // ── Internal ──────────────────────────────────────────────────────────────\n\n /**\n * Tracks which tab IDs have been activated at least once.\n * Used for lazy-rendering: a panel is only projected into the DOM\n * after the user first visits that tab.\n */\n activatedIds = signal<Set<string>>(new Set());\n\n // ── Computed ──────────────────────────────────────────────────────────────\n\n /** Resolved active ID — falls back to first enabled tab when empty. */\n resolvedActiveId = computed<string>(() => {\n const id = this.activeId();\n if (id) return id;\n return this.tabs().find(t => !t.disabled)?.id ?? '';\n });\n\n navigableTabs = computed(() => this.tabs().filter(t => !t.disabled));\n\n // ── Size maps ─────────────────────────────────────────────────────────────\n\n tabTextClass = computed<string>(() => ({\n sm: 'text-xs',\n md: 'text-sm',\n lg: 'text-base',\n })[this.size()]);\n\n tabPadClass = computed<string>(() => {\n const s = this.size();\n const v = this.tabsStyle();\n if (this.layout() === 'vertical') {\n return { sm: 'px-3 py-1.5', md: 'px-4 py-2', lg: 'px-5 py-2.5' }[s];\n }\n if (v === 'boxed' || v === 'lifted' || v === 'border-card') {\n return { sm: 'px-3 py-1.5', md: 'px-4 py-2', lg: 'px-5 py-2.5' }[s];\n }\n return { sm: 'px-2.5 py-1.5', md: 'px-4 py-2.5', lg: 'px-5 py-3' }[s];\n });\n\n iconClass = computed<string>(() => ({\n sm: 'h-3.5 w-3.5',\n md: 'h-4 w-4',\n lg: 'h-5 w-5',\n })[this.size()]);\n\n // ── Root + list classes ───────────────────────────────────────────────────\n\n rootClasses = computed<string>(() => {\n const base = this.layout() === 'vertical'\n ? 'flex gap-4'\n : 'flex flex-col';\n const cardWrap = this.tabsStyle() === 'border-card'\n ? 'overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900'\n : '';\n return [base, cardWrap, this.customClass()].filter(Boolean).join(' ');\n });\n\n listClasses = computed<string>(() => {\n const v = this.tabsStyle();\n const isV = this.layout() === 'vertical';\n // Horizontal tab lists always allow horizontal scroll: `overflow-x-auto`\n // shows no scrollbar while the tabs fit, and scrolls instead of clipping /\n // forcing page overflow once they don't (e.g. many tabs on a phone). The\n // `scrollable` input still controls whether tabs keep their natural width\n // (`shrink-0`, below) or shrink to fit first.\n const scroll = !isV ? 'overflow-x-auto' : '';\n\n if (v === 'unstyled') {\n return [\n isV ? 'flex flex-col gap-1 shrink-0' : 'flex gap-1',\n this.fullWidth() && !isV ? 'w-full' : '',\n scroll,\n ].filter(Boolean).join(' ');\n }\n\n if (v === 'border-card') {\n return [\n isV\n ? 'flex flex-col gap-0 shrink-0 border-r border-gray-200 bg-gray-50/60 p-2 dark:border-gray-800 dark:bg-gray-800/30'\n : 'flex gap-0 border-b border-gray-200 bg-gray-50/60 px-2 pt-2 dark:border-gray-800 dark:bg-gray-800/30',\n this.fullWidth() && !isV ? 'w-full' : '',\n scroll,\n ].filter(Boolean).join(' ');\n }\n\n if (v === 'boxed') {\n return [\n isV ? 'flex flex-col gap-1 shrink-0' : 'flex gap-1',\n 'rounded-xl bg-gray-100 p-1 dark:bg-gray-800/60',\n this.fullWidth() && !isV ? 'w-full' : '',\n scroll,\n ].filter(Boolean).join(' ');\n }\n\n if (v === 'lifted') {\n return [\n isV ? 'flex flex-col gap-0 shrink-0 border-r border-gray-200/80 dark:border-gray-800/60 pr-0'\n : 'flex gap-0 border-b border-gray-200/80 dark:border-gray-800/60',\n this.fullWidth() && !isV ? 'w-full' : '',\n scroll,\n ].filter(Boolean).join(' ');\n }\n\n if (v === 'pills') {\n return [\n isV ? 'flex flex-col gap-1 shrink-0' : 'flex gap-1',\n this.fullWidth() && !isV ? 'w-full' : '',\n scroll,\n ].filter(Boolean).join(' ');\n }\n\n // underline (default)\n return [\n isV ? 'flex flex-col gap-0 shrink-0 border-r border-gray-200/80 dark:border-gray-800/60'\n : 'flex gap-0 border-b border-gray-200/80 dark:border-gray-800/60',\n this.fullWidth() && !isV ? 'w-full' : '',\n scroll,\n ].filter(Boolean).join(' ');\n });\n\n // ── Per-tab classes ───────────────────────────────────────────────────────\n\n tabClasses(tab: TabItem): string {\n const isActive = tab.id === this.resolvedActiveId();\n const isDisabled = !!tab.disabled;\n const v = this.tabsStyle();\n const isV = this.layout() === 'vertical';\n const fw = this.fullWidth() && !isV;\n\n const base = [\n 'inline-flex items-center gap-2 font-medium transition-colors duration-150',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-1',\n this.tabPadClass(),\n this.tabTextClass(),\n isDisabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer',\n fw ? 'flex-1 justify-center' : '',\n this.scrollable() && !isV ? 'shrink-0' : '',\n ].filter(Boolean);\n\n if (v === 'unstyled') {\n // No decorative classes — consumers theme via [data-active]/[data-disabled]\n // hooks (see template) and their own CSS/customClass.\n } else if (v === 'border-card') {\n if (isV) {\n base.push('-mr-px rounded-l-lg border border-r-0 border-transparent');\n base.push(isActive\n ? 'border-gray-200 bg-white font-semibold text-gray-900 dark:border-gray-800 dark:bg-gray-900 dark:text-white'\n : isDisabled ? 'text-gray-400' : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200');\n } else {\n base.push('-mb-px rounded-t-lg border border-b-0 border-transparent');\n base.push(isActive\n ? 'border-gray-200 bg-white text-gray-900 dark:border-gray-800 dark:bg-gray-900 dark:text-white'\n : isDisabled ? 'text-gray-400' : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200');\n }\n } else if (v === 'pills') {\n base.push('rounded-lg');\n base.push(isActive\n ? 'bg-primary-600 text-white shadow-sm dark:bg-primary-600'\n : isDisabled ? 'text-gray-400' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200');\n } else if (v === 'boxed') {\n base.push('rounded-lg');\n base.push(isActive\n ? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700/80 dark:text-white'\n : isDisabled ? 'text-gray-400' : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200');\n } else if (v === 'lifted') {\n if (isV) {\n base.push('border-r-2 -mr-px');\n base.push(isActive\n ? 'border-primary-600 text-gray-900 font-semibold dark:text-white dark:border-primary-500 bg-primary-50/50 dark:bg-primary-950/20'\n : isDisabled ? 'border-transparent text-gray-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600');\n } else {\n base.push('rounded-t-lg border border-b-0 -mb-px');\n base.push(isActive\n ? 'border-gray-200/80 bg-white text-gray-900 dark:border-gray-700/60 dark:bg-gray-900 dark:text-white'\n : isDisabled ? 'border-transparent text-gray-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-200 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-700');\n }\n } else {\n // underline\n if (isV) {\n base.push('border-r-2 -mr-px');\n base.push(isActive\n ? 'border-primary-600 text-gray-900 font-semibold dark:border-primary-500 dark:text-white'\n : isDisabled ? 'border-transparent text-gray-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600');\n } else {\n base.push('border-b-2 -mb-px');\n base.push(isActive\n ? 'border-primary-600 text-gray-900 font-semibold dark:border-primary-500 dark:text-white'\n : isDisabled ? 'border-transparent text-gray-400' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600');\n }\n }\n\n return base.join(' ');\n }\n\n // ── Panel visibility ──────────────────────────────────────────────────────\n\n isPanelVisible(tab: TabItem): boolean {\n return tab.id === this.resolvedActiveId();\n }\n\n /** Returns true once the tab has been activated (for lazy rendering). */\n isPanelMounted(tab: TabItem): boolean {\n return this.activatedIds().has(tab.id) || tab.id === this.resolvedActiveId();\n }\n\n // ── Event handlers ────────────────────────────────────────────────────────\n\n selectTab(tab: TabItem): void {\n if (tab.disabled || tab.id === this.resolvedActiveId()) return;\n this.activatedIds.update(s => new Set([...s, tab.id]));\n this.activeId.set(tab.id);\n }\n\n /** Close (×) button handler — emits `tabClose`; the consumer owns removing the tab. */\n onCloseTab(event: Event, tab: TabItem): void {\n event.stopPropagation();\n this.tabClose.emit(tab);\n }\n\n onTabKeydown(event: KeyboardEvent, tab: TabItem): void {\n const navigable = this.navigableTabs();\n const idx = navigable.findIndex(t => t.id === tab.id);\n const isV = this.layout() === 'vertical';\n\n let next: TabItem | undefined;\n\n if (isV) {\n if (event.key === 'ArrowDown') next = navigable[idx + 1];\n if (event.key === 'ArrowUp') next = navigable[idx - 1];\n } else {\n if (event.key === 'ArrowRight') next = navigable[idx + 1];\n if (event.key === 'ArrowLeft') next = navigable[idx - 1];\n }\n if (event.key === 'Home') next = navigable[0];\n if (event.key === 'End') next = navigable[navigable.length - 1];\n\n if (next) {\n event.preventDefault();\n this.selectTab(next);\n const el = document.getElementById(`tab-${next.id}`);\n el?.focus();\n }\n }\n}\n","<div [ngClass]=\"rootClasses()\">\n\n <!-- ── Tab list ──────────────────────────────────────────────────────── -->\n <div\n role=\"tablist\"\n [ngClass]=\"listClasses()\"\n [attr.aria-orientation]=\"layout() === 'vertical' ? 'vertical' : 'horizontal'\"\n >\n @for (tab of tabs(); track tab.id) {\n <!--\n Wrapper carries the visual treatment (padding, color, background, border) so a\n `closable` tab's × button can sit as a real sibling <button> instead of being\n nested inside the tab's own <button> — nesting interactive controls inside a\n <button> is invalid HTML and gets silently mis-parsed by browsers.\n -->\n <span\n [attr.data-active]=\"tab.id === resolvedActiveId() ? '' : null\"\n [attr.data-disabled]=\"tab.disabled ? '' : null\"\n [ngClass]=\"tabClasses(tab)\"\n >\n <button\n [id]=\"'tab-' + tab.id\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"tab.id === resolvedActiveId()\"\n [attr.aria-controls]=\"'panel-' + tab.id\"\n [attr.aria-disabled]=\"tab.disabled || null\"\n [attr.tabindex]=\"tab.id === resolvedActiveId() ? 0 : -1\"\n class=\"inline-flex items-center gap-2 rounded-[inherit] bg-transparent\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n focus-visible:ring-offset-1\"\n (click)=\"selectTab(tab)\"\n (keydown)=\"onTabKeydown($event, tab)\"\n >\n <!-- Icon -->\n @if (tab.iconPath) {\n <svg\n [ngClass]=\"iconClass()\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path [attr.d]=\"tab.iconPath\" />\n </svg>\n }\n\n {{ tab.label }}\n\n <!-- Unread/status dot -->\n @if (tab.dot) {\n <span class=\"h-1.5 w-1.5 shrink-0 rounded-full bg-red-500\" aria-hidden=\"true\"></span>\n <span class=\"sr-only\">(unread)</span>\n }\n\n <!-- Badge -->\n @if (tab.badge) {\n <span\n class=\"inline-flex items-center justify-center rounded-full\n bg-gray-200 px-1.5 py-0.5 text-[10px] font-semibold\n leading-none text-gray-700 dark:bg-gray-700 dark:text-gray-200\"\n [class.bg-white]=\"tab.id === resolvedActiveId() && tabsStyle() === 'pills'\"\n [class.text-primary-600]=\"tab.id === resolvedActiveId() && tabsStyle() === 'pills'\"\n >\n {{ tab.badge }}\n </span>\n }\n </button>\n\n <!-- Close button — a real sibling <button>, not nested inside the tab button -->\n @if (tab.closable) {\n <button\n type=\"button\"\n class=\"-mr-1 ml-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded\n text-current/60 transition-colors hover:bg-black/10 hover:text-current\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n dark:hover:bg-white/10\"\n [attr.aria-label]=\"'Close ' + tab.label\"\n (click)=\"onCloseTab($event, tab)\"\n >\n <svg class=\"h-3 w-3\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n </span>\n }\n </div>\n\n <!-- ── Tab panels ─────────────────────────────────────────────────────── -->\n <div class=\"flex-1 min-w-0\" [ngClass]=\"tabsStyle() === 'border-card' ? 'p-4' : ''\">\n @for (tab of tabs(); track tab.id) {\n @if (isPanelMounted(tab)) {\n <div\n [id]=\"'panel-' + tab.id\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"'tab-' + tab.id\"\n [attr.tabindex]=\"isPanelVisible(tab) ? 0 : null\"\n [ngClass]=\"isPanelVisible(tab) ? 'block focus:outline-none' : 'hidden'\"\n >\n @if (panelFor(tab.id); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n }\n </div>\n }\n }\n </div>\n\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;AAyCA;AAEA;;;;;;;;;;AAUG;MAKU,mBAAmB,CAAA;;IAE9B,KAAK,GAAG,KAAK,CAAC,QAAQ,4EAAW,KAAK,EAAE,YAAY,EAAA,CAAG;AAE9C,IAAA,WAAW,GAAG,MAAM,EAAC,WAAoB,EAAC;wGAJxC,mBAAmB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;4FAAnB,mBAAmB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,cAAA,EAAA,MAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,YAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;4FAAnB,mBAAmB,EAAA,UAAA,EAAA,CAAA;kBAJ/B,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,cAAc;AACxB,oBAAA,UAAU,EAAE,IAAI;AACjB,iBAAA;;AAQD;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BG;MAQU,eAAe,CAAA;;;AAI1B,IAAA,IAAI,GAAG,KAAK,CAAY,EAAE,2EAAC;;AAG3B,IAAA,QAAQ,GAAG,KAAK,CAAS,EAAE,+EAAC;;AAG5B,IAAA,SAAS,GAAG,KAAK,CAAY,WAAW,gFAAC;;AAGzC,IAAA,IAAI,GAAG,KAAK,CAAW,IAAI,2EAAC;;AAG5B,IAAA,MAAM,GAAG,KAAK,CAAa,YAAY,6EAAC;AAExC;;;AAGG;AACH,IAAA,SAAS,GAAG,KAAK,CAAU,KAAK,gFAAC;AAEjC;;;AAGG;AACH,IAAA,UAAU,GAAG,KAAK,CAAU,KAAK,iFAAC;;AAGlC,IAAA,WAAW,GAAG,KAAK,CAAS,EAAE,kFAAC;;;IAK/B,QAAQ,GAAG,MAAM,EAAW;;AAIpB,IAAA,MAAM,GAAG,eAAe,CAAC,mBAAmB,6EAAC;;AAGrD,IAAA,QAAQ,CAAC,KAAa,EAAA;QACpB,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,CAAC,EAAE,WAAW,IAAI,IAAI;IAC1E;;AAIA;;;;AAIG;AACH,IAAA,YAAY,GAAG,MAAM,CAAc,IAAI,GAAG,EAAE,mFAAC;;;AAK7C,IAAA,gBAAgB,GAAG,QAAQ,CAAS,MAAK;AACvC,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE;AAC1B,QAAA,IAAI,EAAE;AAAE,YAAA,OAAO,EAAE;QACjB,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE;AACrD,IAAA,CAAC,uFAAC;IAEF,aAAa,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,eAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAC;;AAIpE,IAAA,YAAY,GAAG,QAAQ,CAAS,MAAM,CAAC;AACrC,QAAA,EAAE,EAAE,SAAS;AACb,QAAA,EAAE,EAAE,SAAS;AACb,QAAA,EAAE,EAAE,WAAW;AAChB,KAAA,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,mFAAC;AAEhB,IAAA,WAAW,GAAG,QAAQ,CAAS,MAAK;AAClC,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE;AACrB,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE;AAC1B,QAAA,IAAI,IAAI,CAAC,MAAM,EAAE,KAAK,UAAU,EAAE;AAChC,YAAA,OAAO,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;QACrE;AACA,QAAA,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,aAAa,EAAE;AAC1D,YAAA,OAAO,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;QACrE;AACA,QAAA,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;AACvE,IAAA,CAAC,kFAAC;AAEF,IAAA,SAAS,GAAG,QAAQ,CAAS,MAAM,CAAC;AAClC,QAAA,EAAE,EAAE,aAAa;AACjB,QAAA,EAAE,EAAE,SAAS;AACb,QAAA,EAAE,EAAE,SAAS;AACd,KAAA,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,gFAAC;;AAIhB,IAAA,WAAW,GAAG,QAAQ,CAAS,MAAK;AAClC,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK;AAC7B,cAAE;cACA,eAAe;AACnB,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK;AACpC,cAAE;cACA,EAAE;QACN,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;AACvE,IAAA,CAAC,kFAAC;AAEF,IAAA,WAAW,GAAG,QAAQ,CAAS,MAAK;AAClC,QAAA,MAAM,CAAC,GAAQ,IAAI,CAAC,SAAS,EAAE;QAC/B,MAAM,GAAG,GAAM,IAAI,CAAC,MAAM,EAAE,KAAK,UAAU;;;;;;AAM3C,QAAA,MAAM,MAAM,GAAG,CAAC,GAAG,GAAG,iBAAiB,GAAG,EAAE;AAE5C,QAAA,IAAI,CAAC,KAAK,UAAU,EAAE;YACpB,OAAO;AACL,gBAAA,GAAG,GAAG,8BAA8B,GAAG,YAAY;AACnD,gBAAA,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,GAAG,QAAQ,GAAG,EAAE;gBACxC,MAAM;aACP,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAC7B;AAEA,QAAA,IAAI,CAAC,KAAK,aAAa,EAAE;YACvB,OAAO;gBACL;AACE,sBAAE;AACF,sBAAE,sGAAsG;AAC1G,gBAAA,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,GAAG,QAAQ,GAAG,EAAE;gBACxC,MAAM;aACP,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAC7B;AAEA,QAAA,IAAI,CAAC,KAAK,OAAO,EAAE;YACjB,OAAO;AACL,gBAAA,GAAG,GAAG,8BAA8B,GAAG,YAAY;gBACnD,gDAAgD;AAChD,gBAAA,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,GAAG,QAAQ,GAAG,EAAE;gBACxC,MAAM;aACP,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAC7B;AAEA,QAAA,IAAI,CAAC,KAAK,QAAQ,EAAE;YAClB,OAAO;gBACL,GAAG,GAAG;AACF,sBAAE,gEAAgE;AACtE,gBAAA,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,GAAG,QAAQ,GAAG,EAAE;gBACxC,MAAM;aACP,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAC7B;AAEA,QAAA,IAAI,CAAC,KAAK,OAAO,EAAE;YACjB,OAAO;AACL,gBAAA,GAAG,GAAG,8BAA8B,GAAG,YAAY;AACnD,gBAAA,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,GAAG,QAAQ,GAAG,EAAE;gBACxC,MAAM;aACP,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAC7B;;QAGA,OAAO;YACL,GAAG,GAAG;AACF,kBAAE,gEAAgE;AACtE,YAAA,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,GAAG,QAAQ,GAAG,EAAE;YACxC,MAAM;SACP,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;AAC7B,IAAA,CAAC,kFAAC;;AAIF,IAAA,UAAU,CAAC,GAAY,EAAA;QACrB,MAAM,QAAQ,GAAK,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,gBAAgB,EAAE;AACrD,QAAA,MAAM,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ;AACjC,QAAA,MAAM,CAAC,GAAY,IAAI,CAAC,SAAS,EAAE;QACnC,MAAM,GAAG,GAAU,IAAI,CAAC,MAAM,EAAE,KAAK,UAAU;QAC/C,MAAM,EAAE,GAAW,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG;AAE3C,QAAA,MAAM,IAAI,GAAG;YACX,2EAA2E;YAC3E,oGAAoG;YACpG,IAAI,CAAC,WAAW,EAAE;YAClB,IAAI,CAAC,YAAY,EAAE;AACnB,YAAA,UAAU,GAAG,+BAA+B,GAAG,gBAAgB;AAC/D,YAAA,EAAE,GAAG,uBAAuB,GAAG,EAAE;AACjC,YAAA,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,GAAG,UAAU,GAAG,EAAE;AAC5C,SAAA,CAAC,MAAM,CAAC,OAAO,CAAC;AAEjB,QAAA,IAAI,CAAC,KAAK,UAAU,EAAE;;;QAGtB;AAAO,aAAA,IAAI,CAAC,KAAK,aAAa,EAAE;YAC9B,IAAI,GAAG,EAAE;AACP,gBAAA,IAAI,CAAC,IAAI,CAAC,0DAA0D,CAAC;gBACrE,IAAI,CAAC,IAAI,CAAC;AACR,sBAAE;sBACA,UAAU,GAAG,eAAe,GAAG,+EAA+E,CAAC;YACrH;iBAAO;AACL,gBAAA,IAAI,CAAC,IAAI,CAAC,0DAA0D,CAAC;gBACrE,IAAI,CAAC,IAAI,CAAC;AACR,sBAAE;sBACA,UAAU,GAAG,eAAe,GAAG,+EAA+E,CAAC;YACrH;QACF;AAAO,aAAA,IAAI,CAAC,KAAK,OAAO,EAAE;AACxB,YAAA,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC;AACR,kBAAE;kBACA,UAAU,GAAG,eAAe,GAAG,wHAAwH,CAAC;QAC9J;AAAO,aAAA,IAAI,CAAC,KAAK,OAAO,EAAE;AACxB,YAAA,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC;AACR,kBAAE;kBACA,UAAU,GAAG,eAAe,GAAG,+EAA+E,CAAC;QACrH;AAAO,aAAA,IAAI,CAAC,KAAK,QAAQ,EAAE;YACzB,IAAI,GAAG,EAAE;AACP,gBAAA,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC;gBAC9B,IAAI,CAAC,IAAI,CAAC;AACR,sBAAE;sBACA,UAAU,GAAG,kCAAkC,GAAG,mJAAmJ,CAAC;YAC5M;iBAAO;AACL,gBAAA,IAAI,CAAC,IAAI,CAAC,uCAAuC,CAAC;gBAClD,IAAI,CAAC,IAAI,CAAC;AACR,sBAAE;sBACA,UAAU,GAAG,kCAAkC,GAAG,mJAAmJ,CAAC;YAC5M;QACF;aAAO;;YAEL,IAAI,GAAG,EAAE;AACP,gBAAA,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC;gBAC9B,IAAI,CAAC,IAAI,CAAC;AACR,sBAAE;sBACA,UAAU,GAAG,kCAAkC,GAAG,mJAAmJ,CAAC;YAC5M;iBAAO;AACL,gBAAA,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC;gBAC9B,IAAI,CAAC,IAAI,CAAC;AACR,sBAAE;sBACA,UAAU,GAAG,kCAAkC,GAAG,mJAAmJ,CAAC;YAC5M;QACF;AAEA,QAAA,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACvB;;AAIA,IAAA,cAAc,CAAC,GAAY,EAAA;QACzB,OAAO,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,gBAAgB,EAAE;IAC3C;;AAGA,IAAA,cAAc,CAAC,GAAY,EAAA;QACzB,OAAO,IAAI,CAAC,YAAY,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,gBAAgB,EAAE;IAC9E;;AAIA,IAAA,SAAS,CAAC,GAAY,EAAA;QACpB,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,gBAAgB,EAAE;YAAE;QACxD,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;IAC3B;;IAGA,UAAU,CAAC,KAAY,EAAE,GAAY,EAAA;QACnC,KAAK,CAAC,eAAe,EAAE;AACvB,QAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;IACzB;IAEA,YAAY,CAAC,KAAoB,EAAE,GAAY,EAAA;AAC7C,QAAA,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,EAAE;AACtC,QAAA,MAAM,GAAG,GAAS,SAAS,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC;QAC3D,MAAM,GAAG,GAAS,IAAI,CAAC,MAAM,EAAE,KAAK,UAAU;AAE9C,QAAA,IAAI,IAAyB;QAE7B,IAAI,GAAG,EAAE;AACP,YAAA,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW;AAAE,gBAAA,IAAI,GAAG,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC;AACxD,YAAA,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS;AAAI,gBAAA,IAAI,GAAG,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC;QAC1D;aAAO;AACL,YAAA,IAAI,KAAK,CAAC,GAAG,KAAK,YAAY;AAAE,gBAAA,IAAI,GAAG,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC;AACzD,YAAA,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW;AAAG,gBAAA,IAAI,GAAG,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC;QAC3D;AACA,QAAA,IAAI,KAAK,CAAC,GAAG,KAAK,MAAM;AAAE,YAAA,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC;AAC7C,QAAA,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK;YAAG,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;QAEhE,IAAI,IAAI,EAAE;YACR,KAAK,CAAC,cAAc,EAAE;AACtB,YAAA,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;AACpB,YAAA,MAAM,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAA,IAAA,EAAO,IAAI,CAAC,EAAE,CAAA,CAAE,CAAC;YACpD,EAAE,EAAE,KAAK,EAAE;QACb;IACF;wGAlSW,eAAe,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAf,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,IAAA,EAAA,eAAe,oqCAwCO,mBAAmB,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EClJtD,6gKAiHA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EDXY,OAAO,oFAAE,gBAAgB,EAAA,QAAA,EAAA,oBAAA,EAAA,MAAA,EAAA,CAAA,yBAAA,EAAA,kBAAA,EAAA,0BAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;4FAIxB,eAAe,EAAA,UAAA,EAAA,CAAA;kBAP3B,SAAS;+BACE,SAAS,EAAA,UAAA,EACP,IAAI,EAAA,OAAA,EACP,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAA,eAAA,EAEnB,uBAAuB,CAAC,MAAM,EAAA,QAAA,EAAA,6gKAAA,EAAA;47BA0Cd,mBAAmB,CAAA,EAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,EAAA,CAAA;;AElJtD;;AAEG;;;;"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@styloviz/tabs",
3
+ "version": "0.1.1",
4
+ "description": "6 visual styles (underline, pills, boxed, lifted, border-card, unstyled), closable and scrollable tabs, vertical layout, keyboard navigation and lazy panels.",
5
+ "license": "MIT",
6
+ "author": "sazzad-bs23",
7
+ "homepage": "https://styloviz.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/rhossain/angular-tailwind-dashboard-components.git",
11
+ "directory": "projects/tabs"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/rhossain/angular-tailwind-dashboard-components/issues"
15
+ },
16
+ "peerDependencies": {
17
+ "@angular/common": ">=21.0.0",
18
+ "@angular/core": ">=21.0.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "engines": {
24
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
25
+ },
26
+ "sideEffects": false,
27
+ "module": "fesm2022/styloviz-tabs.mjs",
28
+ "typings": "types/styloviz-tabs.d.ts",
29
+ "exports": {
30
+ "./package.json": {
31
+ "default": "./package.json"
32
+ },
33
+ ".": {
34
+ "types": "./types/styloviz-tabs.d.ts",
35
+ "default": "./fesm2022/styloviz-tabs.mjs"
36
+ }
37
+ },
38
+ "type": "module",
39
+ "dependencies": {
40
+ "tslib": "^2.3.0"
41
+ }
42
+ }
@@ -0,0 +1,131 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { TemplateRef } from '@angular/core';
3
+
4
+ type TabsStyle = 'underline' | 'pills' | 'boxed' | 'lifted' | 'border-card' | 'unstyled';
5
+ type TabsSize = 'sm' | 'md' | 'lg';
6
+ type TabsLayout = 'horizontal' | 'vertical';
7
+ /** A single tab definition. */
8
+ interface TabItem {
9
+ /** Unique key — used as the active-tab identifier. */
10
+ id: string;
11
+ /** Display label. */
12
+ label: string;
13
+ /** Optional SVG `<path d="...">` shown before the label. */
14
+ iconPath?: string;
15
+ /** Suppress rendering this tab's content until it is first activated. @default false */
16
+ lazy?: boolean;
17
+ /** Prevent the user from switching to this tab. @default false */
18
+ disabled?: boolean;
19
+ /** Small badge text (e.g. count). */
20
+ badge?: string;
21
+ /** Small unread/status dot rendered next to the label (independent of `badge`). @default false */
22
+ dot?: boolean;
23
+ /** Shows a close (×) button on the tab and emits `tabClose` when clicked. @default false */
24
+ closable?: boolean;
25
+ }
26
+ /**
27
+ * TabPanelDirective — marks a `<ng-template>` as content for a specific tab.
28
+ *
29
+ * Usage:
30
+ * ```html
31
+ * <sv-tabs [tabs]="tabs" [(activeId)]="active">
32
+ * <ng-template svTabPanel="settings">Settings content…</ng-template>
33
+ * <ng-template svTabPanel="profile">Profile content…</ng-template>
34
+ * </sv-tabs>
35
+ * ```
36
+ */
37
+ declare class SvTabPanelDirective {
38
+ /** Must match a `TabItem.id`. */
39
+ tabId: _angular_core.InputSignal<string>;
40
+ readonly templateRef: TemplateRef<any>;
41
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvTabPanelDirective, never>;
42
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SvTabPanelDirective, "[svTabPanel]", never, { "tabId": { "alias": "svTabPanel"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
43
+ }
44
+ /**
45
+ * Tabs — A horizontal (or vertical) tab switcher with content projection.
46
+ *
47
+ * Features:
48
+ * - 6 visual styles: underline · pills · boxed · lifted · border-card · unstyled
49
+ * - `unstyled` strips all decorative classes for fully headless theming — pairs with the
50
+ * `data-active`/`data-disabled` attributes exposed on every tab button for custom CSS
51
+ * - `border-card` wraps the tab row and panel in one cohesive bordered card
52
+ * - Horizontal and vertical layout modes
53
+ * - Keyboard navigation: ← → (horizontal) / ↑ ↓ (vertical), Home, End
54
+ * - Optional icon, badge, unread dot, and closable (×) button per tab
55
+ * - Optional horizontal scrolling for tab lists that overflow their container
56
+ * - Disabled tab support
57
+ * - Lazy rendering — tab panel content is only inserted into the DOM after
58
+ * first activation (prevents layout shift on initial paint)
59
+ * - Two-way `activeId` binding via `model()`
60
+ * - Content projection via `svTabPanel` directive on `<ng-template>` tags
61
+ * - Full dark-mode support
62
+ * - Accessible: `role="tablist"`, `role="tab"`, `role="tabpanel"`,
63
+ * `aria-selected`, `aria-controls`, `aria-disabled`
64
+ *
65
+ * **Content projection:**
66
+ * ```html
67
+ * <sv-tabs [tabs]="myTabs" [(activeId)]="activeTab">
68
+ * <ng-template svTabPanel="general">General settings…</ng-template>
69
+ * <ng-template svTabPanel="security">Security settings…</ng-template>
70
+ * </sv-tabs>
71
+ * ```
72
+ *
73
+ * **No-projection (external panel control):**
74
+ * Omit `<ng-template>` children and react to `(activeIdChange)` instead.
75
+ */
76
+ declare class SvTabsComponent {
77
+ /** Tab definitions in display order. */
78
+ tabs: _angular_core.InputSignal<TabItem[]>;
79
+ /** ID of the currently active tab. Use `[(activeId)]` for two-way binding. */
80
+ activeId: _angular_core.ModelSignal<string>;
81
+ /** Visual style variant. @default 'underline' */
82
+ tabsStyle: _angular_core.InputSignal<TabsStyle>;
83
+ /** Size of the tab text and icons. @default 'md' */
84
+ size: _angular_core.InputSignal<TabsSize>;
85
+ /** Layout direction. @default 'horizontal' */
86
+ layout: _angular_core.InputSignal<TabsLayout>;
87
+ /**
88
+ * Whether tab buttons stretch to fill all available width.
89
+ * Only applicable to the `'horizontal'` layout. @default false
90
+ */
91
+ fullWidth: _angular_core.InputSignal<boolean>;
92
+ /**
93
+ * Allows the tab list to scroll horizontally instead of wrapping/overflowing
94
+ * when there are more tabs than fit the available width. @default false
95
+ */
96
+ scrollable: _angular_core.InputSignal<boolean>;
97
+ /** Additional CSS classes on the root wrapper. */
98
+ customClass: _angular_core.InputSignal<string>;
99
+ /** Emitted when a tab's close (×) button is clicked. The tab is NOT removed automatically — remove it from `tabs` yourself. */
100
+ tabClose: _angular_core.OutputEmitterRef<TabItem>;
101
+ private panels;
102
+ /** Look up the TemplateRef for a given tab ID. Returns null when no panel registered. */
103
+ panelFor(tabId: string): TemplateRef<unknown> | null;
104
+ /**
105
+ * Tracks which tab IDs have been activated at least once.
106
+ * Used for lazy-rendering: a panel is only projected into the DOM
107
+ * after the user first visits that tab.
108
+ */
109
+ activatedIds: _angular_core.WritableSignal<Set<string>>;
110
+ /** Resolved active ID — falls back to first enabled tab when empty. */
111
+ resolvedActiveId: _angular_core.Signal<string>;
112
+ navigableTabs: _angular_core.Signal<TabItem[]>;
113
+ tabTextClass: _angular_core.Signal<string>;
114
+ tabPadClass: _angular_core.Signal<string>;
115
+ iconClass: _angular_core.Signal<string>;
116
+ rootClasses: _angular_core.Signal<string>;
117
+ listClasses: _angular_core.Signal<string>;
118
+ tabClasses(tab: TabItem): string;
119
+ isPanelVisible(tab: TabItem): boolean;
120
+ /** Returns true once the tab has been activated (for lazy rendering). */
121
+ isPanelMounted(tab: TabItem): boolean;
122
+ selectTab(tab: TabItem): void;
123
+ /** Close (×) button handler — emits `tabClose`; the consumer owns removing the tab. */
124
+ onCloseTab(event: Event, tab: TabItem): void;
125
+ onTabKeydown(event: KeyboardEvent, tab: TabItem): void;
126
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvTabsComponent, never>;
127
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SvTabsComponent, "sv-tabs", never, { "tabs": { "alias": "tabs"; "required": false; "isSignal": true; }; "activeId": { "alias": "activeId"; "required": false; "isSignal": true; }; "tabsStyle": { "alias": "tabsStyle"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "layout": { "alias": "layout"; "required": false; "isSignal": true; }; "fullWidth": { "alias": "fullWidth"; "required": false; "isSignal": true; }; "scrollable": { "alias": "scrollable"; "required": false; "isSignal": true; }; "customClass": { "alias": "customClass"; "required": false; "isSignal": true; }; }, { "activeId": "activeIdChange"; "tabClose": "tabClose"; }, ["panels"], never, true, never>;
128
+ }
129
+
130
+ export { SvTabPanelDirective, SvTabsComponent };
131
+ export type { TabItem, TabsLayout, TabsSize, TabsStyle };