nativecorejs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/components/builtinRegistry.d.ts +2 -0
- package/dist/components/builtinRegistry.js +72 -0
- package/dist/components/index.d.ts +59 -0
- package/dist/components/index.js +59 -0
- package/dist/components/loading-spinner.d.ts +5 -0
- package/dist/components/loading-spinner.js +48 -0
- package/dist/components/nc-a.d.ts +45 -0
- package/dist/components/nc-a.js +290 -0
- package/dist/components/nc-accordion.d.ts +36 -0
- package/dist/components/nc-accordion.js +186 -0
- package/dist/components/nc-alert.d.ts +11 -0
- package/dist/components/nc-alert.js +127 -0
- package/dist/components/nc-animation.d.ts +117 -0
- package/dist/components/nc-animation.js +1053 -0
- package/dist/components/nc-autocomplete.d.ts +41 -0
- package/dist/components/nc-autocomplete.js +275 -0
- package/dist/components/nc-avatar-group.d.ts +7 -0
- package/dist/components/nc-avatar-group.js +85 -0
- package/dist/components/nc-avatar.d.ts +9 -0
- package/dist/components/nc-avatar.js +127 -0
- package/dist/components/nc-badge.d.ts +7 -0
- package/dist/components/nc-badge.js +63 -0
- package/dist/components/nc-bottom-nav.d.ts +53 -0
- package/dist/components/nc-bottom-nav.js +198 -0
- package/dist/components/nc-breadcrumb.d.ts +10 -0
- package/dist/components/nc-breadcrumb.js +71 -0
- package/dist/components/nc-button.d.ts +38 -0
- package/dist/components/nc-button.js +293 -0
- package/dist/components/nc-card.d.ts +11 -0
- package/dist/components/nc-card.js +74 -0
- package/dist/components/nc-checkbox.d.ts +16 -0
- package/dist/components/nc-checkbox.js +194 -0
- package/dist/components/nc-chip.d.ts +8 -0
- package/dist/components/nc-chip.js +89 -0
- package/dist/components/nc-code.d.ts +37 -0
- package/dist/components/nc-code.js +315 -0
- package/dist/components/nc-collapsible.d.ts +33 -0
- package/dist/components/nc-collapsible.js +148 -0
- package/dist/components/nc-color-picker.d.ts +33 -0
- package/dist/components/nc-color-picker.js +265 -0
- package/dist/components/nc-copy-button.d.ts +10 -0
- package/dist/components/nc-copy-button.js +94 -0
- package/dist/components/nc-date-picker.d.ts +41 -0
- package/dist/components/nc-date-picker.js +443 -0
- package/dist/components/nc-div.d.ts +53 -0
- package/dist/components/nc-div.js +270 -0
- package/dist/components/nc-divider.d.ts +7 -0
- package/dist/components/nc-divider.js +57 -0
- package/dist/components/nc-drawer.d.ts +40 -0
- package/dist/components/nc-drawer.js +217 -0
- package/dist/components/nc-dropdown.d.ts +41 -0
- package/dist/components/nc-dropdown.js +170 -0
- package/dist/components/nc-empty-state.d.ts +5 -0
- package/dist/components/nc-empty-state.js +76 -0
- package/dist/components/nc-file-upload.d.ts +40 -0
- package/dist/components/nc-file-upload.js +336 -0
- package/dist/components/nc-form.d.ts +70 -0
- package/dist/components/nc-form.js +273 -0
- package/dist/components/nc-image.d.ts +10 -0
- package/dist/components/nc-image.js +139 -0
- package/dist/components/nc-input.d.ts +25 -0
- package/dist/components/nc-input.js +302 -0
- package/dist/components/nc-kbd.d.ts +5 -0
- package/dist/components/nc-kbd.js +34 -0
- package/dist/components/nc-menu-item.d.ts +43 -0
- package/dist/components/nc-menu-item.js +182 -0
- package/dist/components/nc-menu.d.ts +76 -0
- package/dist/components/nc-menu.js +360 -0
- package/dist/components/nc-modal.d.ts +51 -0
- package/dist/components/nc-modal.js +231 -0
- package/dist/components/nc-nav-item.d.ts +35 -0
- package/dist/components/nc-nav-item.js +142 -0
- package/dist/components/nc-number-input.d.ts +22 -0
- package/dist/components/nc-number-input.js +270 -0
- package/dist/components/nc-otp-input.d.ts +41 -0
- package/dist/components/nc-otp-input.js +227 -0
- package/dist/components/nc-pagination.d.ts +28 -0
- package/dist/components/nc-pagination.js +171 -0
- package/dist/components/nc-popover.d.ts +58 -0
- package/dist/components/nc-popover.js +301 -0
- package/dist/components/nc-progress-circular.d.ts +7 -0
- package/dist/components/nc-progress-circular.js +67 -0
- package/dist/components/nc-progress.d.ts +7 -0
- package/dist/components/nc-progress.js +109 -0
- package/dist/components/nc-radio.d.ts +13 -0
- package/dist/components/nc-radio.js +169 -0
- package/dist/components/nc-rating.d.ts +19 -0
- package/dist/components/nc-rating.js +187 -0
- package/dist/components/nc-rich-text.d.ts +43 -0
- package/dist/components/nc-rich-text.js +310 -0
- package/dist/components/nc-scroll-top.d.ts +28 -0
- package/dist/components/nc-scroll-top.js +103 -0
- package/dist/components/nc-select.d.ts +51 -0
- package/dist/components/nc-select.js +425 -0
- package/dist/components/nc-skeleton.d.ts +7 -0
- package/dist/components/nc-skeleton.js +90 -0
- package/dist/components/nc-slider.d.ts +41 -0
- package/dist/components/nc-slider.js +268 -0
- package/dist/components/nc-snackbar.d.ts +51 -0
- package/dist/components/nc-snackbar.js +200 -0
- package/dist/components/nc-splash.d.ts +25 -0
- package/dist/components/nc-splash.js +296 -0
- package/dist/components/nc-stepper.d.ts +50 -0
- package/dist/components/nc-stepper.js +236 -0
- package/dist/components/nc-switch.d.ts +14 -0
- package/dist/components/nc-switch.js +194 -0
- package/dist/components/nc-tab-item.d.ts +39 -0
- package/dist/components/nc-tab-item.js +127 -0
- package/dist/components/nc-table.d.ts +44 -0
- package/dist/components/nc-table.js +265 -0
- package/dist/components/nc-tabs.d.ts +79 -0
- package/dist/components/nc-tabs.js +519 -0
- package/dist/components/nc-tag-input.d.ts +49 -0
- package/dist/components/nc-tag-input.js +268 -0
- package/dist/components/nc-textarea.d.ts +15 -0
- package/dist/components/nc-textarea.js +164 -0
- package/dist/components/nc-time-picker.d.ts +51 -0
- package/dist/components/nc-time-picker.js +452 -0
- package/dist/components/nc-timeline.d.ts +53 -0
- package/dist/components/nc-timeline.js +171 -0
- package/dist/components/nc-tooltip.d.ts +27 -0
- package/dist/components/nc-tooltip.js +135 -0
- package/dist/core/component.d.ts +33 -0
- package/dist/core/component.js +208 -0
- package/dist/core/gpu-animation.d.ts +141 -0
- package/dist/core/gpu-animation.js +474 -0
- package/dist/core/lazyComponents.d.ts +13 -0
- package/dist/core/lazyComponents.js +73 -0
- package/dist/core/router.d.ts +55 -0
- package/dist/core/router.js +424 -0
- package/dist/core/state.d.ts +18 -0
- package/dist/core/state.js +153 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/utils/cacheBuster.d.ts +9 -0
- package/dist/utils/cacheBuster.js +12 -0
- package/dist/utils/dom.d.ts +16 -0
- package/dist/utils/dom.js +70 -0
- package/dist/utils/events.d.ts +20 -0
- package/dist/utils/events.js +80 -0
- package/dist/utils/templates.d.ts +2 -0
- package/dist/utils/templates.js +2 -0
- package/package.json +53 -0
- package/src/styles/base.css +40 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeCore Tabs Component (nc-tabs)
|
|
3
|
+
*
|
|
4
|
+
* Container that manages a group of nc-tab-item panels. Renders the tab bar,
|
|
5
|
+
* handles selection, keyboard navigation, and emits change events.
|
|
6
|
+
*
|
|
7
|
+
* Attributes:
|
|
8
|
+
* variant - 'line' | 'pills' | 'boxed' (default: 'line')
|
|
9
|
+
* active - Zero-based index of the currently active tab (default: 0).
|
|
10
|
+
* Can be set externally to programmatically switch tabs.
|
|
11
|
+
* persist - Boolean. When present, persist the selected tab in sessionStorage.
|
|
12
|
+
* Uses `persist-key` if provided, else falls back to path + element id.
|
|
13
|
+
* persist-key - Optional storage key override for tab persistence.
|
|
14
|
+
* transition - Panel enter animation.
|
|
15
|
+
* 'fade' | 'slide-up' | 'slide-down' | 'slide-right' | 'slide-left' | 'none' (default: 'fade')
|
|
16
|
+
*
|
|
17
|
+
* Slots:
|
|
18
|
+
* default - Place nc-tab-item elements here.
|
|
19
|
+
*
|
|
20
|
+
* Events emitted:
|
|
21
|
+
* nc-tab-change - { index: number, label: string | null }
|
|
22
|
+
* Fires whenever the active tab changes.
|
|
23
|
+
*
|
|
24
|
+
* Keyboard support:
|
|
25
|
+
* ArrowRight / ArrowLeft - next / previous tab
|
|
26
|
+
* Home / End - first / last tab
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* <nc-tabs variant="line" active="0">
|
|
30
|
+
* <nc-tab-item label="Overview">Overview content</nc-tab-item>
|
|
31
|
+
* <nc-tab-item label="Activity">Activity content</nc-tab-item>
|
|
32
|
+
* <nc-tab-item label="Settings" disabled>Settings</nc-tab-item>
|
|
33
|
+
* </nc-tabs>
|
|
34
|
+
*
|
|
35
|
+
* // Programmatic control
|
|
36
|
+
* document.querySelector('nc-tabs').setAttribute('active', '1');
|
|
37
|
+
*
|
|
38
|
+
* // Listen for changes
|
|
39
|
+
* document.querySelector('nc-tabs').addEventListener('nc-tab-change', e => {
|
|
40
|
+
* console.log(e.detail.index, e.detail.label);
|
|
41
|
+
* });
|
|
42
|
+
*/
|
|
43
|
+
import { Component } from '../core/component.js';
|
|
44
|
+
export declare class NcTabs extends Component {
|
|
45
|
+
static useShadowDOM: boolean;
|
|
46
|
+
static attributeOptions: {
|
|
47
|
+
variant: string[];
|
|
48
|
+
transition: string[];
|
|
49
|
+
};
|
|
50
|
+
static get observedAttributes(): string[];
|
|
51
|
+
private _activeIndex;
|
|
52
|
+
private _onSlotChange;
|
|
53
|
+
private _onShadowClick;
|
|
54
|
+
private _onShadowKeydown;
|
|
55
|
+
private _scrollListenerSet;
|
|
56
|
+
private _resizeObserver;
|
|
57
|
+
template(): string;
|
|
58
|
+
onMount(): void;
|
|
59
|
+
onUnmount(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Attribute changes from outside (e.g. setting active="2" programmatically).
|
|
62
|
+
* variant change is handled automatically by :host([variant]) CSS selectors.
|
|
63
|
+
*/
|
|
64
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
65
|
+
/** Returns all direct nc-tab-item children (light DOM). */
|
|
66
|
+
private _getTabItems;
|
|
67
|
+
/**
|
|
68
|
+
* Rebuilds the tab bar buttons from child nc-tab-item labels,
|
|
69
|
+
* then syncs the active attribute on each tab item panel.
|
|
70
|
+
*/
|
|
71
|
+
private _buildTabBar;
|
|
72
|
+
private _setupScrollArrows;
|
|
73
|
+
private _updateScrollBtns;
|
|
74
|
+
/** Selects a tab by index - updates state, DOM, host attribute, and emits event. */
|
|
75
|
+
private _selectTab;
|
|
76
|
+
private _storageKey;
|
|
77
|
+
private _readPersistedIndex;
|
|
78
|
+
private _persistActiveIndex;
|
|
79
|
+
}
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeCore Tabs Component (nc-tabs)
|
|
3
|
+
*
|
|
4
|
+
* Container that manages a group of nc-tab-item panels. Renders the tab bar,
|
|
5
|
+
* handles selection, keyboard navigation, and emits change events.
|
|
6
|
+
*
|
|
7
|
+
* Attributes:
|
|
8
|
+
* variant - 'line' | 'pills' | 'boxed' (default: 'line')
|
|
9
|
+
* active - Zero-based index of the currently active tab (default: 0).
|
|
10
|
+
* Can be set externally to programmatically switch tabs.
|
|
11
|
+
* persist - Boolean. When present, persist the selected tab in sessionStorage.
|
|
12
|
+
* Uses `persist-key` if provided, else falls back to path + element id.
|
|
13
|
+
* persist-key - Optional storage key override for tab persistence.
|
|
14
|
+
* transition - Panel enter animation.
|
|
15
|
+
* 'fade' | 'slide-up' | 'slide-down' | 'slide-right' | 'slide-left' | 'none' (default: 'fade')
|
|
16
|
+
*
|
|
17
|
+
* Slots:
|
|
18
|
+
* default - Place nc-tab-item elements here.
|
|
19
|
+
*
|
|
20
|
+
* Events emitted:
|
|
21
|
+
* nc-tab-change - { index: number, label: string | null }
|
|
22
|
+
* Fires whenever the active tab changes.
|
|
23
|
+
*
|
|
24
|
+
* Keyboard support:
|
|
25
|
+
* ArrowRight / ArrowLeft - next / previous tab
|
|
26
|
+
* Home / End - first / last tab
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* <nc-tabs variant="line" active="0">
|
|
30
|
+
* <nc-tab-item label="Overview">Overview content</nc-tab-item>
|
|
31
|
+
* <nc-tab-item label="Activity">Activity content</nc-tab-item>
|
|
32
|
+
* <nc-tab-item label="Settings" disabled>Settings</nc-tab-item>
|
|
33
|
+
* </nc-tabs>
|
|
34
|
+
*
|
|
35
|
+
* // Programmatic control
|
|
36
|
+
* document.querySelector('nc-tabs').setAttribute('active', '1');
|
|
37
|
+
*
|
|
38
|
+
* // Listen for changes
|
|
39
|
+
* document.querySelector('nc-tabs').addEventListener('nc-tab-change', e => {
|
|
40
|
+
* console.log(e.detail.index, e.detail.label);
|
|
41
|
+
* });
|
|
42
|
+
*/
|
|
43
|
+
import { Component, defineComponent } from '../core/component.js';
|
|
44
|
+
import { html } from '../utils/templates.js';
|
|
45
|
+
export class NcTabs extends Component {
|
|
46
|
+
static useShadowDOM = true;
|
|
47
|
+
static attributeOptions = {
|
|
48
|
+
variant: ['line', 'pills', 'boxed'],
|
|
49
|
+
transition: ['fade', 'slide-up', 'slide-down', 'slide-right', 'slide-left', 'none'],
|
|
50
|
+
};
|
|
51
|
+
static get observedAttributes() {
|
|
52
|
+
return ['variant', 'active', 'transition', 'persist', 'persist-key'];
|
|
53
|
+
}
|
|
54
|
+
// ─── internal state ────────────────────────────────────────────────────────
|
|
55
|
+
_activeIndex = 0;
|
|
56
|
+
// ─── cleanup refs ──────────────────────────────────────────────────────────
|
|
57
|
+
_onSlotChange = null;
|
|
58
|
+
_onShadowClick = null;
|
|
59
|
+
_onShadowKeydown = null;
|
|
60
|
+
// ─── scroll arrow state ────────────────────────────────────────────────────
|
|
61
|
+
_scrollListenerSet = false;
|
|
62
|
+
_resizeObserver = null;
|
|
63
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
template() {
|
|
65
|
+
const variant = this.attr('variant', 'line');
|
|
66
|
+
return html `
|
|
67
|
+
<style>
|
|
68
|
+
/* ── Host layout ─────────────────────────────────────────────── */
|
|
69
|
+
:host {
|
|
70
|
+
display: block;
|
|
71
|
+
width: 100%;
|
|
72
|
+
font-family: var(--nc-font-family);
|
|
73
|
+
box-sizing: border-box;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.nc-tabs {
|
|
77
|
+
width: 100%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ── Tab bar ─────────────────────────────────────────────────── */
|
|
81
|
+
.nc-tabs__bar {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: flex-end;
|
|
84
|
+
position: relative;
|
|
85
|
+
/* line variant default: bottom border */
|
|
86
|
+
border-bottom: 2px solid var(--nc-border);
|
|
87
|
+
gap: 0;
|
|
88
|
+
/* - critical for scroll: width must be bounded, not grow to content - */
|
|
89
|
+
width: 100%;
|
|
90
|
+
overflow-x: auto;
|
|
91
|
+
scrollbar-width: none;
|
|
92
|
+
-webkit-overflow-scrolling: touch;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.nc-tabs__bar::-webkit-scrollbar {
|
|
96
|
+
display: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ── Tab buttons ─────────────────────────────────────────────── */
|
|
100
|
+
.nc-tabs__btn {
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
gap: var(--nc-spacing-xs);
|
|
105
|
+
background: transparent;
|
|
106
|
+
border: none;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
font-family: inherit;
|
|
109
|
+
font-size: var(--nc-font-size-sm);
|
|
110
|
+
font-weight: var(--nc-font-weight-medium);
|
|
111
|
+
color: var(--nc-text-secondary);
|
|
112
|
+
padding: var(--nc-spacing-sm) var(--nc-spacing-md);
|
|
113
|
+
position: relative;
|
|
114
|
+
margin-bottom: -2px;
|
|
115
|
+
white-space: nowrap;
|
|
116
|
+
outline: none;
|
|
117
|
+
transition:
|
|
118
|
+
color var(--nc-transition-fast),
|
|
119
|
+
background var(--nc-transition-fast);
|
|
120
|
+
border-radius: var(--nc-radius-sm) var(--nc-radius-sm) 0 0;
|
|
121
|
+
user-select: none;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Sliding underline indicator (line variant) */
|
|
125
|
+
.nc-tabs__btn::after {
|
|
126
|
+
content: '';
|
|
127
|
+
position: absolute;
|
|
128
|
+
bottom: 0;
|
|
129
|
+
left: 0;
|
|
130
|
+
right: 0;
|
|
131
|
+
height: 2px;
|
|
132
|
+
background: var(--nc-primary);
|
|
133
|
+
border-radius: 2px 2px 0 0;
|
|
134
|
+
transform: scaleX(0);
|
|
135
|
+
transition: transform var(--nc-transition-fast) var(--nc-ease-out);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.nc-tabs__btn:hover:not([disabled]) {
|
|
139
|
+
color: var(--nc-text);
|
|
140
|
+
background: rgba(0, 0, 0, 0.03);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.nc-tabs__btn--active {
|
|
144
|
+
color: var(--nc-primary);
|
|
145
|
+
font-weight: var(--nc-font-weight-semibold);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.nc-tabs__btn--active::after {
|
|
149
|
+
transform: scaleX(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.nc-tabs__btn--disabled {
|
|
153
|
+
opacity: 0.4;
|
|
154
|
+
cursor: not-allowed;
|
|
155
|
+
pointer-events: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.nc-tabs__btn:focus-visible {
|
|
159
|
+
outline: 2px solid var(--nc-primary);
|
|
160
|
+
outline-offset: -2px;
|
|
161
|
+
border-radius: var(--nc-radius-sm);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ── Pills variant ───────────────────────────────────────────── */
|
|
165
|
+
:host([variant="pills"]) .nc-tabs__bar {
|
|
166
|
+
border-bottom: none;
|
|
167
|
+
gap: var(--nc-spacing-xs);
|
|
168
|
+
padding: var(--nc-spacing-xs);
|
|
169
|
+
background: var(--nc-bg-secondary);
|
|
170
|
+
border-radius: var(--nc-radius-lg);
|
|
171
|
+
/* width: 100% inherited from base - pills scrolls rather than spills */
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
:host([variant="pills"]) .nc-tabs__btn {
|
|
175
|
+
border-radius: var(--nc-radius-md);
|
|
176
|
+
margin-bottom: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
:host([variant="pills"]) .nc-tabs__btn::after {
|
|
180
|
+
display: none;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
:host([variant="pills"]) .nc-tabs__btn--active {
|
|
184
|
+
background: var(--nc-primary);
|
|
185
|
+
color: var(--nc-white);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
:host([variant="pills"]) .nc-tabs__btn:hover:not([disabled]):not(.nc-tabs__btn--active) {
|
|
189
|
+
background: var(--nc-bg-tertiary);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ── Boxed variant ───────────────────────────────────────────── */
|
|
193
|
+
:host([variant="boxed"]) .nc-tabs__bar {
|
|
194
|
+
border-bottom: 1px solid var(--nc-border);
|
|
195
|
+
gap: 2px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
:host([variant="boxed"]) .nc-tabs__btn {
|
|
199
|
+
border: 1px solid transparent;
|
|
200
|
+
border-bottom: none;
|
|
201
|
+
border-radius: var(--nc-radius-md) var(--nc-radius-md) 0 0;
|
|
202
|
+
margin-bottom: -1px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
:host([variant="boxed"]) .nc-tabs__btn::after {
|
|
206
|
+
display: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
:host([variant="boxed"]) .nc-tabs__btn--active {
|
|
210
|
+
border-color: var(--nc-border);
|
|
211
|
+
background: var(--nc-bg);
|
|
212
|
+
color: var(--nc-primary);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* ── Content panels ──────────────────────────────────────────── */
|
|
216
|
+
.nc-tabs__panels {
|
|
217
|
+
padding-top: var(--nc-spacing-sm);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* ── Bar wrapper (anchors scroll arrows) ─────────────────────── */
|
|
221
|
+
.nc-tabs__bar-wrap {
|
|
222
|
+
position: relative;
|
|
223
|
+
/* contain the bar so overflow-x:auto has a definite boundary */
|
|
224
|
+
min-width: 0;
|
|
225
|
+
overflow: hidden;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ── Scroll arrow buttons ────────────────────────────────────── */
|
|
229
|
+
.nc-tabs__scroll-btn {
|
|
230
|
+
display: none; /* JS sets to flex when scrollable */
|
|
231
|
+
position: absolute;
|
|
232
|
+
top: 0;
|
|
233
|
+
bottom: 0;
|
|
234
|
+
width: 36px;
|
|
235
|
+
align-items: center;
|
|
236
|
+
justify-content: center;
|
|
237
|
+
border: none;
|
|
238
|
+
padding: 0;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
z-index: 2;
|
|
241
|
+
color: var(--nc-text-muted);
|
|
242
|
+
transition: color var(--nc-transition-fast);
|
|
243
|
+
}
|
|
244
|
+
.nc-tabs__scroll-btn:hover { color: var(--nc-text); }
|
|
245
|
+
|
|
246
|
+
.nc-tabs__scroll-btn--prev {
|
|
247
|
+
left: 0;
|
|
248
|
+
background: linear-gradient(to right, var(--nc-bg) 45%, transparent 100%);
|
|
249
|
+
}
|
|
250
|
+
.nc-tabs__scroll-btn--next {
|
|
251
|
+
right: 0;
|
|
252
|
+
background: linear-gradient(to left, var(--nc-bg) 45%, transparent 100%);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* ── Responsive ──────────────────────────────────────────────── */
|
|
256
|
+
@media (max-width: 640px) {
|
|
257
|
+
.nc-tabs__panels {
|
|
258
|
+
padding-top: 0;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
</style>
|
|
262
|
+
|
|
263
|
+
<div class="nc-tabs nc-tabs--${variant}">
|
|
264
|
+
<div class="nc-tabs__bar-wrap">
|
|
265
|
+
<button class="nc-tabs__scroll-btn nc-tabs__scroll-btn--prev" type="button" aria-label="Scroll tabs back" aria-hidden="true">
|
|
266
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M8 2L4 6l4 4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
267
|
+
</button>
|
|
268
|
+
<div class="nc-tabs__bar" role="tablist" aria-label="Tabs"></div>
|
|
269
|
+
<button class="nc-tabs__scroll-btn nc-tabs__scroll-btn--next" type="button" aria-label="Scroll tabs forward" aria-hidden="true">
|
|
270
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="nc-tabs__panels">
|
|
274
|
+
<slot></slot>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
onMount() {
|
|
280
|
+
this._activeIndex = parseInt(this.attr('active', '0') ?? '0', 10);
|
|
281
|
+
const persistedIndex = this._readPersistedIndex();
|
|
282
|
+
if (persistedIndex !== null) {
|
|
283
|
+
this._activeIndex = persistedIndex;
|
|
284
|
+
this.setAttribute('active', String(persistedIndex));
|
|
285
|
+
}
|
|
286
|
+
// Defer first build one tick - child nc-tab-item elements upgrade after parent
|
|
287
|
+
Promise.resolve().then(() => this._buildTabBar());
|
|
288
|
+
// Re-build when tab items are added or removed dynamically
|
|
289
|
+
const slot = this.$('slot');
|
|
290
|
+
if (slot) {
|
|
291
|
+
this._onSlotChange = () => this._buildTabBar();
|
|
292
|
+
slot.addEventListener('slotchange', this._onSlotChange);
|
|
293
|
+
}
|
|
294
|
+
// Click delegation on shadow root - handles all tab button clicks
|
|
295
|
+
this._onShadowClick = (e) => {
|
|
296
|
+
const btn = e.target.closest('[data-tab-index]');
|
|
297
|
+
if (!btn || btn.hasAttribute('disabled'))
|
|
298
|
+
return;
|
|
299
|
+
const index = parseInt(btn.dataset.tabIndex, 10);
|
|
300
|
+
if (!isNaN(index))
|
|
301
|
+
this._selectTab(index);
|
|
302
|
+
};
|
|
303
|
+
this.shadowRoot.addEventListener('click', this._onShadowClick);
|
|
304
|
+
// Keyboard navigation on the tab bar
|
|
305
|
+
this._onShadowKeydown = (e) => {
|
|
306
|
+
const ke = e;
|
|
307
|
+
if (!['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(ke.key))
|
|
308
|
+
return;
|
|
309
|
+
ke.preventDefault();
|
|
310
|
+
const tabs = this._getTabItems();
|
|
311
|
+
const len = tabs.length;
|
|
312
|
+
if (len === 0)
|
|
313
|
+
return;
|
|
314
|
+
let next = this._activeIndex;
|
|
315
|
+
if (ke.key === 'ArrowRight') {
|
|
316
|
+
next = (this._activeIndex + 1) % len;
|
|
317
|
+
}
|
|
318
|
+
else if (ke.key === 'ArrowLeft') {
|
|
319
|
+
next = (this._activeIndex - 1 + len) % len;
|
|
320
|
+
}
|
|
321
|
+
else if (ke.key === 'Home') {
|
|
322
|
+
next = 0;
|
|
323
|
+
}
|
|
324
|
+
else if (ke.key === 'End') {
|
|
325
|
+
next = len - 1;
|
|
326
|
+
}
|
|
327
|
+
// Skip disabled tabs
|
|
328
|
+
let attempts = 0;
|
|
329
|
+
while (tabs[next]?.hasAttribute('disabled') && attempts < len) {
|
|
330
|
+
next = ke.key === 'ArrowLeft'
|
|
331
|
+
? (next - 1 + len) % len
|
|
332
|
+
: (next + 1) % len;
|
|
333
|
+
attempts++;
|
|
334
|
+
}
|
|
335
|
+
this._selectTab(next);
|
|
336
|
+
// Focus the newly active button
|
|
337
|
+
const activeBtn = this.shadowRoot?.querySelector(`[data-tab-index="${next}"]`);
|
|
338
|
+
activeBtn?.focus();
|
|
339
|
+
};
|
|
340
|
+
this.shadowRoot.addEventListener('keydown', this._onShadowKeydown);
|
|
341
|
+
}
|
|
342
|
+
onUnmount() {
|
|
343
|
+
const slot = this.$('slot');
|
|
344
|
+
if (slot && this._onSlotChange) {
|
|
345
|
+
slot.removeEventListener('slotchange', this._onSlotChange);
|
|
346
|
+
}
|
|
347
|
+
if (this._onShadowClick) {
|
|
348
|
+
this.shadowRoot?.removeEventListener('click', this._onShadowClick);
|
|
349
|
+
}
|
|
350
|
+
if (this._onShadowKeydown) {
|
|
351
|
+
this.shadowRoot?.removeEventListener('keydown', this._onShadowKeydown);
|
|
352
|
+
}
|
|
353
|
+
this._onSlotChange = null;
|
|
354
|
+
this._onShadowClick = null;
|
|
355
|
+
this._onShadowKeydown = null;
|
|
356
|
+
this._resizeObserver?.disconnect();
|
|
357
|
+
this._resizeObserver = null;
|
|
358
|
+
this._scrollListenerSet = false;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Attribute changes from outside (e.g. setting active="2" programmatically).
|
|
362
|
+
* variant change is handled automatically by :host([variant]) CSS selectors.
|
|
363
|
+
*/
|
|
364
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
365
|
+
if (!this._mounted || oldValue === newValue)
|
|
366
|
+
return;
|
|
367
|
+
if (name === 'active') {
|
|
368
|
+
const index = parseInt(newValue ?? '0', 10);
|
|
369
|
+
if (!isNaN(index) && index !== this._activeIndex) {
|
|
370
|
+
this._activeIndex = index;
|
|
371
|
+
this._buildTabBar();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// variant + transition: :host([attr="..."]) CSS handles appearance - no JS needed
|
|
375
|
+
}
|
|
376
|
+
// ─── Private helpers ────────────────────────────────────────────────────────
|
|
377
|
+
/** Returns all direct nc-tab-item children (light DOM). */
|
|
378
|
+
_getTabItems() {
|
|
379
|
+
return Array.from(this.querySelectorAll(':scope > nc-tab-item'));
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Rebuilds the tab bar buttons from child nc-tab-item labels,
|
|
383
|
+
* then syncs the active attribute on each tab item panel.
|
|
384
|
+
*/
|
|
385
|
+
_buildTabBar() {
|
|
386
|
+
const bar = this.$('.nc-tabs__bar');
|
|
387
|
+
if (!bar)
|
|
388
|
+
return;
|
|
389
|
+
const tabs = this._getTabItems();
|
|
390
|
+
// Clamp active index to valid range
|
|
391
|
+
if (this._activeIndex >= tabs.length) {
|
|
392
|
+
this._activeIndex = Math.max(0, tabs.length - 1);
|
|
393
|
+
}
|
|
394
|
+
this._persistActiveIndex();
|
|
395
|
+
bar.innerHTML = tabs.map((tab, i) => {
|
|
396
|
+
const label = tab.getAttribute('label') ?? `Tab ${i + 1}`;
|
|
397
|
+
const disabled = tab.hasAttribute('disabled');
|
|
398
|
+
const isActive = i === this._activeIndex;
|
|
399
|
+
const classes = [
|
|
400
|
+
'nc-tabs__btn',
|
|
401
|
+
isActive ? 'nc-tabs__btn--active' : '',
|
|
402
|
+
disabled ? 'nc-tabs__btn--disabled' : '',
|
|
403
|
+
].filter(Boolean).join(' ');
|
|
404
|
+
return `<button
|
|
405
|
+
class="${classes}"
|
|
406
|
+
role="tab"
|
|
407
|
+
aria-selected="${isActive}"
|
|
408
|
+
aria-disabled="${disabled}"
|
|
409
|
+
tabindex="${isActive ? 0 : -1}"
|
|
410
|
+
data-tab-index="${i}"
|
|
411
|
+
${disabled ? 'disabled' : ''}
|
|
412
|
+
>${label}</button>`;
|
|
413
|
+
}).join('');
|
|
414
|
+
// Sync active attribute on each panel (drives :host([active]) CSS in nc-tab-item)
|
|
415
|
+
// Also sync data-nc-transition so nc-tab-item picks the correct animation variant
|
|
416
|
+
const transition = this.getAttribute('transition') || 'fade';
|
|
417
|
+
tabs.forEach((tab, i) => {
|
|
418
|
+
tab.setAttribute('data-nc-transition', transition);
|
|
419
|
+
if (i === this._activeIndex) {
|
|
420
|
+
tab.setAttribute('active', '');
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
tab.removeAttribute('active');
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
this._setupScrollArrows();
|
|
427
|
+
}
|
|
428
|
+
// ─── Scroll arrow helpers ───────────────────────────────────────────────────
|
|
429
|
+
_setupScrollArrows() {
|
|
430
|
+
const bar = this.$('.nc-tabs__bar');
|
|
431
|
+
if (!bar)
|
|
432
|
+
return;
|
|
433
|
+
if (!this._scrollListenerSet) {
|
|
434
|
+
bar.addEventListener('scroll', () => this._updateScrollBtns(), { passive: true });
|
|
435
|
+
this.$('.nc-tabs__scroll-btn--prev')
|
|
436
|
+
?.addEventListener('click', () => { bar.scrollBy({ left: -150, behavior: 'smooth' }); });
|
|
437
|
+
this.$('.nc-tabs__scroll-btn--next')
|
|
438
|
+
?.addEventListener('click', () => { bar.scrollBy({ left: 150, behavior: 'smooth' }); });
|
|
439
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
440
|
+
this._resizeObserver = new ResizeObserver(() => this._updateScrollBtns());
|
|
441
|
+
this._resizeObserver.observe(bar);
|
|
442
|
+
}
|
|
443
|
+
this._scrollListenerSet = true;
|
|
444
|
+
}
|
|
445
|
+
this._updateScrollBtns();
|
|
446
|
+
// Scroll the active tab into view after rebuilding the bar
|
|
447
|
+
requestAnimationFrame(() => {
|
|
448
|
+
this.shadowRoot?.querySelector('.nc-tabs__btn--active')
|
|
449
|
+
?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
_updateScrollBtns() {
|
|
453
|
+
const bar = this.$('.nc-tabs__bar');
|
|
454
|
+
const prevBtn = this.$('.nc-tabs__scroll-btn--prev');
|
|
455
|
+
const nextBtn = this.$('.nc-tabs__scroll-btn--next');
|
|
456
|
+
if (!bar || !prevBtn || !nextBtn)
|
|
457
|
+
return;
|
|
458
|
+
const canScrollLeft = bar.scrollLeft > 1;
|
|
459
|
+
const canScrollRight = bar.scrollLeft < bar.scrollWidth - bar.clientWidth - 1;
|
|
460
|
+
prevBtn.style.display = canScrollLeft ? 'flex' : 'none';
|
|
461
|
+
nextBtn.style.display = canScrollRight ? 'flex' : 'none';
|
|
462
|
+
}
|
|
463
|
+
/** Selects a tab by index - updates state, DOM, host attribute, and emits event. */
|
|
464
|
+
_selectTab(index) {
|
|
465
|
+
const tabs = this._getTabItems();
|
|
466
|
+
if (index < 0 || index >= tabs.length)
|
|
467
|
+
return;
|
|
468
|
+
if (tabs[index]?.hasAttribute('disabled'))
|
|
469
|
+
return;
|
|
470
|
+
if (index === this._activeIndex)
|
|
471
|
+
return;
|
|
472
|
+
this._activeIndex = index;
|
|
473
|
+
this._buildTabBar();
|
|
474
|
+
// Keep host attribute in sync so external code can read it
|
|
475
|
+
// Use setAttribute silently - attributeChangedCallback guard (oldValue !== newValue) prevents loops
|
|
476
|
+
this.setAttribute('active', String(index));
|
|
477
|
+
this.emitEvent('nc-tab-change', {
|
|
478
|
+
index,
|
|
479
|
+
label: tabs[index].getAttribute('label'),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
_storageKey() {
|
|
483
|
+
if (!this.hasAttribute('persist'))
|
|
484
|
+
return null;
|
|
485
|
+
const explicit = this.getAttribute('persist-key');
|
|
486
|
+
if (explicit)
|
|
487
|
+
return explicit;
|
|
488
|
+
if (this.id)
|
|
489
|
+
return `nc-tabs:${window.location.pathname}:${this.id}`;
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
_readPersistedIndex() {
|
|
493
|
+
const key = this._storageKey();
|
|
494
|
+
if (!key)
|
|
495
|
+
return null;
|
|
496
|
+
try {
|
|
497
|
+
const raw = sessionStorage.getItem(key);
|
|
498
|
+
if (raw === null)
|
|
499
|
+
return null;
|
|
500
|
+
const index = parseInt(raw, 10);
|
|
501
|
+
return Number.isNaN(index) ? null : index;
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
_persistActiveIndex() {
|
|
508
|
+
const key = this._storageKey();
|
|
509
|
+
if (!key)
|
|
510
|
+
return;
|
|
511
|
+
try {
|
|
512
|
+
sessionStorage.setItem(key, String(this._activeIndex));
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// Ignore storage failures (private mode / quota / disabled storage)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
defineComponent('nc-tabs', NcTabs);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcTagInput Component - text input that creates dismissible tag chips
|
|
3
|
+
*
|
|
4
|
+
* Attributes:
|
|
5
|
+
* placeholder - input placeholder text
|
|
6
|
+
* value - comma-separated initial tags (e.g. "react,vue,svelte")
|
|
7
|
+
* max - maximum number of tags allowed
|
|
8
|
+
* min-length - minimum character length for a tag (default: 1)
|
|
9
|
+
* max-length - maximum character length per tag
|
|
10
|
+
* delimiter - character(s) that trigger tag creation in addition to Enter (default: ',')
|
|
11
|
+
* disabled - boolean
|
|
12
|
+
* readonly - boolean - show tags but cannot add/remove
|
|
13
|
+
* duplicate - boolean - allow duplicate tags (default: false)
|
|
14
|
+
* variant - 'default'|'filled' (default: 'default')
|
|
15
|
+
* label - visible label text
|
|
16
|
+
* hint - helper text below input
|
|
17
|
+
* error - error message (shown in red)
|
|
18
|
+
*
|
|
19
|
+
* Events:
|
|
20
|
+
* change - CustomEvent<{ tags: string[] }> - tag list changed
|
|
21
|
+
* add - CustomEvent<{ tag: string }>
|
|
22
|
+
* remove - CustomEvent<{ tag: string; index: number }>
|
|
23
|
+
* max-reached - CustomEvent - fired when max is exceeded
|
|
24
|
+
*
|
|
25
|
+
* Methods:
|
|
26
|
+
* el.getTags() - string[]
|
|
27
|
+
* el.setTags(tags) - replace all tags
|
|
28
|
+
* el.addTag(tag) - programmatic add
|
|
29
|
+
* el.removeTag(index) - programmatic remove
|
|
30
|
+
* el.clear() - remove all tags
|
|
31
|
+
*/
|
|
32
|
+
import { Component } from '../core/component.js';
|
|
33
|
+
export declare class NcTagInput extends Component {
|
|
34
|
+
static useShadowDOM: boolean;
|
|
35
|
+
private _tags;
|
|
36
|
+
static get observedAttributes(): string[];
|
|
37
|
+
connectedCallback(): void;
|
|
38
|
+
template(): string;
|
|
39
|
+
onMount(): void;
|
|
40
|
+
private _bindEvents;
|
|
41
|
+
getTags(): string[];
|
|
42
|
+
setTags(tags: string[]): void;
|
|
43
|
+
addTag(tag: string): void;
|
|
44
|
+
removeTag(index: number): void;
|
|
45
|
+
clear(): void;
|
|
46
|
+
private _emit;
|
|
47
|
+
private _escape;
|
|
48
|
+
attributeChangedCallback(name: string, oldVal: string, newVal: string): void;
|
|
49
|
+
}
|