create-nativecore 0.1.1 → 0.2.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.
Files changed (175) hide show
  1. package/README.md +6 -14
  2. package/bin/index.mjs +403 -431
  3. package/package.json +3 -2
  4. package/template/.env.example +28 -0
  5. package/template/.htmlhintrc +14 -0
  6. package/template/api/data/dashboard.json +11 -0
  7. package/template/api/data/users.json +18 -0
  8. package/template/api/mockApi.js +161 -0
  9. package/template/assets/icon.svg +13 -0
  10. package/template/assets/logo.svg +25 -0
  11. package/template/eslint.config.js +94 -0
  12. package/template/index.html +137 -0
  13. package/template/manifest.json +19 -0
  14. package/template/public/.well-known/security.txt +9 -0
  15. package/template/public/_headers +24 -0
  16. package/template/public/_redirects +14 -0
  17. package/template/public/assets/icon.svg +13 -0
  18. package/template/public/assets/logo.svg +25 -0
  19. package/template/public/manifest.json +19 -0
  20. package/template/public/robots.txt +13 -0
  21. package/template/public/sitemap.xml +27 -0
  22. package/template/scripts/build-for-bots.mjs +121 -0
  23. package/template/scripts/convert-to-ts.mjs +106 -0
  24. package/template/scripts/fix-encoding.mjs +38 -0
  25. package/template/scripts/fix-svg-paths.mjs +32 -0
  26. package/template/scripts/generate-cf-router.mjs +52 -0
  27. package/template/scripts/inject-dev-tools.mjs +41 -0
  28. package/template/scripts/inject-version.mjs +65 -0
  29. package/template/scripts/make-component.mjs +445 -0
  30. package/template/scripts/make-component.mjs.backup +432 -0
  31. package/template/scripts/make-controller.mjs +119 -0
  32. package/template/scripts/make-core-component.mjs +303 -0
  33. package/template/scripts/make-view.mjs +346 -0
  34. package/template/scripts/minify.mjs +71 -0
  35. package/template/scripts/prepare-static-assets.mjs +141 -0
  36. package/template/scripts/prompt-bot-build.mjs +223 -0
  37. package/template/scripts/remove-component.mjs +170 -0
  38. package/template/scripts/remove-core-component.mjs +156 -0
  39. package/template/scripts/remove-dev.mjs +13 -0
  40. package/template/scripts/remove-view.mjs +200 -0
  41. package/template/scripts/strip-dev-blocks.mjs +30 -0
  42. package/template/scripts/watch-compile.mjs +69 -0
  43. package/template/server.js +1066 -0
  44. package/template/src/app.ts +115 -0
  45. package/template/src/components/appRegistry.ts +8 -0
  46. package/template/src/components/core/app-footer.ts +27 -0
  47. package/template/src/components/core/app-header.ts +175 -0
  48. package/template/src/components/core/app-sidebar.ts +238 -0
  49. package/template/src/components/core/loading-spinner.ts +25 -0
  50. package/template/src/components/core/nc-a.ts +313 -0
  51. package/template/src/components/core/nc-accordion.ts +186 -0
  52. package/template/src/components/core/nc-alert.ts +153 -0
  53. package/template/src/components/core/nc-animation.ts +1150 -0
  54. package/template/src/components/core/nc-autocomplete.ts +271 -0
  55. package/template/src/components/core/nc-avatar-group.ts +113 -0
  56. package/template/src/components/core/nc-avatar.ts +148 -0
  57. package/template/src/components/core/nc-badge.ts +86 -0
  58. package/template/src/components/core/nc-bottom-nav.ts +214 -0
  59. package/template/src/components/core/nc-breadcrumb.ts +96 -0
  60. package/template/src/components/core/nc-button.ts +307 -0
  61. package/template/src/components/core/nc-card.ts +160 -0
  62. package/template/src/components/core/nc-checkbox.ts +282 -0
  63. package/template/src/components/core/nc-chip.ts +115 -0
  64. package/template/src/components/core/nc-code.ts +314 -0
  65. package/template/src/components/core/nc-collapsible.ts +154 -0
  66. package/template/src/components/core/nc-color-picker.ts +268 -0
  67. package/template/src/components/core/nc-copy-button.ts +119 -0
  68. package/template/src/components/core/nc-date-picker.ts +443 -0
  69. package/template/src/components/core/nc-div.ts +280 -0
  70. package/template/src/components/core/nc-divider.ts +81 -0
  71. package/template/src/components/core/nc-drawer.ts +230 -0
  72. package/template/src/components/core/nc-dropdown.ts +178 -0
  73. package/template/src/components/core/nc-empty-state.ts +134 -0
  74. package/template/src/components/core/nc-file-upload.ts +354 -0
  75. package/template/src/components/core/nc-form.ts +312 -0
  76. package/template/src/components/core/nc-image.ts +184 -0
  77. package/template/src/components/core/nc-input.ts +383 -0
  78. package/template/src/components/core/nc-kbd.ts +48 -0
  79. package/template/src/components/core/nc-menu-item.ts +193 -0
  80. package/template/src/components/core/nc-menu.ts +376 -0
  81. package/template/src/components/core/nc-modal.ts +238 -0
  82. package/template/src/components/core/nc-nav-item.ts +151 -0
  83. package/template/src/components/core/nc-number-input.ts +350 -0
  84. package/template/src/components/core/nc-otp-input.ts +235 -0
  85. package/template/src/components/core/nc-pagination.ts +178 -0
  86. package/template/src/components/core/nc-popover.ts +260 -0
  87. package/template/src/components/core/nc-progress-circular.ts +119 -0
  88. package/template/src/components/core/nc-progress.ts +134 -0
  89. package/template/src/components/core/nc-radio.ts +235 -0
  90. package/template/src/components/core/nc-rating.ts +266 -0
  91. package/template/src/components/core/nc-rich-text.ts +283 -0
  92. package/template/src/components/core/nc-scroll-top.ts +116 -0
  93. package/template/src/components/core/nc-select.ts +452 -0
  94. package/template/src/components/core/nc-skeleton.ts +107 -0
  95. package/template/src/components/core/nc-slider.ts +285 -0
  96. package/template/src/components/core/nc-snackbar.ts +230 -0
  97. package/template/src/components/core/nc-splash.ts +343 -0
  98. package/template/src/components/core/nc-stepper.ts +247 -0
  99. package/template/src/components/core/nc-switch.ts +281 -0
  100. package/template/src/components/core/nc-tab-item.ts +138 -0
  101. package/template/src/components/core/nc-table.ts +279 -0
  102. package/template/src/components/core/nc-tabs.ts +554 -0
  103. package/template/src/components/core/nc-tag-input.ts +279 -0
  104. package/template/src/components/core/nc-textarea.ts +216 -0
  105. package/template/src/components/core/nc-time-picker.ts +438 -0
  106. package/template/src/components/core/nc-timeline.ts +186 -0
  107. package/template/src/components/core/nc-tooltip.ts +143 -0
  108. package/template/src/components/frameworkRegistry.ts +68 -0
  109. package/template/src/components/preloadRegistry.ts +28 -0
  110. package/template/src/components/registry.ts +8 -0
  111. package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
  112. package/template/src/constants/apiEndpoints.ts +27 -0
  113. package/template/src/constants/errorMessages.ts +23 -0
  114. package/template/src/constants/index.ts +8 -0
  115. package/template/src/constants/routePaths.ts +15 -0
  116. package/template/src/constants/storageKeys.ts +18 -0
  117. package/template/src/controllers/dashboard.controller.ts +200 -0
  118. package/template/src/controllers/home.controller.ts +21 -0
  119. package/template/src/controllers/index.ts +11 -0
  120. package/template/src/controllers/login.controller.ts +131 -0
  121. package/template/src/core/component.ts +354 -0
  122. package/template/src/core/errorHandler.ts +85 -0
  123. package/template/src/core/gpu-animation.ts +604 -0
  124. package/template/src/core/http.ts +173 -0
  125. package/template/src/core/lazyComponents.ts +90 -0
  126. package/template/src/core/router.ts +653 -0
  127. package/template/src/core/signals.ts +146 -0
  128. package/template/src/core/state.ts +248 -0
  129. package/template/src/dev/component-editor.ts +1363 -0
  130. package/template/src/dev/component-overlay.ts +278 -0
  131. package/template/src/dev/context-menu.ts +223 -0
  132. package/template/src/dev/denc-tools.ts +250 -0
  133. package/template/src/dev/hmr.ts +189 -0
  134. package/template/src/dev/nfbs.code-workspace +27 -0
  135. package/template/src/dev/outline-panel.ts +1247 -0
  136. package/template/src/middleware/auth.middleware.ts +23 -0
  137. package/template/src/routes/routes.ts +38 -0
  138. package/template/src/services/api.service.ts +394 -0
  139. package/template/src/services/auth.service.ts +176 -0
  140. package/template/src/services/index.ts +8 -0
  141. package/template/src/services/logger.service.ts +74 -0
  142. package/template/src/services/storage.service.ts +88 -0
  143. package/template/src/stores/appStore.ts +57 -0
  144. package/template/src/stores/uiStore.ts +36 -0
  145. package/template/src/styles/core-variables.css +219 -0
  146. package/template/src/styles/core.css +710 -0
  147. package/template/src/styles/main.css +3164 -0
  148. package/template/src/styles/variables.css +152 -0
  149. package/template/src/types/global.d.ts +47 -0
  150. package/template/src/utils/cacheBuster.ts +20 -0
  151. package/template/src/utils/dom.ts +149 -0
  152. package/template/src/utils/events.ts +203 -0
  153. package/template/src/utils/form.ts +176 -0
  154. package/template/src/utils/formatters.ts +169 -0
  155. package/template/src/utils/helpers.ts +195 -0
  156. package/template/src/utils/markdown.ts +307 -0
  157. package/template/src/utils/sidebar.ts +96 -0
  158. package/template/src/utils/smoothScroll.ts +85 -0
  159. package/template/src/utils/templates.ts +23 -0
  160. package/template/src/utils/validation.ts +73 -0
  161. package/template/src/views/protected/dashboard.html +293 -0
  162. package/template/src/views/public/home.html +150 -0
  163. package/template/src/views/public/login.html +102 -0
  164. package/template/tests/unit/component.test.ts +87 -0
  165. package/template/tests/unit/computed.test.ts +79 -0
  166. package/template/tests/unit/form.test.ts +68 -0
  167. package/template/tests/unit/formatters.test.ts +49 -0
  168. package/template/tests/unit/lazy-components.test.ts +59 -0
  169. package/template/tests/unit/markdown.test.ts +62 -0
  170. package/template/tests/unit/router.test.ts +112 -0
  171. package/template/tests/unit/signals.test.ts +54 -0
  172. package/template/tests/unit/validation.test.ts +50 -0
  173. package/template/tsconfig.build.json +21 -0
  174. package/template/tsconfig.json +51 -0
  175. package/template/vitest.config.ts +36 -0
