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.
- package/README.md +6 -14
- package/bin/index.mjs +403 -431
- package/package.json +3 -2
- package/template/.env.example +28 -0
- package/template/.htmlhintrc +14 -0
- package/template/api/data/dashboard.json +11 -0
- package/template/api/data/users.json +18 -0
- package/template/api/mockApi.js +161 -0
- package/template/assets/icon.svg +13 -0
- package/template/assets/logo.svg +25 -0
- package/template/eslint.config.js +94 -0
- package/template/index.html +137 -0
- package/template/manifest.json +19 -0
- package/template/public/.well-known/security.txt +9 -0
- package/template/public/_headers +24 -0
- package/template/public/_redirects +14 -0
- package/template/public/assets/icon.svg +13 -0
- package/template/public/assets/logo.svg +25 -0
- package/template/public/manifest.json +19 -0
- package/template/public/robots.txt +13 -0
- package/template/public/sitemap.xml +27 -0
- package/template/scripts/build-for-bots.mjs +121 -0
- package/template/scripts/convert-to-ts.mjs +106 -0
- package/template/scripts/fix-encoding.mjs +38 -0
- package/template/scripts/fix-svg-paths.mjs +32 -0
- package/template/scripts/generate-cf-router.mjs +52 -0
- package/template/scripts/inject-dev-tools.mjs +41 -0
- package/template/scripts/inject-version.mjs +65 -0
- package/template/scripts/make-component.mjs +445 -0
- package/template/scripts/make-component.mjs.backup +432 -0
- package/template/scripts/make-controller.mjs +119 -0
- package/template/scripts/make-core-component.mjs +303 -0
- package/template/scripts/make-view.mjs +346 -0
- package/template/scripts/minify.mjs +71 -0
- package/template/scripts/prepare-static-assets.mjs +141 -0
- package/template/scripts/prompt-bot-build.mjs +223 -0
- package/template/scripts/remove-component.mjs +170 -0
- package/template/scripts/remove-core-component.mjs +156 -0
- package/template/scripts/remove-dev.mjs +13 -0
- package/template/scripts/remove-view.mjs +200 -0
- package/template/scripts/strip-dev-blocks.mjs +30 -0
- package/template/scripts/watch-compile.mjs +69 -0
- package/template/server.js +1066 -0
- package/template/src/app.ts +115 -0
- package/template/src/components/appRegistry.ts +8 -0
- package/template/src/components/core/app-footer.ts +27 -0
- package/template/src/components/core/app-header.ts +175 -0
- package/template/src/components/core/app-sidebar.ts +238 -0
- package/template/src/components/core/loading-spinner.ts +25 -0
- package/template/src/components/core/nc-a.ts +313 -0
- package/template/src/components/core/nc-accordion.ts +186 -0
- package/template/src/components/core/nc-alert.ts +153 -0
- package/template/src/components/core/nc-animation.ts +1150 -0
- package/template/src/components/core/nc-autocomplete.ts +271 -0
- package/template/src/components/core/nc-avatar-group.ts +113 -0
- package/template/src/components/core/nc-avatar.ts +148 -0
- package/template/src/components/core/nc-badge.ts +86 -0
- package/template/src/components/core/nc-bottom-nav.ts +214 -0
- package/template/src/components/core/nc-breadcrumb.ts +96 -0
- package/template/src/components/core/nc-button.ts +307 -0
- package/template/src/components/core/nc-card.ts +160 -0
- package/template/src/components/core/nc-checkbox.ts +282 -0
- package/template/src/components/core/nc-chip.ts +115 -0
- package/template/src/components/core/nc-code.ts +314 -0
- package/template/src/components/core/nc-collapsible.ts +154 -0
- package/template/src/components/core/nc-color-picker.ts +268 -0
- package/template/src/components/core/nc-copy-button.ts +119 -0
- package/template/src/components/core/nc-date-picker.ts +443 -0
- package/template/src/components/core/nc-div.ts +280 -0
- package/template/src/components/core/nc-divider.ts +81 -0
- package/template/src/components/core/nc-drawer.ts +230 -0
- package/template/src/components/core/nc-dropdown.ts +178 -0
- package/template/src/components/core/nc-empty-state.ts +134 -0
- package/template/src/components/core/nc-file-upload.ts +354 -0
- package/template/src/components/core/nc-form.ts +312 -0
- package/template/src/components/core/nc-image.ts +184 -0
- package/template/src/components/core/nc-input.ts +383 -0
- package/template/src/components/core/nc-kbd.ts +48 -0
- package/template/src/components/core/nc-menu-item.ts +193 -0
- package/template/src/components/core/nc-menu.ts +376 -0
- package/template/src/components/core/nc-modal.ts +238 -0
- package/template/src/components/core/nc-nav-item.ts +151 -0
- package/template/src/components/core/nc-number-input.ts +350 -0
- package/template/src/components/core/nc-otp-input.ts +235 -0
- package/template/src/components/core/nc-pagination.ts +178 -0
- package/template/src/components/core/nc-popover.ts +260 -0
- package/template/src/components/core/nc-progress-circular.ts +119 -0
- package/template/src/components/core/nc-progress.ts +134 -0
- package/template/src/components/core/nc-radio.ts +235 -0
- package/template/src/components/core/nc-rating.ts +266 -0
- package/template/src/components/core/nc-rich-text.ts +283 -0
- package/template/src/components/core/nc-scroll-top.ts +116 -0
- package/template/src/components/core/nc-select.ts +452 -0
- package/template/src/components/core/nc-skeleton.ts +107 -0
- package/template/src/components/core/nc-slider.ts +285 -0
- package/template/src/components/core/nc-snackbar.ts +230 -0
- package/template/src/components/core/nc-splash.ts +343 -0
- package/template/src/components/core/nc-stepper.ts +247 -0
- package/template/src/components/core/nc-switch.ts +281 -0
- package/template/src/components/core/nc-tab-item.ts +138 -0
- package/template/src/components/core/nc-table.ts +279 -0
- package/template/src/components/core/nc-tabs.ts +554 -0
- package/template/src/components/core/nc-tag-input.ts +279 -0
- package/template/src/components/core/nc-textarea.ts +216 -0
- package/template/src/components/core/nc-time-picker.ts +438 -0
- package/template/src/components/core/nc-timeline.ts +186 -0
- package/template/src/components/core/nc-tooltip.ts +143 -0
- package/template/src/components/frameworkRegistry.ts +68 -0
- package/template/src/components/preloadRegistry.ts +28 -0
- package/template/src/components/registry.ts +8 -0
- package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
- package/template/src/constants/apiEndpoints.ts +27 -0
- package/template/src/constants/errorMessages.ts +23 -0
- package/template/src/constants/index.ts +8 -0
- package/template/src/constants/routePaths.ts +15 -0
- package/template/src/constants/storageKeys.ts +18 -0
- package/template/src/controllers/dashboard.controller.ts +200 -0
- package/template/src/controllers/home.controller.ts +21 -0
- package/template/src/controllers/index.ts +11 -0
- package/template/src/controllers/login.controller.ts +131 -0
- package/template/src/core/component.ts +354 -0
- package/template/src/core/errorHandler.ts +85 -0
- package/template/src/core/gpu-animation.ts +604 -0
- package/template/src/core/http.ts +173 -0
- package/template/src/core/lazyComponents.ts +90 -0
- package/template/src/core/router.ts +653 -0
- package/template/src/core/signals.ts +146 -0
- package/template/src/core/state.ts +248 -0
- package/template/src/dev/component-editor.ts +1363 -0
- package/template/src/dev/component-overlay.ts +278 -0
- package/template/src/dev/context-menu.ts +223 -0
- package/template/src/dev/denc-tools.ts +250 -0
- package/template/src/dev/hmr.ts +189 -0
- package/template/src/dev/nfbs.code-workspace +27 -0
- package/template/src/dev/outline-panel.ts +1247 -0
- package/template/src/middleware/auth.middleware.ts +23 -0
- package/template/src/routes/routes.ts +38 -0
- package/template/src/services/api.service.ts +394 -0
- package/template/src/services/auth.service.ts +176 -0
- package/template/src/services/index.ts +8 -0
- package/template/src/services/logger.service.ts +74 -0
- package/template/src/services/storage.service.ts +88 -0
- package/template/src/stores/appStore.ts +57 -0
- package/template/src/stores/uiStore.ts +36 -0
- package/template/src/styles/core-variables.css +219 -0
- package/template/src/styles/core.css +710 -0
- package/template/src/styles/main.css +3164 -0
- package/template/src/styles/variables.css +152 -0
- package/template/src/types/global.d.ts +47 -0
- package/template/src/utils/cacheBuster.ts +20 -0
- package/template/src/utils/dom.ts +149 -0
- package/template/src/utils/events.ts +203 -0
- package/template/src/utils/form.ts +176 -0
- package/template/src/utils/formatters.ts +169 -0
- package/template/src/utils/helpers.ts +195 -0
- package/template/src/utils/markdown.ts +307 -0
- package/template/src/utils/sidebar.ts +96 -0
- package/template/src/utils/smoothScroll.ts +85 -0
- package/template/src/utils/templates.ts +23 -0
- package/template/src/utils/validation.ts +73 -0
- package/template/src/views/protected/dashboard.html +293 -0
- package/template/src/views/public/home.html +150 -0
- package/template/src/views/public/login.html +102 -0
- package/template/tests/unit/component.test.ts +87 -0
- package/template/tests/unit/computed.test.ts +79 -0
- package/template/tests/unit/form.test.ts +68 -0
- package/template/tests/unit/formatters.test.ts +49 -0
- package/template/tests/unit/lazy-components.test.ts +59 -0
- package/template/tests/unit/markdown.test.ts +62 -0
- package/template/tests/unit/router.test.ts +112 -0
- package/template/tests/unit/signals.test.ts +54 -0
- package/template/tests/unit/validation.test.ts +50 -0
- package/template/tsconfig.build.json +21 -0
- package/template/tsconfig.json +51 -0
- package/template/vitest.config.ts +36 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Application Entry Point
|
|
3
|
+
*/
|
|
4
|
+
import router from './core/router.js';
|
|
5
|
+
import auth from './services/auth.service.js';
|
|
6
|
+
import type { User } from './services/auth.service.js';
|
|
7
|
+
import api from './services/api.service.js';
|
|
8
|
+
import { authMiddleware } from './middleware/auth.middleware.js';
|
|
9
|
+
import { registerRoutes, protectedRoutes } from './routes/routes.js';
|
|
10
|
+
import { initSidebar } from './utils/sidebar.js';
|
|
11
|
+
import { initLazyComponents } from './core/lazyComponents.js';
|
|
12
|
+
import './utils/dom.js';
|
|
13
|
+
import './components/registry.js';
|
|
14
|
+
|
|
15
|
+
function isLocalhost(): boolean {
|
|
16
|
+
const hostname = window.location.hostname;
|
|
17
|
+
return hostname === 'localhost' ||
|
|
18
|
+
hostname === '127.0.0.1' ||
|
|
19
|
+
hostname.startsWith('192.168.') ||
|
|
20
|
+
hostname.endsWith('.local');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function updateSidebarVisibility() {
|
|
24
|
+
const isAuthenticated = auth.isAuthenticated();
|
|
25
|
+
const currentPath = window.location.pathname;
|
|
26
|
+
const isProtectedRoute = protectedRoutes.some(route => currentPath.startsWith(route));
|
|
27
|
+
const app = document.getElementById('app');
|
|
28
|
+
|
|
29
|
+
// Show sidebar only when authenticated AND on a protected route
|
|
30
|
+
if (isAuthenticated && isProtectedRoute) {
|
|
31
|
+
document.body.classList.add('sidebar-enabled');
|
|
32
|
+
app?.classList.remove('no-sidebar');
|
|
33
|
+
} else {
|
|
34
|
+
document.body.classList.remove('sidebar-enabled');
|
|
35
|
+
app?.classList.add('no-sidebar');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function verifyExistingSession(): Promise<void> {
|
|
40
|
+
if (!auth.getToken()) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const response = await api.get<{ authenticated: boolean; user?: User }>('/auth/verify');
|
|
46
|
+
if (!response?.authenticated || !response.user) {
|
|
47
|
+
auth.logout();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
auth.setUser(response.user);
|
|
52
|
+
} catch {
|
|
53
|
+
auth.logout();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function init(){
|
|
58
|
+
await verifyExistingSession();
|
|
59
|
+
await initLazyComponents();
|
|
60
|
+
|
|
61
|
+
// Expose router globally for components
|
|
62
|
+
(window as any).router = router;
|
|
63
|
+
|
|
64
|
+
router.use(authMiddleware);
|
|
65
|
+
registerRoutes(router);
|
|
66
|
+
router.start();
|
|
67
|
+
|
|
68
|
+
initSidebar();
|
|
69
|
+
|
|
70
|
+
// Update sidebar on auth changes
|
|
71
|
+
window.addEventListener('auth-change', () => {
|
|
72
|
+
const isAuth = auth.isAuthenticated();
|
|
73
|
+
if (!isAuth) {
|
|
74
|
+
router.replace('/login');
|
|
75
|
+
document.body.classList.remove('sidebar-enabled');
|
|
76
|
+
document.getElementById('app')?.classList.remove('sidebar-collapsed');
|
|
77
|
+
document.getElementById('app')?.classList.add('no-sidebar');
|
|
78
|
+
} else {
|
|
79
|
+
updateSidebarVisibility();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Update sidebar on navigation
|
|
84
|
+
window.addEventListener('pageloaded', () => {
|
|
85
|
+
updateSidebarVisibility();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Initialize Dev Tools (ONLY in development - localhost)
|
|
89
|
+
initDevTools();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Initialize Dev Tools
|
|
94
|
+
* SECURITY: Only loads on localhost, completely excluded from production builds
|
|
95
|
+
*/
|
|
96
|
+
function initDevTools(): void {
|
|
97
|
+
if (!isLocalhost()) {
|
|
98
|
+
return; // SECURITY: Never load dev tools on production
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Load HMR and dev tools (both TypeScript modules now)
|
|
102
|
+
Promise.all([
|
|
103
|
+
import('./dev/hmr.js'),
|
|
104
|
+
import('./dev/denc-tools.js')
|
|
105
|
+
])
|
|
106
|
+
.then(() => {
|
|
107
|
+
console.warn('[NativeCore] Dev tools loaded');
|
|
108
|
+
(window as any).__NATIVECORE_DEV__ = true;
|
|
109
|
+
})
|
|
110
|
+
.catch(() => {
|
|
111
|
+
// Dev tools not available - that's fine in production
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
init();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { componentRegistry } from '@core/lazyComponents.js';
|
|
2
|
+
|
|
3
|
+
export function registerAppComponents(): void {
|
|
4
|
+
componentRegistry.register('app-footer', './core/app-footer.js');
|
|
5
|
+
componentRegistry.register('app-header', './core/app-header.js');
|
|
6
|
+
componentRegistry.register('app-sidebar', './core/app-sidebar.js');
|
|
7
|
+
componentRegistry.register('dashboard-signal-lab', './ui/dashboard-signal-lab.js');
|
|
8
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Footer Component
|
|
3
|
+
* Reusable footer
|
|
4
|
+
*/
|
|
5
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
6
|
+
|
|
7
|
+
class AppFooter extends Component {
|
|
8
|
+
template() {
|
|
9
|
+
const year = new Date().getFullYear();
|
|
10
|
+
|
|
11
|
+
return `
|
|
12
|
+
<footer class="app-footer">
|
|
13
|
+
<div class="container">
|
|
14
|
+
<p>© ${year} MyApp. All rights reserved.</p>
|
|
15
|
+
<div class="footer-links">
|
|
16
|
+
<a href="/about" data-link>About</a>
|
|
17
|
+
<a href="/privacy" data-link>Privacy</a>
|
|
18
|
+
<a href="/terms" data-link>Terms</a>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</footer>
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
defineComponent('app-footer', AppFooter);
|
|
27
|
+
export default AppFooter;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Header Component
|
|
3
|
+
* Beautiful responsive header with mobile side drawer
|
|
4
|
+
*/
|
|
5
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
6
|
+
import router from '@core/router.js';
|
|
7
|
+
import auth from '@services/auth.service.js';
|
|
8
|
+
import './nc-avatar.js';
|
|
9
|
+
|
|
10
|
+
class AppHeader extends Component {
|
|
11
|
+
// Bound references so addEventListener and removeEventListener use the same fn
|
|
12
|
+
private readonly _handleClick = (e: Event) => this._onClick(e);
|
|
13
|
+
private readonly _onAuthChange = () => this.updateAuthSection();
|
|
14
|
+
private readonly _onUnauthorized = () => this.updateAuthSection();
|
|
15
|
+
private readonly _onPageLoaded = (event: Event) => {
|
|
16
|
+
const routeEvent = event as CustomEvent<{ path?: string }>;
|
|
17
|
+
this.updateActiveLink(routeEvent.detail?.path);
|
|
18
|
+
};
|
|
19
|
+
private _onScroll: (() => void) | null = null;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private renderUserMenu(): string {
|
|
26
|
+
const user = auth.getUser();
|
|
27
|
+
const userName = user?.name || 'User';
|
|
28
|
+
|
|
29
|
+
return `
|
|
30
|
+
<div class="user-menu desktop-only">
|
|
31
|
+
<nc-avatar alt="${userName}" size="sm" variant="primary"></nc-avatar>
|
|
32
|
+
<span class="user-name">${userName}</span>
|
|
33
|
+
<button class="header-logout-btn" id="logoutBtn">Sign out</button>
|
|
34
|
+
</div>
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
template() {
|
|
39
|
+
const isAuthenticated = auth.isAuthenticated();
|
|
40
|
+
const logoHref = isAuthenticated ? '/dashboard' : '/';
|
|
41
|
+
|
|
42
|
+
return `
|
|
43
|
+
<div class="header-container">
|
|
44
|
+
<div class="header-left">
|
|
45
|
+
${isAuthenticated ? `
|
|
46
|
+
<button class="mobile-menu-toggle" id="mobileMenuToggle" aria-label="Toggle sidebar">
|
|
47
|
+
<span class="burger-line"></span>
|
|
48
|
+
<span class="burger-line"></span>
|
|
49
|
+
<span class="burger-line"></span>
|
|
50
|
+
</button>
|
|
51
|
+
` : ''}
|
|
52
|
+
|
|
53
|
+
<a href="${logoHref}" class="logo" id="logoLink">
|
|
54
|
+
<img class="logo-mark" src="/assets/logo.svg" alt="NativeCore logo">
|
|
55
|
+
<span class="logo-title">NativeCore</span>
|
|
56
|
+
</a>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<nav class="header-nav desktop-nav">
|
|
60
|
+
${!isAuthenticated ? `
|
|
61
|
+
<a href="/" data-link class="nanc-link">Home</a>
|
|
62
|
+
<a href="/docs" data-link class="nanc-link">Docs</a>
|
|
63
|
+
<a href="/components" data-link class="nanc-link">Components</a>
|
|
64
|
+
` : ''}
|
|
65
|
+
</nav>
|
|
66
|
+
|
|
67
|
+
<div class="header-right">
|
|
68
|
+
${isAuthenticated ? `
|
|
69
|
+
${this.renderUserMenu()}
|
|
70
|
+
` : `
|
|
71
|
+
<a href="/login" data-link class="header-login-btn desktop-only">Sign in</a>
|
|
72
|
+
`}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onMount() {
|
|
79
|
+
// Single delegated click handler — covers all interactive elements,
|
|
80
|
+
// even those re-rendered by updateAuthSection()
|
|
81
|
+
this.addEventListener('click', this._handleClick);
|
|
82
|
+
|
|
83
|
+
window.addEventListener('auth-change', this._onAuthChange);
|
|
84
|
+
window.addEventListener('unauthorized', this._onUnauthorized);
|
|
85
|
+
window.addEventListener('pageloaded', this._onPageLoaded);
|
|
86
|
+
|
|
87
|
+
this._onScroll = () => {
|
|
88
|
+
this.classList.toggle('scrolled', window.scrollY > 10);
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener('scroll', this._onScroll, { passive: true });
|
|
91
|
+
this._onScroll();
|
|
92
|
+
|
|
93
|
+
this.updateActiveLink(router.getCurrentRoute()?.path);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onUnmount() {
|
|
97
|
+
this.removeEventListener('click', this._handleClick);
|
|
98
|
+
window.removeEventListener('auth-change', this._onAuthChange);
|
|
99
|
+
window.removeEventListener('unauthorized', this._onUnauthorized);
|
|
100
|
+
window.removeEventListener('pageloaded', this._onPageLoaded);
|
|
101
|
+
if (this._onScroll) window.removeEventListener('scroll', this._onScroll);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private _onClick(e: Event) {
|
|
105
|
+
const target = e.target as HTMLElement;
|
|
106
|
+
|
|
107
|
+
if (target.closest('#logoutBtn')) {
|
|
108
|
+
auth.logout();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (target.closest('#logoLink')) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
router.navigate(auth.isAuthenticated() ? '/dashboard' : '/');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (target.closest('#mobileMenuToggle')) {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
window.dispatchEvent(new CustomEvent('sidebar-toggle'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
updateAuthSection() {
|
|
126
|
+
const isAuthenticated = auth.isAuthenticated();
|
|
127
|
+
const headerRight = this.$('.header-right');
|
|
128
|
+
const headerNav = this.$('.header-nav');
|
|
129
|
+
const logoLink = this.$<HTMLAnchorElement>('#logoLink');
|
|
130
|
+
|
|
131
|
+
if (logoLink) {
|
|
132
|
+
logoLink.setAttribute('href', isAuthenticated ? '/dashboard' : '/');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (headerRight) {
|
|
136
|
+
headerRight.innerHTML = isAuthenticated ? `
|
|
137
|
+
${this.renderUserMenu()}
|
|
138
|
+
` : `
|
|
139
|
+
<a href="/login" data-link class="header-login-btn desktop-only">Sign in</a>
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (headerNav) {
|
|
144
|
+
headerNav.innerHTML = !isAuthenticated ? `
|
|
145
|
+
<a href="/" data-link class="nanc-link">Home</a>
|
|
146
|
+
<a href="/docs" data-link class="nanc-link">Docs</a>
|
|
147
|
+
<a href="/components" data-link class="nanc-link">Components</a>
|
|
148
|
+
` : '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.updateActiveLink(router.getCurrentRoute()?.path);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private normalizePath(path: string | null | undefined): string {
|
|
155
|
+
if (!path || path === '/') {
|
|
156
|
+
return '/';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const normalizedPath = path.replace(/[?#].*$/, '').replace(/\/+$/, '');
|
|
160
|
+
return normalizedPath || '/';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
updateActiveLink(routePath?: string) {
|
|
164
|
+
const currentPath = this.normalizePath(routePath ?? router.getCurrentRoute()?.path ?? window.location.pathname);
|
|
165
|
+
this.$$('.nanc-link').forEach(link => {
|
|
166
|
+
link.classList.remove('active');
|
|
167
|
+
const href = this.normalizePath(link.getAttribute('href'));
|
|
168
|
+
if (href === currentPath) {
|
|
169
|
+
link.classList.add('active');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
defineComponent('app-header', AppHeader);
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
2
|
+
import { useState } from '@core/state.js';
|
|
3
|
+
import type { State } from '@core/state.js';
|
|
4
|
+
import { html } from '@utils/templates.js';
|
|
5
|
+
import { dom } from '@utils/dom.js';
|
|
6
|
+
import auth from '@services/auth.service.js';
|
|
7
|
+
import router from '@core/router.js';
|
|
8
|
+
|
|
9
|
+
export class AppSidebar extends Component {
|
|
10
|
+
isCollapsed: State<boolean>;
|
|
11
|
+
isMobileOpen: State<boolean>;
|
|
12
|
+
|
|
13
|
+
private _unwatchCollapsed?: () => void;
|
|
14
|
+
private _unwatchMobileOpen?: () => void;
|
|
15
|
+
private _overlayEl: HTMLDivElement | null = null;
|
|
16
|
+
private readonly _handleClick = (e: Event) => {
|
|
17
|
+
const target = e.target as HTMLElement;
|
|
18
|
+
|
|
19
|
+
if (target.closest('.sidebar-collapse-btn')) {
|
|
20
|
+
this.toggle();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (target.closest('#sidebarLogoutBtn')) {
|
|
25
|
+
auth.logout();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sidebarLink = target.closest('.sidebar-item[data-link]') as HTMLAnchorElement | null;
|
|
30
|
+
if (sidebarLink && this.isMobileViewport()) {
|
|
31
|
+
this.closeMobileSidebar();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
private readonly _onSidebarToggle = () => {
|
|
35
|
+
if (this.isMobileViewport()) {
|
|
36
|
+
this.toggleMobileSidebar();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.toggle();
|
|
40
|
+
};
|
|
41
|
+
private readonly _onOverlayClick = () => this.closeMobileSidebar();
|
|
42
|
+
private readonly _onPageLoaded = () => {
|
|
43
|
+
this.updateActiveLink();
|
|
44
|
+
if (this.isMobileViewport()) {
|
|
45
|
+
this.closeMobileSidebar();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
private readonly _onResize = () => {
|
|
49
|
+
if (!this.isMobileViewport()) {
|
|
50
|
+
this.closeMobileSidebar();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
static get observedAttributes() {
|
|
55
|
+
return ['collapsed', 'width', 'sticky'];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
constructor() {
|
|
59
|
+
super();
|
|
60
|
+
this.isCollapsed = useState(this.hasAttribute('collapsed'));
|
|
61
|
+
this.isMobileOpen = useState(false);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
template() {
|
|
65
|
+
return html`
|
|
66
|
+
<div class="app-sidebar ${this.isCollapsed.value ? 'collapsed' : ''}">
|
|
67
|
+
<div class="sidebar-header">
|
|
68
|
+
<div class="sidebar-branding">
|
|
69
|
+
<span class="sidebar-branding__eyebrow">Protected</span>
|
|
70
|
+
<strong class="sidebar-branding__title">Workspace</strong>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<button
|
|
74
|
+
class="sidebar-collapse-btn ${this.isCollapsed.value ? 'is-collapsed' : 'is-expanded'}"
|
|
75
|
+
type="button"
|
|
76
|
+
aria-label="${this.isCollapsed.value ? 'Expand sidebar' : 'Collapse sidebar'}"
|
|
77
|
+
>
|
|
78
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
79
|
+
<rect x="4" y="5" width="16" height="14" rx="3"></rect>
|
|
80
|
+
<path d="M9 5v14" class="sidebar-collapse-btn__divider"></path>
|
|
81
|
+
<path d="M15 9l-3 3 3 3" class="sidebar-collapse-btn__chevron"></path>
|
|
82
|
+
</svg>
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<nav class="sidebar-nav">
|
|
87
|
+
<a href="/dashboard" data-link class="sidebar-item dashboard-link">
|
|
88
|
+
<span class="sidebar-icon">
|
|
89
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
90
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
91
|
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
92
|
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
93
|
+
</svg>
|
|
94
|
+
</span>
|
|
95
|
+
<span class="sidebar-text">Dashboard</span>
|
|
96
|
+
</a>
|
|
97
|
+
</nav>
|
|
98
|
+
|
|
99
|
+
<div class="sidebar-footer">
|
|
100
|
+
<button class="sidebar-item logout-link" id="sidebarLogoutBtn" type="button">
|
|
101
|
+
<span class="sidebar-icon">
|
|
102
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
103
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
|
104
|
+
<polyline points="16 17 21 12 16 7"></polyline>
|
|
105
|
+
<line x1="21" y1="12" x2="9" y2="12"></line>
|
|
106
|
+
</svg>
|
|
107
|
+
</span>
|
|
108
|
+
<span class="sidebar-text">Logout</span>
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
116
|
+
if (name === 'collapsed') {
|
|
117
|
+
this.isCollapsed.value = newValue !== null;
|
|
118
|
+
} else if (this._mounted) {
|
|
119
|
+
this.render();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onMount() {
|
|
124
|
+
// Restore saved collapsed state
|
|
125
|
+
const savedCollapsed = localStorage.getItem('sidebar-collapsed') === 'true';
|
|
126
|
+
if (savedCollapsed) {
|
|
127
|
+
this.isCollapsed.value = true;
|
|
128
|
+
this.setAttribute('collapsed', '');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Watch state — update classes without full re-render
|
|
132
|
+
this._unwatchCollapsed = this.isCollapsed.watch(() => {
|
|
133
|
+
this.updateSidebar();
|
|
134
|
+
});
|
|
135
|
+
this._unwatchMobileOpen = this.isMobileOpen.watch(() => {
|
|
136
|
+
this.updateMobileState();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this.ensureOverlay();
|
|
140
|
+
this.updateSidebar();
|
|
141
|
+
this.updateMobileState();
|
|
142
|
+
this.updateActiveLink();
|
|
143
|
+
|
|
144
|
+
this.addEventListener('click', this._handleClick);
|
|
145
|
+
window.addEventListener('sidebar-toggle', this._onSidebarToggle);
|
|
146
|
+
window.addEventListener('pageloaded', this._onPageLoaded);
|
|
147
|
+
window.addEventListener('resize', this._onResize);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onUnmount() {
|
|
151
|
+
this.removeEventListener('click', this._handleClick);
|
|
152
|
+
window.removeEventListener('sidebar-toggle', this._onSidebarToggle);
|
|
153
|
+
window.removeEventListener('pageloaded', this._onPageLoaded);
|
|
154
|
+
window.removeEventListener('resize', this._onResize);
|
|
155
|
+
this._unwatchCollapsed?.();
|
|
156
|
+
this._unwatchMobileOpen?.();
|
|
157
|
+
if (this._overlayEl) {
|
|
158
|
+
this._overlayEl.removeEventListener('click', this._onOverlayClick);
|
|
159
|
+
this._overlayEl.remove();
|
|
160
|
+
this._overlayEl = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
toggle() {
|
|
165
|
+
if (this.isMobileViewport()) {
|
|
166
|
+
this.toggleMobileSidebar();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.isCollapsed.value = !this.isCollapsed.value;
|
|
171
|
+
|
|
172
|
+
if (this.isCollapsed.value) {
|
|
173
|
+
this.setAttribute('collapsed', '');
|
|
174
|
+
} else {
|
|
175
|
+
this.removeAttribute('collapsed');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
localStorage.setItem('sidebar-collapsed', this.isCollapsed.value.toString());
|
|
179
|
+
|
|
180
|
+
this.emitEvent('toggle', { collapsed: this.isCollapsed.value }, {
|
|
181
|
+
bubbles: true,
|
|
182
|
+
composed: true
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private isMobileViewport(): boolean {
|
|
187
|
+
return window.innerWidth <= 768;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private toggleMobileSidebar() {
|
|
191
|
+
this.isMobileOpen.value = !this.isMobileOpen.value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private closeMobileSidebar() {
|
|
195
|
+
this.isMobileOpen.value = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private ensureOverlay() {
|
|
199
|
+
let overlay = dom.query<HTMLDivElement>('.sidebar-overlay');
|
|
200
|
+
if (!overlay) {
|
|
201
|
+
overlay = document.createElement('div');
|
|
202
|
+
overlay.className = 'sidebar-overlay';
|
|
203
|
+
document.body.appendChild(overlay);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
overlay.removeEventListener('click', this._onOverlayClick);
|
|
207
|
+
overlay.addEventListener('click', this._onOverlayClick);
|
|
208
|
+
this._overlayEl = overlay;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private updateActiveLink() {
|
|
212
|
+
const currentPath = router.getCurrentRoute()?.path ?? window.location.pathname;
|
|
213
|
+
this.$$('.sidebar-item[data-link]').forEach(link => {
|
|
214
|
+
link.classList.remove('active');
|
|
215
|
+
if (link.getAttribute('href') === currentPath) {
|
|
216
|
+
link.classList.add('active');
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
updateSidebar() {
|
|
222
|
+
const sidebar = this.$('.app-sidebar');
|
|
223
|
+
sidebar?.classList.toggle('collapsed', this.isCollapsed.value);
|
|
224
|
+
this.classList.toggle('collapsed', this.isCollapsed.value);
|
|
225
|
+
|
|
226
|
+
// Drive the grid column via a class on #app
|
|
227
|
+
const app = dom.query<HTMLElement>('#app');
|
|
228
|
+
app?.classList.toggle('sidebar-collapsed', this.isCollapsed.value);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private updateMobileState() {
|
|
232
|
+
this.classList.toggle('mobile-open', this.isMobileOpen.value);
|
|
233
|
+
document.body.classList.toggle('sidebar-open', this.isMobileOpen.value);
|
|
234
|
+
this._overlayEl?.classList.toggle('active', this.isMobileOpen.value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
defineComponent('app-sidebar', AppSidebar);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading Spinner Component
|
|
3
|
+
* Simple reusable loading indicator
|
|
4
|
+
*/
|
|
5
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
6
|
+
|
|
7
|
+
class LoadingSpinner extends Component {
|
|
8
|
+
static get observedAttributes() {
|
|
9
|
+
return ['size', 'message'];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
template() {
|
|
13
|
+
const size = this.attr('size', 'medium');
|
|
14
|
+
const message = this.attr('message', 'Loading...');
|
|
15
|
+
|
|
16
|
+
return `
|
|
17
|
+
<div class="loading-spinner ${size}">
|
|
18
|
+
<div class="spinner"></div>
|
|
19
|
+
${message ? `<p class="loading-message">${message}</p>` : ''}
|
|
20
|
+
</div>
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
defineComponent('loading-spinner', LoadingSpinner);
|