@zentto/studio 0.8.4 → 0.9.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.
@@ -66,6 +66,10 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
66
66
  this.provider = {};
67
67
  this.autoSaveMs = 1000;
68
68
  this.gridSnap = 1;
69
+ /** API base URL for save/load (e.g. "http://localhost:4000") */
70
+ this.saveApiUrl = '';
71
+ /** Auth token for save/load API */
72
+ this.saveApiToken = '';
69
73
  // ─── State ────────────────────────────────────────
70
74
  this.selectedFieldId = null;
71
75
  this.viewMode = 'design';
@@ -74,6 +78,17 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
74
78
  this.editingTitle = false;
75
79
  this.collapsedSections = new Set();
76
80
  this.zoom = 1;
81
+ // Save/Load state
82
+ this.saveDialogOpen = false;
83
+ this.loadDialogOpen = false;
84
+ this.serverAddons = [];
85
+ this.serverLoading = false;
86
+ this.serverSaving = false;
87
+ this.editingAddonId = null;
88
+ this.saveName = '';
89
+ this.saveDesc = '';
90
+ this.saveIconEmoji = '📋';
91
+ this.saveModules = ['global'];
77
92
  // Undo/Redo
78
93
  this.undoStack = [];
79
94
  this.redoStack = [];
@@ -88,6 +103,15 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
88
103
  this.dragSourceSectionIndex = -1;
89
104
  this.dragInsertIndex = -1;
90
105
  this.dragTargetSectionIndex = -1;
106
+ // ─── Dialog Styles (inline, no shadow DOM issues) ─
107
+ this.dialogOverlay = 'position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;';
108
+ this.dialogBox = 'background:white;border-radius:12px;width:480px;max-width:90vw;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.3);';
109
+ this.dialogHeader = 'padding:16px 20px;border-bottom:1px solid #eee;font-size:16px;font-weight:600;color:#333;display:flex;justify-content:space-between;align-items:center;';
110
+ this.dialogBody = 'padding:16px 20px;overflow-y:auto;flex:1;';
111
+ this.dialogFooter = 'padding:12px 20px;border-top:1px solid #eee;display:flex;gap:8px;justify-content:flex-end;';
112
+ this.inputStyle = 'width:100%;padding:8px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px;font-family:inherit;box-sizing:border-box;';
113
+ this.btnPrimary = 'padding:8px 20px;border:none;border-radius:6px;background:#1976d2;color:white;cursor:pointer;font-size:13px;font-weight:600;font-family:inherit;';
114
+ this.btnSecondary = 'padding:8px 20px;border:1px solid #ddd;border-radius:6px;background:white;cursor:pointer;font-size:13px;font-family:inherit;';
91
115
  // ─── API Panel (Data Sources) ──────────────────────
92
116
  this.showTemplateMenu = false;
93
117
  this.apiSources = [];
@@ -1147,6 +1171,182 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1147
1171
  <div class="panel-resize"></div>
1148
1172
  ${this.renderRightPanel()}
1149
1173
  </div>