@@ -0,0 +1,376 @@
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
+
50
+ import { Component, defineComponent } from '@core/component.js';
51
+ import { html } from '@utils/templates.js';
52
+
53
+ export class NcMenu extends Component {
54
+ static useShadowDOM = true;
55
+
56
+ static attributeOptions = {
57
+ variant: ['default', 'compact', 'inset', 'bordered'],
58
+ };
59
+
60
+ static get observedAttributes() {
61
+ return ['variant', 'searchable', 'label', 'width', 'auto-active'];
62
+ }
63
+
64
+ private _onSlotChange: (() => void) | null = null;
65
+ private _onSelect: ((e: Event) => void) | null = null;
66
+ private _onNavigate: ((e: Event) => void) | null = null;
67
+ private _onKeydown: ((e: Event) => void) | null = null;
68
+ private _onSearchInput: ((e: Event) => void) | null = null;
69
+
70
+ template(): string {
71
+ const label = this.getAttribute('label') || '';
72
+ const searchable = this.hasAttribute('searchable');
73
+ const width = this.getAttribute('width') || 'fit-content';
74
+
75
+ const labelHTML = label
76
+ ? `<div class="menu__label">${label}</div>`
77
+ : '';
78
+
79
+ const searchHTML = searchable
80
+ ? `<div class="menu__search-wrap">
81
+ <svg class="menu__search-icon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
82
+ <circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" stroke-width="1.5"/>
83
+ <path d="M15 15l-3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
84
+ </svg>
85
+ <input class="menu__search" type="text" placeholder="Search..." autocomplete="off" spellcheck="false" />
86
+ </div>`
87
+ : '';
88
+
89
+ return html`
90
+ <style>
91
+ :host {
92
+ display: inline-block;
93
+ width: ${width};
94
+ font-family: var(--nc-font-family);
95
+ box-sizing: border-box;
96
+ }
97
+
98
+ .menu {
99
+ background: var(--nc-bg);
100
+ border-radius: var(--nc-radius-lg);
101
+ padding: var(--nc-spacing-xs);
102
+ min-width: 180px;
103
+ box-sizing: border-box;
104
+ width: 100%;
105
+ }
106
+
107
+ /* ── Variants ──────────────────────────────────────────────── */
108
+ :host([variant="default"]) .menu,
109
+ :host(:not([variant])) .menu {
110
+ background: var(--nc-bg);
111
+ padding: var(--nc-spacing-xs);
112
+ }
113
+
114
+ :host([variant="compact"]) .menu {
115
+ padding: 2px;
116
+ }
117
+
118
+ :host([variant="compact"]) ::slotted(nc-menu-item) {
119
+ --nc-menu-item-py: var(--nc-spacing-xs);
120
+ }
121
+
122
+ :host([variant="inset"]) .menu {
123
+ background: var(--nc-bg-secondary);
124
+ padding: var(--nc-spacing-sm);
125
+ border-radius: var(--nc-radius-xl);
126
+ }
127
+
128
+ :host([variant="bordered"]) .menu {
129
+ background: var(--nc-bg);
130
+ border: 1px solid var(--nc-border);
131
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
132
+ padding: var(--nc-spacing-xs);
133
+ }
134
+
135
+ /* ── Label ─────────────────────────────────────────────────── */
136
+ .menu__label {
137
+ font-size: var(--nc-font-size-xs, 0.7rem);
138
+ font-weight: 700;
139
+ text-transform: uppercase;
140
+ letter-spacing: 0.08em;
141
+ color: var(--nc-text-secondary);
142
+ padding: var(--nc-spacing-xs) var(--nc-spacing-md) var(--nc-spacing-xs);
143
+ margin-bottom: 2px;
144
+ }
145
+
146
+ /* ── Search ────────────────────────────────────────────────── */
147
+ .menu__search-wrap {
148
+ position: relative;
149
+ margin-bottom: var(--nc-spacing-xs);
150
+ }
151
+
152
+ .menu__search-icon {
153
+ position: absolute;
154
+ left: 10px;
155
+ top: 50%;
156
+ transform: translateY(-50%);
157
+ width: 14px;
158
+ height: 14px;
159
+ color: var(--nc-text-secondary);
160
+ pointer-events: none;
161
+ }
162
+
163
+ .menu__search {
164
+ width: 100%;
165
+ box-sizing: border-box;
166
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm) var(--nc-spacing-xs) 30px;
167
+ font-family: var(--nc-font-family);
168
+ font-size: var(--nc-font-size-sm);
169
+ background: var(--nc-bg-secondary);
170
+ border: 1px solid var(--nc-border);
171
+ border-radius: var(--nc-radius-md);
172
+ color: var(--nc-text);
173
+ outline: none;
174
+ transition: border-color var(--nc-transition-fast);
175
+ }
176
+
177
+ .menu__search:focus {
178
+ border-color: var(--nc-primary);
179
+ }
180
+
181
+ .menu__search::placeholder {
182
+ color: var(--nc-text-secondary);
183
+ opacity: 0.6;
184
+ }
185
+
186
+ /* ── Empty state ───────────────────────────────────────────── */
187
+ .menu__empty {
188
+ display: none;
189
+ font-size: var(--nc-font-size-sm);
190
+ color: var(--nc-text-secondary);
191
+ padding: var(--nc-spacing-md);
192
+ text-align: center;
193
+ opacity: 0.6;
194
+ }
195
+
196
+ .menu__empty.visible {
197
+ display: block;
198
+ }
199
+ </style>
200
+ <div class="menu" role="menu">
201
+ ${labelHTML}
202
+ ${searchHTML}
203
+ <slot></slot>
204
+ <div class="menu__empty">No results</div>
205
+ </div>
206
+ `;
207
+ }
208
+
209
+ onMount(): void {
210
+ // Delegate nc-select events from nc-menu-item children
211
+ this._onSelect = (e: Event) => {
212
+ const item = e.target as HTMLElement;
213
+ if (item.tagName.toLowerCase() !== 'nc-menu-item') return;
214
+ const label = item.textContent?.trim() ?? '';
215
+ this.emitEvent('nc-menu-select', { item, label });
216
+ // Always track active for nc-menu-item — no opt-in required
217
+ this._setActive(item);
218
+ };
219
+ this.addEventListener('nc-select', this._onSelect);
220
+
221
+ // Delegate nc-navigate events from nc-a children
222
+ this._onNavigate = (e: Event) => {
223
+ const item = e.target as HTMLElement;
224
+ if (item.tagName.toLowerCase() !== 'nc-a') return;
225
+ if (this.hasAttribute('auto-active')) this._setActive(item);
226
+ };
227
+ this.addEventListener('nc-navigate', this._onNavigate);
228
+
229
+ // Arrow key navigation (supports both nc-menu-item and nc-a)
230
+ this._onKeydown = (e: Event) => {
231
+ const ke = e as KeyboardEvent;
232
+ if (!['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape'].includes(ke.key)) return;
233
+ ke.preventDefault();
234
+
235
+ const items = this._getEnabledItems();
236
+ if (!items.length) return;
237
+
238
+ const focused = document.activeElement;
239
+ const idx = items.findIndex(el => el === focused || el.shadowRoot?.contains(focused));
240
+
241
+ if (ke.key === 'Escape') { (focused as HTMLElement)?.blur?.(); return; }
242
+ if (ke.key === 'Home') { this._focusItem(items[0]); return; }
243
+ if (ke.key === 'End') { this._focusItem(items[items.length - 1]); return; }
244
+
245
+ const next = ke.key === 'ArrowDown'
246
+ ? items[Math.min(idx + 1, items.length - 1)]
247
+ : items[Math.max(idx - 1, 0)];
248
+ this._focusItem(next);
249
+ };
250
+ this.addEventListener('keydown', this._onKeydown);
251
+
252
+ // Auto-active: match current path for nc-a items on mount
253
+ if (this.hasAttribute('auto-active')) {
254
+ Promise.resolve().then(() => this._syncActiveFromPath());
255
+ }
256
+
257
+ // Search filtering
258
+ if (this.hasAttribute('searchable')) {
259
+ this._attachSearch();
260
+ }
261
+
262
+ // Re-sync search + path when slot contents change
263
+ const slot = this.$<HTMLSlotElement>('slot');
264
+ if (slot) {
265
+ this._onSlotChange = () => {
266
+ if (this.hasAttribute('searchable')) this._filterItems('');
267
+ if (this.hasAttribute('auto-active')) this._syncActiveFromPath();
268
+ };
269
+ slot.addEventListener('slotchange', this._onSlotChange);
270
+ }
271
+ }
272
+
273
+ onUnmount(): void {
274
+ if (this._onSelect) this.removeEventListener('nc-select', this._onSelect);
275
+ if (this._onNavigate) this.removeEventListener('nc-navigate', this._onNavigate);
276
+ if (this._onKeydown) this.removeEventListener('keydown', this._onKeydown);
277
+ const slot = this.$<HTMLSlotElement>('slot');
278
+ if (slot && this._onSlotChange) 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
+
286
+ attributeChangedCallback(
287
+ _name: string,
288
+ oldValue: string | null,
289
+ newValue: string | null
290
+ ): void {
291
+ if (this._mounted && oldValue !== newValue) this.render();
292
+ }
293
+
294
+ // ─── Private ────────────────────────────────────────────────────────────────
295
+
296
+ private _getEnabledItems(): HTMLElement[] {
297
+ return Array.from(
298
+ this.querySelectorAll<HTMLElement>(
299
+ 'nc-menu-item:not([disabled]), nc-a:not([disabled])'
300
+ )
301
+ );
302
+ }
303
+
304
+ private _focusItem(item: HTMLElement): void {
305
+ const tag = item.tagName.toLowerCase();
306
+ const selector = tag === 'nc-a' ? 'a' : '[role="menuitem"]';
307
+ item.shadowRoot?.querySelector<HTMLElement>(selector)?.focus();
308
+ }
309
+
310
+ /** Moves `active` to `target`, removes it from all siblings. */
311
+ private _setActive(target: HTMLElement): void {
312
+ const all = Array.from(
313
+ this.querySelectorAll<HTMLElement>('nc-menu-item, nc-a')
314
+ );
315
+ all.forEach(el => {
316
+ if (el === target) {
317
+ el.setAttribute('active', '');
318
+ } else {
319
+ el.removeAttribute('active');
320
+ }
321
+ });
322
+ }
323
+
324
+ /**
325
+ * For nc-a items with auto-active: set active on the item whose href
326
+ * matches the current pathname. Handles exact and prefix matches.
327
+ */
328
+ private _syncActiveFromPath(): void {
329
+ const path = window.location.pathname;
330
+ const links = Array.from(this.querySelectorAll<HTMLElement>('nc-a[href]'));
331
+ if (!links.length) return;
332
+
333
+ // Prefer exact match, fall back to longest prefix
334
+ let best: HTMLElement | null = null;
335
+ let bestLen = 0;
336
+
337
+ links.forEach(link => {
338
+ const href = link.getAttribute('href') ?? '';
339
+ if (href === path) { best = link; bestLen = Infinity; return; }
340
+ if (bestLen < Infinity && path.startsWith(href) && href.length > bestLen) {
341
+ best = link;
342
+ bestLen = href.length;
343
+ }
344
+ });
345
+
346
+ if (best) this._setActive(best);
347
+ }
348
+
349
+ private _attachSearch(): void {
350
+ const input = this.$<HTMLInputElement>('.menu__search');
351
+ if (!input) return;
352
+
353
+ this._onSearchInput = () => this._filterItems(input.value);
354
+ input.addEventListener('input', this._onSearchInput);
355
+ }
356
+
357
+ private _filterItems(query: string): void {
358
+ const q = query.toLowerCase().trim();
359
+ const items = Array.from(
360
+ this.querySelectorAll<HTMLElement>('nc-menu-item, nc-a')
361
+ );
362
+ let visible = 0;
363
+
364
+ items.forEach(item => {
365
+ const text = (item.textContent ?? '').toLowerCase();
366
+ const show = !q || text.includes(q);
367
+ item.style.display = show ? '' : 'none';
368
+ if (show) visible++;
369
+ });
370
+
371
+ const empty = this.$<HTMLElement>('.menu__empty');
372
+ if (empty) empty.classList.toggle('visible', visible === 0);
373
+ }
374
+ }
375
+
376
+ defineComponent('nc-menu', NcMenu);
@@ -0,0 +1,238 @@
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
+
39
+ import { Component, defineComponent } from '@core/component.js';
40
+ import { dom } from '@utils/dom.js';
41
+
42
+ export class NcModal extends Component {
43
+ static useShadowDOM = true;
44
+
45
+ static get observedAttributes() {
46
+ return ['open', 'size', 'no-close-btn', 'close-on-overlay', 'no-overlay', 'sticky-header', 'sticky-footer'];
47
+ }
48
+
49
+ static open(id: string) { dom.query<NcModal>(`#${id}`)?.setAttribute('open', ''); }
50
+ static close(id: string) { dom.query<NcModal>(`#${id}`)?.removeAttribute('open'); }
51
+
52
+ private _onKeydown: ((e: KeyboardEvent) => void) | null = null;
53
+
54
+ template() {
55
+ const open = this.hasAttribute('open');
56
+ const size = this.getAttribute('size') || 'md';
57
+ const noOverlay = this.hasAttribute('no-overlay');
58
+ const noCloseBtn = this.hasAttribute('no-close-btn');
59
+
60
+ const widths: Record<string, string> = {
61
+ sm: '420px', md: '560px', lg: '720px', xl: '960px', full: '100vw'
62
+ };
63
+ const maxWidth = widths[size] ?? widths.md;
64
+
65
+ return `
66
+ <style>
67
+ :host {
68
+ display: block;
69
+ position: fixed;
70
+ inset: 0;
71
+ z-index: 1000;
72
+ pointer-events: none;
73
+ }
74
+
75
+ .overlay {
76
+ position: absolute;
77
+ inset: 0;
78
+ background: rgba(0,0,0,.5);
79
+ z-index: 1000;
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ padding: var(--nc-spacing-lg);
84
+ opacity: ${open ? '1' : '0'};
85
+ pointer-events: ${open ? 'auto' : 'none'};
86
+ transition: opacity var(--nc-transition-base);
87
+ ${noOverlay ? 'background: transparent;' : ''}
88
+ }
89
+
90
+ .dialog {
91
+ background: var(--nc-bg);
92
+ border-radius: var(--nc-radius-lg, 12px);
93
+ box-shadow: var(--nc-shadow-xl, 0 25px 60px rgba(0,0,0,.35));
94
+ width: 100%;
95
+ max-width: ${maxWidth};
96
+ max-height: calc(100vh - 2 * var(--nc-spacing-lg));
97
+ display: flex;
98
+ flex-direction: column;
99
+ transform: ${open ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.97)'};
100
+ transition: transform var(--nc-transition-base);
101
+ overflow: hidden;
102
+ ${size === 'full' ? 'height: calc(100vh - 2 * var(--nc-spacing-lg));' : ''}
103
+ }
104
+
105
+ .dialog__header {
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: space-between;
109
+ padding: var(--nc-spacing-md) var(--nc-spacing-lg);
110
+ border-bottom: 1px solid var(--nc-border);
111
+ flex-shrink: 0;
112
+ font-family: var(--nc-font-family);
113
+ font-weight: var(--nc-font-weight-semibold);
114
+ font-size: var(--nc-font-size-lg);
115
+ color: var(--nc-text);
116
+ }
117
+
118
+ .dialog__body {
119
+ flex: 1;
120
+ overflow-y: auto;
121
+ padding: var(--nc-spacing-lg);
122
+ font-family: var(--nc-font-family);
123
+ font-size: var(--nc-font-size-base);
124
+ color: var(--nc-text);
125
+ line-height: var(--nc-line-height-relaxed, 1.7);
126
+ }
127
+
128
+ .dialog__footer {
129
+ padding: var(--nc-spacing-md) var(--nc-spacing-lg);
130
+ border-top: 1px solid var(--nc-border);
131
+ flex-shrink: 0;
132
+ display: flex;
133
+ justify-content: flex-end;
134
+ gap: var(--nc-spacing-sm);
135
+ }
136
+ .dialog__footer:empty { display: none; }
137
+
138
+ .close-btn {
139
+ background: none;
140
+ border: none;
141
+ cursor: pointer;
142
+ padding: 4px;
143
+ color: var(--nc-text-muted);
144
+ border-radius: var(--nc-radius-sm, 4px);
145
+ display: flex;
146
+ transition: color var(--nc-transition-fast), background var(--nc-transition-fast);
147
+ flex-shrink: 0;
148
+ }
149
+ .close-btn:hover { color: var(--nc-text); background: var(--nc-bg-secondary); }
150
+ </style>
151
+
152
+ <div
153
+ class="overlay"
154
+ role="dialog"
155
+ aria-modal="true"
156
+ aria-hidden="${!open}"
157
+ tabindex="-1"
158
+ >
159
+ <div class="dialog">
160
+ <div class="dialog__header">
161
+ <slot name="header"></slot>
162
+ ${!noCloseBtn ? `
163
+ <button class="close-btn" type="button" aria-label="Close modal">
164
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="18" height="18">
165
+ <path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
166
+ </svg>
167
+ </button>` : ''}
168
+ </div>
169
+ <div class="dialog__body">
170
+ <slot></slot>
171
+ </div>
172
+ <div class="dialog__footer">
173
+ <slot name="footer"></slot>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ `;
178
+ }
179
+
180
+ onMount() {
181
+ this._bindEvents();
182
+ }
183
+
184
+ private _bindEvents() {
185
+ const overlay = this.$<HTMLElement>('.overlay')!;
186
+ const dialog = this.$<HTMLElement>('.dialog')!;
187
+ const closeBtn = this.$<HTMLButtonElement>('.close-btn');
188
+
189
+ if (closeBtn) closeBtn.addEventListener('click', () => this._close());
190
+
191
+ overlay.addEventListener('click', (e) => {
192
+ if (this.getAttribute('close-on-overlay') === 'false') return;
193
+ if (!dialog.contains(e.target as Node)) this._close();
194
+ });
195
+
196
+ this._onKeydown = (e: KeyboardEvent) => {
197
+ if (e.key === 'Escape' && this.hasAttribute('open')) this._close();
198
+ };
199
+ document.addEventListener('keydown', this._onKeydown);
200
+ }
201
+
202
+ private _close() {
203
+ this.removeAttribute('open');
204
+ }
205
+
206
+ onUnmount() {
207
+ if (this._onKeydown) document.removeEventListener('keydown', this._onKeydown);
208
+ document.body.style.overflow = '';
209
+ }
210
+
211
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
212
+ if (oldValue === newValue) return;
213
+ if (name === 'open' && this._mounted) {
214
+ const open = this.hasAttribute('open');
215
+ const overlay = this.$<HTMLElement>('.overlay');
216
+ const dialog = this.$<HTMLElement>('.dialog');
217
+ if (overlay) {
218
+ overlay.style.opacity = open ? '1' : '0';
219
+ overlay.style.pointerEvents = open ? 'auto' : 'none';
220
+ overlay.setAttribute('aria-hidden', String(!open));
221
+ }
222
+ if (dialog) {
223
+ dialog.style.transform = open
224
+ ? 'translateY(0) scale(1)'
225
+ : 'translateY(12px) scale(0.97)';
226
+ if (open) (dialog as HTMLElement).focus();
227
+ }
228
+ document.body.style.overflow = open ? 'hidden' : '';
229
+ this.dispatchEvent(new CustomEvent(open ? 'open' : 'close', {
230
+ bubbles: true, composed: true
231
+ }));
232
+ return;
233
+ }
234
+ if (this._mounted) { this.render(); this._bindEvents(); }
235
+ }
236
+ }
237
+
238
+ defineComponent('nc-modal', NcModal);