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,343 @@
1
+ /**
2
+ * nc-splash Component
3
+ *
4
+ * Standalone full-screen splash screen with dissolve effect.
5
+ * Shows branding then dissolves into particles.
6
+ * Use anywhere without needing to wrap content.
7
+ *
8
+ * Usage:
9
+ * </nc-splash>
10
+ *
11
+ *
12
+ */
13
+ import { Component, defineComponent } from '@core/component.js';
14
+ import { useState } from '@core/state.js';
15
+ import { createAnimationLoop, type AnimationLoop } from '@core/gpu-animation.js';
16
+ import type { State } from '@core/state.js';
17
+
18
+ interface Particle {
19
+ x: number;
20
+ y: number;
21
+ vx: number;
22
+ vy: number;
23
+ size: number;
24
+ color: string;
25
+ alpha: number;
26
+ rotation: number;
27
+ rotationSpeed: number;
28
+ }
29
+
30
+ export class NcSplash extends Component {
31
+ static useShadowDOM = true;
32
+
33
+ private isComplete: State<boolean>;
34
+ private canvas?: HTMLCanvasElement;
35
+ private animationLoop?: AnimationLoop;
36
+
37
+ static get observedAttributes() {
38
+ return ['particles', 'duration', 'delay', 'title', 'subtitle'];
39
+ }
40
+
41
+ constructor() {
42
+ super();
43
+ this.isComplete = useState(false);
44
+ }
45
+
46
+ template() {
47
+ const title = this.attr('title', 'NativeCore');
48
+ const subtitle = this.attr('subtitle', '');
49
+ return `
50
+ <style>
51
+ :host {
52
+ display: block;
53
+ position: fixed;
54
+ inset: 0;
55
+ width: auto;
56
+ height: auto;
57
+ z-index: 99999;
58
+ }
59
+
60
+ .splash-overlay {
61
+ position: absolute;
62
+ top: 0;
63
+ left: 0;
64
+ width: 100%;
65
+ height: 100%;
66
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.98) 100%);
67
+ backdrop-filter: blur(20px);
68
+ -webkit-backdrop-filter: blur(20px);
69
+ display: flex;
70
+ flex-direction: column;
71
+ align-items: center;
72
+ justify-content: center;
73
+ transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1);
74
+ }
75
+
76
+ .splash-overlay.fade-out {
77
+ opacity: 0;
78
+ pointer-events: none;
79
+ }
80
+
81
+ .splash-overlay.hidden {
82
+ display: none;
83
+ }
84
+
85
+ .splash-content {
86
+ text-align: center;
87
+ z-index: 1;
88
+ }
89
+
90
+ .splash-prompt {
91
+ margin-top: 2.5rem;
92
+ font-size: 1.25rem;
93
+ color: #fff;
94
+ opacity: 0.85;
95
+ letter-spacing: 0.1em;
96
+ font-family: 'Fira Mono', 'Fira Code', monospace;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ gap: 0.5em;
101
+ user-select: none;
102
+ }
103
+
104
+ .splash-cursor {
105
+ display: inline-block;
106
+ width: 1ch;
107
+ height: 1.2em;
108
+ background: none;
109
+ border-right: 2px solid #fff;
110
+ animation: blink-cursor 1s steps(1) infinite;
111
+ margin-left: 0.1em;
112
+ }
113
+
114
+ @keyframes blink-cursor {
115
+ 0%, 49% { opacity: 1; }
116
+ 50%, 100% { opacity: 0; }
117
+ }
118
+
119
+ .splash-title {
120
+ font-size: clamp(3rem, 12vw, 8rem);
121
+ font-weight: 800;
122
+ background: linear-gradient(135deg, #10b981 0%, #3b82f6 50%, #06b6d4 100%);
123
+ -webkit-background-clip: text;
124
+ -webkit-text-fill-color: transparent;
125
+ background-clip: text;
126
+ letter-spacing: -0.02em;
127
+ margin: 0;
128
+ text-shadow: 0 0 80px rgba(16, 185, 129, 0.5);
129
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
130
+ }
131
+
132
+ .splash-subtitle {
133
+ font-size: clamp(0.875rem, 2vw, 1.25rem);
134
+ color: rgba(255, 255, 255, 0.6);
135
+ margin-top: 1rem;
136
+ font-weight: 400;
137
+ letter-spacing: 0.2em;
138
+ text-transform: uppercase;
139
+ }
140
+
141
+ .splash-glow {
142
+ position: absolute;
143
+ width: 600px;
144
+ height: 600px;
145
+ background: radial-gradient(circle, rgba(16, 185, 129, 0.15) 0%, transparent 70%);
146
+ pointer-events: none;
147
+ }
148
+
149
+ .splash-canvas {
150
+ position: absolute;
151
+ top: 0;
152
+ left: 0;
153
+ width: 100%;
154
+ height: 100%;
155
+ pointer-events: none;
156
+ z-index: 100000;
157
+ }
158
+ </style>
159
+
160
+ <div class="splash-overlay" id="overlay">
161
+ <div class="splash-glow"></div>
162
+ <div class="splash-content" id="content">
163
+ <h1 class="splash-title">${title}</h1>
164
+ ${subtitle ? `<p class="splash-subtitle">${subtitle}</p>` : ''}
165
+ <div class="splash-prompt" id="prompt">
166
+ Click to start <span class="splash-cursor"></span>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ `;
171
+ }
172
+
173
+ onMount() {
174
+ // Check if splash has already been shown this session
175
+ const splashShown = sessionStorage.getItem('splash-shown');
176
+ const connection = (navigator as Navigator & { connection?: { saveData?: boolean } }).connection;
177
+ const shouldSkipSplash =
178
+ window.matchMedia('(max-width: 768px)').matches ||
179
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches ||
180
+ connection?.saveData === true;
181
+
182
+ if (splashShown === 'true' || shouldSkipSplash) {
183
+ // Skip splash, hide component but keep in DOM for dev tools
184
+ this.style.display = 'none';
185
+ return;
186
+ }
187
+
188
+ const overlay = this.$('#overlay') as HTMLElement;
189
+ const prompt = this.$('#prompt') as HTMLElement;
190
+ if (overlay && prompt) {
191
+ const clickHandler = () => {
192
+ prompt.style.opacity = '0.5';
193
+ prompt.style.pointerEvents = 'none';
194
+ setTimeout(() => {
195
+ prompt.style.display = 'none';
196
+ }, 300);
197
+ this.startDissolve();
198
+ overlay.removeEventListener('click', clickHandler);
199
+ window.removeEventListener('keydown', keyHandler);
200
+ };
201
+ const keyHandler = (e: KeyboardEvent) => {
202
+ if (e.key === 'Enter' || e.key === ' ') {
203
+ clickHandler();
204
+ }
205
+ };
206
+ overlay.addEventListener('click', clickHandler);
207
+ window.addEventListener('keydown', keyHandler);
208
+ }
209
+ }
210
+
211
+ private startDissolve() {
212
+ const overlay = this.$('#overlay') as HTMLElement;
213
+ const content = this.$('#content') as HTMLElement;
214
+
215
+ if (!overlay || !content) return;
216
+
217
+ const particleCount = parseInt(this.attr('particles', '10000'), 10);
218
+ const duration = parseInt(this.attr('duration', '2500'), 10);
219
+
220
+ // Get content bounding rect
221
+ const rect = content.getBoundingClientRect();
222
+
223
+ // Sample colors from the gradient (emerald green palette)
224
+ const colors = [
225
+ '#10b981', '#059669', '#34d399', '#14b8a6',
226
+ '#3b82f6', '#2563eb', '#06b6d4', '#0891b2'
227
+ ];
228
+
229
+ // Create canvas
230
+ const dpr = window.devicePixelRatio || 1;
231
+ this.canvas = document.createElement('canvas');
232
+ this.canvas.className = 'splash-canvas';
233
+ this.canvas.width = window.innerWidth * dpr;
234
+ this.canvas.height = window.innerHeight * dpr;
235
+ this.canvas.style.cssText = `position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:100000`;
236
+
237
+ const ctx = this.canvas.getContext('2d', { alpha: true });
238
+ if (!ctx) return;
239
+
240
+ ctx.scale(dpr, dpr);
241
+
242
+ // Create particles in a grid covering the content area
243
+ const particles: Particle[] = [];
244
+ const gridSize = Math.ceil(Math.sqrt(particleCount));
245
+ const cellWidth = rect.width / gridSize;
246
+ const cellHeight = rect.height / gridSize;
247
+
248
+ for (let i = 0; i < particleCount; i++) {
249
+ const row = Math.floor(i / gridSize);
250
+ const col = i % gridSize;
251
+ const angle = Math.random() * Math.PI * 2;
252
+ const speed = 1 + Math.random() * 4;
253
+
254
+ particles.push({
255
+ x: rect.left + col * cellWidth + cellWidth / 2 + (Math.random() - 0.5) * cellWidth * 0.5,
256
+ y: rect.top + row * cellHeight + cellHeight / 2 + (Math.random() - 0.5) * cellHeight * 0.5,
257
+ vx: Math.cos(angle) * speed,
258
+ vy: Math.sin(angle) * speed - 0.5, // slight upward bias
259
+ size: Math.max(cellWidth, cellHeight) * (0.6 + Math.random() * 0.4),
260
+ color: colors[Math.floor(Math.random() * colors.length)],
261
+ alpha: 0.8 + Math.random() * 0.2,
262
+ rotation: Math.random() * Math.PI * 2,
263
+ rotationSpeed: (Math.random() - 0.5) * 0.15
264
+ });
265
+ }
266
+
267
+ // Hide original content, show canvas
268
+ content.style.opacity = '0';
269
+ this.shadowRoot?.appendChild(this.canvas);
270
+
271
+ const startTime = performance.now();
272
+ const fadeStart = duration * 0.4;
273
+
274
+ this.animationLoop = createAnimationLoop((dt) => {
275
+ const elapsed = performance.now() - startTime;
276
+ const progress = Math.min(elapsed / duration, 1);
277
+
278
+ ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
279
+
280
+ for (const p of particles) {
281
+ // Physics
282
+ p.x += p.vx * dt * 60;
283
+ p.y += p.vy * dt * 60;
284
+ p.vy += 0.08 * dt * 60; // gravity
285
+ p.rotation += p.rotationSpeed;
286
+
287
+ // Fade out
288
+ if (elapsed > fadeStart) {
289
+ p.alpha = Math.max(0, (1 - ((elapsed - fadeStart) / (duration - fadeStart))) * 0.9);
290
+ }
291
+
292
+ // Draw
293
+ if (p.alpha > 0.01) {
294
+ ctx.save();
295
+ ctx.translate(p.x, p.y);
296
+ ctx.rotate(p.rotation);
297
+ ctx.globalAlpha = p.alpha;
298
+ ctx.fillStyle = p.color;
299
+ ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
300
+ ctx.restore();
301
+ }
302
+ }
303
+
304
+ if (progress >= 1) {
305
+ return false;
306
+ }
307
+ return true;
308
+ });
309
+
310
+ this.animationLoop.start();
311
+
312
+ // Start fading overlay
313
+ setTimeout(() => {
314
+ overlay.classList.add('fade-out');
315
+ }, duration * 0.6);
316
+
317
+ // Complete cleanup - mark as shown and hide component
318
+ setTimeout(() => {
319
+ this.animationLoop?.stop();
320
+ this.canvas?.remove();
321
+ overlay.classList.add('hidden');
322
+ this.isComplete.value = true;
323
+
324
+ // Mark splash as shown in sessionStorage
325
+ sessionStorage.setItem('splash-shown', 'true');
326
+
327
+ this.dispatchEvent(new CustomEvent('splash-complete', {
328
+ bubbles: true,
329
+ composed: true
330
+ }));
331
+
332
+ // Hide the component but keep in DOM for dev tools
333
+ setTimeout(() => this.style.display = 'none', 100);
334
+ }, duration + 800);
335
+ }
336
+
337
+ disconnectedCallback() {
338
+ this.animationLoop?.stop();
339
+ this.canvas?.remove();
340
+ }
341
+ }
342
+
343
+ defineComponent('nc-splash', NcSplash);
@@ -0,0 +1,247 @@
1
+ /**
2
+ * NcStepper + NcStep Components
3
+ *
4
+ * Multi-step wizard with numbered step indicators.
5
+ *
6
+ * nc-stepper:
7
+ * - step: number — current step index (0-based, default: 0)
8
+ * - orientation: 'horizontal'|'vertical' (default: 'horizontal')
9
+ * - variant: 'default'|'simple' (default: 'default')
10
+ * - linear: boolean — prevent skipping ahead (default: false)
11
+ *
12
+ * nc-step:
13
+ * - label: string — step label
14
+ * - description: string — optional sub-label
15
+ * - status: 'complete'|'error'|'' — force a status icon (auto-set by stepper normally)
16
+ *
17
+ * Events (on nc-stepper):
18
+ * - change: CustomEvent<{ step: number; prev: number }>
19
+ *
20
+ * Methods (call on the element):
21
+ * stepper.next()
22
+ * stepper.prev()
23
+ * stepper.goTo(index)
24
+ *
25
+ * Usage:
26
+ * <nc-stepper id="wizard" step="0">
27
+ * <nc-step label="Account" description="Basic info"></nc-step>
28
+ * <nc-step label="Profile"></nc-step>
29
+ * <nc-step label="Review"></nc-step>
30
+ * </nc-stepper>
31
+ * <div id="wizard-content">Step 1 content...</div>
32
+ */
33
+
34
+ import { Component, defineComponent } from '@core/component.js';
35
+
36
+ const CHECK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M3 8l4 4 6-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
37
+ const ERROR_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M8 5v4M8 11v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`;
38
+
39
+ // ── NcStep ───────────────────────────────────────────────────────────────────
40
+
41
+ export class NcStep extends Component {
42
+ static useShadowDOM = false; // light DOM — stepper queries children directly
43
+
44
+ static get observedAttributes() { return ['label', 'description', 'status']; }
45
+
46
+ template() { return ''; } // rendered by NcStepper
47
+ }
48
+
49
+ defineComponent('nc-step', NcStep);
50
+
51
+ // ── NcStepper ────────────────────────────────────────────────────────────────
52
+
53
+ export class NcStepper extends Component {
54
+ static useShadowDOM = true;
55
+
56
+ static get observedAttributes() {
57
+ return ['step', 'orientation', 'variant', 'linear'];
58
+ }
59
+
60
+ private _getSteps(): NcStep[] {
61
+ return Array.from(this.querySelectorAll<NcStep>('nc-step'));
62
+ }
63
+
64
+ template() {
65
+ const current = Number(this.getAttribute('step') || 0);
66
+ const orientation = this.getAttribute('orientation') || 'horizontal';
67
+ const isHorizontal = orientation === 'horizontal';
68
+ const steps = this._getSteps();
69
+ const total = steps.length;
70
+
71
+ const stepItems = steps.map((step, i) => {
72
+ const label = step.getAttribute('label') || `Step ${i + 1}`;
73
+ const desc = step.getAttribute('description') || '';
74
+ const forcedStatus = step.getAttribute('status') || '';
75
+
76
+ const isDone = forcedStatus === 'complete' || (!forcedStatus && i < current);
77
+ const isError = forcedStatus === 'error';
78
+ const isActive = i === current;
79
+
80
+ let stateClass = 'step--pending';
81
+ if (isActive) stateClass = 'step--active';
82
+ else if (isDone) stateClass = 'step--done';
83
+ else if (isError) stateClass = 'step--error';
84
+
85
+ const iconContent = isDone
86
+ ? CHECK_ICON
87
+ : isError
88
+ ? ERROR_ICON
89
+ : String(i + 1);
90
+
91
+ return `
92
+ <div
93
+ class="step ${stateClass}"
94
+ data-index="${i}"
95
+ role="tab"
96
+ aria-selected="${isActive}"
97
+ aria-label="Step ${i + 1}: ${label}"
98
+ tabindex="${isActive ? '0' : '-1'}"
99
+ >
100
+ <div class="step__indicator">${iconContent}</div>
101
+ <div class="step__text">
102
+ <span class="step__label">${label}</span>
103
+ ${desc ? `<span class="step__desc">${desc}</span>` : ''}
104
+ </div>
105
+ ${i < total - 1 ? `<div class="step__connector"></div>` : ''}
106
+ </div>`;
107
+ }).join('');
108
+
109
+ return `
110
+ <style>
111
+ :host { display: block; font-family: var(--nc-font-family); }
112
+
113
+ .stepper {
114
+ display: flex;
115
+ flex-direction: ${isHorizontal ? 'row' : 'column'};
116
+ gap: 0;
117
+ }
118
+
119
+ .step {
120
+ display: flex;
121
+ flex-direction: ${isHorizontal ? 'column' : 'row'};
122
+ align-items: ${isHorizontal ? 'center' : 'flex-start'};
123
+ flex: ${isHorizontal ? '1' : 'none'};
124
+ position: relative;
125
+ cursor: pointer;
126
+ gap: ${isHorizontal ? '6px' : 'var(--nc-spacing-sm)'};
127
+ padding: ${isHorizontal ? '0 8px' : 'var(--nc-spacing-sm) 0'};
128
+ outline: none;
129
+ }
130
+ .step:first-child { padding-left: 0; }
131
+ .step:last-child { padding-right: 0; }
132
+
133
+ .step__indicator {
134
+ width: 32px;
135
+ height: 32px;
136
+ border-radius: 50%;
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ font-size: var(--nc-font-size-sm);
141
+ font-weight: var(--nc-font-weight-semibold);
142
+ flex-shrink: 0;
143
+ border: 2px solid var(--nc-border);
144
+ background: var(--nc-bg);
145
+ color: var(--nc-text-muted);
146
+ transition: background var(--nc-transition-fast), border-color var(--nc-transition-fast), color var(--nc-transition-fast);
147
+ z-index: 1;
148
+ }
149
+
150
+ .step--active .step__indicator { border-color: var(--nc-primary); background: var(--nc-primary); color: #fff; }
151
+ .step--done .step__indicator { border-color: var(--nc-primary); background: var(--nc-primary); color: #fff; }
152
+ .step--error .step__indicator { border-color: var(--nc-danger, #ef4444); background: var(--nc-danger, #ef4444); color: #fff; }
153
+
154
+ .step__text {
155
+ display: flex;
156
+ flex-direction: column;
157
+ align-items: ${isHorizontal ? 'center' : 'flex-start'};
158
+ text-align: ${isHorizontal ? 'center' : 'left'};
159
+ }
160
+
161
+ .step__label {
162
+ font-size: var(--nc-font-size-sm);
163
+ font-weight: var(--nc-font-weight-medium);
164
+ color: var(--nc-text-muted);
165
+ white-space: nowrap;
166
+ }
167
+ .step--active .step__label { color: var(--nc-text); font-weight: var(--nc-font-weight-semibold); }
168
+ .step--done .step__label { color: var(--nc-text); }
169
+ .step--error .step__label { color: var(--nc-danger, #ef4444); }
170
+
171
+ .step__desc {
172
+ font-size: var(--nc-font-size-xs);
173
+ color: var(--nc-text-muted);
174
+ white-space: nowrap;
175
+ }
176
+
177
+ /* Connector line */
178
+ .step__connector {
179
+ position: absolute;
180
+ background: var(--nc-border);
181
+ transition: background var(--nc-transition-fast);
182
+ ${isHorizontal
183
+ ? `top: 16px; left: calc(50% + 20px); right: calc(-50% + 20px); height: 2px;`
184
+ : `top: 36px; left: 15px; width: 2px; height: calc(100% - 4px);`}
185
+ }
186
+ .step--done .step__connector,
187
+ .step--active .step__connector { background: var(--nc-primary); }
188
+ </style>
189
+ <div class="stepper" role="tablist" aria-orientation="${orientation}">
190
+ ${stepItems}
191
+ </div>
192
+ `;
193
+ }
194
+
195
+ onMount() {
196
+ this._bindEvents();
197
+ }
198
+
199
+ private _bindEvents() {
200
+ this.$<HTMLElement>('.stepper')!.addEventListener('click', (e) => {
201
+ const stepEl = (e.target as HTMLElement).closest<HTMLElement>('[data-index]');
202
+ if (!stepEl) return;
203
+ const index = Number(stepEl.dataset.index);
204
+ const current = Number(this.getAttribute('step') || 0);
205
+ if (this.hasAttribute('linear') && index > current) return;
206
+ this.goTo(index);
207
+ });
208
+
209
+ this.$<HTMLElement>('.stepper')!.addEventListener('keydown', (e: KeyboardEvent) => {
210
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); this.next(); }
211
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); this.prev(); }
212
+ if (e.key === 'Home') this.goTo(0);
213
+ if (e.key === 'End') this.goTo(this._getSteps().length - 1);
214
+ });
215
+ }
216
+
217
+ next() {
218
+ const current = Number(this.getAttribute('step') || 0);
219
+ this.goTo(Math.min(current + 1, this._getSteps().length - 1));
220
+ }
221
+
222
+ prev() {
223
+ const current = Number(this.getAttribute('step') || 0);
224
+ this.goTo(Math.max(current - 1, 0));
225
+ }
226
+
227
+ goTo(index: number) {
228
+ const steps = this._getSteps();
229
+ if (index < 0 || index >= steps.length) return;
230
+ const prev = Number(this.getAttribute('step') || 0);
231
+ if (index === prev) return;
232
+ this.setAttribute('step', String(index));
233
+ this.dispatchEvent(new CustomEvent('change', {
234
+ bubbles: true, composed: true,
235
+ detail: { step: index, prev }
236
+ }));
237
+ }
238
+
239
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
240
+ if (oldValue !== newValue && this._mounted) {
241
+ this.render();
242
+ this._bindEvents();
243
+ }
244
+ }
245
+ }
246
+
247
+ defineComponent('nc-stepper', NcStepper);