create-nativecore 0.1.1 → 0.2.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.
Files changed (175) hide show
  1. package/README.md +6 -14
  2. package/bin/index.mjs +402 -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 +642 -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,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>&copy; ${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);