@zentto/studio 0.5.1 → 0.6.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.
@@ -10,20 +10,35 @@ export declare class ZsAppWizard extends LitElement {
10
10
  private editingNavIndex;
11
11
  private showIconPicker;
12
12
  private iconPickerTarget;
13
+ private codeModalOpen;
14
+ private codeModalTitle;
15
+ private codeModalContent;
13
16
  connectedCallback(): void;
14
17
  private canNext;
15
18
  private next;
16
19
  private prev;
17
20
  private finish;
21
+ private ensureTheme;
22
+ private ensureDataSources;
18
23
  render(): import("lit-html").TemplateResult<1>;
19
24
  private renderTemplateStep;
20
25
  private renderBrandingStep;
21
26
  private renderNavStep;
22
27
  private addNavItem;
23
28
  private moveNav;
29
+ private renderDataSourcesStep;
24
30
  private renderPagesStep;
25
31
  private getContentIcon;
32
+ private renderThemeStep;
26
33
  private renderPreviewStep;
34
+ private exportJson;
35
+ private exportReactCode;
36
+ private exportNextCode;
37
+ private openCodeModal;
38
+ private closeCodeModal;
39
+ private copyCodeToClipboard;
40
+ private downloadCode;
41
+ private renderCodeModal;
27
42
  }
28
43
  declare global {
29
44
  interface HTMLElementTagNameMap {
@@ -1 +1 @@
1
- {"version":3,"file":"zs-app-wizard.d.ts","sourceRoot":"","sources":["../../src/designer/zs-app-wizard.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAGrD,OAAO,KAAK,EAAE,SAAS,EAAiD,MAAM,qBAAqB,CAAC;AAKpG,OAAO,yBAAyB,CAAC;AAqCjC,qBACa,WAAY,SAAQ,UAAU;IACzC,MAAM,CAAC,MAAM,4BAiJV;IAEyB,aAAa,EAAE,SAAS,GAAG,IAAI,CAAQ;IAE1D,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,eAAe,CAAM;IAC7B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,gBAAgB,CAA6B;IAE9D,iBAAiB;IAUjB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,MAAM;IAUd,MAAM;IAqCN,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,kBAAkB;IAkD1B,OAAO,CAAC,aAAa;IA8CrB,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,eAAe;IAqCvB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,iBAAiB;CAqB1B;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,eAAe,EAAE,WAAW,CAAC;KAC9B;CACF"}
1
+ {"version":3,"file":"zs-app-wizard.d.ts","sourceRoot":"","sources":["../../src/designer/zs-app-wizard.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAGrD,OAAO,KAAK,EAAE,SAAS,EAAiD,MAAM,qBAAqB,CAAC;AAOpG,OAAO,yBAAyB,CAAC;AAkEjC,qBACa,WAAY,SAAQ,UAAU;IACzC,MAAM,CAAC,MAAM,4BAmOV;IAEyB,aAAa,EAAE,SAAS,GAAG,IAAI,CAAQ;IAE1D,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,eAAe,CAAM;IAC7B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,gBAAgB,CAA6B;IAGrD,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,cAAc,CAAM;IAC5B,OAAO,CAAC,gBAAgB,CAAM;IAEvC,iBAAiB;IAUjB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,iBAAiB;IASzB,MAAM;IAyCN,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,kBAAkB;IAkD1B,OAAO,CAAC,aAAa;IA8CrB,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,qBAAqB;IAuG7B,OAAO,CAAC,eAAe;IAqCvB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,eAAe;IAuHvB,OAAO,CAAC,iBAAiB;IAyCzB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,YAAY;IAiBpB,OAAO,CAAC,eAAe;CAqBxB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,eAAe,EAAE,WAAW,CAAC;KAC9B;CACF"}
@@ -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,6 +73,10 @@ 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
81
  static { this.styles = [studioTokens, fieldBaseStyles, css `
52
82
  :host { display: block; font-family: var(--zs-font-family); }
@@ -62,13 +92,14 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
62
92
  .wizard-steps {
63
93
  display: flex; background: var(--zs-bg-secondary);
64
94
  border-bottom: 1px solid var(--zs-border); padding: 0;
95
+ overflow-x: auto;
65
96
  }
66
97
  .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;
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;
70
101
  border-bottom: 3px solid transparent;
71
- transition: all 150ms;
102
+ transition: all 150ms; min-width: 0; white-space: nowrap;
72
103
  }
73
104
  .wizard-step:hover { color: var(--zs-text-secondary); }
74
105
  .wizard-step--active {
@@ -76,8 +107,8 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
76
107
  font-weight: 500;
77
108
  }
78
109
  .wizard-step--done { color: var(--zs-success); }
79
- .wizard-step-icon { font-size: 18px; }
80
- .wizard-step-title { white-space: nowrap; }
110
+ .wizard-step-icon { font-size: 16px; }
111
+ .wizard-step-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
81
112
 
82
113
  /* Body */
83
114
  .wizard-body { padding: 32px; min-height: 400px; }
@@ -109,6 +140,7 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
109
140
  .form-label { font-size: 13px; font-weight: 500; color: var(--zs-text); margin-bottom: 6px; display: block; }
110
141
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
111
142
  .form-row-3 { grid-template-columns: 1fr 1fr 1fr; }
143
+ .form-row-4 { grid-template-columns: 1fr 1fr 1fr 1fr; }
112
144
 
113
145
  /* Color picker grid */
114
146
  .color-grid { display: flex; flex-wrap: wrap; gap: 8px; }
@@ -193,6 +225,86 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
193
225
  .btn--success { background: var(--zs-success); color: white; }
194
226
  .btn--success:hover { opacity: 0.9; }
195
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,6 +338,19 @@ 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
356
  return html `
@@ -248,8 +373,10 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
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()}
376
+ this.currentStep === 3 ? this.renderDataSourcesStep() :
377
+ this.currentStep === 4 ? this.renderPagesStep() :
378
+ this.currentStep === 5 ? this.renderThemeStep() :
379
+ this.renderPreviewStep()}
253
380
  </div>
254
381
 
255
382
  <div class="wizard-footer">
@@ -259,6 +386,8 @@ let ZsAppWizard = class ZsAppWizard extends LitElement {
259
386
  : html `<button class="btn btn--success" @click="${this.finish}">Crear Aplicacion</button>`}
260
387
  </div>
261
388
  </div>
389
+
390
+ ${this.codeModalOpen ? this.renderCodeModal() : nothing}
262
391
  `;
263
392
  }
264
393
  // ─── Step 1: Template Selection ──────────────────
@@ -397,7 +526,115 @@ 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;
@@ -442,28 +679,236 @@ 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;
803
+ const navPageCount = this.config.navigation.filter(n => n.kind !== 'divider' && n.kind !== 'header').length;
804
+ const dsCount = this.config.dataSources?.length ?? 0;
449
805
  return html `
450
- <div style="display:flex;gap:16px;margin-bottom:16px;">
451
- <div style="flex:1;">
806
+ <div style="display:flex;gap:16px;margin-bottom:16px;align-items:center;flex-wrap:wrap;">
807
+ <div style="flex:1;min-width:200px;">
452
808
  <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>
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>
454
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>
827
+ }}">📎 Copiar JSON</button>
461
828
  </div>
829
+
462
830
  <div class="preview-container">
463
831
  <zentto-studio-app .config="${this.config}"></zentto-studio-app>
464
832
  </div>
465
833
  `;
466
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>
910
+ `;
911
+ }
467
912
  };
468
913
  __decorate([
469
914
  property({ type: Object })
@@ -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);