1174
+ ${this.saveDialogOpen ? this.renderSaveDialog() : nothing}
1175
+ ${this.loadDialogOpen ? this.renderLoadDialog() : nothing}
1176
+ `;
1177
+ }
1178
+ // ─── Save/Load Server Methods ─────────────────────
1179
+ getApiHeaders() {
1180
+ const h = { 'Content-Type': 'application/json' };
1181
+ if (this.saveApiToken)
1182
+ h['Authorization'] = `Bearer ${this.saveApiToken}`;
1183
+ else if (this.apiToken)
1184
+ h['Authorization'] = `Bearer ${this.apiToken}`;
1185
+ return h;
1186
+ }
1187
+ getApiBase() {
1188
+ return (this.saveApiUrl || this.apiBaseUrl || '').replace(/\/+$/, '');
1189
+ }
1190
+ openSaveDialog() {
1191
+ if (!this.schema)
1192
+ return;
1193
+ this.saveName = this.saveName || this.schema.title || '';
1194
+ this.saveDialogOpen = true;
1195
+ }
1196
+ async openLoadDialog() {
1197
+ this.loadDialogOpen = true;
1198
+ this.serverLoading = true;
1199
+ try {
1200
+ const base = this.getApiBase();
1201
+ const res = await fetch(`${base}/v1/studio/addons`, { headers: this.getApiHeaders() });
1202
+ const json = await res.json();
1203
+ this.serverAddons = (json.data ?? []).map((r) => ({
1204
+ id: r.AddonId ?? r.id, title: r.Title ?? r.title, icon: r.Icon ?? r.icon ?? '📋',
1205
+ modules: r.modules ?? [], createdAt: r.CreatedAt ?? r.createdAt ?? '',
1206
+ config: r.config ?? (r.Config ? JSON.parse(r.Config) : {}),
1207
+ }));
1208
+ }
1209
+ catch {
1210
+ this.serverAddons = [];
1211
+ }
1212
+ this.serverLoading = false;
1213
+ }
1214
+ async saveToServer() {
1215
+ if (!this.schema || !this.saveName.trim())
1216
+ return;
1217
+ this.serverSaving = true;
1218
+ const base = this.getApiBase();
1219
+ const body = JSON.stringify({
1220
+ title: this.saveName.trim(), description: this.saveDesc.trim(),
1221
+ icon: this.saveIconEmoji || '📋', modules: this.saveModules,
1222
+ config: { type: 'schema', schema: this.schema },
1223
+ });
1224
+ try {
1225
+ if (this.editingAddonId) {
1226
+ await fetch(`${base}/v1/studio/addons/${this.editingAddonId}`, { method: 'PUT', headers: this.getApiHeaders(), body });
1227
+ }
1228
+ else {
1229
+ const res = await fetch(`${base}/v1/studio/addons`, { method: 'POST', headers: this.getApiHeaders(), body });
1230
+ const json = await res.json();
1231
+ if (json.data?.addonId)
1232
+ this.editingAddonId = json.data.addonId;
1233
+ }
1234
+ this.saveDialogOpen = false;
1235
+ }
1236
+ catch (err) {
1237
+ console.error('[designer] save error:', err);
1238
+ }
1239
+ this.serverSaving = false;
1240
+ }
1241
+ loadFromServer(addon) {
1242
+ if (!this.schema)
1243
+ return;
1244
+ const config = addon.config;
1245
+ const s = config?.schema ?? config;
1246
+ this.schema = s;
1247
+ this.undoStack = [JSON.stringify(s)];
1248
+ this.redoStack = [];
1249
+ this.selectedFieldId = null;
1250
+ this.editingAddonId = addon.id;
1251
+ this.saveName = addon.title;
1252
+ this.loadDialogOpen = false;
1253
+ this.emitChange();
1254
+ }
1255
+ async deleteFromServer(id) {
1256
+ const base = this.getApiBase();
1257
+ try {
1258
+ await fetch(`${base}/v1/studio/addons/${id}`, { method: 'DELETE', headers: this.getApiHeaders() });
1259
+ }
1260
+ catch { }
1261
+ this.serverAddons = this.serverAddons.filter(a => a.id !== id);
1262
+ }
1263
+ renderSaveDialog() {
1264
+ const MODS = [
1265
+ { v: 'global', l: 'Global' }, { v: 'compras', l: 'Compras' }, { v: 'ventas', l: 'Ventas' },
1266
+ { v: 'inventario', l: 'Inventario' }, { v: 'contabilidad', l: 'Contabilidad' },
1267
+ { v: 'nomina', l: 'Nómina' }, { v: 'bancos', l: 'Bancos' }, { v: 'crm', l: 'CRM' },
1268
+ ];
1269
+ return html `
1270
+ <div style="${this.dialogOverlay}" @click="${(e) => { if (e.target === e.currentTarget)
1271
+ this.saveDialogOpen = false; }}">
1272
+ <div style="${this.dialogBox}" @click="${(e) => e.stopPropagation()}">
1273
+ <div style="${this.dialogHeader}">
1274
+ <span>${this.editingAddonId ? '💾 Actualizar' : '💾 Guardar'} en servidor</span>
1275
+ <button style="border:none;background:none;font-size:18px;cursor:pointer;color:#999;" @click="${() => { this.saveDialogOpen = false; }}">✕</button>
1276
+ </div>
1277
+ <div style="${this.dialogBody}">
1278
+ <div style="margin-bottom:12px;">
1279
+ <label style="font-size:12px;font-weight:600;color:#555;display:block;margin-bottom:4px;">Nombre *</label>
1280
+ <input style="${this.inputStyle}" .value="${this.saveName}" @input="${(e) => { this.saveName = e.target.value; }}" placeholder="Mi formulario" />
1281
+ </div>
1282
+ <div style="margin-bottom:12px;">
1283
+ <label style="font-size:12px;font-weight:600;color:#555;display:block;margin-bottom:4px;">Descripcion</label>
1284
+ <textarea style="${this.inputStyle}resize:vertical;min-height:50px;" .value="${this.saveDesc}" @input="${(e) => { this.saveDesc = e.target.value; }}"></textarea>
1285
+ </div>
1286
+ <div style="display:flex;gap:12px;margin-bottom:12px;">
1287
+ <div>
1288
+ <label style="font-size:12px;font-weight:600;color:#555;display:block;margin-bottom:4px;">Icono</label>
1289
+ <input style="${this.inputStyle}width:60px;font-size:20px;text-align:center;" .value="${this.saveIconEmoji}" maxlength="4" @input="${(e) => { this.saveIconEmoji = e.target.value; }}" />
1290
+ </div>
1291
+ </div>
1292
+ <div>
1293
+ <label style="font-size:12px;font-weight:600;color:#555;display:block;margin-bottom:6px;">Modulos donde aparece</label>
1294
+ <div style="display:flex;flex-wrap:wrap;gap:6px;">
1295
+ ${MODS.map(m => {
1296
+ const active = this.saveModules.includes(m.v);
1297
+ return html `<button style="padding:4px 10px;border:1px solid ${active ? '#1976d2' : '#ddd'};border-radius:14px;background:${active ? '#e3f2fd' : 'white'};color:${active ? '#1976d2' : '#666'};cursor:pointer;font-size:11px;font-family:inherit;font-weight:${active ? '600' : '400'};"
1298
+ @click="${() => { this.saveModules = active ? this.saveModules.filter(x => x !== m.v) : [...this.saveModules, m.v]; this.requestUpdate(); }}"
1299
+ >${m.l}</button>`;
1300
+ })}
1301
+ </div>
1302
+ </div>
1303
+ </div>
1304
+ <div style="${this.dialogFooter}">
1305
+ <button style="${this.btnSecondary}" @click="${() => { this.saveDialogOpen = false; }}">Cancelar</button>
1306
+ <button style="${this.btnPrimary}${!this.saveName.trim() || this.serverSaving ? 'opacity:0.5;pointer-events:none;' : ''}" @click="${this.saveToServer}">
1307
+ ${this.serverSaving ? '⏳ Guardando...' : this.editingAddonId ? '💾 Actualizar' : '💾 Guardar'}
1308
+ </button>
1309
+ </div>
1310
+ </div>
1311
+ </div>
1312
+ `;
1313
+ }
1314
+ renderLoadDialog() {
1315
+ return html `
1316
+ <div style="${this.dialogOverlay}" @click="${(e) => { if (e.target === e.currentTarget)
1317
+ this.loadDialogOpen = false; }}">
1318
+ <div style="${this.dialogBox}max-height:70vh;" @click="${(e) => e.stopPropagation()}">
1319
+ <div style="${this.dialogHeader}">
1320
+ <span>📂 Cargar desde servidor</span>
1321
+ <button style="border:none;background:none;font-size:18px;cursor:pointer;color:#999;" @click="${() => { this.loadDialogOpen = false; }}">✕</button>
1322
+ </div>
1323
+ <div style="${this.dialogBody}">
1324
+ ${this.serverLoading
1325
+ ? html `<div style="text-align:center;padding:30px;color:#999;">⏳ Cargando...</div>`
1326
+ : this.serverAddons.length === 0
1327
+ ? html `<div style="text-align:center;padding:30px;color:#999;">No hay schemas guardados en el servidor.</div>`
1328
+ : this.serverAddons.map(a => html `
1329
+ <div style="display:flex;align-items:center;gap:10px;padding:10px 8px;border-bottom:1px solid #f0f0f0;cursor:pointer;border-radius:6px;transition:background 0.1s;"
1330
+ @mouseover="${(e) => { e.currentTarget.style.background = '#f5f5f5'; }}"
1331
+ @mouseout="${(e) => { e.currentTarget.style.background = ''; }}"
1332
+ @click="${() => this.loadFromServer(a)}"
1333
+ >
1334
+ <span style="font-size:22px;">${a.icon}</span>
1335
+ <div style="flex:1;min-width:0;">
1336
+ <div style="font-size:13px;font-weight:600;color:#333;">${a.title}</div>
1337
+ <div style="font-size:10px;color:#999;">${a.modules.join(', ')} — ${a.createdAt ? new Date(a.createdAt).toLocaleDateString() : ''}</div>
1338
+ </div>
1339
+ <button style="border:none;background:none;cursor:pointer;color:#d32f2f;font-size:14px;padding:4px;" title="Eliminar"
1340
+ @click="${(e) => { e.stopPropagation(); this.deleteFromServer(a.id); }}"
1341
+ >🗑</button>
1342
+ </div>
1343
+ `)}
1344
+ </div>
1345
+ <div style="${this.dialogFooter}">
1346
+ <button style="${this.btnSecondary}" @click="${() => { this.loadDialogOpen = false; }}">Cerrar</button>
1347
+ </div>
1348
+ </div>
1349
+ </div>
1150
1350
  `;
1151
1351
  }
1152
1352
  // ─── Toolbar ──────────────────────────────────────
@@ -1197,6 +1397,8 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1197
1397
 
1198
1398
  <span class="tb-spacer"></span>
1199
1399
 
1400
+ <button class="tb-btn" style="color:#2e7d32;font-weight:600;" @click="${this.openSaveDialog}" title="Guardar en servidor">💾 Guardar</button>
1401
+ <button class="tb-btn" @click="${this.openLoadDialog}" title="Cargar desde servidor">📂 Cargar</button>
1200
1402
  <button class="tb-btn" @click="${() => {
1201
1403
  if (this.schema) {
1202
1404
  navigator.clipboard.writeText(JSON.stringify(this.schema, null, 2));
@@ -1385,7 +1587,7 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1385
1587
  if (field.type === 'heading')
1386
1588
  return field.label ?? 'Titulo';
1387
1589
  if (field.type === 'datagrid')
1388
- return this.renderLiveDataGrid(field);
1590
+ return '◫ ZenttoDataGrid';
1389
1591
  if (field.type === 'report')
1390
1592
  return '◫ ZenttoReportViewer';
1391
1593
  if (field.type === 'chart')
@@ -1406,34 +1608,6 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1406
1608
  return '▸ Nodo 1\n ▸ Nodo 2';
1407
1609
  return field.placeholder ?? field.type;
1408
1610
  }
1409
- /** Render live datagrid inside canvas — props update in real time */
1410
- renderLiveDataGrid(field) {
1411
- const p = field.props ?? {};
1412
- const rawEp = p.endpoint || '';
1413
- // Build full URL: if relative path and we have apiBaseUrl, prepend it
1414
- const ep = rawEp && !rawEp.startsWith('http') && this.apiBaseUrl
1415
- ? `${this.apiBaseUrl.replace(/\/+$/, '')}${rawEp.startsWith('/') ? '' : '/'}${rawEp}`
1416
- : rawEp;
1417
- const headers = {};
1418
- if (this.apiToken) {
1419
- headers['Authorization'] = `Bearer ${this.apiToken}`;
1420
- if (this.apiCompany)
1421
- headers['x-empresa'] = this.apiCompany;
1422
- if (this.apiBranch)
1423
- headers['x-sucursal'] = this.apiBranch;
1424
- }
1425
- return html `
1426
- <zs-field-datagrid
1427
- .config="${field}"
1428
- .endpoint="${ep}"
1429
- .authToken="${this.apiToken}"
1430
- .authHeaders="${headers}"
1431
- .designMode="${true}"
1432
- .theme="${'light'}"
1433
- style="display:block;width:100%;min-height:200px;"
1434
- ></zs-field-datagrid>
1435
- `;
1436
- }
1437
1611
  // ─── Right Panel (Properties — Figma-quality) ──────
1438
1612
  renderRightPanel() {
1439
1613
  const field = this.selectedField;
@@ -1567,345 +1741,9 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1567
1741
  ` : ''}
