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,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeCore Menu Component (nc-menu)
|
|
3
|
+
*
|
|
4
|
+
* A vertical command menu. Place nc-menu-item elements inside as direct children.
|
|
5
|
+
* Supports grouped items (via nc-menu-divider or label attribute), a searchable
|
|
6
|
+
* filter box, and several visual variants.
|
|
7
|
+
*
|
|
8
|
+
* Attributes:
|
|
9
|
+
* variant - 'default' | 'compact' | 'inset' | 'bordered' (default: 'default')
|
|
10
|
+
* searchable - Boolean. Adds a filter input at the top that hides non-matching items.
|
|
11
|
+
* label - Optional header label shown above the items.
|
|
12
|
+
* width - CSS width value, e.g. '220px'. Defaults to 'fit-content'.
|
|
13
|
+
* auto-active - Boolean. Automatically moves the `active` attribute to whichever
|
|
14
|
+
* item was last selected or navigated to. For nc-a items, also
|
|
15
|
+
* matches the current path on mount.
|
|
16
|
+
*
|
|
17
|
+
* Slots:
|
|
18
|
+
* default - nc-menu-item (and nc-menu-divider) elements.
|
|
19
|
+
*
|
|
20
|
+
* Events emitted:
|
|
21
|
+
* nc-menu-select - { item: HTMLElement, label: string } - fires when any
|
|
22
|
+
* nc-menu-item inside emits nc-select.
|
|
23
|
+
*
|
|
24
|
+
* Keyboard:
|
|
25
|
+
* ArrowDown / ArrowUp - move focus between items.
|
|
26
|
+
* Home / End - jump to first / last item.
|
|
27
|
+
* Escape - blur the menu.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* <nc-menu label="Actions">
|
|
31
|
+
* <nc-menu-item icon="/icons/edit.svg">Edit</nc-menu-item>
|
|
32
|
+
* <nc-menu-item icon="/icons/copy.svg">Duplicate</nc-menu-item>
|
|
33
|
+
* <nc-menu-item danger icon="/icons/trash.svg">Delete</nc-menu-item>
|
|
34
|
+
* </nc-menu>
|
|
35
|
+
*
|
|
36
|
+
* <nc-menu searchable variant="bordered" width="260px">
|
|
37
|
+
* <nc-menu-item>Dashboard</nc-menu-item>
|
|
38
|
+
* <nc-menu-item active>Components</nc-menu-item>
|
|
39
|
+
* <nc-menu-item>Settings</nc-menu-item>
|
|
40
|
+
* </nc-menu>
|
|
41
|
+
*
|
|
42
|
+
* <!-- Navigation sidebar: nc-a items, active managed automatically -->
|
|
43
|
+
* <nc-menu auto-active variant="inset" width="220px">
|
|
44
|
+
* <nc-a href="/dashboard" variant="ghost">Dashboard</nc-a>
|
|
45
|
+
* <nc-a href="/components" variant="ghost">Components</nc-a>
|
|
46
|
+
* <nc-a href="/settings" variant="ghost">Settings</nc-a>
|
|
47
|
+
* </nc-menu>
|
|
48
|
+
*/
|
|
49
|
+
import { Component, defineComponent } from '../core/component.js';
|
|
50
|
+
import { html } from '../utils/templates.js';
|
|
51
|
+
export class NcMenu extends Component {
|
|
52
|
+
static useShadowDOM = true;
|
|
53
|
+
static attributeOptions = {
|
|
54
|
+
variant: ['default', 'compact', 'inset', 'bordered'],
|
|
55
|
+
};
|
|
56
|
+
static get observedAttributes() {
|
|
57
|
+
return ['variant', 'searchable', 'label', 'width', 'auto-active'];
|
|
58
|
+
}
|
|
59
|
+
_onSlotChange = null;
|
|
60
|
+
_onSelect = null;
|
|
61
|
+
_onNavigate = null;
|
|
62
|
+
_onKeydown = null;
|
|
63
|
+
_onSearchInput = null;
|
|
64
|
+
template() {
|
|
65
|
+
const label = this.getAttribute('label') || '';
|
|
66
|
+
const searchable = this.hasAttribute('searchable');
|
|
67
|
+
const width = this.getAttribute('width') || 'fit-content';
|
|
68
|
+
const labelHTML = label
|
|
69
|
+
? `<div class="menu__label">${label}</div>`
|
|
70
|
+
: '';
|
|
71
|
+
const searchHTML = searchable
|
|
72
|
+
? `<div class="menu__search-wrap">
|
|
73
|
+
<svg class="menu__search-icon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
74
|
+
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" stroke-width="1.5"/>
|
|
75
|
+
<path d="M15 15l-3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
76
|
+
</svg>
|
|
77
|
+
<input class="menu__search" type="text" placeholder="Search..." autocomplete="off" spellcheck="false" />
|
|
78
|
+
</div>`
|
|
79
|
+
: '';
|
|
80
|
+
return html `
|
|
81
|
+
<style>
|
|
82
|
+
:host {
|
|
83
|
+
display: inline-block;
|
|
84
|
+
width: ${width};
|
|
85
|
+
font-family: var(--nc-font-family);
|
|
86
|
+
box-sizing: border-box;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.menu {
|
|
90
|
+
background: var(--nc-bg);
|
|
91
|
+
border-radius: var(--nc-radius-lg);
|
|
92
|
+
padding: var(--nc-spacing-xs);
|
|
93
|
+
min-width: 180px;
|
|
94
|
+
box-sizing: border-box;
|
|
95
|
+
width: 100%;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ── Variants ──────────────────────────────────────────────── */
|
|
99
|
+
:host([variant="default"]) .menu,
|
|
100
|
+
:host(:not([variant])) .menu {
|
|
101
|
+
background: var(--nc-bg);
|
|
102
|
+
padding: var(--nc-spacing-xs);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
:host([variant="compact"]) .menu {
|
|
106
|
+
padding: 2px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
:host([variant="compact"]) ::slotted(nc-menu-item) {
|
|
110
|
+
--nc-menu-item-py: var(--nc-spacing-xs);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:host([variant="inset"]) .menu {
|
|
114
|
+
background: var(--nc-bg-secondary);
|
|
115
|
+
padding: var(--nc-spacing-sm);
|
|
116
|
+
border-radius: var(--nc-radius-xl);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
:host([variant="bordered"]) .menu {
|
|
120
|
+
background: var(--nc-bg);
|
|
121
|
+
border: 1px solid var(--nc-border);
|
|
122
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
123
|
+
padding: var(--nc-spacing-xs);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ── Label ─────────────────────────────────────────────────── */
|
|
127
|
+
.menu__label {
|
|
128
|
+
font-size: var(--nc-font-size-xs, 0.7rem);
|
|
129
|
+
font-weight: 700;
|
|
130
|
+
text-transform: uppercase;
|
|
131
|
+
letter-spacing: 0.08em;
|
|
132
|
+
color: var(--nc-text-secondary);
|
|
133
|
+
padding: var(--nc-spacing-xs) var(--nc-spacing-md) var(--nc-spacing-xs);
|
|
134
|
+
margin-bottom: 2px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ── Search ────────────────────────────────────────────────── */
|
|
138
|
+
.menu__search-wrap {
|
|
139
|
+
position: relative;
|
|
140
|
+
margin-bottom: var(--nc-spacing-xs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.menu__search-icon {
|
|
144
|
+
position: absolute;
|
|
145
|
+
left: 10px;
|
|
146
|
+
top: 50%;
|
|
147
|
+
transform: translateY(-50%);
|
|
148
|
+
width: 14px;
|
|
149
|
+
height: 14px;
|
|
150
|
+
color: var(--nc-text-secondary);
|
|
151
|
+
pointer-events: none;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.menu__search {
|
|
155
|
+
width: 100%;
|
|
156
|
+
box-sizing: border-box;
|
|
157
|
+
padding: var(--nc-spacing-xs) var(--nc-spacing-sm) var(--nc-spacing-xs) 30px;
|
|
158
|
+
font-family: var(--nc-font-family);
|
|
159
|
+
font-size: var(--nc-font-size-sm);
|
|
160
|
+
background: var(--nc-bg-secondary);
|
|
161
|
+
border: 1px solid var(--nc-border);
|
|
162
|
+
border-radius: var(--nc-radius-md);
|
|
163
|
+
color: var(--nc-text);
|
|
164
|
+
outline: none;
|
|
165
|
+
transition: border-color var(--nc-transition-fast);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.menu__search:focus {
|
|
169
|
+
border-color: var(--nc-primary);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.menu__search::placeholder {
|
|
173
|
+
color: var(--nc-text-secondary);
|
|
174
|
+
opacity: 0.6;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ── Empty state ───────────────────────────────────────────── */
|
|
178
|
+
.menu__empty {
|
|
179
|
+
display: none;
|
|
180
|
+
font-size: var(--nc-font-size-sm);
|
|
181
|
+
color: var(--nc-text-secondary);
|
|
182
|
+
padding: var(--nc-spacing-md);
|
|
183
|
+
text-align: center;
|
|
184
|
+
opacity: 0.6;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.menu__empty.visible {
|
|
188
|
+
display: block;
|
|
189
|
+
}
|
|
190
|
+
</style>
|
|
191
|
+
<div class="menu" role="menu">
|
|
192
|
+
${labelHTML}
|
|
193
|
+
${searchHTML}
|
|
194
|
+
<slot></slot>
|
|
195
|
+
<div class="menu__empty">No results</div>
|
|
196
|
+
</div>
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
onMount() {
|
|
200
|
+
// Delegate nc-select events from nc-menu-item children
|
|
201
|
+
this._onSelect = (e) => {
|
|
202
|
+
const item = e.target;
|
|
203
|
+
if (item.tagName.toLowerCase() !== 'nc-menu-item')
|
|
204
|
+
return;
|
|
205
|
+
const label = item.textContent?.trim() ?? '';
|
|
206
|
+
this.emitEvent('nc-menu-select', { item, label });
|
|
207
|
+
// Always track active for nc-menu-item - no opt-in required
|
|
208
|
+
this._setActive(item);
|
|
209
|
+
};
|
|
210
|
+
this.addEventListener('nc-select', this._onSelect);
|
|
211
|
+
// Delegate nc-navigate events from nc-a children
|
|
212
|
+
this._onNavigate = (e) => {
|
|
213
|
+
const item = e.target;
|
|
214
|
+
if (item.tagName.toLowerCase() !== 'nc-a')
|
|
215
|
+
return;
|
|
216
|
+
if (this.hasAttribute('auto-active'))
|
|
217
|
+
this._setActive(item);
|
|
218
|
+
};
|
|
219
|
+
this.addEventListener('nc-navigate', this._onNavigate);
|
|
220
|
+
// Arrow key navigation (supports both nc-menu-item and nc-a)
|
|
221
|
+
this._onKeydown = (e) => {
|
|
222
|
+
const ke = e;
|
|
223
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape'].includes(ke.key))
|
|
224
|
+
return;
|
|
225
|
+
ke.preventDefault();
|
|
226
|
+
const items = this._getEnabledItems();
|
|
227
|
+
if (!items.length)
|
|
228
|
+
return;
|
|
229
|
+
const focused = document.activeElement;
|
|
230
|
+
const idx = items.findIndex(el => el === focused || el.shadowRoot?.contains(focused));
|
|
231
|
+
if (ke.key === 'Escape') {
|
|
232
|
+
focused?.blur?.();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (ke.key === 'Home') {
|
|
236
|
+
this._focusItem(items[0]);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (ke.key === 'End') {
|
|
240
|
+
this._focusItem(items[items.length - 1]);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const next = ke.key === 'ArrowDown'
|
|
244
|
+
? items[Math.min(idx + 1, items.length - 1)]
|
|
245
|
+
: items[Math.max(idx - 1, 0)];
|
|
246
|
+
this._focusItem(next);
|
|
247
|
+
};
|
|
248
|
+
this.addEventListener('keydown', this._onKeydown);
|
|
249
|
+
// Auto-active: match current path for nc-a items on mount
|
|
250
|
+
if (this.hasAttribute('auto-active')) {
|
|
251
|
+
Promise.resolve().then(() => this._syncActiveFromPath());
|
|
252
|
+
}
|
|
253
|
+
// Search filtering
|
|
254
|
+
if (this.hasAttribute('searchable')) {
|
|
255
|
+
this._attachSearch();
|
|
256
|
+
}
|
|
257
|
+
// Re-sync search + path when slot contents change
|
|
258
|
+
const slot = this.$('slot');
|
|
259
|
+
if (slot) {
|
|
260
|
+
this._onSlotChange = () => {
|
|
261
|
+
if (this.hasAttribute('searchable'))
|
|
262
|
+
this._filterItems('');
|
|
263
|
+
if (this.hasAttribute('auto-active'))
|
|
264
|
+
this._syncActiveFromPath();
|
|
265
|
+
};
|
|
266
|
+
slot.addEventListener('slotchange', this._onSlotChange);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
onUnmount() {
|
|
270
|
+
if (this._onSelect)
|
|
271
|
+
this.removeEventListener('nc-select', this._onSelect);
|
|
272
|
+
if (this._onNavigate)
|
|
273
|
+
this.removeEventListener('nc-navigate', this._onNavigate);
|
|
274
|
+
if (this._onKeydown)
|
|
275
|
+
this.removeEventListener('keydown', this._onKeydown);
|
|
276
|
+
const slot = this.$('slot');
|
|
277
|
+
if (slot && this._onSlotChange)
|
|
278
|
+
slot.removeEventListener('slotchange', this._onSlotChange);
|
|
279
|
+
this._onSelect = null;
|
|
280
|
+
this._onNavigate = null;
|
|
281
|
+
this._onKeydown = null;
|
|
282
|
+
this._onSlotChange = null;
|
|
283
|
+
this._onSearchInput = null;
|
|
284
|
+
}
|
|
285
|
+
attributeChangedCallback(_name, oldValue, newValue) {
|
|
286
|
+
if (this._mounted && oldValue !== newValue)
|
|
287
|
+
this.render();
|
|
288
|
+
}
|
|
289
|
+
// ─── Private ────────────────────────────────────────────────────────────────
|
|
290
|
+
_getEnabledItems() {
|
|
291
|
+
return Array.from(this.querySelectorAll('nc-menu-item:not([disabled]), nc-a:not([disabled])'));
|
|
292
|
+
}
|
|
293
|
+
_focusItem(item) {
|
|
294
|
+
const tag = item.tagName.toLowerCase();
|
|
295
|
+
const selector = tag === 'nc-a' ? 'a' : '[role="menuitem"]';
|
|
296
|
+
item.shadowRoot?.querySelector(selector)?.focus();
|
|
297
|
+
}
|
|
298
|
+
/** Moves `active` to `target`, removes it from all siblings. */
|
|
299
|
+
_setActive(target) {
|
|
300
|
+
const all = Array.from(this.querySelectorAll('nc-menu-item, nc-a'));
|
|
301
|
+
all.forEach(el => {
|
|
302
|
+
if (el === target) {
|
|
303
|
+
el.setAttribute('active', '');
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
el.removeAttribute('active');
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* For nc-a items with auto-active: set active on the item whose href
|
|
312
|
+
* matches the current pathname. Handles exact and prefix matches.
|
|
313
|
+
*/
|
|
314
|
+
_syncActiveFromPath() {
|
|
315
|
+
const path = window.location.pathname;
|
|
316
|
+
const links = Array.from(this.querySelectorAll('nc-a[href]'));
|
|
317
|
+
if (!links.length)
|
|
318
|
+
return;
|
|
319
|
+
// Prefer exact match, fall back to longest prefix
|
|
320
|
+
let best = null;
|
|
321
|
+
let bestLen = 0;
|
|
322
|
+
links.forEach(link => {
|
|
323
|
+
const href = link.getAttribute('href') ?? '';
|
|
324
|
+
if (href === path) {
|
|
325
|
+
best = link;
|
|
326
|
+
bestLen = Infinity;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (bestLen < Infinity && path.startsWith(href) && href.length > bestLen) {
|
|
330
|
+
best = link;
|
|
331
|
+
bestLen = href.length;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
if (best)
|
|
335
|
+
this._setActive(best);
|
|
336
|
+
}
|
|
337
|
+
_attachSearch() {
|
|
338
|
+
const input = this.$('.menu__search');
|
|
339
|
+
if (!input)
|
|
340
|
+
return;
|
|
341
|
+
this._onSearchInput = () => this._filterItems(input.value);
|
|
342
|
+
input.addEventListener('input', this._onSearchInput);
|
|
343
|
+
}
|
|
344
|
+
_filterItems(query) {
|
|
345
|
+
const q = query.toLowerCase().trim();
|
|
346
|
+
const items = Array.from(this.querySelectorAll('nc-menu-item, nc-a'));
|
|
347
|
+
let visible = 0;
|
|
348
|
+
items.forEach(item => {
|
|
349
|
+
const text = (item.textContent ?? '').toLowerCase();
|
|
350
|
+
const show = !q || text.includes(q);
|
|
351
|
+
item.style.display = show ? '' : 'none';
|
|
352
|
+
if (show)
|
|
353
|
+
visible++;
|
|
354
|
+
});
|
|
355
|
+
const empty = this.$('.menu__empty');
|
|
356
|
+
if (empty)
|
|
357
|
+
empty.classList.toggle('visible', visible === 0);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
defineComponent('nc-menu', NcMenu);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcModal Component
|
|
3
|
+
*
|
|
4
|
+
* Attributes:
|
|
5
|
+
* - open: boolean - visible state
|
|
6
|
+
* - size: 'sm'|'md'|'lg'|'xl'|'full' (default: 'md')
|
|
7
|
+
* - no-close-btn: boolean - hide header × button
|
|
8
|
+
* - close-on-overlay: boolean - click backdrop to close (default: true)
|
|
9
|
+
* - no-overlay: boolean - skip backdrop rendering
|
|
10
|
+
* - sticky-header: boolean - header doesn't scroll with body
|
|
11
|
+
* - sticky-footer: boolean - footer stays at bottom
|
|
12
|
+
*
|
|
13
|
+
* Slots:
|
|
14
|
+
* - header - modal title / header area
|
|
15
|
+
* - (default) - modal body
|
|
16
|
+
* - footer - action buttons area
|
|
17
|
+
*
|
|
18
|
+
* Events:
|
|
19
|
+
* - open: CustomEvent - after modal opens
|
|
20
|
+
* - close: CustomEvent - after modal closes
|
|
21
|
+
*
|
|
22
|
+
* Static API:
|
|
23
|
+
* NcModal.open(id) - open a modal by id
|
|
24
|
+
* NcModal.close(id) - close a modal by id
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* <nc-modal id="confirm-modal">
|
|
28
|
+
* <span slot="header">Confirm Delete</span>
|
|
29
|
+
* <p>Are you sure?</p>
|
|
30
|
+
* <div slot="footer">
|
|
31
|
+
* <nc-button variant="ghost" id="cancel-btn">Cancel</nc-button>
|
|
32
|
+
* <nc-button variant="danger" id="confirm-btn">Delete</nc-button>
|
|
33
|
+
* </div>
|
|
34
|
+
* </nc-modal>
|
|
35
|
+
*
|
|
36
|
+
* NcModal.open('confirm-modal');
|
|
37
|
+
*/
|
|
38
|
+
import { Component } from '../core/component.js';
|
|
39
|
+
export declare class NcModal extends Component {
|
|
40
|
+
static useShadowDOM: boolean;
|
|
41
|
+
static get observedAttributes(): string[];
|
|
42
|
+
static open(id: string): void;
|
|
43
|
+
static close(id: string): void;
|
|
44
|
+
private _onKeydown;
|
|
45
|
+
template(): string;
|
|
46
|
+
onMount(): void;
|
|
47
|
+
private _bindEvents;
|
|
48
|
+
private _close;
|
|
49
|
+
onUnmount(): void;
|
|
50
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcModal Component
|
|
3
|
+
*
|
|
4
|
+
* Attributes:
|
|
5
|
+
* - open: boolean - visible state
|
|
6
|
+
* - size: 'sm'|'md'|'lg'|'xl'|'full' (default: 'md')
|
|
7
|
+
* - no-close-btn: boolean - hide header × button
|
|
8
|
+
* - close-on-overlay: boolean - click backdrop to close (default: true)
|
|
9
|
+
* - no-overlay: boolean - skip backdrop rendering
|
|
10
|
+
* - sticky-header: boolean - header doesn't scroll with body
|
|
11
|
+
* - sticky-footer: boolean - footer stays at bottom
|
|
12
|
+
*
|
|
13
|
+
* Slots:
|
|
14
|
+
* - header - modal title / header area
|
|
15
|
+
* - (default) - modal body
|
|
16
|
+
* - footer - action buttons area
|
|
17
|
+
*
|
|
18
|
+
* Events:
|
|
19
|
+
* - open: CustomEvent - after modal opens
|
|
20
|
+
* - close: CustomEvent - after modal closes
|
|
21
|
+
*
|
|
22
|
+
* Static API:
|
|
23
|
+
* NcModal.open(id) - open a modal by id
|
|
24
|
+
* NcModal.close(id) - close a modal by id
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* <nc-modal id="confirm-modal">
|
|
28
|
+
* <span slot="header">Confirm Delete</span>
|
|
29
|
+
* <p>Are you sure?</p>
|
|
30
|
+
* <div slot="footer">
|
|
31
|
+
* <nc-button variant="ghost" id="cancel-btn">Cancel</nc-button>
|
|
32
|
+
* <nc-button variant="danger" id="confirm-btn">Delete</nc-button>
|
|
33
|
+
* </div>
|
|
34
|
+
* </nc-modal>
|
|
35
|
+
*
|
|
36
|
+
* NcModal.open('confirm-modal');
|
|
37
|
+
*/
|
|
38
|
+
import { Component, defineComponent } from '../core/component.js';
|
|
39
|
+
import { dom } from '../utils/dom.js';
|
|
40
|
+
export class NcModal extends Component {
|
|
41
|
+
static useShadowDOM = true;
|
|
42
|
+
static get observedAttributes() {
|
|
43
|
+
return ['open', 'size', 'no-close-btn', 'close-on-overlay', 'no-overlay', 'sticky-header', 'sticky-footer'];
|
|
44
|
+
}
|
|
45
|
+
static open(id) { dom.query(`#${id}`)?.setAttribute('open', ''); }
|
|
46
|
+
static close(id) { dom.query(`#${id}`)?.removeAttribute('open'); }
|
|
47
|
+
_onKeydown = null;
|
|
48
|
+
template() {
|
|
49
|
+
const open = this.hasAttribute('open');
|
|
50
|
+
const size = this.getAttribute('size') || 'md';
|
|
51
|
+
const noOverlay = this.hasAttribute('no-overlay');
|
|
52
|
+
const noCloseBtn = this.hasAttribute('no-close-btn');
|
|
53
|
+
const widths = {
|
|
54
|
+
sm: '420px', md: '560px', lg: '720px', xl: '960px', full: '100vw'
|
|
55
|
+
};
|
|
56
|
+
const maxWidth = widths[size] ?? widths.md;
|
|
57
|
+
return `
|
|
58
|
+
<style>
|
|
59
|
+
:host {
|
|
60
|
+
display: block;
|
|
61
|
+
position: fixed;
|
|
62
|
+
inset: 0;
|
|
63
|
+
z-index: 1000;
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.overlay {
|
|
68
|
+
position: absolute;
|
|
69
|
+
inset: 0;
|
|
70
|
+
background: rgba(0,0,0,.5);
|
|
71
|
+
z-index: 1000;
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
padding: var(--nc-spacing-lg);
|
|
76
|
+
opacity: ${open ? '1' : '0'};
|
|
77
|
+
pointer-events: ${open ? 'auto' : 'none'};
|
|
78
|
+
transition: opacity var(--nc-transition-base);
|
|
79
|
+
${noOverlay ? 'background: transparent;' : ''}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dialog {
|
|
83
|
+
background: var(--nc-bg);
|
|
84
|
+
border-radius: var(--nc-radius-lg, 12px);
|
|
85
|
+
box-shadow: var(--nc-shadow-xl, 0 25px 60px rgba(0,0,0,.35));
|
|
86
|
+
width: 100%;
|
|
87
|
+
max-width: ${maxWidth};
|
|
88
|
+
max-height: calc(100vh - 2 * var(--nc-spacing-lg));
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
transform: ${open ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.97)'};
|
|
92
|
+
transition: transform var(--nc-transition-base);
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
${size === 'full' ? 'height: calc(100vh - 2 * var(--nc-spacing-lg));' : ''}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.dialog__header {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: space-between;
|
|
101
|
+
padding: var(--nc-spacing-md) var(--nc-spacing-lg);
|
|
102
|
+
border-bottom: 1px solid var(--nc-border);
|
|
103
|
+
flex-shrink: 0;
|
|
104
|
+
font-family: var(--nc-font-family);
|
|
105
|
+
font-weight: var(--nc-font-weight-semibold);
|
|
106
|
+
font-size: var(--nc-font-size-lg);
|
|
107
|
+
color: var(--nc-text);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.dialog__body {
|
|
111
|
+
flex: 1;
|
|
112
|
+
overflow-y: auto;
|
|
113
|
+
padding: var(--nc-spacing-lg);
|
|
114
|
+
font-family: var(--nc-font-family);
|
|
115
|
+
font-size: var(--nc-font-size-base);
|
|
116
|
+
color: var(--nc-text);
|
|
117
|
+
line-height: var(--nc-line-height-relaxed, 1.7);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.dialog__footer {
|
|
121
|
+
padding: var(--nc-spacing-md) var(--nc-spacing-lg);
|
|
122
|
+
border-top: 1px solid var(--nc-border);
|
|
123
|
+
flex-shrink: 0;
|
|
124
|
+
display: flex;
|
|
125
|
+
justify-content: flex-end;
|
|
126
|
+
gap: var(--nc-spacing-sm);
|
|
127
|
+
}
|
|
128
|
+
.dialog__footer:empty { display: none; }
|
|
129
|
+
|
|
130
|
+
.close-btn {
|
|
131
|
+
background: none;
|
|
132
|
+
border: none;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
padding: 4px;
|
|
135
|
+
color: var(--nc-text-muted);
|
|
136
|
+
border-radius: var(--nc-radius-sm, 4px);
|
|
137
|
+
display: flex;
|
|
138
|
+
transition: color var(--nc-transition-fast), background var(--nc-transition-fast);
|
|
139
|
+
flex-shrink: 0;
|
|
140
|
+
}
|
|
141
|
+
.close-btn:hover { color: var(--nc-text); background: var(--nc-bg-secondary); }
|
|
142
|
+
</style>
|
|
143
|
+
|
|
144
|
+
<div
|
|
145
|
+
class="overlay"
|
|
146
|
+
role="dialog"
|
|
147
|
+
aria-modal="true"
|
|
148
|
+
aria-hidden="${!open}"
|
|
149
|
+
tabindex="-1"
|
|
150
|
+
>
|
|
151
|
+
<div class="dialog">
|
|
152
|
+
<div class="dialog__header">
|
|
153
|
+
<slot name="header"></slot>
|
|
154
|
+
${!noCloseBtn ? `
|
|
155
|
+
<button class="close-btn" type="button" aria-label="Close modal">
|
|
156
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="18" height="18">
|
|
157
|
+
<path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
158
|
+
</svg>
|
|
159
|
+
</button>` : ''}
|
|
160
|
+
</div>
|
|
161
|
+
<div class="dialog__body">
|
|
162
|
+
<slot></slot>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="dialog__footer">
|
|
165
|
+
<slot name="footer"></slot>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
onMount() {
|
|
172
|
+
this._bindEvents();
|
|
173
|
+
}
|
|
174
|
+
_bindEvents() {
|
|
175
|
+
const overlay = this.$('.overlay');
|
|
176
|
+
const dialog = this.$('.dialog');
|
|
177
|
+
const closeBtn = this.$('.close-btn');
|
|
178
|
+
if (closeBtn)
|
|
179
|
+
closeBtn.addEventListener('click', () => this._close());
|
|
180
|
+
overlay.addEventListener('click', (e) => {
|
|
181
|
+
if (this.getAttribute('close-on-overlay') === 'false')
|
|
182
|
+
return;
|
|
183
|
+
if (!dialog.contains(e.target))
|
|
184
|
+
this._close();
|
|
185
|
+
});
|
|
186
|
+
this._onKeydown = (e) => {
|
|
187
|
+
if (e.key === 'Escape' && this.hasAttribute('open'))
|
|
188
|
+
this._close();
|
|
189
|
+
};
|
|
190
|
+
document.addEventListener('keydown', this._onKeydown);
|
|
191
|
+
}
|
|
192
|
+
_close() {
|
|
193
|
+
this.removeAttribute('open');
|
|
194
|
+
}
|
|
195
|
+
onUnmount() {
|
|
196
|
+
if (this._onKeydown)
|
|
197
|
+
document.removeEventListener('keydown', this._onKeydown);
|
|
198
|
+
document.body.style.overflow = '';
|
|
199
|
+
}
|
|
200
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
201
|
+
if (oldValue === newValue)
|
|
202
|
+
return;
|
|
203
|
+
if (name === 'open' && this._mounted) {
|
|
204
|
+
const open = this.hasAttribute('open');
|
|
205
|
+
const overlay = this.$('.overlay');
|
|
206
|
+
const dialog = this.$('.dialog');
|
|
207
|
+
if (overlay) {
|
|
208
|
+
overlay.style.opacity = open ? '1' : '0';
|
|
209
|
+
overlay.style.pointerEvents = open ? 'auto' : 'none';
|
|
210
|
+
overlay.setAttribute('aria-hidden', String(!open));
|
|
211
|
+
}
|
|
212
|
+
if (dialog) {
|
|
213
|
+
dialog.style.transform = open
|
|
214
|
+
? 'translateY(0) scale(1)'
|
|
215
|
+
: 'translateY(12px) scale(0.97)';
|
|
216
|
+
if (open)
|
|
217
|
+
dialog.focus();
|
|
218
|
+
}
|
|
219
|
+
document.body.style.overflow = open ? 'hidden' : '';
|
|
220
|
+
this.dispatchEvent(new CustomEvent(open ? 'open' : 'close', {
|
|
221
|
+
bubbles: true, composed: true
|
|
222
|
+
}));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (this._mounted) {
|
|
226
|
+
this.render();
|
|
227
|
+
this._bindEvents();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
defineComponent('nc-modal', NcModal);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcNavItem Component - sidebar / navigation link with icon, label, badge, active state
|
|
3
|
+
*
|
|
4
|
+
* Attributes:
|
|
5
|
+
* href - link URL; if omitted renders as a <button>
|
|
6
|
+
* label - visible text (can also use default slot)
|
|
7
|
+
* icon - preset icon name OR raw SVG string (see below)
|
|
8
|
+
* active - boolean - mark as active
|
|
9
|
+
* disabled - boolean
|
|
10
|
+
* badge - badge count (number string); shown as a pill on the right
|
|
11
|
+
* badge-variant - 'primary'(default)|'success'|'danger'|'warning'
|
|
12
|
+
* indent - indentation level for nested nav (default: 0)
|
|
13
|
+
* target - anchor target (default: '_self')
|
|
14
|
+
* exact - boolean - only activate on exact URL match (router integration)
|
|
15
|
+
*
|
|
16
|
+
* Slots:
|
|
17
|
+
* icon - custom icon (overrides icon attribute)
|
|
18
|
+
* (default) - label text (overrides label attribute)
|
|
19
|
+
* badge - custom badge content
|
|
20
|
+
*
|
|
21
|
+
* Events:
|
|
22
|
+
* nav-click - CustomEvent<{ href: string | null }> - bubbles from both <a> and <button>
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* <nc-nav-item href="/dashboard" label="Dashboard" icon="home" active></nc-nav-item>
|
|
26
|
+
* <nc-nav-item href="/users" label="Users" icon="users" badge="14"></nc-nav-item>
|
|
27
|
+
*/
|
|
28
|
+
import { Component } from '../core/component.js';
|
|
29
|
+
export declare class NcNavItem extends Component {
|
|
30
|
+
static useShadowDOM: boolean;
|
|
31
|
+
static get observedAttributes(): string[];
|
|
32
|
+
template(): string;
|
|
33
|
+
onMount(): void;
|
|
34
|
+
attributeChangedCallback(n: string, o: string, v: string): void;
|
|
35
|
+
}
|