@zentto/studio 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  // @zentto/studio — App Creation Wizard
2
2
  // Step-by-step wizard to create an AppConfig from a template
3
- // Steps: 1) Template → 2) Branding → 3) Navigation → 4) Pages → 5) Preview
3
+ // Steps: 1) Template → 2) Branding → 3) Menu → 4) Data Sources → 5) Pages → 6) Style & Theme → 7) Preview
4
4
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
5
5
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
6
6
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -11,14 +11,17 @@ import { LitElement, html, css, nothing } from 'lit';
11
11
  import { customElement, property, state } from 'lit/decorators.js';
12
12
  import { studioTokens, fieldBaseStyles } from '../styles/tokens.js';
13
13
  import { listAppTemplates, getAppTemplate } from '@zentto/studio-core';
14
+ import { generateAppPage } from '@zentto/studio-core';
14
15
  // Import the app component for preview
15
16
  import '../zentto-studio-app.js';
16
17
  const STEPS = [
17
18
  { id: 'template', title: 'Plantilla', icon: '📋', description: 'Elige una plantilla base' },
18
19
  { id: 'branding', title: 'Marca', icon: '🎨', description: 'Personaliza tu aplicacion' },
19
20
  { id: 'navigation', title: 'Menu', icon: '📑', description: 'Configura el sidebar' },
21
+ { id: 'datasources', title: 'Fuentes de Datos', icon: '🔗', description: 'Conecta tus APIs y servicios' },
20
22
  { id: 'pages', title: 'Paginas', icon: '📄', description: 'Agrega paginas y contenido' },
21
- { id: 'preview', title: 'Vista Previa', icon: '👁️', description: 'Revisa y finaliza' },
23
+ { id: 'theme', title: 'Estilo y Tema', icon: '🎭', description: 'Personaliza colores y tipografia' },
24
+ { id: 'preview', title: 'Vista Previa', icon: '👁️', description: 'Revisa, exporta y finaliza' },
22
25
  ];