1568
1742
  ` : ''}
1569
1743
  </div>
1570
-
1571
- <!-- Type-specific props -->
1572
- ${this.renderTypeSpecificProps(field)}
1573
- </div>
1574
- `;
1575
- }
1576
- // ─── Type-Specific Property Panels ────────────────
1577
- renderTypeSpecificProps(field) {
1578
- switch (field.type) {
1579
- case 'datagrid': return this.renderDataGridProps(field);
1580
- case 'chart': return this.renderChartProps(field);
1581
- case 'report': return this.renderReportProps(field);
1582
- case 'select':
1583
- case 'multiselect':
1584
- case 'radio': return this.renderSelectProps(field);
1585
- case 'number':
1586
- case 'currency':
1587
- case 'percentage':
1588
- case 'slider':
1589
- case 'rating': return this.renderNumberProps(field);
1590
- case 'lookup': return this.renderLookupProps(field);
1591
- case 'chips':
1592
- case 'tags': return this.renderChipsProps(field);
1593
- case 'treeview': return this.renderTreeViewProps(field);
1594
- case 'date':
1595
- case 'time':
1596
- case 'datetime': return this.renderDateProps(field);
1597
- case 'file':
1598
- case 'image': return this.renderFileProps(field);
1599
- case 'signature': return this.renderSignatureProps(field);
1600
- default: return nothing;
1601
- }
1602
- }
1603
- setProp(field, key, value) {
1604
- if (!field.props)
1605
- field.props = {};
1606
- field.props[key] = value;
1607
- this.commitChange();
1608
- }
1609
- // ─── DataGrid Props ───────────────────────────────
1610
- renderDataGridProps(field) {
1611
- const p = field.props ?? {};
1612
- return html `
1613
- <div class="prop-section">
1614
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('grid-config')}">
1615
- <span class="collapse-icon ${this.collapsedSections.has('grid-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1616
- <h4>DataGrid Config</h4>
1617
- </div>
1618
- ${!this.collapsedSections.has('grid-config') ? html `
1619
- <div class="prop-info">◫ Conecta un endpoint y el grid cargara los datos en el canvas</div>
1620
- <div class="prop-row"><span class="prop-label">Altura</span><input class="prop-input" .value="${p.height ?? '400px'}" placeholder="400px" @change="${(e) => this.setProp(field, 'height', e.target.value)}" /></div>
1621
- <div class="prop-row"><span class="prop-label">Page Size</span><input class="prop-input" type="number" min="5" .value="${String(p.pageSize ?? 25)}" @change="${(e) => this.setProp(field, 'pageSize', parseInt(e.target.value) || 25)}" /></div>
1622
- <div class="prop-row"><span class="prop-label">Densidad</span>
1623
- <select class="prop-input" .value="${p.density ?? 'compact'}" @change="${(e) => this.setProp(field, 'density', e.target.value)}">
1624
- <option value="compact">Compacta</option>
1625
- <option value="standard">Estandar</option>
1626
- <option value="comfortable">Comoda</option>
1627
- </select>
1628
- </div>
1629
- <div class="prop-row"><span class="prop-label">Row Click</span>
1630
- <select class="prop-input" .value="${p.onRowClick ?? 'emit'}" @change="${(e) => this.setProp(field, 'onRowClick', e.target.value)}">
1631
- <option value="emit">Emitir evento</option>
1632
- <option value="navigate">Navegar</option>
1633
- <option value="select">Seleccionar</option>
1634
- <option value="detail">Detalle</option>
1635
- </select>
1636
- </div>
1637
- ${p.onRowClick === 'navigate' ? html `
1638
- <div class="prop-row"><span class="prop-label">Nav Segment</span><input class="prop-input" .value="${p.rowClickSegment ?? ''}" placeholder="/clientes/{id}" @change="${(e) => this.setProp(field, 'rowClickSegment', e.target.value)}" /></div>
1639
- ` : ''}
1640
- <div class="prop-divider"></div>
1641
- ${this.renderToggle('Toolbar', p.enableToolbar ?? true, (v) => this.setProp(field, 'enableToolbar', v))}
1642
- ${this.renderToggle('Busqueda', p.enableSearch ?? true, (v) => this.setProp(field, 'enableSearch', v))}
1643
- ${this.renderToggle('Exportar', p.enableExport ?? false, (v) => this.setProp(field, 'enableExport', v))}
1644
- ${this.renderToggle('Paginacion', p.enablePagination ?? true, (v) => this.setProp(field, 'enablePagination', v))}
1645
- ${this.renderToggle('Filtros Header', p.enableHeaderFilters ?? false, (v) => this.setProp(field, 'enableHeaderFilters', v))}
1646
- ${this.renderToggle('Clipboard', p.enableClipboard ?? false, (v) => this.setProp(field, 'enableClipboard', v))}
1647
- ${this.renderToggle('Seleccion Filas', p.enableRowSelection ?? false, (v) => this.setProp(field, 'enableRowSelection', v))}
1648
- ${this.renderToggle('Master-Detail', p.enableMasterDetail ?? false, (v) => this.setProp(field, 'enableMasterDetail', v))}
1649
- ${this.renderToggle('Totales', p.showTotals ?? false, (v) => this.setProp(field, 'showTotals', v))}
1650
- ${this.renderToggle('Context Menu', p.enableContextMenu ?? false, (v) => this.setProp(field, 'enableContextMenu', v))}
1651
- ${this.renderToggle('Find (Ctrl+F)', p.enableFind ?? false, (v) => this.setProp(field, 'enableFind', v))}
1652
- ${this.renderToggle('Status Bar', p.enableStatusBar ?? false, (v) => this.setProp(field, 'enableStatusBar', v))}
1653
- ${this.renderToggle('Filter Panel', p.enableFilterPanel ?? false, (v) => this.setProp(field, 'enableFilterPanel', v))}
1654
- ${this.renderToggle('Configurador', p.enableConfigurator ?? false, (v) => this.setProp(field, 'enableConfigurator', v))}
1655
- <div class="prop-divider"></div>
1656
- <div class="prop-row"><span class="prop-label">Moneda</span><input class="prop-input" .value="${p.defaultCurrency ?? ''}" placeholder="USD, VES, EUR..." @change="${(e) => this.setProp(field, 'defaultCurrency', e.target.value)}" /></div>
1657
- <div class="prop-row"><span class="prop-label">Archivo Export</span><input class="prop-input" .value="${p.exportFilename ?? ''}" placeholder="clientes" @change="${(e) => this.setProp(field, 'exportFilename', e.target.value)}" /></div>
1658
- <div class="prop-row"><span class="prop-label">Grid ID</span><input class="prop-input" style="font-family:'SF Mono','Consolas',monospace;font-size:10px;" .value="${p.gridId ?? ''}" placeholder="clientes-grid" @change="${(e) => this.setProp(field, 'gridId', e.target.value)}" /></div>
1659
- ` : ''}
1660
- </div>
1661
- `;
1662
- }
1663
- // ─── Chart Props ──────────────────────────────────
1664
- renderChartProps(field) {
1665
- const p = field.props ?? {};
1666
- return html `
1667
- <div class="prop-section">
1668
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('chart-config')}">
1669
- <span class="collapse-icon ${this.collapsedSections.has('chart-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1670
- <h4>Chart Config</h4>
1671
- </div>
1672
- ${!this.collapsedSections.has('chart-config') ? html `
1673
- <div class="prop-info">📊 Configuracion del grafico SVG</div>
1674
- <div class="prop-row"><span class="prop-label">Tipo</span>
1675
- <select class="prop-input" .value="${p.chartType ?? 'bar'}" @change="${(e) => this.setProp(field, 'chartType', e.target.value)}">
1676
- <option value="bar">Barras</option>
1677
- <option value="line">Lineas</option>
1678
- <option value="pie">Torta</option>
1679
- <option value="donut">Donut</option>
1680
- <option value="area">Area</option>
1681
- </select>
1682
- </div>
1683
- <div class="prop-row"><span class="prop-label">Titulo</span><input class="prop-input" .value="${p.chartTitle ?? ''}" placeholder="Ventas por Mes" @change="${(e) => this.setProp(field, 'chartTitle', e.target.value)}" /></div>
1684
- <div class="prop-row"><span class="prop-label">Label Field</span><input class="prop-input" .value="${p.labelField ?? ''}" placeholder="mes" @change="${(e) => this.setProp(field, 'labelField', e.target.value)}" /></div>
1685
- <div class="prop-row"><span class="prop-label">Value Field</span><input class="prop-input" .value="${p.valueField ?? ''}" placeholder="total" @change="${(e) => this.setProp(field, 'valueField', e.target.value)}" /></div>
1686
- <div class="prop-divider"></div>
1687
- <div class="prop-pos-grid">
1688
- <div class="prop-pos-cell">
1689
- <span class="prop-pos-label prop-pos-label--w">W</span>
1690
- <input type="number" min="100" .value="${String(p.width ?? 400)}" @change="${(e) => this.setProp(field, 'width', parseInt(e.target.value) || 400)}" />
1691
- </div>
1692
- <div class="prop-pos-cell">
1693
- <span class="prop-pos-label prop-pos-label--h">H</span>
1694
- <input type="number" min="100" .value="${String(p.height ?? 250)}" @change="${(e) => this.setProp(field, 'height', parseInt(e.target.value) || 250)}" />
1695
- </div>
1696
- </div>
1697
- <div class="prop-divider"></div>
1698
- ${this.renderToggle('Leyenda', p.showLegend ?? true, (v) => this.setProp(field, 'showLegend', v))}
1699
- ${this.renderToggle('Animacion', p.animated ?? false, (v) => this.setProp(field, 'animated', v))}
1700
- ` : ''}
1701
1744
  </div>
1702
1745
  `;
1703
1746
  }
