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,1363 @@
1
+ /**
2
+ * Component Editor Drawer
3
+ *
4
+ * Right-side sliding drawer that dynamically displays
5
+ * only the properties that exist in the component's source.
6
+ */
7
+
8
+ import type { ComponentMetadata } from './denc-tools.js';
9
+ import dom from '../utils/dom.js';
10
+
11
+ interface ExtendedMetadata extends ComponentMetadata {
12
+ hostStyles?: { property: string; value: string }[];
13
+ inlineStyles?: { property: string; value: string }[];
14
+ usesShadowDOM?: boolean;
15
+ }
16
+
17
+ export class ComponentEditor {
18
+ private drawer: HTMLElement | null = null;
19
+ private currentElement: HTMLElement | null = null;
20
+ private currentMetadata: ExtendedMetadata | null = null;
21
+ private originalStyles: Map<string, string> = new Map();
22
+ private originalAttributes: Map<string, string> = new Map();
23
+ private componentSnapshot: any = null;
24
+
25
+ constructor() {
26
+ this.injectStyles();
27
+ this.createDrawer();
28
+ }
29
+
30
+ private injectStyles(): void {
31
+ const styleId = 'nativecore-editor-styles';
32
+ if ((window.dom?.query?.(`#${styleId}`) || document.getElementById(styleId))) return;
33
+
34
+ const style = document.createElement('style');
35
+ style.id = styleId;
36
+ style.textContent = `
37
+ .nc-editor-overlay {
38
+ position: fixed;
39
+ top: 0; left: 0; right: 0; bottom: 0;
40
+ z-index: 1000000;
41
+ background: rgba(0, 0, 0, 0.15);
42
+ opacity: 0;
43
+ pointer-events: none;
44
+ transition: opacity 0.3s ease;
45
+ }
46
+ .nc-editor-overlay.active {
47
+ opacity: 1;
48
+ visibility: visible;
49
+ }
50
+ .nc-editor-drawer {
51
+ position: fixed;
52
+ top: 0;
53
+ right: 0;
54
+ width: 400px;
55
+ height: 100vh;
56
+ background: #1e1e2e;
57
+ box-shadow: -4px 0 24px rgba(0,0,0,0.3);
58
+ display: flex;
59
+ flex-direction: column;
60
+ color: #cdd6f4;
61
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
62
+ font-size: 11px;
63
+ transform: translateX(100%);
64
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
65
+ pointer-events: auto;
66
+ }
67
+ .nc-editor-overlay.active .nc-editor-drawer {
68
+ transform: translateX(0);
69
+ }
70
+ .nc-editor-header {
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: space-between;
74
+ padding: 16px 20px;
75
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
76
+ color: white;
77
+ flex-shrink: 0;
78
+ gap: 8px;
79
+ }
80
+ .nc-editor-title {
81
+ font-size: 14px;
82
+ font-weight: 600;
83
+ flex: 1;
84
+ }
85
+ .nc-editor-file-path {
86
+ font-size: 10px;
87
+ font-weight: 400;
88
+ color: rgba(255, 255, 255, 0.7);
89
+ font-family: 'Fira Code', monospace;
90
+ margin-top: 4px;
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 6px;
94
+ }
95
+ .nc-editor-file-path a {
96
+ color: rgba(255, 255, 255, 0.9);
97
+ text-decoration: none;
98
+ transition: color 0.2s;
99
+ }
100
+ .nc-editor-file-path a:hover {
101
+ color: white;
102
+ text-decoration: underline;
103
+ }
104
+ .nc-editor-header-actions {
105
+ display: flex;
106
+ gap: 6px;
107
+ }
108
+ .nc-editor-icon-btn {
109
+ background: rgba(255, 255, 255, 0.2);
110
+ border: 1px solid rgba(255, 255, 255, 0.3);
111
+ color: white;
112
+ width: 28px;
113
+ height: 28px;
114
+ border-radius: 4px;
115
+ cursor: pointer;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ font-size: 14px;
120
+ transition: all 0.2s;
121
+ }
122
+ .nc-editor-icon-btn:hover {
123
+ background: rgba(255, 255, 255, 0.3);
124
+ }
125
+ .nc-editor-btn-danger {
126
+ background: rgba(239, 68, 68, 0.2) !important;
127
+ border-color: rgba(239, 68, 68, 0.4) !important;
128
+ }
129
+ .nc-editor-btn-danger:hover {
130
+ background: rgba(239, 68, 68, 0.4) !important;
131
+ }
132
+ .nc-editor-btn-danger:disabled {
133
+ opacity: 0.3;
134
+ cursor: not-allowed;
135
+ }
136
+ .nc-editor-title code {
137
+ background: rgba(255,255,255,0.2);
138
+ padding: 3px 8px;
139
+ border-radius: 4px;
140
+ font-size: 12px;
141
+ margin-left: 6px;
142
+ }
143
+ .nc-editor-mode-selector {
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 4px;
147
+ padding: 16px 20px 12px 20px;
148
+ background: #181825;
149
+ border-bottom: 1px solid #313244;
150
+ }
151
+ .nc-editor-mode-label {
152
+ font-size: 10px;
153
+ color: #cdd6f4;
154
+ font-weight: 600;
155
+ text-transform: uppercase;
156
+ letter-spacing: 0.5px;
157
+ }
158
+ .nc-editor-mode-select {
159
+ background: #313244;
160
+ border: 1px solid #45475a;
161
+ border-radius: 6px;
162
+ padding: 8px 10px;
163
+ color: #cdd6f4;
164
+ font-size: 11px;
165
+ cursor: pointer;
166
+ outline: none;
167
+ }
168
+ .nc-editor-mode-select:focus {
169
+ border-color: #89b4fa;
170
+ }
171
+ .nc-editor-mode-description {
172
+ font-size: 10px;
173
+ color: #cdd6f4;
174
+ line-height: 1.4;
175
+ margin-top: 4px;
176
+ }
177
+ .nc-editor-close {
178
+ background: rgba(255,255,255,0.2);
179
+ border: none;
180
+ color: white;
181
+ width: 28px;
182
+ height: 28px;
183
+ border-radius: 6px;
184
+ cursor: pointer;
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ font-size: 18px;
189
+ font-weight: 300;
190
+ transition: background 0.2s;
191
+ }
192
+ .nc-editor-close:hover { background: rgba(255,255,255,0.3); }
193
+ .nc-editor-body {
194
+ flex: 1;
195
+ overflow-y: auto;
196
+ padding: 20px;
197
+ }
198
+ .nc-editor-body::-webkit-scrollbar {
199
+ width: 8px;
200
+ }
201
+ .nc-editor-body::-webkit-scrollbar-track {
202
+ background: #181825;
203
+ }
204
+ .nc-editor-body::-webkit-scrollbar-thumb {
205
+ background: #45475a;
206
+ border-radius: 4px;
207
+ }
208
+ .nc-editor-body::-webkit-scrollbar-thumb:hover {
209
+ background: #585b70;
210
+ }
211
+ .nc-editor-empty {
212
+ text-align: center;
213
+ padding: 40px 20px;
214
+ color: #6c7086;
215
+ }
216
+ .nc-editor-section {
217
+ margin-bottom: 16px;
218
+ background: #181825;
219
+ border-radius: 8px;
220
+ overflow: hidden;
221
+ border: 1px solid #313244;
222
+ }
223
+ .nc-editor-section-header {
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: space-between;
227
+ padding: 12px 16px;
228
+ cursor: pointer;
229
+ user-select: none;
230
+ }
231
+ .nc-editor-section-header:hover { background: rgba(255,255,255,0.03); }
232
+ .nc-editor-section-title {
233
+ font-size: 11px;
234
+ font-weight: 600;
235
+ text-transform: uppercase;
236
+ letter-spacing: 0.5px;
237
+ color: #a6adc8;
238
+ }
239
+ .nc-editor-section-count {
240
+ font-size: 10px;
241
+ background: #313244;
242
+ padding: 2px 8px;
243
+ border-radius: 10px;
244
+ color: #6c7086;
245
+ }
246
+ .nc-editor-section-toggle {
247
+ width: 14px;
248
+ height: 14px;
249
+ fill: #6c7086;
250
+ transition: transform 0.2s;
251
+ }
252
+ .nc-editor-section.collapsed .nc-editor-section-toggle { transform: rotate(-90deg); }
253
+ .nc-editor-section-content { padding: 12px 16px 16px; }
254
+ .nc-editor-section.collapsed .nc-editor-section-content { display: none; }
255
+ .nc-editor-field {
256
+ display: flex;
257
+ flex-direction: column;
258
+ gap: 6px;
259
+ margin-bottom: 12px;
260
+ }
261
+ .nc-editor-field:last-child { margin-bottom: 0; }
262
+ .nc-editor-label {
263
+ font-size: 10px;
264
+ color: #bac2de;
265
+ font-weight: 500;
266
+ font-family: 'Fira Code', monospace;
267
+ }
268
+ .nc-editor-input {
269
+ background: #313244;
270
+ border: 1px solid #45475a;
271
+ border: 1px solid #45475a;
272
+ border-radius: 6px;
273
+ padding: 8px 10px;
274
+ color: #cdd6f4;
275
+ font-size: 11px;
276
+ width: 100%;
277
+ }
278
+ .nc-editor-input:focus {
279
+ outline: none;
280
+ border-color: #89b4fa;
281
+ background: #3a3d52;
282
+ }
283
+ .nc-editor-select {
284
+ cursor: pointer;
285
+ }
286
+ .nc-editor-select option {
287
+ background: #313244;
288
+ color: #cdd6f4;
289
+ }
290
+ .nc-editor-slider-group {
291
+ display: flex;
292
+ flex-direction: column;
293
+ gap: 4px;
294
+ }
295
+ .nc-editor-slider-wrapper {
296
+ display: flex;
297
+ gap: 8px;
298
+ align-items: center;
299
+ }
300
+ .nc-editor-slider {
301
+ flex: 1;
302
+ }
303
+ .nc-editor-slider-value {
304
+ background: #313244;
305
+ border: 1px solid #45475a;
306
+ border-radius: 4px;
307
+ padding: 4px 8px;
308
+ color: #cdd6f4;
309
+ font-size: 11px;
310
+ min-width: 50px;
311
+ text-align: center;
312
+ }
313
+ .nc-editor-color-wrap {
314
+ display: flex;
315
+ gap: 8px;
316
+ align-items: center;
317
+ }
318
+ .nc-editor-color-wrap input[type="color"] {
319
+ width: 40px;
320
+ height: 36px;
321
+ border: 1px solid #45475a;
322
+ border-radius: 6px;
323
+ cursor: pointer;
324
+ padding: 0;
325
+ }
326
+ .nc-editor-footer {
327
+ display: flex;
328
+ justify-content: flex-end;
329
+ padding: 16px 20px;
330
+ background: #181825;
331
+ border-top: 1px solid #313244;
332
+ gap: 10px;
333
+ flex-shrink: 0;
334
+ }
335
+ .nc-editor-btn {
336
+ padding: 8px 16px;
337
+ border-radius: 6px;
338
+ font-size: 11px;
339
+ font-weight: 600;
340
+ cursor: pointer;
341
+ border: none;
342
+ transition: all 0.2s;
343
+ }
344
+ .nc-editor-btn-secondary {
345
+ background: #313244;
346
+ color: #cdd6f4;
347
+ }
348
+ .nc-editor-btn-secondary:hover { background: #45475a; }
349
+ .nc-editor-footer {
350
+ position: sticky;
351
+ bottom: 0;
352
+ left: 0;
353
+ right: 0;
354
+ display: flex;
355
+ justify-content: flex-end;
356
+ gap: 8px;
357
+ padding: 16px 20px;
358
+ background: #1e1e2e;
359
+ border-top: 1px solid #313244;
360
+ flex-shrink: 0;
361
+ z-index: 10;
362
+ box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.2);
363
+ }
364
+ .nc-editor-btn {
365
+ padding: 8px 16px;
366
+ border-radius: 6px;
367
+ font-size: 11px;
368
+ font-weight: 600;
369
+ cursor: pointer;
370
+ border: none;
371
+ transition: all 0.2s;
372
+ }
373
+ .nc-editor-btn-primary {
374
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
375
+ color: white;
376
+ }
377
+ .nc-editor-btn-primary:hover {
378
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
379
+ transform: translateY(-1px);
380
+ }
381
+ input[type="range"] {
382
+ height: 6px;
383
+ -webkit-appearance: none;
384
+ background: #45475a;
385
+ border-radius: 3px;
386
+ width: 100%;
387
+ }
388
+ input[type="range"]::-webkit-slider-thumb {
389
+ -webkit-appearance: none;
390
+ width: 16px;
391
+ height: 16px;
392
+ background: #667eea;
393
+ border-radius: 50%;
394
+ cursor: pointer;
395
+ }
396
+ .nc-editor-checkbox {
397
+ width: 18px;
398
+ height: 18px;
399
+ accent-color: #667eea;
400
+ cursor: pointer;
401
+ }
402
+
403
+ /* Confirmation Modal */
404
+ .nc-editor-modal {
405
+ position: fixed;
406
+ inset: 0;
407
+ background: rgba(0, 0, 0, 0.8);
408
+ backdrop-filter: blur(4px);
409
+ display: none;
410
+ align-items: center;
411
+ justify-content: center;
412
+ z-index: 1000002;
413
+ animation: fadeIn 0.15s ease;
414
+ }
415
+ .nc-editor-modal.active {
416
+ display: flex;
417
+ }
418
+ @keyframes fadeIn {
419
+ from { opacity: 0; }
420
+ to { opacity: 1; }
421
+ }
422
+ .nc-editor-modal-content {
423
+ background: #1e1e2e;
424
+ border-radius: 12px;
425
+ width: 90%;
426
+ max-width: 420px;
427
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
428
+ border: 1px solid #313244;
429
+ animation: slideUp 0.2s ease;
430
+ }
431
+ @keyframes slideUp {
432
+ from { transform: translateY(20px); opacity: 0; }
433
+ to { transform: translateY(0); opacity: 1; }
434
+ }
435
+ .nc-editor-modal-header {
436
+ padding: 20px;
437
+ border-radius: 12px 12px 0 0;
438
+ display: flex;
439
+ align-items: center;
440
+ gap: 12px;
441
+ }
442
+ .nc-editor-modal-icon {
443
+ width: 32px;
444
+ height: 32px;
445
+ background: rgba(255, 255, 255, 0.2);
446
+ border-radius: 50%;
447
+ display: flex;
448
+ align-items: center;
449
+ justify-content: center;
450
+ font-size: 18px;
451
+ }
452
+ .nc-editor-modal-title {
453
+ font-size: 16px;
454
+ font-weight: 600;
455
+ color: white;
456
+ }
457
+ .nc-editor-modal-body {
458
+ padding: 24px;
459
+ color: #cdd6f4;
460
+ line-height: 1.6;
461
+ }
462
+ .nc-editor-modal-component {
463
+ font-family: 'Fira Code', monospace;
464
+ background: #181825;
465
+ padding: 8px 12px;
466
+ border-radius: 6px;
467
+ color: #89b4fa;
468
+ margin: 12px 0;
469
+ border: 1px solid #313244;
470
+ }
471
+ .nc-editor-modal-footer {
472
+ padding: 16px 24px;
473
+ display: flex;
474
+ gap: 12px;
475
+ justify-content: flex-end;
476
+ border-top: 1px solid #313244;
477
+ }
478
+ .nc-editor-modal-btn {
479
+ padding: 10px 20px;
480
+ border-radius: 8px;
481
+ font-size: 13px;
482
+ font-weight: 600;
483
+ cursor: pointer;
484
+ border: none;
485
+ transition: all 0.2s;
486
+ }
487
+ .nc-editor-modal-btn-cancel {
488
+ background: #313244;
489
+ color: #cdd6f4;
490
+ }
491
+ .nc-editor-modal-btn-cancel:hover {
492
+ background: #45475a;
493
+ }
494
+ .nc-editor-modal-btn-delete {
495
+ background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
496
+ color: white;
497
+ }
498
+ .nc-editor-modal-btn-delete:hover {
499
+ box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
500
+ transform: translateY(-1px);
501
+ }
502
+ `;
503
+ document.head.appendChild(style);
504
+ }
505
+
506
+ private createDrawer(): void {
507
+ this.drawer = document.createElement('div');
508
+ this.drawer.className = 'nc-editor-overlay';
509
+ this.drawer.innerHTML = `
510
+ <div class="nc-editor-drawer" id="ncEditorDrawer">
511
+ <div class="nc-editor-header">
512
+ <div style="flex: 1;">
513
+ <div class="nc-editor-title">Edit Component<code id="ncEditorTagName"></code></div>
514
+ <div class="nc-editor-file-path" id="ncEditorFilePath"></div>
515
+ </div>
516
+ <div class="nc-editor-header-actions">
517
+ <button class="nc-editor-icon-btn" id="ncEditorCopy" title="Copy Component Values">📋</button>
518
+ <button class="nc-editor-icon-btn" id="ncEditorPaste" title="Paste Component Values">📄</button>
519
+ <button class="nc-editor-icon-btn" id="ncEditorReset" title="Reset to Default">↺</button>
520
+ <button class="nc-editor-icon-btn nc-editor-btn-danger" id="ncEditorDelete" title="Delete Instance">🗑️</button>
521
+ <button class="nc-editor-icon-btn" id="ncEditorCloseBtn" title="Close" style="margin-left: 8px;">✕</button>
522
+ </div>
523
+ </div>
524
+ <div class="nc-editor-body" id="ncEditorContent"></div>
525
+ <div class="nc-editor-footer">
526
+ <button class="nc-editor-btn nc-editor-btn-secondary" id="ncEditorClose">Close</button>
527
+ <button class="nc-editor-btn nc-editor-btn-primary" id="ncEditorSave">Apply Changes</button>
528
+ </div>
529
+ </div>
530
+ `;
531
+
532
+ this.drawer.querySelector('#ncEditorClose')?.addEventListener('click', () => this.close());
533
+ this.drawer.querySelector('#ncEditorCloseBtn')?.addEventListener('click', () => this.close());
534
+ this.drawer.querySelector('#ncEditorSave')?.addEventListener('click', () => this.save());
535
+ this.drawer.querySelector('#ncEditorCopy')?.addEventListener('click', () => this.copyValues());
536
+ this.drawer.querySelector('#ncEditorPaste')?.addEventListener('click', () => this.pasteValues());
537
+ this.drawer.querySelector('#ncEditorReset')?.addEventListener('click', () => this.resetToDefault());
538
+ this.drawer.querySelector('#ncEditorDelete')?.addEventListener('click', () => this.deleteInstance());
539
+ document.addEventListener('keydown', (e) => {
540
+ if (e.key === 'Escape' && this.drawer?.classList.contains('active')) this.close();
541
+ });
542
+
543
+ // Listen for close editor event from outline panel
544
+ document.addEventListener('nc-close-editor', () => {
545
+ if (this.drawer?.classList.contains('active')) {
546
+ this.close();
547
+ }
548
+ });
549
+
550
+ // Listen for requests for current element from outline panel
551
+ document.addEventListener('nc-request-current-element', () => {
552
+ if (this.currentElement) {
553
+ document.dispatchEvent(new CustomEvent('nc-current-element-response', {
554
+ detail: { element: this.currentElement }
555
+ }));
556
+ }
557
+ });
558
+
559
+ document.body.appendChild(this.drawer);
560
+
561
+ // Create confirmation modal
562
+ this.createConfirmModal();
563
+ }
564
+
565
+ private createConfirmModal(): void {
566
+ const modal = document.createElement('div');
567
+ modal.className = 'nc-editor-modal';
568
+ modal.id = 'ncEditorModal';
569
+ modal.innerHTML = `
570
+ <div class="nc-editor-modal-content">
571
+ <div class="nc-editor-modal-header">
572
+ <div class="nc-editor-modal-icon">🗑️</div>
573
+ <div class="nc-editor-modal-title">Delete Component Instance</div>
574
+ </div>
575
+ <div class="nc-editor-modal-body">
576
+ <p>Are you sure you want to delete this component instance?</p>
577
+ <div class="nc-editor-modal-component" id="ncModalComponentName"></div>
578
+ <p style="color: #ef4444; font-size: 12px; margin-top: 16px;">
579
+ ⚠️ This will permanently remove it from the HTML file. This action cannot be undone.
580
+ </p>
581
+ </div>
582
+ <div class="nc-editor-modal-footer">
583
+ <button class="nc-editor-modal-btn nc-editor-modal-btn-cancel" id="ncModalCancel">Cancel</button>
584
+ <button class="nc-editor-modal-btn nc-editor-modal-btn-delete" id="ncModalConfirm">Delete</button>
585
+ </div>
586
+ </div>
587
+ `;
588
+
589
+ modal.addEventListener('click', (e) => {
590
+ if (e.target === modal) {
591
+ modal.classList.remove('active');
592
+ }
593
+ });
594
+
595
+ document.body.appendChild(modal);
596
+ }
597
+
598
+ private showConfirmModal(componentName: string): Promise<boolean> {
599
+ return new Promise((resolve) => {
600
+ const modal = document.getElementById('ncEditorModal');
601
+ const nameEl = document.getElementById('ncModalComponentName');
602
+ const cancelBtn = document.getElementById('ncModalCancel');
603
+ const confirmBtn = document.getElementById('ncModalConfirm');
604
+
605
+ if (!modal || !nameEl || !cancelBtn || !confirmBtn) {
606
+ resolve(false);
607
+ return;
608
+ }
609
+
610
+ nameEl.textContent = `<${componentName}>`;
611
+ modal.classList.add('active');
612
+
613
+ const cleanup = () => {
614
+ modal.classList.remove('active');
615
+ cancelBtn.removeEventListener('click', handleCancel);
616
+ confirmBtn.removeEventListener('click', handleConfirm);
617
+ document.removeEventListener('keydown', handleEscape);
618
+ };
619
+
620
+ const handleCancel = () => {
621
+ cleanup();
622
+ resolve(false);
623
+ };
624
+
625
+ const handleConfirm = () => {
626
+ cleanup();
627
+ resolve(true);
628
+ };
629
+
630
+ const handleEscape = (e: KeyboardEvent) => {
631
+ if (e.key === 'Escape') {
632
+ handleCancel();
633
+ }
634
+ };
635
+
636
+ cancelBtn.addEventListener('click', handleCancel);
637
+ confirmBtn.addEventListener('click', handleConfirm);
638
+ document.addEventListener('keydown', handleEscape);
639
+ });
640
+ }
641
+
642
+
643
+
644
+ /**
645
+ * Copy component values to clipboard (Unity-like)
646
+ */
647
+ private copyValues(): void {
648
+ if (!this.currentElement || !this.currentMetadata) return;
649
+
650
+ const snapshot: any = {
651
+ tagName: this.currentMetadata.tagName,
652
+ attributes: {},
653
+ styles: {}
654
+ };
655
+
656
+ // Copy attributes
657
+ this.currentMetadata.attributes?.forEach(attr => {
658
+ const value = this.currentElement!.getAttribute(attr.name);
659
+ if (value !== null) {
660
+ snapshot.attributes[attr.name] = value;
661
+ }
662
+ });
663
+
664
+ // Copy inline styles
665
+ const styleProps = ['color', 'backgroundColor', 'padding', 'margin', 'borderRadius',
666
+ 'borderWidth', 'borderColor', 'fontSize', 'width', 'height'];
667
+ styleProps.forEach(prop => {
668
+ const value = (this.currentElement!.style as any)[prop];
669
+ if (value) {
670
+ snapshot.styles[prop] = value;
671
+ }
672
+ });
673
+
674
+ this.componentSnapshot = snapshot;
675
+ this.showToast('Values copied to clipboard');
676
+ }
677
+
678
+ /**
679
+ * Paste component values (Unity-like)
680
+ */
681
+ private pasteValues(): void {
682
+ if (!this.componentSnapshot || !this.currentElement) {
683
+ this.showToast('No values to paste', 'warning');
684
+ return;
685
+ }
686
+
687
+ // Apply attributes
688
+ for (const [key, value] of Object.entries(this.componentSnapshot.attributes || {})) {
689
+ this.currentElement.setAttribute(key, value as string);
690
+ }
691
+
692
+ // Apply styles
693
+ for (const [key, value] of Object.entries(this.componentSnapshot.styles || {})) {
694
+ (this.currentElement.style as any)[key] = value;
695
+ }
696
+
697
+ this.showToast('Values pasted successfully');
698
+ }
699
+
700
+ /**
701
+ * Reset component to default values (Unity-like)
702
+ */
703
+ private resetToDefault(): void {
704
+ if (!this.currentElement || !this.currentMetadata) return;
705
+
706
+ // Reset attributes to original values
707
+ this.originalAttributes.forEach((value, key) => {
708
+ if (value) {
709
+ this.currentElement!.setAttribute(key, value);
710
+ } else {
711
+ this.currentElement!.removeAttribute(key);
712
+ }
713
+ });
714
+
715
+ // Reset styles to original values
716
+ this.originalStyles.forEach((value, key) => {
717
+ if (value) {
718
+ (this.currentElement!.style as any)[key] = value;
719
+ } else {
720
+ (this.currentElement!.style as any)[key] = '';
721
+ }
722
+ });
723
+
724
+ this.showToast('Reset to default values');
725
+
726
+ // Refresh the editor UI
727
+ this.close();
728
+ setTimeout(() => this.open(this.currentElement!, this.currentMetadata!), 100);
729
+ }
730
+
731
+ /**
732
+ * Show toast notification
733
+ */
734
+ private showToast(message: string, type: 'success' | 'warning' | 'error' = 'success'): void {
735
+ const toast = document.createElement('div');
736
+ toast.style.cssText = `
737
+ position: fixed;
738
+ bottom: 20px;
739
+ right: 20px;
740
+ background: ${type === 'success' ? '#10b981' : type === 'warning' ? '#f59e0b' : '#ef4444'};
741
+ color: white;
742
+ padding: 12px 20px;
743
+ border-radius: 8px;
744
+ font-size: 14px;
745
+ font-weight: 500;
746
+ z-index: 1000003;
747
+ animation: slideIn 0.3s ease;
748
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
749
+ `;
750
+ toast.textContent = message;
751
+ document.body.appendChild(toast);
752
+
753
+ setTimeout(() => {
754
+ toast.style.animation = 'slideOut 0.3s ease';
755
+ setTimeout(() => toast.remove(), 300);
756
+ }, 2000);
757
+ }
758
+
759
+ /**
760
+ * Delete component instance from HTML (Instance mode only)
761
+ */
762
+ private async deleteInstance(): Promise<void> {
763
+ if (!this.currentElement || !this.currentMetadata) return;
764
+
765
+ const tagName = this.currentMetadata.tagName;
766
+
767
+ // Show custom confirmation modal
768
+ const confirmed = await this.showConfirmModal(tagName);
769
+
770
+ if (!confirmed) return;
771
+
772
+ try {
773
+ const viewPath = window.location.pathname.replace(/^\//, '');
774
+ const htmlPath = viewPath === '' ? 'views/public/home.html' : `views/${viewPath}.html`;
775
+
776
+ const response = await fetch('/api/dev/component/delete-instance', {
777
+ method: 'POST',
778
+ headers: { 'Content-Type': 'application/json' },
779
+ body: JSON.stringify({
780
+ tagName,
781
+ htmlPath: `src/${htmlPath}`,
782
+ outerHTML: this.currentElement.outerHTML
783
+ })
784
+ });
785
+
786
+ const result = await response.json();
787
+
788
+ if (result.success) {
789
+ this.showToast('Instance deleted successfully');
790
+ this.currentElement.remove();
791
+ this.close();
792
+ } else {
793
+ this.showToast(result.error || 'Failed to delete instance', 'error');
794
+ }
795
+ } catch (error) {
796
+ console.error('[DevTools] Delete failed:', error);
797
+ this.showToast('Delete failed', 'error');
798
+ }
799
+ }
800
+
801
+ open(element: HTMLElement, metadata: ExtendedMetadata): void {
802
+ if (!this.drawer) return;
803
+
804
+ console.log('[ComponentEditor] Opening with metadata:', metadata);
805
+ console.log('[ComponentEditor] Attributes:', metadata.attributes);
806
+
807
+ // Clean up previous element's outline before switching to new element
808
+ if (this.currentElement && this.currentElement !== element) {
809
+ this.currentElement.style.outline = '';
810
+ this.currentElement.style.outlineOffset = '';
811
+ }
812
+
813
+ this.currentElement = element;
814
+ this.currentMetadata = metadata;
815
+ this.originalStyles.clear();
816
+ this.originalAttributes.clear();
817
+
818
+ // Store original attribute values
819
+ metadata.attributes?.forEach(attr => {
820
+ const currentValue = element.getAttribute(attr.name);
821
+ this.originalAttributes.set(attr.name, currentValue || '');
822
+ });
823
+
824
+ const tagNameEl = this.drawer.querySelector('#ncEditorTagName');
825
+ if (tagNameEl) tagNameEl.textContent = `<${metadata.tagName}>`;
826
+
827
+ // Display file path
828
+ const filePathEl = this.drawer.querySelector('#ncEditorFilePath');
829
+ if (filePathEl && metadata.filePath) {
830
+ filePathEl.innerHTML = `
831
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
832
+ <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
833
+ <polyline points="13 2 13 9 20 9"></polyline>
834
+ </svg>
835
+ <span>${metadata.filePath}</span>
836
+ `;
837
+ }
838
+
839
+ const contentEl = this.drawer.querySelector('#ncEditorContent');
840
+ if (contentEl) {
841
+ contentEl.innerHTML = this.buildDynamicContent(element, metadata);
842
+ this.attachListeners(contentEl as HTMLElement, element);
843
+ }
844
+
845
+ this.drawer.classList.add('active');
846
+ element.style.outline = '2px dashed rgba(102, 126, 234, 0.6)';
847
+ element.style.outlineOffset = '4px';
848
+ }
849
+
850
+ private buildDynamicContent(element: HTMLElement, meta: ExtendedMetadata): string {
851
+ const chevron = '<svg class="nc-editor-section-toggle" viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>';
852
+ let html = '';
853
+ let sectionCount = 0;
854
+
855
+ // Element Properties section
856
+ sectionCount++;
857
+ const elementId = element.getAttribute('id') || '';
858
+ const elementClass = element.getAttribute('class') || '';
859
+ html += this.buildSection('Element Properties', 2, chevron, sectionCount === 1, `
860
+ <div class="nc-editor-field">
861
+ <label class="nc-editor-label">id</label>
862
+ <input type="text" class="nc-editor-input" data-attr="id" value="${elementId}" placeholder="element-id">
863
+ </div>
864
+ <div class="nc-editor-field">
865
+ <label class="nc-editor-label">class</label>
866
+ <input type="text" class="nc-editor-input" data-attr="class" value="${elementClass}" placeholder="class-names">
867
+ </div>
868
+ `);
869
+
870
+ // Attributes section
871
+ if (meta.attributes && meta.attributes.length > 0) {
872
+ // Filter attributes based on conditions if they exist
873
+ const componentClass = (element.constructor as any);
874
+ const conditions = componentClass.attributeConditions || {};
875
+
876
+ const visibleAttributes = meta.attributes.filter(attr => {
877
+ // If there's a condition for this attribute, check it
878
+ if (conditions[attr.name]) {
879
+ return conditions[attr.name](element);
880
+ }
881
+ // If no condition, always show
882
+ return true;
883
+ });
884
+
885
+ if (visibleAttributes.length > 0) {
886
+ sectionCount++;
887
+ html += this.buildSection('Attributes', visibleAttributes.length, chevron, sectionCount === 1,
888
+ visibleAttributes.map(attr => this.buildAttributeField(element, attr)).join('')
889
+ );
890
+ }
891
+ }
892
+
893
+ // CSS Variables section
894
+ if (meta.cssVariables && meta.cssVariables.length > 0) {
895
+ sectionCount++;
896
+ html += this.buildSection('CSS Variables', meta.cssVariables.length, chevron, sectionCount === 1,
897
+ meta.cssVariables.map(v => this.buildCssVarField(element, v)).join('')
898
+ );
899
+ }
900
+
901
+ // Host Styles section
902
+ if (meta.hostStyles && meta.hostStyles.length > 0) {
903
+ sectionCount++;
904
+ html += this.buildSection('Host Styles', meta.hostStyles.length, chevron, sectionCount === 1,
905
+ meta.hostStyles.map(s => this.buildHostStyleField(element, s)).join('')
906
+ );
907
+ }
908
+
909
+ // Common styles section (always show for quick edits)
910
+ sectionCount++;
911
+ const cs = getComputedStyle(element);
912
+ html += this.buildSection('Quick Styles', 4, chevron, sectionCount === 1 && html === '', `
913
+ ${this.buildStyleField('color', 'color', cs.color)}
914
+ ${this.buildStyleField('backgroundColor', 'bg-color', cs.backgroundColor)}
915
+ ${this.buildStyleField('padding', 'padding', cs.padding)}
916
+ ${this.buildStyleField('borderRadius', 'radius', cs.borderRadius)}
917
+ `);
918
+
919
+ if (html === '') {
920
+ html = '<div class="nc-editor-empty">No editable properties found</div>';
921
+ }
922
+
923
+ return html;
924
+ }
925
+
926
+ private buildSection(title: string, count: number, chevron: string, expanded: boolean, content: string): string {
927
+ return `
928
+ <div class="nc-editor-section ${expanded ? '' : 'collapsed'}">
929
+ <div class="nc-editor-section-header">
930
+ <span class="nc-editor-section-title">${title}</span>
931
+ <span class="nc-editor-section-count">${count}</span>
932
+ ${chevron}
933
+ </div>
934
+ <div class="nc-editor-section-content">${content}</div>
935
+ </div>
936
+ `;
937
+ }
938
+
939
+ private buildAttributeField(element: HTMLElement, attr: { name: string; type: string; defaultValue: string; variantOptions?: string[] }): string {
940
+ const currentValue = element.getAttribute(attr.name) || attr.defaultValue || '';
941
+
942
+ if (attr.type === 'boolean') {
943
+ const checked = element.hasAttribute(attr.name) ? 'checked' : '';
944
+ return `<div class="nc-editor-field">
945
+ <label class="nc-editor-label">${attr.name}</label>
946
+ <input type="checkbox" class="nc-editor-checkbox" data-attr="${attr.name}" ${checked}>
947
+ </div>`;
948
+ }
949
+
950
+ if (attr.type === 'number') {
951
+ // Slider for numbers with visual value display
952
+ const numValue = parseInt(currentValue) || 0;
953
+ return `<div class="nc-editor-field">
954
+ <label class="nc-editor-label">${attr.name}</label>
955
+ <div class="nc-editor-slider-group">
956
+ <div class="nc-editor-slider-wrapper">
957
+ <input type="range" class="nc-editor-input nc-editor-slider"
958
+ data-attr="${attr.name}"
959
+ data-attr-type="number-slider"
960
+ value="${numValue}"
961
+ min="0" max="100" step="1">
962
+ <span class="nc-editor-slider-value" data-slider-display="${attr.name}">${numValue}</span>
963
+ </div>
964
+ <input type="number" class="nc-editor-input"
965
+ data-attr="${attr.name}"
966
+ data-attr-type="number-input"
967
+ value="${numValue}"
968
+ style="margin-top: 4px;">
969
+ </div>
970
+ </div>`;
971
+ }
972
+
973
+ // Variant dropdown
974
+ if (attr.type === 'variant' && attr.variantOptions && attr.variantOptions.length > 0) {
975
+ const emptyOption = `<option value="" ${!currentValue ? 'selected' : ''}>(none)</option>`;
976
+ const options = attr.variantOptions.map(opt =>
977
+ `<option value="${opt}" ${opt === currentValue ? 'selected' : ''}>${opt}</option>`
978
+ ).join('');
979
+
980
+ return `<div class="nc-editor-field">
981
+ <label class="nc-editor-label">${attr.name}</label>
982
+ <select class="nc-editor-input nc-editor-select" data-attr="${attr.name}">
983
+ ${emptyOption}
984
+ ${options}
985
+ </select>
986
+ </div>`;
987
+ }
988
+
989
+ // Color attribute detection
990
+ if (attr.name.includes('color') || attr.name.includes('bg')) {
991
+ return `<div class="nc-editor-field">
992
+ <label class="nc-editor-label">${attr.name}</label>
993
+ <div class="nc-editor-color-wrap">
994
+ <input type="color" data-attr="${attr.name}" value="${currentValue || '#000000'}">
995
+ <input type="text" class="nc-editor-input" data-attr-text="${attr.name}" value="${currentValue}">
996
+ </div>
997
+ </div>`;
998
+ }
999
+
1000
+ // Get placeholder from component's static attributePlaceholders property
1001
+ const componentClass = (element.constructor as any);
1002
+ const placeholder = componentClass.attributePlaceholders?.[attr.name] || '';
1003
+
1004
+ return `<div class="nc-editor-field">
1005
+ <label class="nc-editor-label">${attr.name}</label>
1006
+ <input type="text" class="nc-editor-input" data-attr="${attr.name}" value="${currentValue}" placeholder="${placeholder}">
1007
+ </div>`;
1008
+ }
1009
+
1010
+ private buildCssVarField(element: HTMLElement, cssVar: { name: string; defaultValue: string }): string {
1011
+ const isColor = cssVar.name.includes('color') || cssVar.defaultValue.startsWith('#') || cssVar.defaultValue.startsWith('rgb');
1012
+
1013
+ if (isColor) {
1014
+ const hex = this.rgbToHex(cssVar.defaultValue);
1015
+ return `<div class="nc-editor-field">
1016
+ <span class="nc-editor-label">${cssVar.name.replace('--', '')}</span>
1017
+ <div class="nc-editor-color-wrap">
1018
+ <input type="color" data-cssvar="${cssVar.name}" value="${hex}">
1019
+ <input type="text" class="nc-editor-input" data-cssvar-text="${cssVar.name}" value="${cssVar.defaultValue}">
1020
+ </div>
1021
+ </div>`;
1022
+ }
1023
+
1024
+ return `<div class="nc-editor-field">
1025
+ <span class="nc-editor-label">${cssVar.name.replace('--', '')}</span>
1026
+ <input type="text" class="nc-editor-input" data-cssvar="${cssVar.name}" value="${cssVar.defaultValue}">
1027
+ </div>`;
1028
+ }
1029
+
1030
+ private buildHostStyleField(element: HTMLElement, style: { property: string; value: string }): string {
1031
+ const isColor = style.property.includes('color') || style.value.startsWith('#') || style.value.startsWith('rgb');
1032
+
1033
+ if (isColor) {
1034
+ const hex = this.rgbToHex(style.value);
1035
+ return `<div class="nc-editor-field">
1036
+ <span class="nc-editor-label">${style.property}</span>
1037
+ <div class="nc-editor-color-wrap">
1038
+ <input type="color" data-host-style="${style.property}" value="${hex}">
1039
+ <input type="text" class="nc-editor-input" data-host-style-text="${style.property}" value="${style.value}">
1040
+ </div>
1041
+ </div>`;
1042
+ }
1043
+
1044
+ const numValue = parseFloat(style.value);
1045
+ if (!isNaN(numValue) && style.value.includes('px')) {
1046
+ return `<div class="nc-editor-field">
1047
+ <span class="nc-editor-label">${style.property}</span>
1048
+ <input type="range" data-host-style="${style.property}" value="${numValue}" min="0" max="50">
1049
+ <input type="number" class="nc-editor-input" data-host-style-num="${style.property}" value="${numValue}" style="width:45px;">
1050
+ </div>`;
1051
+ }
1052
+
1053
+ return `<div class="nc-editor-field">
1054
+ <span class="nc-editor-label">${style.property}</span>
1055
+ <input type="text" class="nc-editor-input" data-host-style="${style.property}" value="${style.value}">
1056
+ </div>`;
1057
+ }
1058
+
1059
+ private buildStyleField(prop: string, label: string, value: string): string {
1060
+ const isColor = prop.includes('color') || prop.includes('Color');
1061
+
1062
+ if (isColor) {
1063
+ const hex = this.rgbToHex(value);
1064
+ return `<div class="nc-editor-field">
1065
+ <span class="nc-editor-label">${label}</span>
1066
+ <div class="nc-editor-color-wrap">
1067
+ <input type="color" data-style="${prop}" value="${hex}">
1068
+ <input type="text" class="nc-editor-input" data-style-text="${prop}" value="${value}">
1069
+ </div>
1070
+ </div>`;
1071
+ }
1072
+
1073
+ const numValue = parseFloat(value) || 0;
1074
+ return `<div class="nc-editor-field">
1075
+ <span class="nc-editor-label">${label}</span>
1076
+ <input type="range" data-style="${prop}" value="${numValue}" min="0" max="50">
1077
+ <input type="number" class="nc-editor-input" data-style-num="${prop}" value="${numValue}" style="width:45px;">
1078
+ </div>`;
1079
+ }
1080
+
1081
+ private attachListeners(container: HTMLElement, element: HTMLElement): void {
1082
+ // Section toggles
1083
+ container.querySelectorAll('.nc-editor-section-header').forEach(h => {
1084
+ h.addEventListener('click', () => h.parentElement?.classList.toggle('collapsed'));
1085
+ });
1086
+
1087
+ // Attribute inputs
1088
+ container.querySelectorAll('[data-attr]').forEach(input => {
1089
+ const eventType = (input as HTMLInputElement).type === 'range' ? 'input' : 'change';
1090
+
1091
+ input.addEventListener(eventType, (e) => {
1092
+ const t = e.target as HTMLInputElement;
1093
+ const attrType = t.dataset.attrType;
1094
+ const attrName = t.dataset.attr!;
1095
+
1096
+ if (t.type === 'checkbox') {
1097
+ if (t.checked) element.setAttribute(attrName, '');
1098
+ else element.removeAttribute(attrName);
1099
+ } else if (attrType === 'number-slider') {
1100
+ // Update both slider and number input
1101
+ element.setAttribute(attrName, t.value);
1102
+ const display = container.querySelector(`[data-slider-display="${attrName}"]`);
1103
+ if (display) display.textContent = t.value;
1104
+ const numberInput = container.querySelector(`[data-attr="${attrName}"][data-attr-type="number-input"]`) as HTMLInputElement;
1105
+ if (numberInput) numberInput.value = t.value;
1106
+ } else if (attrType === 'number-input') {
1107
+ // Update both number input and slider
1108
+ element.setAttribute(attrName, t.value);
1109
+ const slider = container.querySelector(`[data-attr="${attrName}"][data-attr-type="number-slider"]`) as HTMLInputElement;
1110
+ if (slider) slider.value = t.value;
1111
+ const display = container.querySelector(`[data-slider-display="${attrName}"]`);
1112
+ if (display) display.textContent = t.value;
1113
+ } else if (t.type === 'color') {
1114
+ // Update color and text input
1115
+ element.setAttribute(attrName, t.value);
1116
+ const textInput = container.querySelector(`[data-attr-text="${attrName}"]`) as HTMLInputElement;
1117
+ if (textInput) textInput.value = t.value;
1118
+ } else {
1119
+ // For dropdowns and text inputs
1120
+ if (t.value === '') {
1121
+ element.removeAttribute(attrName);
1122
+ } else {
1123
+ element.setAttribute(attrName, t.value);
1124
+ }
1125
+ }
1126
+
1127
+ // Check if this attribute change should trigger a rebuild (for conditional attributes)
1128
+ const componentClass = (element.constructor as any);
1129
+ const conditions = componentClass.attributeConditions || {};
1130
+
1131
+ // If any other attribute has a condition that depends on this one, rebuild
1132
+ if (Object.keys(conditions).some(key => key !== attrName)) {
1133
+ // Check if changing this attribute affects visibility of other attributes
1134
+ // by testing if this is something conditions check (like 'layout')
1135
+ if (attrName === 'layout' || Object.values(conditions).some((fn: any) => {
1136
+ // Simple heuristic: if condition function mentions this attribute
1137
+ return fn.toString().includes(`'${attrName}'`);
1138
+ })) {
1139
+ // Save section states before rebuilding
1140
+ const contentEl = this.drawer?.querySelector('#ncEditorContent');
1141
+ const sectionStates = new Map<string, boolean>();
1142
+ if (contentEl) {
1143
+ contentEl.querySelectorAll('.nc-editor-section').forEach((section: Element) => {
1144
+ const title = section.querySelector('.nc-editor-section-title')?.textContent || '';
1145
+ const isCollapsed = section.classList.contains('collapsed');
1146
+ sectionStates.set(title, isCollapsed);
1147
+ });
1148
+ }
1149
+
1150
+ // Rebuild the editor content with new conditional visibility
1151
+ setTimeout(() => {
1152
+ if (this.currentElement && this.currentMetadata) {
1153
+ const contentEl = this.drawer?.querySelector('#ncEditorContent');
1154
+ if (contentEl) {
1155
+ contentEl.innerHTML = this.buildDynamicContent(this.currentElement, this.currentMetadata);
1156
+ this.attachListeners(contentEl as HTMLElement, this.currentElement);
1157
+
1158
+ // Restore section states
1159
+ contentEl.querySelectorAll('.nc-editor-section').forEach((section: Element) => {
1160
+ const title = section.querySelector('.nc-editor-section-title')?.textContent || '';
1161
+ const wasCollapsed = sectionStates.get(title);
1162
+ if (wasCollapsed === true) {
1163
+ section.classList.add('collapsed');
1164
+ } else if (wasCollapsed === false) {
1165
+ section.classList.remove('collapsed');
1166
+ }
1167
+ });
1168
+ }
1169
+ }
1170
+ }, 50);
1171
+ }
1172
+ }
1173
+ });
1174
+ });
1175
+
1176
+ // Color text inputs
1177
+ container.querySelectorAll('[data-attr-text]').forEach(input => {
1178
+ input.addEventListener('change', (e) => {
1179
+ const t = e.target as HTMLInputElement;
1180
+ const attrName = t.dataset.attrText!;
1181
+ element.setAttribute(attrName, t.value);
1182
+ const colorInput = container.querySelector(`[data-attr="${attrName}"][type="color"]`) as HTMLInputElement;
1183
+ if (colorInput && /^#[0-9A-F]{6}$/i.test(t.value)) {
1184
+ colorInput.value = t.value;
1185
+ }
1186
+ });
1187
+ });
1188
+
1189
+ // CSS Variable inputs
1190
+ container.querySelectorAll('[data-cssvar]').forEach(input => {
1191
+ input.addEventListener('input', (e) => {
1192
+ const t = e.target as HTMLInputElement;
1193
+ element.style.setProperty(t.dataset.cssvar!, t.value);
1194
+ const txt = container.querySelector(`[data-cssvar-text="${t.dataset.cssvar}"]`) as HTMLInputElement;
1195
+ if (txt) txt.value = t.value;
1196
+ });
1197
+ });
1198
+
1199
+ // Host style inputs
1200
+ container.querySelectorAll('[data-host-style]').forEach(input => {
1201
+ input.addEventListener('input', (e) => {
1202
+ const t = e.target as HTMLInputElement;
1203
+ const prop = t.dataset.hostStyle!;
1204
+ const val = t.type === 'range' ? t.value + 'px' : t.value;
1205
+ (element.style as any)[prop] = val;
1206
+
1207
+ const num = container.querySelector(`[data-host-style-num="${prop}"]`) as HTMLInputElement;
1208
+ if (num && t.type === 'range') num.value = t.value;
1209
+ const txt = container.querySelector(`[data-host-style-text="${prop}"]`) as HTMLInputElement;
1210
+ if (txt) txt.value = val;
1211
+ });
1212
+ });
1213
+
1214
+ container.querySelectorAll('[data-host-style-num]').forEach(input => {
1215
+ input.addEventListener('input', (e) => {
1216
+ const t = e.target as HTMLInputElement;
1217
+ const prop = t.dataset.hostStyleNum!;
1218
+ (element.style as any)[prop] = t.value + 'px';
1219
+ const range = container.querySelector(`[data-host-style="${prop}"]`) as HTMLInputElement;
1220
+ if (range) range.value = t.value;
1221
+ });
1222
+ });
1223
+
1224
+ // Quick style inputs
1225
+ container.querySelectorAll('[data-style]').forEach(input => {
1226
+ input.addEventListener('input', (e) => {
1227
+ const t = e.target as HTMLInputElement;
1228
+ const prop = t.dataset.style!;
1229
+ const val = t.type === 'range' ? t.value + 'px' : t.value;
1230
+ (element.style as any)[prop] = val;
1231
+
1232
+ const num = container.querySelector(`[data-style-num="${prop}"]`) as HTMLInputElement;
1233
+ if (num && t.type === 'range') num.value = t.value;
1234
+ const txt = container.querySelector(`[data-style-text="${prop}"]`) as HTMLInputElement;
1235
+ if (txt) txt.value = val;
1236
+ });
1237
+ });
1238
+
1239
+ container.querySelectorAll('[data-style-num]').forEach(input => {
1240
+ input.addEventListener('input', (e) => {
1241
+ const t = e.target as HTMLInputElement;
1242
+ const prop = t.dataset.styleNum!;
1243
+ (element.style as any)[prop] = t.value + 'px';
1244
+ const range = container.querySelector(`[data-style="${prop}"]`) as HTMLInputElement;
1245
+ if (range) range.value = t.value;
1246
+ });
1247
+ });
1248
+ }
1249
+
1250
+ private rgbToHex(rgb: string): string {
1251
+ if (rgb.startsWith('#')) return rgb;
1252
+ if (rgb === 'transparent' || rgb === 'rgba(0, 0, 0, 0)') return '#ffffff';
1253
+ const m = rgb.match(/\d+/g);
1254
+ if (!m || m.length < 3) return '#000000';
1255
+ return '#' + m.slice(0, 3).map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
1256
+ }
1257
+
1258
+ private async save(): Promise<void> {
1259
+ if (!this.currentMetadata || !this.currentElement) return;
1260
+ await this.saveInstanceChanges();
1261
+ }
1262
+
1263
+ private async saveInstanceChanges(): Promise<void> {
1264
+ if (!this.currentElement || !this.currentMetadata) return;
1265
+
1266
+ // Collect ALL current attribute values (not just from metadata)
1267
+ const attributes: Record<string, string> = {};
1268
+ const attrs = this.currentElement.attributes;
1269
+ for (let i = 0; i < attrs.length; i++) {
1270
+ const attr = attrs[i];
1271
+ if (attr.name !== 'style' && attr.name !== 'class') {
1272
+ attributes[attr.name] = attr.value;
1273
+ }
1274
+ }
1275
+
1276
+ // NO inline styles - attributes only!
1277
+
1278
+ const payload = {
1279
+ tagName: this.currentMetadata.tagName,
1280
+ viewPath: window.location.pathname,
1281
+ attributes,
1282
+ elementIndex: this.getElementIndex()
1283
+ };
1284
+
1285
+ console.log('[DevTools] Saving instance changes:', payload);
1286
+
1287
+ try {
1288
+ const response = await fetch('/api/dev/component/save-instance', {
1289
+ method: 'POST',
1290
+ headers: { 'Content-Type': 'application/json' },
1291
+ body: JSON.stringify(payload)
1292
+ });
1293
+
1294
+ if (response.ok) {
1295
+ const result = await response.json();
1296
+ console.log('[DevTools] Save successful:', result);
1297
+ this.close();
1298
+ } else {
1299
+ const error = await response.json();
1300
+ console.error('[DevTools] Failed to save instance changes:', error);
1301
+ alert(`Failed to save: ${error.message || 'Unknown error'}`);
1302
+ }
1303
+ } catch (error) {
1304
+ console.error('[DevTools] Error saving instance changes:', error);
1305
+ alert(`Error saving: ${error instanceof Error ? error.message : String(error)}`);
1306
+ }
1307
+ }
1308
+
1309
+
1310
+
1311
+ private getElementIndex(): number {
1312
+ if (!this.currentElement || !this.currentMetadata) return 0;
1313
+
1314
+ const allElements = Array.from(dom.queryAll(this.currentMetadata.tagName));
1315
+ return allElements.indexOf(this.currentElement);
1316
+ }
1317
+
1318
+ private async saveOld(): Promise<void> {
1319
+ if (!this.currentMetadata || !this.currentElement) return;
1320
+
1321
+ const changes: any = {
1322
+ tagName: this.currentMetadata.tagName,
1323
+ filePath: this.currentMetadata.filePath,
1324
+ styleChanges: {},
1325
+ attributeChanges: {},
1326
+ cssVarChanges: {}
1327
+ };
1328
+
1329
+ // Collect style changes from the element
1330
+ const el = this.currentElement;
1331
+ ['color', 'backgroundColor', 'padding', 'margin', 'borderRadius', 'borderWidth', 'borderColor'].forEach(prop => {
1332
+ const val = (el.style as any)[prop];
1333
+ if (val) changes.styleChanges[prop] = val;
1334
+ });
1335
+
1336
+ try {
1337
+ await fetch('/api/dev/component/edit', {
1338
+ method: 'POST',
1339
+ headers: { 'Content-Type': 'application/json' },
1340
+ body: JSON.stringify(changes)
1341
+ });
1342
+ setTimeout(() => this.close(), 500);
1343
+ } catch (e) {
1344
+ console.error('[ComponentEditor] Save failed:', e);
1345
+ }
1346
+ }
1347
+
1348
+ close(): void {
1349
+ this.drawer?.classList.remove('active');
1350
+ document.getElementById('ncEditorModal')?.classList.remove('active');
1351
+ if (this.currentElement) {
1352
+ this.currentElement.style.outline = '';
1353
+ this.currentElement.style.outlineOffset = '';
1354
+ }
1355
+ this.currentElement = null;
1356
+ this.currentMetadata = null;
1357
+ }
1358
+
1359
+ destroy(): void {
1360
+ this.drawer?.remove();
1361
+ (window.dom?.query?.('#nativecore-editor-styles') || document.getElementById('nativecore-editor-styles'))?.remove();
1362
+ }
1363
+ }