23
26
  const SIDEBAR_STYLES = [
24
27
  { value: 'dark', label: 'Oscuro', preview: '#1e1e2d' },
@@ -37,6 +40,29 @@ const COLOR_PALETTE = [
37
40
  '#ff6b6b', '#6bcb77', '#4ecdc4', '#45b7d1', '#96ceb4',
38
41
  '#ffd93d', '#ff69b4', '#a29bfe', '#fd79a8', '#2d3436',
39
42
  ];
43
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
44
+ const THEME_MODES = [
45
+ { value: 'light', label: 'Claro', icon: '☀️' },
46
+ { value: 'dark', label: 'Oscuro', icon: '🌙' },
47
+ { value: 'auto', label: 'Automatico', icon: '🔄' },
48
+ ];
49
+ const FONT_OPTIONS = [
50
+ 'Inter, sans-serif',
51
+ 'Roboto, sans-serif',
52
+ 'Open Sans, sans-serif',
53
+ 'Poppins, sans-serif',
54
+ 'Nunito, sans-serif',
55
+ 'system-ui, sans-serif',
56
+ 'Georgia, serif',
57
+ 'Fira Code, monospace',
58
+ ];
59
+ const RADIUS_OPTIONS = [
60
+ { value: 0, label: 'Sin bordes' },
61
+ { value: 4, label: 'Sutil (4px)' },
62
+ { value: 8, label: 'Medio (8px)' },
63
+ { value: 12, label: 'Redondeado (12px)' },
64
+ { value: 20, label: 'Pill (20px)' },
65
+ ];
40
66
  let ZsAppWizard = class ZsAppWizard extends LitElement {
41
67
  constructor() {
42
68
  super(...arguments);
@@ -47,152 +73,238 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
47
73
  this.editingNavIndex = -1;
48
74
  this.showIconPicker = false;
49
75
  this.iconPickerTarget = 'nav';
76
+ // Code export modal
77
+ this.codeModalOpen = false;
78
+ this.codeModalTitle = '';
79
+ this.codeModalContent = '';
50
80
  }
51
- static { this.styles = [studioTokens, fieldBaseStyles, css `
52
- :host { display: block; font-family: var(--zs-font-family); }
53
-
54
- .wizard {
55
- max-width: 900px; margin: 0 auto;
56
- background: var(--zs-bg); border-radius: 12px;
57
- border: 1px solid var(--zs-border);
58
- overflow: hidden;
59
- }
60
-
61
- /* Steps bar */
62
- .wizard-steps {
63
- display: flex; background: var(--zs-bg-secondary);
64
- border-bottom: 1px solid var(--zs-border); padding: 0;
65
- }
66
- .wizard-step {
67
- flex: 1; display: flex; align-items: center; gap: 8px;
68
- padding: 16px 20px; cursor: pointer;
69
- color: var(--zs-text-muted); font-size: 13px;
70
- border-bottom: 3px solid transparent;
71
- transition: all 150ms;
72
- }
73
- .wizard-step:hover { color: var(--zs-text-secondary); }
74
- .wizard-step--active {
75
- color: var(--zs-primary); border-bottom-color: var(--zs-primary);
76
- font-weight: 500;
77
- }
78
- .wizard-step--done { color: var(--zs-success); }
79
- .wizard-step-icon { font-size: 18px; }
80
- .wizard-step-title { white-space: nowrap; }
81
-
82
- /* Body */
83
- .wizard-body { padding: 32px; min-height: 400px; }
84
- .wizard-title { font-size: 22px; font-weight: 600; margin: 0 0 4px; color: var(--zs-text); }
85
- .wizard-desc { font-size: 14px; color: var(--zs-text-secondary); margin: 0 0 24px; }
86
-
87
- /* Footer */
88
- .wizard-footer {
89
- display: flex; justify-content: space-between;
90
- padding: 16px 32px; border-top: 1px solid var(--zs-border);
91
- background: var(--zs-bg-secondary);
92
- }
93
-
94
- /* Template cards */
95
- .template-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
96
- .template-card {
97
- padding: 24px; border: 2px solid var(--zs-border);
98
- border-radius: 12px; cursor: pointer;
99
- transition: all 200ms; text-align: center;
100
- }
101
- .template-card:hover { border-color: var(--zs-primary); transform: translateY(-2px); box-shadow: 0 4px 12px var(--zs-shadow); }
102
- .template-card--selected { border-color: var(--zs-primary); background: var(--zs-primary-light); }
103
- .template-icon { font-size: 40px; margin-bottom: 12px; }
104
- .template-title { font-size: 16px; font-weight: 600; color: var(--zs-text); }
105
- .template-desc { font-size: 13px; color: var(--zs-text-secondary); margin-top: 4px; }
106
-
107
- /* Form groups */
108
- .form-group { margin-bottom: 20px; }
109
- .form-label { font-size: 13px; font-weight: 500; color: var(--zs-text); margin-bottom: 6px; display: block; }
110
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
111
- .form-row-3 { grid-template-columns: 1fr 1fr 1fr; }
112
-
113
- /* Color picker grid */
114
- .color-grid { display: flex; flex-wrap: wrap; gap: 8px; }
115
- .color-swatch {
116
- width: 36px; height: 36px; border-radius: 8px;
117
- cursor: pointer; border: 3px solid transparent;
118
- transition: all 150ms;
119
- }
120
- .color-swatch:hover { transform: scale(1.15); }
121
- .color-swatch--selected { border-color: var(--zs-text); box-shadow: 0 0 0 2px white, 0 0 0 4px var(--zs-text); }
122
-
123
- /* Sidebar style picker */
124
- .style-grid { display: flex; gap: 12px; }
125
- .style-option {
126
- flex: 1; padding: 16px; text-align: center;
127
- border: 2px solid var(--zs-border); border-radius: 8px;
128
- cursor: pointer; transition: all 150ms;
129
- }
130
- .style-option:hover { border-color: var(--zs-primary); }
131
- .style-option--selected { border-color: var(--zs-primary); background: var(--zs-primary-light); }
132
- .style-preview {
133
- width: 60px; height: 40px; border-radius: 4px;
134
- margin: 0 auto 8px; border: 1px solid var(--zs-border);
135
- }
136
-
137
- /* Nav editor */
138
- .nav-list { border: 1px solid var(--zs-border); border-radius: 8px; overflow: hidden; }
139
- .nav-item-row {
140
- display: flex; align-items: center; gap: 8px;
141
- padding: 10px 12px; border-bottom: 1px solid var(--zs-border);
142
- font-size: 14px;
143
- }
144
- .nav-item-row:last-child { border-bottom: none; }
145
- .nav-item-icon { font-size: 18px; cursor: pointer; }
146
- .nav-item-title { flex: 1; }
147
- .nav-item-actions { display: flex; gap: 4px; }
148
- .nav-item-btn {
149
- border: none; background: none; cursor: pointer;
150
- font-size: 14px; padding: 2px 6px; border-radius: 4px;
151
- color: var(--zs-text-muted); transition: all 100ms;
152
- }
153
- .nav-item-btn:hover { background: var(--zs-bg-hover); color: var(--zs-text); }
154
- .nav-item-btn--danger:hover { color: var(--zs-danger); }
155
- .add-btn {
156
- display: flex; align-items: center; gap: 6px;
157
- padding: 8px 16px; margin-top: 12px;
158
- border: 1px dashed var(--zs-border); border-radius: 8px;
159
- background: none; cursor: pointer;
160
- font-family: var(--zs-font-family); font-size: 13px;
161
- color: var(--zs-text-secondary); transition: all 150ms;
162
- }
163
- .add-btn:hover { border-color: var(--zs-primary); color: var(--zs-primary); }
164
-
165
- /* Icon picker */
166
- .icon-grid { display: flex; flex-wrap: wrap; gap: 4px; }
167
- .icon-btn {
168
- width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
169
- border: 1px solid transparent; border-radius: 6px;
170
- cursor: pointer; font-size: 18px; background: none;
171
- transition: all 100ms;
172
- }
173
- .icon-btn:hover { background: var(--zs-bg-hover); border-color: var(--zs-border); }
174
- .icon-btn--selected { background: var(--zs-primary-light); border-color: var(--zs-primary); }
175
-
176
- /* Preview */
177
- .preview-container {
178
- border: 1px solid var(--zs-border); border-radius: 8px;
179
- height: 500px; overflow: hidden;
180
- }
181
-
182
- /* Buttons */
183
- .btn {
184
- padding: 10px 24px; border-radius: var(--zs-radius);
185
- font-family: var(--zs-font-family); font-size: 14px;
186
- font-weight: 500; cursor: pointer; border: 1px solid transparent;
187
- transition: all 150ms;
188
- }
189
- .btn--primary { background: var(--zs-primary); color: white; }
190
- .btn--primary:hover { background: var(--zs-primary-hover); }
191
- .btn--secondary { background: var(--zs-bg); color: var(--zs-text); border-color: var(--zs-border); }
192
- .btn--secondary:hover { background: var(--zs-bg-hover); }
193
- .btn--success { background: var(--zs-success); color: white; }
194
- .btn--success:hover { opacity: 0.9; }
195
- .btn:disabled { opacity: 0.5; cursor: not-allowed; }
81
+ static { this.styles = [studioTokens, fieldBaseStyles, css `
82
+ :host { display: block; font-family: var(--zs-font-family); }
83
+
84
+ .wizard {
85
+ max-width: 900px; margin: 0 auto;
86
+ background: var(--zs-bg); border-radius: 12px;
87
+ border: 1px solid var(--zs-border);
88
+ overflow: hidden;
89
+ }
90
+
91
+ /* Steps bar */
92
+ .wizard-steps {
93
+ display: flex; background: var(--zs-bg-secondary);
94
+ border-bottom: 1px solid var(--zs-border); padding: 0;
95
+ overflow-x: auto;
96
+ }
97
+ .wizard-step {
98
+ flex: 1; display: flex; align-items: center; gap: 6px;
99
+ padding: 14px 12px; cursor: pointer;
100
+ color: var(--zs-text-muted); font-size: 12px;
101
+ border-bottom: 3px solid transparent;
102
+ transition: all 150ms; min-width: 0; white-space: nowrap;
103
+ }
104
+ .wizard-step:hover { color: var(--zs-text-secondary); }
105
+ .wizard-step--active {
106
+ color: var(--zs-primary); border-bottom-color: var(--zs-primary);
107
+ font-weight: 500;
108
+ }
109
+ .wizard-step--done { color: var(--zs-success); }
110
+ .wizard-step-icon { font-size: 16px; }
111
+ .wizard-step-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
112
+
113
+ /* Body */
114
+ .wizard-body { padding: 32px; min-height: 400px; }
115
+ .wizard-title { font-size: 22px; font-weight: 600; margin: 0 0 4px; color: var(--zs-text); }
116
+ .wizard-desc { font-size: 14px; color: var(--zs-text-secondary); margin: 0 0 24px; }
117
+
118
+ /* Footer */
119
+ .wizard-footer {
120
+ display: flex; justify-content: space-between;
121
+ padding: 16px 32px; border-top: 1px solid var(--zs-border);
122
+ background: var(--zs-bg-secondary);
123
+ }
124
+
125
+ /* Template cards */
126
+ .template-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
127
+ .template-card {
128
+ padding: 24px; border: 2px solid var(--zs-border);
129
+ border-radius: 12px; cursor: pointer;
130
+ transition: all 200ms; text-align: center;
131
+ }
132
+ .template-card:hover { border-color: var(--zs-primary); transform: translateY(-2px); box-shadow: 0 4px 12px var(--zs-shadow); }
133
+ .template-card--selected { border-color: var(--zs-primary); background: var(--zs-primary-light); }
134
+ .template-icon { font-size: 40px; margin-bottom: 12px; }
135
+ .template-title { font-size: 16px; font-weight: 600; color: var(--zs-text); }
136
+ .template-desc { font-size: 13px; color: var(--zs-text-secondary); margin-top: 4px; }
137
+
138
+ /* Form groups */
139
+ .form-group { margin-bottom: 20px; }
140
+ .form-label { font-size: 13px; font-weight: 500; color: var(--zs-text); margin-bottom: 6px; display: block; }
141
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
142
+ .form-row-3 { grid-template-columns: 1fr 1fr 1fr; }
143
+ .form-row-4 { grid-template-columns: 1fr 1fr 1fr 1fr; }
144
+
145
+ /* Color picker grid */
146
+ .color-grid { display: flex; flex-wrap: wrap; gap: 8px; }
147
+ .color-swatch {
148
+ width: 36px; height: 36px; border-radius: 8px;
149
+ cursor: pointer; border: 3px solid transparent;
150
+ transition: all 150ms;
151
+ }
152
+ .color-swatch:hover { transform: scale(1.15); }
153
+ .color-swatch--selected { border-color: var(--zs-text); box-shadow: 0 0 0 2px white, 0 0 0 4px var(--zs-text); }
154
+
155
+ /* Sidebar style picker */
156
+ .style-grid { display: flex; gap: 12px; }
157
+ .style-option {
158
+ flex: 1; padding: 16px; text-align: center;
159
+ border: 2px solid var(--zs-border); border-radius: 8px;
160
+ cursor: pointer; transition: all 150ms;
161
+ }
162
+ .style-option:hover { border-color: var(--zs-primary); }
163
+ .style-option--selected { border-color: var(--zs-primary); background: var(--zs-primary-light); }
164
+ .style-preview {
165
+ width: 60px; height: 40px; border-radius: 4px;
166
+ margin: 0 auto 8px; border: 1px solid var(--zs-border);
167
+ }
168
+
169
+ /* Nav editor */
170
+ .nav-list { border: 1px solid var(--zs-border); border-radius: 8px; overflow: hidden; }
171
+ .nav-item-row {
172
+ display: flex; align-items: center; gap: 8px;
173
+ padding: 10px 12px; border-bottom: 1px solid var(--zs-border);
174
+ font-size: 14px;
175
+ }
176
+ .nav-item-row:last-child { border-bottom: none; }
177
+ .nav-item-icon { font-size: 18px; cursor: pointer; }
178
+ .nav-item-title { flex: 1; }
179
+ .nav-item-actions { display: flex; gap: 4px; }
180
+ .nav-item-btn {
181
+ border: none; background: none; cursor: pointer;
182
+ font-size: 14px; padding: 2px 6px; border-radius: 4px;
183
+ color: var(--zs-text-muted); transition: all 100ms;
184
+ }
185
+ .nav-item-btn:hover { background: var(--zs-bg-hover); color: var(--zs-text); }
186
+ .nav-item-btn--danger:hover { color: var(--zs-danger); }
187
+ .add-btn {
188
+ display: flex; align-items: center; gap: 6px;
189
+ padding: 8px 16px; margin-top: 12px;
190
+ border: 1px dashed var(--zs-border); border-radius: 8px;
191
+ background: none; cursor: pointer;
192
+ font-family: var(--zs-font-family); font-size: 13px;
193
+ color: var(--zs-text-secondary); transition: all 150ms;
194
+ }
195
+ .add-btn:hover { border-color: var(--zs-primary); color: var(--zs-primary); }
196
+
197
+ /* Icon picker */
198
+ .icon-grid { display: flex; flex-wrap: wrap; gap: 4px; }
199
+ .icon-btn {
200
+ width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
201
+ border: 1px solid transparent; border-radius: 6px;
202
+ cursor: pointer; font-size: 18px; background: none;
203
+ transition: all 100ms;
204
+ }
205
+ .icon-btn:hover { background: var(--zs-bg-hover); border-color: var(--zs-border); }
206
+ .icon-btn--selected { background: var(--zs-primary-light); border-color: var(--zs-primary); }
207
+
208
+ /* Preview */
209
+ .preview-container {
210
+ border: 1px solid var(--zs-border); border-radius: 8px;
211
+ height: 500px; overflow: hidden;
212
+ }
213
+
214
+ /* Buttons */
215
+ .btn {
216
+ padding: 10px 24px; border-radius: var(--zs-radius);
217
+ font-family: var(--zs-font-family); font-size: 14px;
218
+ font-weight: 500; cursor: pointer; border: 1px solid transparent;
219
+ transition: all 150ms;
220
+ }
221
+ .btn--primary { background: var(--zs-primary); color: white; }
222
+ .btn--primary:hover { background: var(--zs-primary-hover); }
223
+ .btn--secondary { background: var(--zs-bg); color: var(--zs-text); border-color: var(--zs-border); }
224
+ .btn--secondary:hover { background: var(--zs-bg-hover); }
225
+ .btn--success { background: var(--zs-success); color: white; }
226
+ .btn--success:hover { opacity: 0.9; }
227
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
228
+
229
+ /* Data source rows */
230
+ .ds-card {
231
+ border: 1px solid var(--zs-border); border-radius: 8px;
232
+ padding: 16px; margin-bottom: 12px;
233
+ background: var(--zs-bg);
234
+ }
235
+ .ds-card-header {
236
+ display: flex; align-items: center; justify-content: space-between;
237
+ margin-bottom: 12px;
238
+ }
239
+ .ds-card-title { font-size: 14px; font-weight: 500; color: var(--zs-text); }
240
+ .ds-method-badge {
241
+ display: inline-block; padding: 2px 8px; border-radius: 4px;
242
+ font-size: 11px; font-weight: 600; color: white;
243
+ }
244
+ .ds-method-GET { background: #27ae60; }
245
+ .ds-method-POST { background: #3498db; }
246
+ .ds-method-PUT { background: #e67e22; }
247
+ .ds-method-PATCH { background: #9b59b6; }
248
+ .ds-method-DELETE { background: #e74c3c; }
249
+
250
+ /* Theme preview card */
251
+ .theme-preview-card {
252
+ border: 1px solid var(--zs-border); border-radius: 12px;
253
+ padding: 24px; margin-top: 20px;
254
+ transition: all 200ms;
255
+ }
256
+ .theme-preview-header {
257
+ font-size: 16px; font-weight: 600; margin-bottom: 8px;
258
+ }
259
+ .theme-preview-text {
260
+ font-size: 13px; margin-bottom: 16px; opacity: 0.7;
261
+ }
262
+ .theme-preview-btn {
263
+ display: inline-block; padding: 8px 20px;
264
+ border: none; border-radius: 8px;
265
+ color: white; font-size: 13px; font-weight: 500;
266
+ }
267
+ .theme-preview-input {
268
+ display: inline-block; padding: 6px 12px;
269
+ border: 1px solid; border-radius: 8px;
270
+ font-size: 13px; margin-left: 8px; width: 150px;
271
+ }
272
+
273
+ /* Code modal */
274
+ .code-modal-overlay {
275
+ position: fixed; inset: 0; z-index: 9999;
276
+ background: rgba(0,0,0,0.5); display: flex;
277
+ align-items: center; justify-content: center;
278
+ }
279
+ .code-modal {
280
+ background: var(--zs-bg); border-radius: 12px;
281
+ width: 90%; max-width: 800px; max-height: 80vh;
282
+ display: flex; flex-direction: column;
283
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
284
+ }
285
+ .code-modal-header {
286
+ display: flex; align-items: center; justify-content: space-between;
287
+ padding: 16px 20px; border-bottom: 1px solid var(--zs-border);
288
+ }
289
+ .code-modal-header h3 { margin: 0; font-size: 16px; }
290
+ .code-modal-body {
291
+ flex: 1; overflow: auto; padding: 0;
292
+ }
293
+ .code-modal-body pre {
294
+ margin: 0; padding: 20px; font-size: 13px;
295
+ font-family: 'Fira Code', 'Cascadia Code', monospace;
296
+ line-height: 1.5; white-space: pre-wrap; word-break: break-word;
297
+ background: var(--zs-bg-secondary); min-height: 200px;
298
+ }
299
+ .code-modal-footer {
300
+ display: flex; gap: 8px; justify-content: flex-end;
301
+ padding: 12px 20px; border-top: 1px solid var(--zs-border);
302
+ }
303
+
304
+ /* Export buttons row */
305
+ .export-row {
306
+ display: flex; gap: 8px; flex-wrap: wrap;
307
+ }
196
308
  `]; }
197
309
  connectedCallback() {
198
310
  super.connectedCallback();
@@ -226,55 +338,72 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
226
338
  bubbles: true, composed: true,
227
339
  }));
228
340
  }
341
+ // ─── Helpers ──────────────────────────────────────
342
+ ensureTheme() {
343
+ if (!this.config.theme) {
344
+ this.config.theme = { mode: 'light' };
345
+ }
346
+ return this.config.theme;
347
+ }
348
+ ensureDataSources() {
349
+ if (!this.config.dataSources) {
350
+ this.config.dataSources = [];
351
+ }
352
+ return this.config.dataSources;
353
+ }
229
354
  // ─── Render ───────────────────────────────────────
230
355
  render() {
231
- return html `
232
- <div class="wizard">
233
- <div class="wizard-steps">
234
- ${STEPS.map((step, i) => html `
235
- <div class="wizard-step ${i === this.currentStep ? 'wizard-step--active' : i < this.currentStep ? 'wizard-step--done' : ''}"
356
+ return html `
357
+ <div class="wizard">
358
+ <div class="wizard-steps">
359
+ ${STEPS.map((step, i) => html `
360
+ <div class="wizard-step ${i === this.currentStep ? 'wizard-step--active' : i < this.currentStep ? 'wizard-step--done' : ''}"
236
361
  @click="${() => { if (i <= this.currentStep || this.config)
237
- this.currentStep = i; }}">
238
- <span class="wizard-step-icon">${i < this.currentStep ? '✓' : step.icon}</span>
239
- <span class="wizard-step-title">${step.title}</span>
240
- </div>
241
- `)}
242
- </div>
243
-
244
- <div class="wizard-body">
245
- <h2 class="wizard-title">${STEPS[this.currentStep].title}</h2>
246
- <p class="wizard-desc">${STEPS[this.currentStep].description}</p>
247
-
362
+ this.currentStep = i; }}">
363
+ <span class="wizard-step-icon">${i < this.currentStep ? '✓' : step.icon}</span>
364
+ <span class="wizard-step-title">${step.title}</span>
365
+ </div>
366
+ `)}
367
+ </div>
368
+
369
+ <div class="wizard-body">
370
+ <h2 class="wizard-title">${STEPS[this.currentStep].title}</h2>
371
+ <p class="wizard-desc">${STEPS[this.currentStep].description}</p>
372
+
248
373
  ${this.currentStep === 0 ? this.renderTemplateStep() :
249
374
  this.currentStep === 1 ? this.renderBrandingStep() :
250
375
  this.currentStep === 2 ? this.renderNavStep() :
251
- this.currentStep === 3 ? this.renderPagesStep() :
252
- this.renderPreviewStep()}
253
- </div>
254
-
255
- <div class="wizard-footer">
256
- <button class="btn btn--secondary" ?disabled="${this.currentStep === 0}" @click="${this.prev}">Anterior</button>
376
+ this.currentStep === 3 ? this.renderDataSourcesStep() :
377
+ this.currentStep === 4 ? this.renderPagesStep() :
378
+ this.currentStep === 5 ? this.renderThemeStep() :
379
+ this.renderPreviewStep()}
380
+ </div>
381
+
382
+ <div class="wizard-footer">
383
+ <button class="btn btn--secondary" ?disabled="${this.currentStep === 0}" @click="${this.prev}">Anterior</button>
257
384
  ${this.currentStep < STEPS.length - 1
258
385
  ? html `<button class="btn btn--primary" ?disabled="${!this.canNext()}" @click="${this.next}">Siguiente</button>`
259
- : html `<button class="btn btn--success" @click="${this.finish}">Crear Aplicacion</button>`}
260
- </div>
261
- </div>
386
+ : html `<button class="btn btn--success" @click="${this.finish}">Crear Aplicacion</button>`}
387
+ </div>
388
+ </div>
389
+
390
+ ${this.codeModalOpen ? this.renderCodeModal() : nothing}
262
391
  `;
263
392
  }
264
393
  // ─── Step 1: Template Selection ──────────────────
265
394
  renderTemplateStep() {
266
395
  const templates = listAppTemplates();
267
- return html `
268
- <div class="template-grid">
269
- ${templates.map(t => html `
270
- <div class="template-card ${this.selectedTemplate === t.id ? 'template-card--selected' : ''}"
271
- @click="${() => { this.selectedTemplate = t.id; }}">
272
- <div class="template-icon">${t.icon}</div>
273
- <div class="template-title">${t.title}</div>
274
- <div class="template-desc">${t.description}</div>
275
- </div>
276
- `)}
277
- </div>
396
+ return html `
397
+ <div class="template-grid">
398
+ ${templates.map(t => html `
399
+ <div class="template-card ${this.selectedTemplate === t.id ? 'template-card--selected' : ''}"
400
+ @click="${() => { this.selectedTemplate = t.id; }}">
401
+ <div class="template-icon">${t.icon}</div>
402
+ <div class="template-title">${t.title}</div>
403
+ <div class="template-desc">${t.description}</div>
404
+ </div>
405
+ `)}
406
+ </div>
278
407
  `;
279
408
  }
280
409
  // ─── Step 2: Branding ─────────────────────────────
@@ -282,92 +411,92 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
282
411
  if (!this.config)
283
412
  return nothing;
284
413
  const b = this.config.branding;
285
- return html `
286
- <div class="form-row">
287
- <div class="form-group">
288
- <label class="form-label">Nombre de la App</label>
289
- <input class="zs-input" .value="${b.title ?? ''}" @input="${(e) => { this.config.branding.title = e.target.value; this.requestUpdate(); }}" />
290
- </div>
291
- <div class="form-group">
292
- <label class="form-label">Subtitulo</label>
293
- <input class="zs-input" .value="${b.subtitle ?? ''}" @input="${(e) => { this.config.branding.subtitle = e.target.value; this.requestUpdate(); }}" />
294
- </div>
295
- </div>
296
-
297
- <div class="form-group">
298
- <label class="form-label">Color Principal</label>
299
- <div class="color-grid">
300
- ${COLOR_PALETTE.map(color => html `
301
- <div class="color-swatch ${b.primaryColor === color ? 'color-swatch--selected' : ''}"
302
- style="background: ${color}"
303
- @click="${() => { this.config.branding.primaryColor = color; this.requestUpdate(); }}"
304
- ></div>
305
- `)}
306
- </div>
307
- </div>
308
-
309
- <div class="form-group">
310
- <label class="form-label">Estilo del Sidebar</label>
311
- <div class="style-grid">
312
- ${SIDEBAR_STYLES.map(s => html `
313
- <div class="style-option ${b.sidebarStyle === s.value ? 'style-option--selected' : ''}"
314
- @click="${() => { this.config.branding.sidebarStyle = s.value; this.requestUpdate(); }}">
315
- <div class="style-preview" style="background: ${s.preview}"></div>
316
- <div style="font-size:13px;font-weight:500;">${s.label}</div>
317
- </div>
318
- `)}
319
- </div>
320
- </div>
321
-
322
- <div class="form-group">
323
- <label class="form-label">URL del Logo (opcional)</label>
324
- <input class="zs-input" .value="${b.logo ?? ''}" placeholder="https://..." @input="${(e) => { this.config.branding.logo = e.target.value; this.requestUpdate(); }}" />
325
- </div>
414
+ return html `
415
+ <div class="form-row">
416
+ <div class="form-group">
417
+ <label class="form-label">Nombre de la App</label>
418
+ <input class="zs-input" .value="${b.title ?? ''}" @input="${(e) => { this.config.branding.title = e.target.value; this.requestUpdate(); }}" />
419
+ </div>
420
+ <div class="form-group">
421
+ <label class="form-label">Subtitulo</label>
422
+ <input class="zs-input" .value="${b.subtitle ?? ''}" @input="${(e) => { this.config.branding.subtitle = e.target.value; this.requestUpdate(); }}" />
423
+ </div>
424
+ </div>
425
+
426
+ <div class="form-group">
427
+ <label class="form-label">Color Principal</label>
428
+ <div class="color-grid">
429
+ ${COLOR_PALETTE.map(color => html `
430
+ <div class="color-swatch ${b.primaryColor === color ? 'color-swatch--selected' : ''}"
431
+ style="background: ${color}"
432
+ @click="${() => { this.config.branding.primaryColor = color; this.requestUpdate(); }}"
433
+ ></div>
434
+ `)}
435
+ </div>
436
+ </div>
437
+
438
+ <div class="form-group">
439
+ <label class="form-label">Estilo del Sidebar</label>
440
+ <div class="style-grid">
441
+ ${SIDEBAR_STYLES.map(s => html `
442
+ <div class="style-option ${b.sidebarStyle === s.value ? 'style-option--selected' : ''}"
443
+ @click="${() => { this.config.branding.sidebarStyle = s.value; this.requestUpdate(); }}">
444
+ <div class="style-preview" style="background: ${s.preview}"></div>
445
+ <div style="font-size:13px;font-weight:500;">${s.label}</div>
446
+ </div>
447
+ `)}
448
+ </div>
449
+ </div>
450
+
451
+ <div class="form-group">
452
+ <label class="form-label">URL del Logo (opcional)</label>
453
+ <input class="zs-input" .value="${b.logo ?? ''}" placeholder="https://..." @input="${(e) => { this.config.branding.logo = e.target.value; this.requestUpdate(); }}" />
454
+ </div>
326
455
  `;
327
456
  }
328
457
  // ─── Step 3: Navigation Editor ────────────────────
329
458
  renderNavStep() {
330
459
  if (!this.config)
331
460
  return nothing;
332
- return html `
333
- <div class="nav-list">
334
- ${this.config.navigation.map((item, i) => html `
335
- <div class="nav-item-row">
336
- <span class="nav-item-icon" @click="${() => { this.editingNavIndex = i; this.showIconPicker = !this.showIconPicker; }}">${item.icon ?? (item.kind === 'header' ? '📌' : item.kind === 'divider' ? '—' : '📄')}</span>
461
+ return html `
462
+ <div class="nav-list">
463
+ ${this.config.navigation.map((item, i) => html `
464
+ <div class="nav-item-row">
465
+ <span class="nav-item-icon" @click="${() => { this.editingNavIndex = i; this.showIconPicker = !this.showIconPicker; }}">${item.icon ?? (item.kind === 'header' ? '📌' : item.kind === 'divider' ? '—' : '📄')}</span>
337
466
  ${item.kind === 'divider'
338
467
  ? html `<span class="nav-item-title" style="color:var(--zs-text-muted);font-style:italic;">Separador</span>`
339
- : html `<input class="zs-input" style="height:32px;font-size:13px;" .value="${item.title ?? ''}" @input="${(e) => { this.config.navigation[i].title = e.target.value; this.requestUpdate(); }}" />`}
340
- <div class="nav-item-actions">
341
- <button class="nav-item-btn" title="Subir" @click="${() => this.moveNav(i, -1)}">↑</button>
342
- <button class="nav-item-btn" title="Bajar" @click="${() => this.moveNav(i, 1)}">↓</button>
343
- <button class="nav-item-btn nav-item-btn--danger" title="Eliminar" @click="${() => { this.config.navigation.splice(i, 1); this.requestUpdate(); }}">✕</button>
344
- </div>
345
- </div>
346
- `)}
347
- </div>
348
-
349
- ${this.showIconPicker ? html `
350
- <div style="margin-top:12px;padding:12px;border:1px solid var(--zs-border);border-radius:8px;">
351
- <div style="font-size:12px;color:var(--zs-text-secondary);margin-bottom:8px;">Selecciona un icono:</div>
352
- <div class="icon-grid">
353
- ${ICON_PALETTE.map(icon => html `
468
+ : html `<input class="zs-input" style="height:32px;font-size:13px;" .value="${item.title ?? ''}" @input="${(e) => { this.config.navigation[i].title = e.target.value; this.requestUpdate(); }}" />`}
469
+ <div class="nav-item-actions">
470
+ <button class="nav-item-btn" title="Subir" @click="${() => this.moveNav(i, -1)}">↑</button>
471
+ <button class="nav-item-btn" title="Bajar" @click="${() => this.moveNav(i, 1)}">↓</button>
472
+ <button class="nav-item-btn nav-item-btn--danger" title="Eliminar" @click="${() => { this.config.navigation.splice(i, 1); this.requestUpdate(); }}">✕</button>
473
+ </div>
474
+ </div>
475
+ `)}
476
+ </div>
477
+
478
+ ${this.showIconPicker ? html `
479
+ <div style="margin-top:12px;padding:12px;border:1px solid var(--zs-border);border-radius:8px;">
480
+ <div style="font-size:12px;color:var(--zs-text-secondary);margin-bottom:8px;">Selecciona un icono:</div>
481
+ <div class="icon-grid">
482
+ ${ICON_PALETTE.map(icon => html `
354
483
  <button class="icon-btn" @click="${() => {
355
484
  if (this.editingNavIndex >= 0) {
356
485
  this.config.navigation[this.editingNavIndex].icon = icon;
357
486
  this.showIconPicker = false;
358
487
  this.requestUpdate();
359
488
  }
360
- }}">${icon}</button>
361
- `)}
362
- </div>
363
- </div>
364
- ` : ''}
365
-
366
- <div style="display:flex;gap:8px;">
367
- <button class="add-btn" @click="${() => this.addNavItem('page')}">➕ Pagina</button>
368
- <button class="add-btn" @click="${() => this.addNavItem('header')}">📌 Seccion</button>
369
- <button class="add-btn" @click="${() => this.addNavItem('divider')}">— Separador</button>
370
- </div>
489
+ }}">${icon}</button>
490
+ `)}
491
+ </div>
492
+ </div>
493
+ ` : ''}
494
+
495
+ <div style="display:flex;gap:8px;">
496
+ <button class="add-btn" @click="${() => this.addNavItem('page')}">➕ Pagina</button>
497
+ <button class="add-btn" @click="${() => this.addNavItem('header')}">📌 Seccion</button>
498
+ <button class="add-btn" @click="${() => this.addNavItem('divider')}">— Separador</button>
499
+ </div>
371
500
  `;
372
501
  }
373
502
  addNavItem(kind) {
@@ -397,41 +526,149 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
397
526
  [items[index], items[newIndex]] = [items[newIndex], items[index]];
398
527
  this.requestUpdate();
399
528
  }
400
- // ─── Step 4: Pages ────────────────────────────────
529
+ // ─── Step 4: Data Sources ─────────────────────────
530
+ renderDataSourcesStep() {
531
+ if (!this.config)
532
+ return nothing;
533
+ const sources = this.ensureDataSources();
534
+ return html `
535
+ ${sources.length === 0 ? html `
536
+ <div style="text-align:center;padding:40px 20px;color:var(--zs-text-muted);">
537
+ <div style="font-size:40px;margin-bottom:12px;">🔗</div>
538
+ <div style="font-size:14px;">No hay fuentes de datos configuradas.</div>
539
+ <div style="font-size:13px;margin-top:4px;">Agrega APIs REST para conectar tus paginas con datos reales.</div>
540
+ </div>
541
+ ` : nothing}
542
+
543
+ ${sources.map((ds, i) => html `
544
+ <div class="ds-card">
545
+ <div class="ds-card-header">
546
+ <span class="ds-card-title">${ds.name || `Fuente ${i + 1}`}</span>
547
+ <div style="display:flex;gap:8px;align-items:center;">
548
+ <span class="ds-method-badge ds-method-${ds.method ?? 'GET'}">${ds.method ?? 'GET'}</span>
549
+ <button class="nav-item-btn nav-item-btn--danger" title="Eliminar"
550
+ @click="${() => { sources.splice(i, 1); this.requestUpdate(); }}">✕</button>
551
+ </div>
552
+ </div>
553
+
554
+ <div class="form-row" style="margin-bottom:12px;">
555
+ <div class="form-group" style="margin-bottom:0;">
556
+ <label class="form-label">ID</label>
557
+ <input class="zs-input" .value="${ds.id}" @input="${(e) => {
558
+ ds.id = e.target.value;
559
+ this.requestUpdate();
560
+ }}" placeholder="customers" />
561
+ </div>
562
+ <div class="form-group" style="margin-bottom:0;">
563
+ <label class="form-label">Nombre</label>
564
+ <input class="zs-input" .value="${ds.name}" @input="${(e) => {
565
+ ds.name = e.target.value;
566
+ this.requestUpdate();
567
+ }}" placeholder="Clientes" />
568
+ </div>
569
+ </div>
570
+
571
+ <div class="form-row" style="margin-bottom:12px;">
572
+ <div class="form-group" style="margin-bottom:0;grid-column:span 1;">
573
+ <label class="form-label">Metodo</label>
574
+ <select class="zs-input" .value="${ds.method ?? 'GET'}" @change="${(e) => {
575
+ ds.method = e.target.value;
576
+ this.requestUpdate();
577
+ }}">
578
+ ${HTTP_METHODS.map(m => html `<option value="${m}" ?selected="${ds.method === m}">${m}</option>`)}
579
+ </select>
580
+ </div>
581
+ <div class="form-group" style="margin-bottom:0;">
582
+ <label class="form-label">Intervalo de refresco (ms)</label>
583
+ <input class="zs-input" type="number" .value="${String(ds.refreshInterval ?? '')}" placeholder="0 = sin polling"
584
+ @input="${(e) => {
585
+ const v = parseInt(e.target.value);
586
+ ds.refreshInterval = isNaN(v) || v <= 0 ? undefined : v;
587
+ this.requestUpdate();
588
+ }}" />
589
+ </div>
590
+ </div>
591
+
592
+ <div class="form-group" style="margin-bottom:12px;">
593
+ <label class="form-label">URL</label>
594
+ <input class="zs-input" .value="${ds.url ?? ''}" @input="${(e) => {
595
+ ds.url = e.target.value;
596
+ this.requestUpdate();
597
+ }}" placeholder="https://api.ejemplo.com/v1/resource" />
598
+ </div>
599
+
600
+ <div class="form-group" style="margin-bottom:0;">
601
+ <label class="form-label">Headers (JSON, opcional)</label>
602
+ <textarea class="zs-input" rows="2" style="font-family:monospace;font-size:12px;resize:vertical;"
603
+ .value="${ds.headers ? JSON.stringify(ds.headers, null, 2) : ''}"
604
+ placeholder='{ "Authorization": "Bearer ..." }'
605
+ @change="${(e) => {
606
+ const raw = e.target.value.trim();
607
+ if (!raw) {
608
+ ds.headers = undefined;
609
+ this.requestUpdate();
610
+ return;
611
+ }
612
+ try {
613
+ ds.headers = JSON.parse(raw);
614
+ }
615
+ catch { /* ignore invalid JSON */ }
616
+ this.requestUpdate();
617
+ }}"
618
+ ></textarea>
619
+ </div>
620
+ </div>
621
+ `)}
622
+
623
+ <button class="add-btn" @click="${() => {
624
+ const id = `ds-${Date.now()}`;
625
+ this.ensureDataSources().push({
626
+ id,
627
+ name: '',
628
+ type: 'rest',
629
+ url: '',
630
+ method: 'GET',
631
+ autoFetch: true,
632
+ });
633
+ this.requestUpdate();
634
+ }}">🔗 Nueva Fuente de Datos</button>
635
+ `;
636
+ }
637
+ // ─── Step 5: Pages ────────────────────────────────
401
638
  renderPagesStep() {
402
639
  if (!this.config)
403
640
  return nothing;
404
- return html `
405
- <div class="nav-list">
406
- ${this.config.pages.map((page, i) => html `
407
- <div class="nav-item-row">
408
- <span class="nav-item-icon">${this.getContentIcon(page.content)}</span>
409
- <div style="flex:1;">
410
- <input class="zs-input" style="height:30px;font-size:13px;margin-bottom:4px;" .value="${page.title}" @input="${(e) => { this.config.pages[i].title = e.target.value; this.requestUpdate(); }}" />
411
- <div style="display:flex;gap:8px;align-items:center;">
412
- <span style="font-size:11px;color:var(--zs-text-muted);">/${page.segment}</span>
413
- <select style="font-size:11px;padding:2px 6px;border:1px solid var(--zs-border);border-radius:4px;" .value="${page.content}" @change="${(e) => { this.config.pages[i].content = e.target.value; this.requestUpdate(); }}">
414
- <option value="empty">Vacia</option>
415
- <option value="cards">Cards/Dashboard</option>
416
- <option value="datagrid">Grid de Datos</option>
417
- <option value="schema">Formulario</option>
418
- <option value="chart">Graficos</option>
419
- <option value="html">HTML</option>
420
- <option value="iframe">iFrame</option>
421
- <option value="tabs">Tabs</option>
422
- <option value="custom">Custom</option>
423
- </select>
424
- </div>
425
- </div>
426
- <button class="nav-item-btn nav-item-btn--danger" @click="${() => { this.config.pages.splice(i, 1); this.requestUpdate(); }}">✕</button>
427
- </div>
428
- `)}
429
- </div>
641
+ return html `
642
+ <div class="nav-list">
643
+ ${this.config.pages.map((page, i) => html `
644
+ <div class="nav-item-row">
645
+ <span class="nav-item-icon">${this.getContentIcon(page.content)}</span>
646
+ <div style="flex:1;">
647
+ <input class="zs-input" style="height:30px;font-size:13px;margin-bottom:4px;" .value="${page.title}" @input="${(e) => { this.config.pages[i].title = e.target.value; this.requestUpdate(); }}" />
648
+ <div style="display:flex;gap:8px;align-items:center;">
649
+ <span style="font-size:11px;color:var(--zs-text-muted);">/${page.segment}</span>
650
+ <select style="font-size:11px;padding:2px 6px;border:1px solid var(--zs-border);border-radius:4px;" .value="${page.content}" @change="${(e) => { this.config.pages[i].content = e.target.value; this.requestUpdate(); }}">
651
+ <option value="empty">Vacia</option>
652
+ <option value="cards">Cards/Dashboard</option>
653
+ <option value="datagrid">Grid de Datos</option>
654
+ <option value="schema">Formulario</option>
655
+ <option value="chart">Graficos</option>
656
+ <option value="html">HTML</option>
657
+ <option value="iframe">iFrame</option>
658
+ <option value="tabs">Tabs</option>
659
+ <option value="custom">Custom</option>
660
+ </select>
661
+ </div>
662
+ </div>
663
+ <button class="nav-item-btn nav-item-btn--danger" @click="${() => { this.config.pages.splice(i, 1); this.requestUpdate(); }}">✕</button>
664
+ </div>
665
+ `)}
666
+ </div>
430
667
  <button class="add-btn" @click="${() => {
431
668
  const id = `page-${Date.now()}`;
432
669
  this.config.pages.push({ id, segment: id, title: 'Nueva Pagina', content: 'empty' });
433
670
  this.requestUpdate();
434
- }}">➕ Nueva Pagina</button>
671
+ }}">➕ Nueva Pagina</button>
435
672
  `;
436
673
  }
437
674
  getContentIcon(type) {
@@ -442,26 +679,234 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
442
679
  };
443
680
  return icons[type] ?? '📄';
444
681
  }
