create-nativecore 0.1.0 → 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 +10 -18
  2. package/bin/index.mjs +407 -489
  3. package/package.json +4 -3
  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,303 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Core Component Generator Script
5
+ * Creates framework core components with --nc- variables
6
+ *
7
+ * Usage:
8
+ * node scripts/make-core-component.mjs button
9
+ * npm run make:core-component button
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ import readline from 'readline';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+
20
+ // Get component name from command line
21
+ const componentName = process.argv[2];
22
+
23
+ if (!componentName) {
24
+ console.error('Error: Component name is required');
25
+ console.log('\nUsage:');
26
+ console.log(' npm run make:core-component <name>');
27
+ console.log('\nExample:');
28
+ console.log(' npm run make:core-component button');
29
+ process.exit(1);
30
+ }
31
+
32
+ // Validate component name (no hyphen needed for core components - we add nc- prefix)
33
+ if (!/^[a-z][a-z0-9-]*$/.test(componentName)) {
34
+ console.error('Error: Component name must be lowercase with optional hyphens');
35
+ console.error('\nValid examples:');
36
+ console.error(' - button');
37
+ console.error(' - card');
38
+ console.error(' - input');
39
+ console.error(' - dropdown-menu');
40
+ process.exit(1);
41
+ }
42
+
43
+ // Generate names
44
+ const tagName = `nc-${componentName}`; // e.g., nc-button
45
+ const className = tagName
46
+ .split('-')
47
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
48
+ .join(''); // e.g., NcButton
49
+
50
+ // Component file path (in components/core folder)
51
+ const componentsDir = path.resolve(__dirname, '..', 'src', 'components');
52
+ const coreDir = path.join(componentsDir, 'core');
53
+ const componentFile = path.join(coreDir, `${tagName}.ts`);
54
+ const preloadRegistryFile = path.join(componentsDir, 'preloadRegistry.ts');
55
+
56
+ // Ensure core directory exists
57
+ if (!fs.existsSync(coreDir)) {
58
+ fs.mkdirSync(coreDir, { recursive: true });
59
+ }
60
+
61
+ // Check if component already exists
62
+ if (fs.existsSync(componentFile)) {
63
+ console.error(`Error: Component "${tagName}.ts" already exists`);
64
+ process.exit(1);
65
+ }
66
+
67
+ // Template for core component
68
+ const componentTemplate = `/**
69
+ * ${className} Component
70
+ *
71
+ * NativeCore Framework Core Component
72
+ * Generated on ${new Date().toLocaleDateString()}
73
+ *
74
+ * FRAMEWORK INTERNAL COMPONENT
75
+ * This component uses --nc- CSS variables from core-variables.css
76
+ * for consistent styling across the framework.
77
+ *
78
+ * ═══════════════════════════════════════════════════════════════════
79
+ * DEV TOOLS INTEGRATION - How to make attributes editable in sidebar:
80
+ * ═══════════════════════════════════════════════════════════════════
81
+ *
82
+ * 1. ADD TO observedAttributes:
83
+ * static get observedAttributes() {
84
+ * return ['variant', 'size', 'disabled']; // ← These become editable
85
+ * }
86
+ *
87
+ * 2. USE getAttribute() IN template():
88
+ * const variant = this.getAttribute('variant') || 'primary';
89
+ * const size = this.getAttribute('size') || 'md';
90
+ *
91
+ * 3. FOR DROPDOWN SELECTORS (variant/size):
92
+ * Name attributes 'variant' or 'size' AND use CSS patterns:
93
+ *
94
+ * Option A - :host() selectors (RECOMMENDED for core components):
95
+ * :host([variant="primary"]) { ... }
96
+ * :host([variant="secondary"]) { ... }
97
+ * :host([size="sm"]) { ... }
98
+ * :host([size="lg"]) { ... }
99
+ *
100
+ * Option B - Class selectors:
101
+ * .primary { ... }
102
+ * .secondary { ... }
103
+ * .btn-sm { ... }
104
+ * .btn-lg { ... }
105
+ *
106
+ * The dev tools will auto-detect these patterns and create dropdowns!
107
+ *
108
+ * 4. ATTRIBUTE TYPES (auto-detected):
109
+ * - Boolean: disabled, loading, hidden, readonly → Checkbox
110
+ * - Variant/Size: variant, size (with CSS) → Dropdown
111
+ * - Number: count, max, min, step → Slider
112
+ * - Everything else → Text input
113
+ *
114
+ * 5. LIVE UPDATES:
115
+ * Implement attributeChangedCallback for instant preview updates
116
+ *
117
+ * ═══════════════════════════════════════════════════════════════════
118
+ *
119
+ * Usage:
120
+ * <${tagName} variant="primary"></${tagName}>
121
+ * <${tagName} variant="secondary" size="lg"></${tagName}>
122
+ *
123
+ * Attributes:
124
+ * - variant: Component style variant
125
+ * - size: Component size (sm, md, lg)
126
+ * - disabled: Disabled state
127
+ */
128
+
129
+ import { Component, defineComponent } from '@core/component.js';
130
+ import { html } from '@utils/templates.js';
131
+
132
+ export class ${className} extends Component {
133
+ static useShadowDOM = true;
134
+
135
+ // ═══ Define dropdown options for dev tools (auto-detected) ═══
136
+ static attributeOptions = {
137
+ variant: ['primary', 'secondary', 'success', 'danger'],
138
+ size: ['sm', 'md', 'lg']
139
+ };
140
+
141
+ // ═══ Attributes listed here become editable in dev tools sidebar ═══
142
+ static get observedAttributes() {
143
+ return ['variant', 'size', 'disabled'];
144
+ }
145
+
146
+ constructor() {
147
+ super();
148
+ }
149
+
150
+ template() {
151
+ // ═══ Use getAttribute() - dev tools reads this to detect attributes ═══
152
+ const variant = this.getAttribute('variant') || 'primary';
153
+ const size = this.getAttribute('size') || 'md';
154
+ const disabled = this.hasAttribute('disabled');
155
+
156
+ return html\`
157
+ <style>
158
+ :host {
159
+ display: inline-block;
160
+ font-family: var(--nc-font-family);
161
+ padding: var(--nc-spacing-md);
162
+ border-radius: var(--nc-radius-md);
163
+ transition: all var(--nc-transition-fast);
164
+ }
165
+
166
+ /* ═══ Variant Options (auto-detected for dropdown) ═══ */
167
+ /* Dev tools will scan :host([variant="..."]) patterns */
168
+
169
+ :host([variant="primary"]) {
170
+ background: var(--nc-gradient-primary);
171
+ color: var(--nc-white);
172
+ }
173
+
174
+ :host([variant="secondary"]) {
175
+ background: var(--nc-bg-secondary);
176
+ color: var(--nc-text);
177
+ border: 1px solid var(--nc-border);
178
+ }
179
+
180
+ :host([variant="success"]) {
181
+ background: var(--nc-gradient-success);
182
+ color: var(--nc-white);
183
+ }
184
+
185
+ :host([variant="danger"]) {
186
+ background: var(--nc-gradient-danger);
187
+ color: var(--nc-white);
188
+ }
189
+
190
+ /* ═══ Size Options (auto-detected for dropdown) ═══ */
191
+ /* Dev tools will scan :host([size="..."]) patterns */
192
+
193
+ :host([size="sm"]) {
194
+ padding: var(--nc-spacing-sm);
195
+ font-size: var(--nc-font-size-sm);
196
+ }
197
+
198
+ :host([size="md"]) {
199
+ padding: var(--nc-spacing-md);
200
+ font-size: var(--nc-font-size-base);
201
+ }
202
+
203
+ :host([size="lg"]) {
204
+ padding: var(--nc-spacing-lg);
205
+ font-size: var(--nc-font-size-lg);
206
+ }
207
+
208
+ /* ═══ Disabled State (auto-detected as checkbox) ═══ */
209
+ :host([disabled]) {
210
+ opacity: 0.5;
211
+ pointer-events: none;
212
+ }
213
+ </style>
214
+
215
+ <slot></slot>
216
+ \`;
217
+ }
218
+
219
+ onMount() {
220
+ // Component logic here
221
+ }
222
+
223
+ // ═══ Makes changes instant in dev tools preview ═══
224
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
225
+ if (oldValue !== newValue && this._mounted) {
226
+ this.render();
227
+ }
228
+ }
229
+ }
230
+
231
+ defineComponent('${tagName}', ${className});
232
+ `;
233
+
234
+ // Create readline interface for prompts
235
+ const rl = readline.createInterface({
236
+ input: process.stdin,
237
+ output: process.stdout
238
+ });
239
+
240
+ function prompt(question) {
241
+ return new Promise((resolve) => {
242
+ rl.question(question, resolve);
243
+ });
244
+ }
245
+
246
+ async function main() {
247
+ console.log(`\nCreating NativeCore framework component: ${tagName}\n`);
248
+
249
+ // Ask about preloading
250
+ const preload = await prompt('Preload this component? (Y/n): ');
251
+ const shouldPreload = !preload || preload.toLowerCase() !== 'n';
252
+
253
+ // Create component file
254
+ fs.writeFileSync(componentFile, componentTemplate);
255
+ console.log(`Created: src/components/core/${tagName}.ts`);
256
+
257
+ // Add to preloadRegistry if requested
258
+ if (shouldPreload) {
259
+ let registryContent = fs.readFileSync(preloadRegistryFile, 'utf-8');
260
+
261
+ // Find the import section for core components
262
+ const coreImportSection = registryContent.indexOf('// Core framework components');
263
+
264
+ if (coreImportSection !== -1) {
265
+ // Find the next line after the comment
266
+ const insertPosition = registryContent.indexOf('\n', coreImportSection) + 1;
267
+ const importStatement = `import './core/${tagName}.js';\n`;
268
+
269
+ // Check if import already exists
270
+ if (!registryContent.includes(importStatement)) {
271
+ registryContent = registryContent.slice(0, insertPosition) +
272
+ importStatement +
273
+ registryContent.slice(insertPosition);
274
+
275
+ fs.writeFileSync(preloadRegistryFile, registryContent);
276
+ console.log(`Added to: src/components/preloadRegistry.ts`);
277
+ }
278
+ } else {
279
+ console.log('Warning: Could not find core components section in preloadRegistry.ts');
280
+ console.log(' Please manually add: import \'./core/${tagName}.js\';');
281
+ }
282
+ }
283
+
284
+ console.log('\nFramework component created successfully!\n');
285
+ console.log('Usage:');
286
+ console.log(` <${tagName} variant="primary">Content</${tagName}>`);
287
+ console.log(` <${tagName} variant="secondary" size="lg">Large</${tagName}>`);
288
+ console.log('\nDev Tools Integration:');
289
+ console.log(' • Attributes in observedAttributes → Editable in sidebar');
290
+ console.log(' • :host([variant="..."]) patterns → Auto-detected dropdown');
291
+ console.log(' • :host([size="..."]) patterns → Auto-detected dropdown');
292
+ console.log(' • Boolean attrs (disabled, etc.) → Checkbox');
293
+ console.log(' • attributeChangedCallback → Live preview updates');
294
+ console.log('\nComponent uses --nc- variables from core-variables.css');
295
+ console.log('Check the generated file for detailed integration comments.\n');
296
+
297
+ rl.close();
298
+ }
299
+
300
+ main().catch(error => {
301
+ console.error('Error:', error.message);
302
+ process.exit(1);
303
+ });
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * View Generator Script
5
+ * Creates views (HTML pages) with optional controllers
6
+ *
7
+ * Usage:
8
+ * node scripts/make-view.mjs profile
9
+ * npm run make:view profile
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import readline from 'readline';
15
+ import { fileURLToPath } from 'url';
16
+ import { generateCfRouter } from './generate-cf-router.mjs';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+
21
+ // Get view name from command line
22
+ const viewName = process.argv[2];
23
+
24
+ if (!viewName) {
25
+ console.error('Error: View name is required');
26
+ console.log('\nUsage:');
27
+ console.log(' npm run make:view <name>');
28
+ console.log('\nExample:');
29
+ console.log(' npm run make:view profile');
30
+ process.exit(1);
31
+ }
32
+
33
+ // Validate view name (kebab-case)
34
+ if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(viewName)) {
35
+ console.error('Error: View name must be in kebab-case (lowercase with hyphens)');
36
+ console.error('\nValid examples:');
37
+ console.error(' - profile');
38
+ console.error(' - user-profile');
39
+ console.error(' - my-settings');
40
+ console.error('\nInvalid examples:');
41
+ console.error(' - Profile (not lowercase)');
42
+ console.error(' - user_profile (underscore instead of hyphen)');
43
+ process.exit(1);
44
+ }
45
+
46
+ // Create readline interface for prompts
47
+ const rl = readline.createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout
50
+ });
51
+
52
+ // Promisify question
53
+ function question(query) {
54
+ return new Promise(resolve => rl.question(query, resolve));
55
+ }
56
+
57
+ // Generate controller name (camelCase)
58
+ const controllerName = viewName
59
+ .split('-')
60
+ .map((word, index) =>
61
+ index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)
62
+ )
63
+ .join('');
64
+
65
+ // Generate title (Title Case)
66
+ const viewTitle = viewName
67
+ .split('-')
68
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
69
+ .join(' ');
70
+
71
+ async function generateView() {
72
+ try {
73
+ // Prompt for route access level within the single-shell SPA
74
+ const isProtectedAnswer = await question('Should this route require login? (y/n): ');
75
+ const isProtected = isProtectedAnswer.toLowerCase().trim() === 'y';
76
+
77
+ // Prompt for controller creation
78
+ const createControllerAnswer = await question('Create a controller for this view? (y/n): ');
79
+ const createController = createControllerAnswer.toLowerCase().trim() === 'y';
80
+
81
+ rl.close();
82
+
83
+ // Determine directories
84
+ const accessFolder = isProtected ? 'protected' : 'public';
85
+ const viewsDir = path.resolve(__dirname, '..', 'src', 'views', accessFolder);
86
+ const viewFile = path.join(viewsDir, `${viewName}.html`);
87
+
88
+ // Check if view already exists
89
+ if (fs.existsSync(viewFile)) {
90
+ console.error(`Error: View "${viewName}.html" already exists in ${accessFolder} folder`);
91
+ process.exit(1);
92
+ }
93
+
94
+ // Ensure directory exists
95
+ if (!fs.existsSync(viewsDir)) {
96
+ fs.mkdirSync(viewsDir, { recursive: true });
97
+ }
98
+
99
+ // Generate HTML template
100
+ const htmlTemplate = createController ? `<div class="${viewName}-page">
101
+ <h1 id="${viewName}-title">${viewTitle}</h1>
102
+ <p>This is the ${viewTitle} page.</p>
103
+
104
+ <div id="${viewName}-content">
105
+ <loading-spinner message="Loading ${viewTitle.toLowerCase()}..."></loading-spinner>
106
+ </div>
107
+ </div>
108
+ ` : `<div class="${viewName}-page">
109
+ <h1>${viewTitle}</h1>
110
+ <p>This is the ${viewTitle} page.</p>
111
+
112
+ <div class="content">
113
+ <!-- Add your content here -->
114
+ </div>
115
+ </div>
116
+ `;
117
+
118
+ // Create HTML file
119
+ fs.writeFileSync(viewFile, htmlTemplate);
120
+ console.log(`Created view: src/views/${accessFolder}/${viewName}.html`);
121
+
122
+ // Create controller if requested
123
+ if (createController) {
124
+ const controllersDir = path.resolve(__dirname, '..', 'src', 'controllers');
125
+ const controllerFile = path.join(controllersDir, `${viewName}.controller.ts`);
126
+ const indexFile = path.join(controllersDir, 'index.ts');
127
+
128
+ // Check if controller already exists
129
+ if (fs.existsSync(controllerFile)) {
130
+ console.error(`Warning: Controller "${viewName}.controller.ts" already exists`);
131
+ } else {
132
+ // Generate controller template
133
+ const controllerTemplate = `/**
134
+ * ${viewTitle} Page Controller
135
+ * Handles dynamic behavior for the ${viewTitle} page
136
+ */
137
+ import { trackEvents, trackSubscriptions } from '@utils/events.js';
138
+ ${isProtected ? "import auth from '@services/auth.service.js';\nimport api from '@services/api.service.js';\n" : ""}
139
+ export async function ${controllerName}Controller(params: Record<string, string> = {}): Promise<() => void> {
140
+ const events = trackEvents();
141
+ const subs = trackSubscriptions();
142
+
143
+ const titleElement = document.getElementById('${viewName}-title') as HTMLElement | null;
144
+ const contentElement = document.getElementById('${viewName}-content') as HTMLElement | null;
145
+ if (!contentElement) return () => { events.cleanup(); subs.cleanup(); };
146
+
147
+ ${isProtected ? `const user = auth.getUser();
148
+ if (user && titleElement) {
149
+ titleElement.textContent = \`${viewTitle} - \${user.name}\`;
150
+ }
151
+
152
+ ` : ''}try {
153
+ ${isProtected ? `// const data = await api.get('/${viewName}');
154
+
155
+ contentElement.innerHTML = \`
156
+ <div class="card">
157
+ <h2>Welcome to ${viewTitle}</h2>
158
+ <p>Your dynamic content goes here.</p>
159
+ <nc-button id="${viewName}-btn" variant="primary">Action</nc-button>
160
+ </div>
161
+ \`;
162
+
163
+ events.onClick('#${viewName}-btn', handleAction);` : `contentElement.innerHTML = \`
164
+ <div class="card">
165
+ <h2>Content Section</h2>
166
+ <p>Your content goes here.</p>
167
+ </div>
168
+ \`;`}
169
+ } catch (error) {
170
+ contentElement.innerHTML = \`
171
+ <div class="alert alert-error">
172
+ Failed to load ${viewTitle.toLowerCase()}: \${(error as Error).message}
173
+ </div>
174
+ \`;
175
+ }
176
+
177
+ return () => {
178
+ events.cleanup();
179
+ subs.cleanup();
180
+ };
181
+
182
+ function handleAction() {
183
+ // Handle action button click
184
+ }
185
+ }
186
+ `;
187
+
188
+ // Create controller file
189
+ fs.writeFileSync(controllerFile, controllerTemplate);
190
+ console.log(`Created controller: src/controllers/${viewName}.controller.ts`);
191
+
192
+ // Update controllers/index.ts
193
+ if (fs.existsSync(indexFile)) {
194
+ let indexContent = fs.readFileSync(indexFile, 'utf8');
195
+
196
+ // Add export using .js extension (ES module requirement)
197
+ const importStatement = `export { ${controllerName}Controller } from './${viewName}.controller.js';\n`;
198
+
199
+ // Check if import already exists
200
+ if (!indexContent.includes(`${viewName}.controller`)) {
201
+ // Add to end of file
202
+ indexContent += importStatement;
203
+ fs.writeFileSync(indexFile, indexContent);
204
+ console.log(`Updated: src/controllers/index.ts`);
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ // Update routes.ts for lazy loading
211
+ const routesPath = path.resolve(__dirname, '..', 'src', 'routes', 'routes.ts');
212
+ if (fs.existsSync(routesPath)) {
213
+ let routesContent = fs.readFileSync(routesPath, 'utf8');
214
+
215
+ // Add route registration with lazy controller
216
+ const routeRegistration = createController
217
+ ? `.register('/${viewName}', 'src/views/${accessFolder}/${viewName}.html', lazyController('${controllerName}Controller', '../controllers/${viewName}.controller.js'))`
218
+ : `.register('/${viewName}', 'src/views/${accessFolder}/${viewName}.html')`;
219
+
220
+ // Find the last .register() call and add after it
221
+ const lastRegisterIndex = routesContent.lastIndexOf('.register(');
222
+ if (lastRegisterIndex !== -1) {
223
+ // Find the end of this line (semicolon or newline)
224
+ const lineEndIndex = routesContent.indexOf('\n', lastRegisterIndex);
225
+ if (lineEndIndex !== -1) {
226
+ // Insert new route after this line
227
+ const insertPoint = lineEndIndex;
228
+ routesContent = routesContent.slice(0, insertPoint) +
229
+ '\n ' + routeRegistration +
230
+ routesContent.slice(insertPoint);
231
+ console.log(`Added route registration to src/routes/routes.ts`);
232
+ }
233
+ }
234
+
235
+ // Add to protected routes array if needed
236
+ if (isProtected) {
237
+ const protectedRoutesRegex = /export const protectedRoutes = \[(.*?)\];/s;
238
+ const match = routesContent.match(protectedRoutesRegex);
239
+ if (match) {
240
+ const routesList = match[1].trim();
241
+ if (routesList && !routesList.includes(`'/${viewName}'`)) {
242
+ const newRoutes = routesList + `, '/${viewName}'`;
243
+ routesContent = routesContent.replace(
244
+ protectedRoutesRegex,
245
+ `export const protectedRoutes = [${newRoutes}];`
246
+ );
247
+ console.log(`Added '/${viewName}' to protected routes array`);
248
+ }
249
+ }
250
+ }
251
+
252
+ fs.writeFileSync(routesPath, routesContent);
253
+ generateCfRouter();
254
+ }
255
+
256
+ // Update sidebar.ts if protected view
257
+ if (isProtected) {
258
+ try {
259
+ const sidebarPath = path.resolve(__dirname, '..', 'src', 'utils', 'sidebar.ts');
260
+ if (fs.existsSync(sidebarPath)) {
261
+ let sidebarContent = fs.readFileSync(sidebarPath, 'utf8');
262
+
263
+ // Add const declaration for the new link
264
+ const linkDeclaration = `const ${viewName}Link = document.querySelector('.sidebar-item.${viewName}-link') as HTMLElement;`;
265
+ const componentsLinkMatch = sidebarContent.match(/const componentsLink[^\n]*\n/);
266
+
267
+ if (componentsLinkMatch) {
268
+ sidebarContent = sidebarContent.replace(
269
+ componentsLinkMatch[0],
270
+ componentsLinkMatch[0] + ' ' + linkDeclaration + '\n'
271
+ );
272
+ }
273
+
274
+ // Add display toggle
275
+ const displayToggle = `if (${viewName}Link) ${viewName}Link.style.display = isAuthenticated ? 'flex' : 'none';`;
276
+ const componentsDisplayMatch = sidebarContent.match(/if \(componentsLink\)[^\n]*\n/);
277
+
278
+ if (componentsDisplayMatch) {
279
+ sidebarContent = sidebarContent.replace(
280
+ componentsDisplayMatch[0],
281
+ componentsDisplayMatch[0] + ' ' + displayToggle + '\n'
282
+ );
283
+ }
284
+
285
+ fs.writeFileSync(sidebarPath, sidebarContent);
286
+ console.log(`Updated sidebar.ts for new link visibility`);
287
+ }
288
+ } catch (e) {
289
+ // sidebar.ts update is optional
290
+ }
291
+ }
292
+
293
+ // Show next steps
294
+ console.log('\nNext steps:');
295
+
296
+ // Automatically add to navigation
297
+ try {
298
+ const indexPath = path.resolve(__dirname, '..', 'index.html');
299
+ let indexContent = fs.readFileSync(indexPath, 'utf-8');
300
+
301
+ if (isProtected) {
302
+ // Add to sidebar if protected
303
+ const sidebarPattern = /<a href="\/components"[^>]*class="sidebar-item components-link"[^>]*>[\s\S]*?<\/a>\s*<button class="sidebar-item logout-link"/;
304
+ const navLink = `<a href="/components" data-link class="sidebar-item components-link" style="display: none;">
305
+ <span class="sidebar-icon"></span>
306
+ <span class="sidebar-text">Components</span>
307
+ </a>
308
+ <a href="/${viewName}" data-link class="sidebar-item ${viewName}-link" style="display: none;">
309
+ <span class="sidebar-icon"></span>
310
+ <span class="sidebar-text">${viewTitle}</span>
311
+ </a>
312
+ <button class="sidebar-item logout-link"`;
313
+
314
+ if (sidebarPattern.test(indexContent)) {
315
+ indexContent = indexContent.replace(sidebarPattern, navLink);
316
+ fs.writeFileSync(indexPath, indexContent);
317
+ console.log(`Added to sidebar menu (protected pages)`);
318
+ }
319
+ } else {
320
+ // Add to header nav if public
321
+ const headerPattern = /<a href="\/about"[^>]*class="nanc-link"[^>]*>About<\/a>\s*<\/nav>/;
322
+ const headerLink = `<a href="/about" data-link class="nanc-link">About</a>
323
+ <a href="/${viewName}" data-link class="nanc-link">${viewTitle}</a>
324
+ </nav>`;
325
+
326
+ if (headerPattern.test(indexContent)) {
327
+ indexContent = indexContent.replace(headerPattern, headerLink);
328
+ fs.writeFileSync(indexPath, indexContent);
329
+ console.log(`Added to public header navigation`);
330
+ }
331
+ }
332
+ } catch (e) {
333
+ console.log(`Note: Could not auto-add navigation link. Add manually if needed.`);
334
+ }
335
+
336
+ console.log('\nDone!\n');
337
+
338
+ } catch (error) {
339
+ console.error('Error:', error.message);
340
+ rl.close();
341
+ process.exit(1);
342
+ }
343
+ }
344
+
345
+ // Run the generator
346
+ generateView();