1704
- // ─── Report Props ─────────────────────────────────
1705
- renderReportProps(field) {
1706
- const p = field.props ?? {};
1707
- return html `
1708
- <div class="prop-section">
1709
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('report-config')}">
1710
- <span class="collapse-icon ${this.collapsedSections.has('report-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1711
- <h4>Report Config</h4>
1712
- </div>
1713
- ${!this.collapsedSections.has('report-config') ? html `
1714
- <div class="prop-info">📋 Configuracion de zentto-report</div>
1715
- <div class="prop-row"><span class="prop-label">Template ID</span><input class="prop-input" .value="${p.templateId ?? ''}" placeholder="invoice-template" @change="${(e) => this.setProp(field, 'templateId', e.target.value)}" /></div>
1716
- <div class="prop-row"><span class="prop-label">Zoom</span><input class="prop-input" type="number" min="25" max="300" step="25" .value="${String(p.zoom ?? 100)}" @change="${(e) => this.setProp(field, 'zoom', parseInt(e.target.value) || 100)}" /></div>
1717
- <div class="prop-row"><span class="prop-label">Altura</span><input class="prop-input" .value="${p.height ?? '500px'}" placeholder="500px" @change="${(e) => this.setProp(field, 'height', e.target.value)}" /></div>
1718
- <div class="prop-divider"></div>
1719
- ${this.renderToggle('Toolbar', p.showToolbar ?? true, (v) => this.setProp(field, 'showToolbar', v))}
1720
- ${this.renderToggle('Imprimir', p.showPrint ?? true, (v) => this.setProp(field, 'showPrint', v))}
1721
- ${this.renderToggle('Exportar PDF', p.showExportPdf ?? true, (v) => this.setProp(field, 'showExportPdf', v))}
1722
- ${this.renderToggle('Navegacion', p.showNavigation ?? true, (v) => this.setProp(field, 'showNavigation', v))}
1723
- ` : ''}
1724
- </div>
1725
- `;
1726
- }
1727
- // ─── Select/Radio Props ───────────────────────────
1728
- renderSelectProps(field) {
1729
- const p = field.props ?? {};
1730
- const options = p.options ?? [];
1731
- return html `
1732
- <div class="prop-section">
1733
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('select-config')}">
1734
- <span class="collapse-icon ${this.collapsedSections.has('select-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1735
- <h4>Opciones</h4>
1736
- </div>
1737
- ${!this.collapsedSections.has('select-config') ? html `
1738
- ${options.map((opt, i) => html `
1739
- <div style="display:flex;gap:3px;margin-bottom:3px;align-items:center;">
1740
- <input class="prop-input" style="flex:1;" .value="${opt.value}" placeholder="valor" @change="${(e) => { options[i].value = e.target.value; this.setProp(field, 'options', [...options]); }}" />
1741
- <input class="prop-input" style="flex:1;" .value="${opt.label}" placeholder="label" @change="${(e) => { options[i].label = e.target.value; this.setProp(field, 'options', [...options]); }}" />
1742
- <button style="border:none;background:none;cursor:pointer;color:#d32f2f;font-size:12px;padding:2px 4px;" @click="${() => { options.splice(i, 1); this.setProp(field, 'options', [...options]); }}">✕</button>
1743
- </div>
1744
- `)}
1745
- <button style="width:100%;padding:4px;border:1px dashed #ccc;border-radius:4px;background:none;cursor:pointer;font-size:10px;color:#888;font-family:inherit;margin-top:4px;"
1746
- @click="${() => { options.push({ value: '', label: '' }); this.setProp(field, 'options', [...options]); }}"
1747
- >+ Agregar opcion</button>
1748
- ${field.type === 'radio' ? html `
1749
- <div class="prop-divider"></div>
1750
- ${this.renderToggle('Horizontal', p.horizontal ?? false, (v) => this.setProp(field, 'horizontal', v))}
1751
- ` : ''}
1752
- ${field.type === 'multiselect' ? html `
1753
- <div class="prop-divider"></div>
1754
- ${this.renderToggle('Multiple', true, () => { })}
1755
- ` : ''}
1756
- ` : ''}
1757
- </div>
1758
- `;
1759
- }
1760
- // ─── Number/Currency/Slider/Rating Props ──────────
1761
- renderNumberProps(field) {
1762
- const p = field.props ?? {};
1763
- return html `
1764
- <div class="prop-section">
1765
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('number-config')}">
1766
- <span class="collapse-icon ${this.collapsedSections.has('number-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1767
- <h4>Numero Config</h4>
1768
- </div>
1769
- ${!this.collapsedSections.has('number-config') ? html `
1770
- ${field.type === 'currency' ? html `
1771
- <div class="prop-row"><span class="prop-label">Simbolo</span><input class="prop-input" .value="${p.currencySymbol ?? '$'}" @change="${(e) => this.setProp(field, 'currencySymbol', e.target.value)}" /></div>
1772
- ` : ''}
1773
- ${field.type === 'slider' || field.type === 'rating' ? html `
1774
- <div class="prop-pos-grid">
1775
- <div class="prop-pos-cell">
1776
- <span class="prop-pos-label prop-pos-label--x">Min</span>
1777
- <input type="number" .value="${String(p.min ?? 0)}" @change="${(e) => this.setProp(field, 'min', parseInt(e.target.value) || 0)}" />
1778
- </div>
1779
- <div class="prop-pos-cell">
1780
- <span class="prop-pos-label prop-pos-label--y">Max</span>
1781
- <input type="number" .value="${String(p.max ?? (field.type === 'rating' ? 5 : 100))}" @change="${(e) => this.setProp(field, 'max', parseInt(e.target.value) || 100)}" />
1782
- </div>
1783
- </div>
1784
- ${field.type === 'slider' ? html `
1785
- <div class="prop-row"><span class="prop-label">Step</span><input class="prop-input" type="number" min="1" .value="${String(p.step ?? 1)}" @change="${(e) => this.setProp(field, 'step', parseInt(e.target.value) || 1)}" /></div>
1786
- ` : ''}
1787
- ${field.type === 'rating' ? html `
1788
- <div class="prop-row"><span class="prop-label">Estrellas</span><input class="prop-input" type="number" min="3" max="10" .value="${String(p.maxRating ?? 5)}" @change="${(e) => this.setProp(field, 'maxRating', parseInt(e.target.value) || 5)}" /></div>
1789
- ` : ''}
1790
- ` : ''}
1791
- ` : ''}
1792
- </div>
1793
- `;
1794
- }
1795
- // ─── Lookup Props ─────────────────────────────────
1796
- renderLookupProps(field) {
1797
- const p = field.props ?? {};
1798
- return html `
1799
- <div class="prop-section">
1800
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('lookup-config')}">
1801
- <span class="collapse-icon ${this.collapsedSections.has('lookup-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1802
- <h4>Lookup Config</h4>
1803
- </div>
1804
- ${!this.collapsedSections.has('lookup-config') ? html `
1805
- <div class="prop-row"><span class="prop-label">Min Chars</span><input class="prop-input" type="number" min="1" .value="${String(p.minChars ?? 2)}" @change="${(e) => this.setProp(field, 'minChars', parseInt(e.target.value) || 2)}" /></div>
1806
- <div class="prop-row"><span class="prop-label">Debounce</span><input class="prop-input" type="number" min="100" step="100" .value="${String(p.debounceMs ?? 300)}" @change="${(e) => this.setProp(field, 'debounceMs', parseInt(e.target.value) || 300)}" /></div>
1807
- ` : ''}
1808
- </div>
1809
- `;
1810
- }
1811
- // ─── Chips/Tags Props ─────────────────────────────
1812
- renderChipsProps(field) {
1813
- const p = field.props ?? {};
1814
- return html `
1815
- <div class="prop-section">
1816
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('chips-config')}">
1817
- <span class="collapse-icon ${this.collapsedSections.has('chips-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1818
- <h4>Chips Config</h4>
1819
- </div>
1820
- ${!this.collapsedSections.has('chips-config') ? html `
1821
- <div class="prop-row"><span class="prop-label">Max Chips</span><input class="prop-input" type="number" min="0" .value="${String(p.maxChips ?? 0)}" @change="${(e) => this.setProp(field, 'maxChips', parseInt(e.target.value) || 0)}" /></div>
1822
- <div class="prop-row"><span class="prop-label">Color Mode</span>
1823
- <select class="prop-input" .value="${p.colorMode ?? 'default'}" @change="${(e) => this.setProp(field, 'colorMode', e.target.value)}">
1824
- <option value="default">Default</option>
1825
- <option value="primary">Primary</option>
1826
- <option value="success">Success</option>
1827
- <option value="auto">Auto (ciclo)</option>
1828
- </select>
1829
- </div>
1830
- ${this.renderToggle('Permitir custom', p.allowCustom ?? true, (v) => this.setProp(field, 'allowCustom', v))}
1831
- ${this.renderSelectProps(field)}
1832
- ` : ''}
1833
- </div>
1834
- `;
1835
- }
1836
- // ─── TreeView Props ───────────────────────────────
1837
- renderTreeViewProps(field) {
1838
- const p = field.props ?? {};
1839
- return html `
1840
- <div class="prop-section">
1841
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('tree-config')}">
1842
- <span class="collapse-icon ${this.collapsedSections.has('tree-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1843
- <h4>TreeView Config</h4>
1844
- </div>
1845
- ${!this.collapsedSections.has('tree-config') ? html `
1846
- ${this.renderToggle('Multi-select', p.multiSelect ?? false, (v) => this.setProp(field, 'multiSelect', v))}
1847
- ${this.renderToggle('Checkboxes', p.showCheckboxes ?? false, (v) => this.setProp(field, 'showCheckboxes', v))}
1848
- ${this.renderToggle('Buscador', p.searchable ?? false, (v) => this.setProp(field, 'searchable', v))}
1849
- ` : ''}
1850
- </div>
1851
- `;
1852
- }
1853
- // ─── Date Props ───────────────────────────────────
1854
- renderDateProps(field) {
1855
- const p = field.props ?? {};
1856
- return html `
1857
- <div class="prop-section">
1858
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('date-config')}">
1859
- <span class="collapse-icon ${this.collapsedSections.has('date-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1860
- <h4>Fecha Config</h4>
1861
- </div>
1862
- ${!this.collapsedSections.has('date-config') ? html `
1863
- <div class="prop-row"><span class="prop-label">Modo</span>
1864
- <select class="prop-input" .value="${p.mode ?? 'date'}" @change="${(e) => this.setProp(field, 'mode', e.target.value)}">
1865
- <option value="date">Fecha</option>
1866
- <option value="time">Hora</option>
1867
- <option value="datetime">Fecha y Hora</option>
1868
- </select>
1869
- </div>
1870
- <div class="prop-row"><span class="prop-label">Min</span><input class="prop-input" type="date" .value="${p.min ?? ''}" @change="${(e) => this.setProp(field, 'min', e.target.value)}" /></div>
1871
- <div class="prop-row"><span class="prop-label">Max</span><input class="prop-input" type="date" .value="${p.max ?? ''}" @change="${(e) => this.setProp(field, 'max', e.target.value)}" /></div>
1872
- ` : ''}
1873
- </div>
1874
- `;
1875
- }
1876
- // ─── File/Image Props ─────────────────────────────
1877
- renderFileProps(field) {
1878
- const p = field.props ?? {};
1879
- return html `
1880
- <div class="prop-section">
1881
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('file-config')}">
1882
- <span class="collapse-icon ${this.collapsedSections.has('file-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1883
- <h4>Archivo Config</h4>
1884
- </div>
1885
- ${!this.collapsedSections.has('file-config') ? html `
1886
- <div class="prop-row"><span class="prop-label">Accept</span><input class="prop-input" .value="${p.accept ?? ''}" placeholder="image/*,.pdf" @change="${(e) => this.setProp(field, 'accept', e.target.value)}" /></div>
1887
- ${this.renderToggle('Multiple', p.multiple ?? false, (v) => this.setProp(field, 'multiple', v))}
1888
- ` : ''}
1889
- </div>
1890
- `;
1891
- }
1892
- // ─── Signature Props ──────────────────────────────
1893
- renderSignatureProps(field) {
1894
- const p = field.props ?? {};
1895
- return html `
1896
- <div class="prop-section">
1897
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('sig-config')}">
1898
- <span class="collapse-icon ${this.collapsedSections.has('sig-config') ? 'collapse-icon--collapsed' : ''}">▾</span>
1899
- <h4>Firma Config</h4>
1900
- </div>
1901
- ${!this.collapsedSections.has('sig-config') ? html `
1902
- <div class="prop-row"><span class="prop-label">Grosor</span><input class="prop-input" type="number" min="1" max="10" .value="${String(p.penWidth ?? 2)}" @change="${(e) => this.setProp(field, 'penWidth', parseInt(e.target.value) || 2)}" /></div>
1903
- <div class="prop-row"><span class="prop-label">Color</span><input class="prop-input" type="color" .value="${p.penColor ?? '#000000'}" @change="${(e) => this.setProp(field, 'penColor', e.target.value)}" /></div>
1904
- ` : ''}
1905
- </div>
1906
- `;
1907
- }
1908
- // ─── Grid Configurator Modal ────────────────────
1909
1747
  renderFormProperties() {
1910
1748
  if (!this.schema)
1911
1749
  return nothing;
@@ -2416,6 +2254,12 @@ __decorate([
2416
2254
  __decorate([
2417
2255
  property({ type: Number, attribute: 'grid-snap' })
2418
2256
  ], ZsPageDesigner.prototype, "gridSnap", void 0);
2257
+ __decorate([
2258
+ property({ type: String, attribute: 'save-api-url' })
2259
+ ], ZsPageDesigner.prototype, "saveApiUrl", void 0);
2260
+ __decorate([
2261
+ property({ type: String, attribute: 'save-api-token' })
2262
+ ], ZsPageDesigner.prototype, "saveApiToken", void 0);
2419
2263
  __decorate([
2420
2264
  state()
2421
2265
  ], ZsPageDesigner.prototype, "selectedFieldId", void 0);
@@ -2437,6 +2281,36 @@ __decorate([
2437
2281
  __decorate([
2438
2282
  state()
2439
2283
  ], ZsPageDesigner.prototype, "zoom", void 0);
2284
+ __decorate([
2285
+ state()
2286
+ ], ZsPageDesigner.prototype, "saveDialogOpen", void 0);
2287
+ __decorate([
2288
+ state()
2289
+ ], ZsPageDesigner.prototype, "loadDialogOpen", void 0);
2290
+ __decorate([
2291
+ state()
2292
+ ], ZsPageDesigner.prototype, "serverAddons", void 0);
2293
+ __decorate([
2294
+ state()
2295
+ ], ZsPageDesigner.prototype, "serverLoading", void 0);
2296
+ __decorate([
2297
+ state()
2298
+ ], ZsPageDesigner.prototype, "serverSaving", void 0);
2299
+ __decorate([
2300
+ state()
2301
+ ], ZsPageDesigner.prototype, "editingAddonId", void 0);
2302
+ __decorate([
2303
+ state()
2304
+ ], ZsPageDesigner.prototype, "saveName", void 0);
2305
+ __decorate([
2306
+ state()
2307
+ ], ZsPageDesigner.prototype, "saveDesc", void 0);
2308
+ __decorate([
2309
+ state()
2310
+ ], ZsPageDesigner.prototype, "saveIconEmoji", void 0);
2311
+ __decorate([
2312
+ state()
2313
+ ], ZsPageDesigner.prototype, "saveModules", void 0);
2440
2314
  __decorate([
2441
2315
  state()
2442
2316
  ], ZsPageDesigner.prototype, "showTemplateMenu", void 0);