445
- // ─── Step 5: Preview ──────────────────────────────
682
+ // ─── Step 6: Style & Theme ────────────────────────
683
+ renderThemeStep() {
684
+ if (!this.config)
685
+ return nothing;
686
+ const theme = this.ensureTheme();
687
+ return html `
688
+ <!-- Theme mode -->
689
+ <div class="form-group">
690
+ <label class="form-label">Modo de Tema</label>
691
+ <div class="style-grid">
692
+ ${THEME_MODES.map(m => html `
693
+ <div class="style-option ${theme.mode === m.value ? 'style-option--selected' : ''}"
694
+ @click="${() => { theme.mode = m.value; this.requestUpdate(); }}">
695
+ <div style="font-size:28px;margin-bottom:6px;">${m.icon}</div>
696
+ <div style="font-size:13px;font-weight:500;">${m.label}</div>
697
+ </div>
698
+ `)}
699
+ </div>
700
+ </div>
701
+
702
+ <!-- Primary color -->
703
+ <div class="form-group">
704
+ <label class="form-label">Color Primario (--zs-primary)</label>
705
+ <div class="color-grid">
706
+ ${COLOR_PALETTE.map(color => html `
707
+ <div class="color-swatch ${theme.primaryColor === color ? 'color-swatch--selected' : ''}"
708
+ style="background: ${color}"
709
+ @click="${() => { theme.primaryColor = color; this.requestUpdate(); }}"
710
+ ></div>
711
+ `)}
712
+ </div>
713
+ </div>
714
+
715
+ <!-- Font family -->
716
+ <div class="form-row">
717
+ <div class="form-group">
718
+ <label class="form-label">Tipografia (--zs-font)</label>
719
+ <select class="zs-input" .value="${theme.fontFamily ?? 'Inter, sans-serif'}" @change="${(e) => {
720
+ theme.fontFamily = e.target.value;
721
+ this.requestUpdate();
722
+ }}">
723
+ ${FONT_OPTIONS.map(f => html `<option value="${f}" ?selected="${theme.fontFamily === f}">${f.split(',')[0]}</option>`)}
724
+ </select>
725
+ </div>
726
+ <div class="form-group">
727
+ <label class="form-label">Border Radius (--zs-radius)</label>
728
+ <select class="zs-input" .value="${String(theme.borderRadius ?? 8)}" @change="${(e) => {
729
+ theme.borderRadius = parseInt(e.target.value);
730
+ this.requestUpdate();
731
+ }}">
732
+ ${RADIUS_OPTIONS.map(r => html `<option value="${r.value}" ?selected="${theme.borderRadius === r.value}">${r.label}</option>`)}
733
+ </select>
734
+ </div>
735
+ </div>
736
+
737
+ <!-- Accent color -->
738
+ <div class="form-group">
739
+ <label class="form-label">Color de Acento (--zs-accent, opcional)</label>
740
+ <div class="color-grid">
741
+ ${COLOR_PALETTE.map(color => html `
742
+ <div class="color-swatch ${theme.accentColor === color ? 'color-swatch--selected' : ''}"
743
+ style="background: ${color}"
744
+ @click="${() => { theme.accentColor = color; this.requestUpdate(); }}"
745
+ ></div>
746
+ `)}
747
+ </div>
748
+ </div>
749
+
750
+ <!-- Font size & spacing -->
751
+ <div class="form-row">
752
+ <div class="form-group">
753
+ <label class="form-label">Tamano de Fuente Base (px)</label>
754
+ <input class="zs-input" type="number" min="10" max="22" .value="${String(theme.fontSize ?? 14)}" @input="${(e) => {
755
+ theme.fontSize = parseInt(e.target.value) || 14;
756
+ this.requestUpdate();
757
+ }}" />
758
+ </div>
759
+ <div class="form-group">
760
+ <label class="form-label">Espaciado Base (px)</label>
761
+ <input class="zs-input" type="number" min="2" max="16" .value="${String(theme.spacing ?? 8)}" @input="${(e) => {
762
+ theme.spacing = parseInt(e.target.value) || 8;
763
+ this.requestUpdate();
764
+ }}" />
765
+ </div>
766
+ </div>
767
+
768
+ <!-- Mini preview -->
769
+ <div class="theme-preview-card" style="
770
+ background: ${theme.mode === 'dark' ? '#1e1e2d' : '#ffffff'};
771
+ color: ${theme.mode === 'dark' ? '#e0e0e0' : '#333333'};
772
+ font-family: ${theme.fontFamily ?? 'Inter, sans-serif'};
773
+ font-size: ${theme.fontSize ?? 14}px;
774
+ border-radius: ${theme.borderRadius ?? 8}px;
775
+ ">
776
+ <div class="theme-preview-header" style="color: ${theme.primaryColor ?? '#3498db'};">
777
+ Vista previa del tema
778
+ </div>
779
+ <div class="theme-preview-text">
780
+ Asi se vera tu aplicacion con estos ajustes de estilo.
781
+ </div>
782
+ <span class="theme-preview-btn" style="
783
+ background: ${theme.primaryColor ?? '#3498db'};
784
+ border-radius: ${theme.borderRadius ?? 8}px;
785
+ ">Boton Primario</span>
786
+ <input class="theme-preview-input" value="Campo de texto"
787
+ style="
788
+ border-color: ${theme.mode === 'dark' ? '#444' : '#ccc'};
789
+ border-radius: ${theme.borderRadius ?? 8}px;
790
+ background: ${theme.mode === 'dark' ? '#2a2a3d' : '#f9f9f9'};
791
+ color: ${theme.mode === 'dark' ? '#e0e0e0' : '#333'};
792
+ font-family: ${theme.fontFamily ?? 'Inter, sans-serif'};
793
+ "
794
+ readonly
795
+ />
796
+ </div>
797
+ `;
798
+ }
799
+ // ─── Step 7: Preview ──────────────────────────────
446
800
  renderPreviewStep() {
447
801
  if (!this.config)
448
802
  return nothing;
449
- return html `
450
- <div style="display:flex;gap:16px;margin-bottom:16px;">
451
- <div style="flex:1;">
452
- <div style="font-size:13px;color:var(--zs-text-secondary);margin-bottom:4px;">Resumen:</div>
453
- <div style="font-size:14px;"><strong>${this.config.branding.title}</strong> — ${this.config.pages.length} paginas, ${this.config.navigation.filter(n => n.kind !== 'divider' && n.kind !== 'header').length} items en menu</div>
454
- </div>
803
+ const navPageCount = this.config.navigation.filter(n => n.kind !== 'divider' && n.kind !== 'header').length;
804
+ const dsCount = this.config.dataSources?.length ?? 0;
805
+ return html `
806
+ <div style="display:flex;gap:16px;margin-bottom:16px;align-items:center;flex-wrap:wrap;">
807
+ <div style="flex:1;min-width:200px;">
808
+ <div style="font-size:13px;color:var(--zs-text-secondary);margin-bottom:4px;">Resumen:</div>
809
+ <div style="font-size:14px;">
810
+ <strong>${this.config.branding.title}</strong> —
811
+ ${this.config.pages.length} paginas,
812
+ ${navPageCount} items en menu${dsCount > 0 ? `, ${dsCount} fuentes de datos` : ''}
813
+ </div>
814
+ </div>
815
+ </div>
816
+
817
+ <!-- Export buttons -->
818
+ <div class="export-row" style="margin-bottom:16px;">
819
+ <button class="btn btn--secondary" @click="${this.exportJson}">📋 Exportar JSON</button>
820
+ <button class="btn btn--secondary" @click="${this.exportReactCode}">⚛️ Exportar Codigo React</button>
821
+ <button class="btn btn--secondary" @click="${this.exportNextCode}">▲ Exportar Codigo Next.js</button>
455
822
  <button class="btn btn--secondary" @click="${() => {
456
823
  const json = JSON.stringify(this.config, null, 2);
457
824
  navigator.clipboard.writeText(json).then(() => {
458
825
  alert('JSON copiado al portapapeles');
459
826
  });
460
- }}">📋 Copiar JSON</button>
461
- </div>
462
- <div class="preview-container">
463
- <zentto-studio-app .config="${this.config}"></zentto-studio-app>
464
- </div>
827
+ }}">📎 Copiar JSON</button>
828
+ </div>
829
+
830
+ <div class="preview-container">
831
+ <zentto-studio-app .config="${this.config}"></zentto-studio-app>
832
+ </div>
833
+ `;
834
+ }
835
+ // ─── Export Handlers ──────────────────────────────
836
+ exportJson() {
837
+ if (!this.config)
838
+ return;
839
+ const json = JSON.stringify(this.config, null, 2);
840
+ this.openCodeModal('Exportar JSON — AppConfig', json);
841
+ }
842
+ exportReactCode() {
843
+ if (!this.config)
844
+ return;
845
+ const code = generateAppPage(this.config, {
846
+ framework: 'react',
847
+ useClientDirective: false,
848
+ });
849
+ this.openCodeModal('Exportar Codigo React', code);
850
+ }
851
+ exportNextCode() {
852
+ if (!this.config)
853
+ return;
854
+ const code = generateAppPage(this.config, {
855
+ framework: 'nextjs',
856
+ useClientDirective: true,
857
+ });
858
+ this.openCodeModal('Exportar Codigo Next.js', code);
859
+ }
860
+ openCodeModal(title, content) {
861
+ this.codeModalTitle = title;
862
+ this.codeModalContent = content;
863
+ this.codeModalOpen = true;
864
+ }
865
+ closeCodeModal() {
866
+ this.codeModalOpen = false;
867
+ this.codeModalTitle = '';
868
+ this.codeModalContent = '';
869
+ }
870
+ copyCodeToClipboard() {
871
+ navigator.clipboard.writeText(this.codeModalContent).then(() => {
872
+ // Briefly change button text via a subtle approach
873
+ alert('Codigo copiado al portapapeles');
874
+ });
875
+ }
876
+ downloadCode() {
877
+ const isJson = this.codeModalTitle.includes('JSON');
878
+ const ext = isJson ? 'json' : 'tsx';
879
+ const mime = isJson ? 'application/json' : 'text/typescript';
880
+ const filename = isJson ? 'app-config.json' : 'StudioApp.tsx';
881
+ const blob = new Blob([this.codeModalContent], { type: mime });
882
+ const url = URL.createObjectURL(blob);
883
+ const a = document.createElement('a');
884
+ a.href = url;
885
+ a.download = filename;
886
+ a.click();
887
+ URL.revokeObjectURL(url);
888
+ }
889
+ // ─── Code Modal ───────────────────────────────────
890
+ renderCodeModal() {
891
+ return html `
892
+ <div class="code-modal-overlay" @click="${(e) => {
893
+ if (e.target === e.currentTarget)
894
+ this.closeCodeModal();
895
+ }}">
896
+ <div class="code-modal">
897
+ <div class="code-modal-header">
898
+ <h3>${this.codeModalTitle}</h3>
899
+ <button class="nav-item-btn" @click="${this.closeCodeModal}" style="font-size:18px;">✕</button>
900
+ </div>
901
+ <div class="code-modal-body">
902
+ <pre>${this.codeModalContent}</pre>
903
+ </div>
904
+ <div class="code-modal-footer">
905
+ <button class="btn btn--secondary" @click="${this.downloadCode}">💾 Descargar</button>
906
+ <button class="btn btn--primary" @click="${this.copyCodeToClipboard}">📋 Copiar</button>
907
+ </div>
908
+ </div>
909
+ </div>
465
910
  `;
466
911
  }
467
912
  };
@@ -486,6 +931,15 @@ __decorate([
486
931
  __decorate([
487
932
  state()
488
933
  ], ZsAppWizard.prototype, "iconPickerTarget", void 0);
934
+ __decorate([
935
+ state()
936
+ ], ZsAppWizard.prototype, "codeModalOpen", void 0);
937
+ __decorate([
938
+ state()
939
+ ], ZsAppWizard.prototype, "codeModalTitle", void 0);
940
+ __decorate([
941
+ state()
942
+ ], ZsAppWizard.prototype, "codeModalContent", void 0);
489
943
  ZsAppWizard = __decorate([
490
944
  customElement('zs-app-wizard')
491
945
  ], ZsAppWizard);