@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.
@@ -78,7 +78,18 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
78
78
  this.undoStack = [];
79
79
  this.redoStack = [];
80
80
  this.saveTimer = null;
81
+ // InteractJS
82
+ this.interactLoaded = false;
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ this.interact = null;
85
+ this.dragClone = null;
86
+ this.dragSourceType = null;
87
+ this.dragSourceFieldId = null;
88
+ this.dragSourceSectionIndex = -1;
89
+ this.dragInsertIndex = -1;
90
+ this.dragTargetSectionIndex = -1;
81
91
  // ─── API Panel (Data Sources) ──────────────────────
92
+ this.showTemplateMenu = false;
82
93
  this.apiSources = [];
83
94
  this.apiLoading = false;
84
95
  this.apiBaseUrl = '';
@@ -539,12 +550,386 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
539
550
  padding: 16px; border-radius: 4px; overflow: auto;
540
551
  white-space: pre; tab-size: 2;
541
552
  }
553
+
554
+ /* ─── InteractJS Drag & Drop ─────────────────── */
555
+ .dragging { opacity: 0.4; cursor: grabbing !important; }
556
+ .drag-clone {
557
+ position: fixed; pointer-events: none; z-index: 9999;
558
+ opacity: 0.85; border: 2px solid var(--zrd-accent);
559
+ border-radius: 6px; background: white;
560
+ box-shadow: 0 8px 24px rgba(25,118,210,0.25);
561
+ padding: 8px 12px; font-size: 12px; font-weight: 500;
562
+ color: var(--zrd-accent); max-width: 200px;
563
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
564
+ }
565
+ .drop-active { border: 2px dashed var(--zrd-accent) !important; background: rgba(25,118,210,0.04) !important; }
566
+ .drop-target { background: rgba(25,118,210,0.1) !important; border-color: var(--zrd-accent) !important; }
567
+ .resize-active { outline: 2px dashed var(--zrd-accent); outline-offset: 2px; }
568
+ .drag-insert-line {
569
+ position: absolute; left: 4px; right: 4px; height: 2px;
570
+ background: var(--zrd-accent); border-radius: 1px; z-index: 10;
571
+ pointer-events: none;
572
+ }
573
+ .drag-insert-line::before, .drag-insert-line::after {
574
+ content: ''; position: absolute; top: -3px;
575
+ width: 8px; height: 8px; border-radius: 50%;
576
+ background: var(--zrd-accent);
577
+ }
578
+ .drag-insert-line::before { left: -4px; }
579
+ .drag-insert-line::after { right: -4px; }
580
+ .canvas-field.can-drop { box-shadow: 0 0 0 2px rgba(25,118,210,0.3); }
581
+ .section-drop-zone {
582
+ min-height: 8px; transition: all 0.15s; border-radius: 4px;
583
+ margin: 2px 4px;
584
+ }
585
+ .section-drop-zone.drop-active {
586
+ min-height: 32px; border: 2px dashed var(--zrd-accent);
587
+ background: rgba(25,118,210,0.06);
588
+ display: flex; align-items: center; justify-content: center;
589
+ font-size: 11px; color: var(--zrd-accent);
590
+ }
542
591
  `; }
543
592
  // ─── Lifecycle ────────────────────────────────────
544
593
  updated(changed) {
545
594
  if (changed.has('schema') && this.schema && this.undoStack.length === 0) {
546
595
  this.undoStack = [JSON.stringify(this.schema)];
547
596
  }
597
+ // Initialize InteractJS after render when in design mode
598
+ if (this.viewMode === 'design' && this.schema) {
599
+ this.initInteract();
600
+ }
601
+ }
602
+ disconnectedCallback() {
603
+ super.disconnectedCallback();
604
+ this.cleanupInteract();
605
+ }
606
+ // ─── InteractJS Integration ───────────────────────
607
+ async initInteract() {
608
+ if (!this.interactLoaded) {
609
+ try {
610
+ // @ts-ignore — optional peer dependency
611
+ const mod = await import('interactjs');
612
+ this.interact = mod.default;
613
+ this.interactLoaded = true;
614
+ }
615
+ catch {
616
+ console.warn('[zs-page-designer] InteractJS not available, falling back to native drag');
617
+ return;
618
+ }
619
+ }
620
+ // Schedule setup after DOM updates
621
+ requestAnimationFrame(() => this.setupInteractions());
622
+ }
623
+ cleanupInteract() {
624
+ if (!this.interact)
625
+ return;
626
+ const root = this.shadowRoot;
627
+ if (!root)
628
+ return;
629
+ // Clean all interact instances within shadow DOM
630
+ root.querySelectorAll('.toolbox-item, .canvas-field, .drop-zone, .section-drop-zone, .canvas-grid').forEach(el => {
631
+ try {
632
+ this.interact.unset(el);
633
+ }
634
+ catch { /* already cleaned */ }
635
+ });
636
+ if (this.dragClone) {
637
+ this.dragClone.remove();
638
+ this.dragClone = null;
639
+ }
640
+ }
641
+ setupInteractions() {
642
+ if (!this.interact || !this.shadowRoot)
643
+ return;
644
+ const interact = this.interact;
645
+ const root = this.shadowRoot;
646
+ // ── Toolbox items: draggable to canvas ──
647
+ root.querySelectorAll('.toolbox-item').forEach(el => {
648
+ // Avoid re-initializing
649
+ if (el.__interactSetup)
650
+ return;
651
+ el.__interactSetup = true;
652
+ interact(el).draggable({
653
+ inertia: false,
654
+ autoScroll: true,
655
+ listeners: {
656
+ start: (event) => {
657
+ const type = event.target.getAttribute('data-field-type');
658
+ if (!type)
659
+ return;
660
+ this.dragSourceType = type;
661
+ this.dragSourceFieldId = null;
662
+ this.dragSourceSectionIndex = -1;
663
+ event.target.classList.add('dragging');
664
+ // Create clone
665
+ this.createDragClone(event.clientX, event.clientY, event.target.querySelector('.toolbox-label')?.textContent ?? type);
666
+ },
667
+ move: (event) => {
668
+ if (this.dragClone) {
669
+ this.dragClone.style.left = `${event.clientX + 12}px`;
670
+ this.dragClone.style.top = `${event.clientY + 12}px`;
671
+ }
672
+ },
673
+ end: (event) => {
674
+ event.target.classList.remove('dragging');
675
+ this.removeDragClone();
676
+ this.dragSourceType = null;
677
+ },
678
+ },
679
+ });
680
+ });
681
+ // ── Canvas fields: draggable for reorder + cross-section ──
682
+ root.querySelectorAll('.canvas-field').forEach(el => {
683
+ if (el.__interactSetup)
684
+ return;
685
+ el.__interactSetup = true;
686
+ interact(el).draggable({
687
+ inertia: false,
688
+ autoScroll: true,
689
+ listeners: {
690
+ start: (event) => {
691
+ const fieldId = event.target.getAttribute('data-field-id');
692
+ const si = parseInt(event.target.getAttribute('data-section-index') ?? '-1');
693
+ if (!fieldId)
694
+ return;
695
+ this.dragSourceFieldId = fieldId;
696
+ this.dragSourceType = null;
697
+ this.dragSourceSectionIndex = si;
698
+ event.target.classList.add('dragging');
699
+ const label = event.target.querySelector('.field-preview-label')?.textContent ?? fieldId;
700
+ this.createDragClone(event.clientX, event.clientY, label);
701
+ },
702
+ move: (event) => {
703
+ if (this.dragClone) {
704
+ this.dragClone.style.left = `${event.clientX + 12}px`;
705
+ this.dragClone.style.top = `${event.clientY + 12}px`;
706
+ }
707
+ // Calculate insert position
708
+ this.updateInsertIndicator(event);
709
+ },
710
+ end: (event) => {
711
+ event.target.classList.remove('dragging');
712
+ this.removeDragClone();
713
+ this.removeInsertIndicator();
714
+ // Perform reorder if we have a valid target
715
+ if (this.dragSourceFieldId && this.dragTargetSectionIndex >= 0 && this.dragInsertIndex >= 0) {
716
+ this.performFieldMove(this.dragSourceFieldId, this.dragSourceSectionIndex, this.dragTargetSectionIndex, this.dragInsertIndex);
717
+ }
718
+ this.dragSourceFieldId = null;
719
+ this.dragSourceSectionIndex = -1;
720
+ this.dragInsertIndex = -1;
721
+ this.dragTargetSectionIndex = -1;
722
+ },
723
+ },
724
+ });
725
+ });
726
+ // ── Drop zones: accept fields from toolbox ──
727
+ root.querySelectorAll('.drop-zone, .section-drop-zone').forEach(el => {
728
+ if (el.__interactSetup)
729
+ return;
730
+ el.__interactSetup = true;
731
+ interact(el).dropzone({
732
+ accept: '.toolbox-item, .canvas-field',
733
+ overlap: 0.25,
734
+ ondragenter: (event) => {
735
+ event.target.classList.add('drop-active', 'drop-target');
736
+ },
737
+ ondragleave: (event) => {
738
+ event.target.classList.remove('drop-active', 'drop-target');
739
+ },
740
+ ondrop: (event) => {
741
+ event.target.classList.remove('drop-active', 'drop-target');
742
+ const si = parseInt(event.target.getAttribute('data-section-index') ?? '0');
743
+ if (this.dragSourceType) {
744
+ // Drop from toolbox
745
+ this.addField(this.dragSourceType, si);
746
+ }
747
+ else if (this.dragSourceFieldId) {
748
+ // Drop from canvas (cross-section move)
749
+ const insertIdx = this.schema?.sections[si]?.fields.length ?? 0;
750
+ this.performFieldMove(this.dragSourceFieldId, this.dragSourceSectionIndex, si, insertIdx);
751
+ }
752
+ },
753
+ });
754
+ });
755
+ // ── Canvas grids: drop zones for reorder within section ──
756
+ root.querySelectorAll('.canvas-grid').forEach(el => {
757
+ if (el.__interactSetup)
758
+ return;
759
+ el.__interactSetup = true;
760
+ interact(el).dropzone({
761
+ accept: '.toolbox-item, .canvas-field',
762
+ overlap: 0.1,
763
+ ondragenter: (event) => {
764
+ event.target.classList.add('drop-active');
765
+ },
766
+ ondragleave: (event) => {
767
+ event.target.classList.remove('drop-active');
768
+ },
769
+ ondrop: (event) => {
770
+ event.target.classList.remove('drop-active');
771
+ const si = parseInt(event.target.getAttribute('data-section-index') ?? '0');
772
+ if (this.dragSourceType) {
773
+ const insertIdx = this.dragInsertIndex >= 0 ? this.dragInsertIndex : (this.schema?.sections[si]?.fields.length ?? 0);
774
+ this.addFieldAtIndex(this.dragSourceType, si, insertIdx);
775
+ }
776
+ },
777
+ });
778
+ });
779
+ // ── Resize handles on selected field ──
780
+ root.querySelectorAll('.canvas-field--selected').forEach(fieldEl => {
781
+ if (fieldEl.__interactResize)
782
+ return;
783
+ fieldEl.__interactResize = true;
784
+ const maxCols = this.schema?.layout.columns ?? 2;
785
+ interact(fieldEl).resizable({
786
+ edges: { left: '.rh-w, .rh-nw, .rh-sw', right: '.rh-e, .rh-ne, .rh-se', top: false, bottom: false },
787
+ listeners: {
788
+ start: (event) => {
789
+ event.target.classList.add('resize-active');
790
+ },
791
+ move: (event) => {
792
+ // Calculate colSpan based on width change
793
+ const grid = event.target.closest('.canvas-grid');
794
+ if (!grid)
795
+ return;
796
+ const gridWidth = grid.getBoundingClientRect().width;
797
+ const colWidth = gridWidth / maxCols;
798
+ const newColSpan = Math.max(1, Math.min(maxCols, Math.round(event.rect.width / colWidth)));
799
+ const fieldId = event.target.getAttribute('data-field-id');
800
+ if (fieldId && this.schema) {
801
+ for (const s of this.schema.sections) {
802
+ const f = s.fields.find(f => f.id === fieldId);
803
+ if (f && f.colSpan !== newColSpan) {
804
+ f.colSpan = newColSpan;
805
+ this.requestUpdate();
806
+ break;
807
+ }
808
+ }
809
+ }
810
+ },
811
+ end: (event) => {
812
+ event.target.classList.remove('resize-active');
813
+ this.commitChange();
814
+ },
815
+ },
816
+ modifiers: interact.modifiers ? [
817
+ interact.modifiers.restrictSize({
818
+ min: { width: 80, height: 30 },
819
+ }),
820
+ ] : [],
821
+ });
822
+ });
823
+ }
824
+ createDragClone(x, y, label) {
825
+ this.removeDragClone();
826
+ const clone = document.createElement('div');
827
+ clone.className = 'drag-clone';
828
+ clone.textContent = label;
829
+ clone.style.left = `${x + 12}px`;
830
+ clone.style.top = `${y + 12}px`;
831
+ document.body.appendChild(clone);
832
+ this.dragClone = clone;
833
+ }
834
+ removeDragClone() {
835
+ if (this.dragClone) {
836
+ this.dragClone.remove();
837
+ this.dragClone = null;
838
+ }
839
+ }
840
+ updateInsertIndicator(event) {
841
+ if (!this.shadowRoot || !this.schema)
842
+ return;
843
+ const root = this.shadowRoot;
844
+ // Find which canvas-grid we're over
845
+ const grids = root.querySelectorAll('.canvas-grid');
846
+ for (let si = 0; si < grids.length; si++) {
847
+ const grid = grids[si];
848
+ const rect = grid.getBoundingClientRect();
849
+ if (event.clientX >= rect.left && event.clientX <= rect.right &&
850
+ event.clientY >= rect.top && event.clientY <= rect.bottom) {
851
+ this.dragTargetSectionIndex = si;
852
+ // Find insert position among fields
853
+ const fields = grid.querySelectorAll('.canvas-field');
854
+ let insertIdx = fields.length;
855
+ for (let fi = 0; fi < fields.length; fi++) {
856
+ const fieldRect = fields[fi].getBoundingClientRect();
857
+ const midY = fieldRect.top + fieldRect.height / 2;
858
+ if (event.clientY < midY) {
859
+ insertIdx = fi;
860
+ break;
861
+ }
862
+ }
863
+ this.dragInsertIndex = insertIdx;
864
+ this.showInsertLine(grid, fields, insertIdx);
865
+ return;
866
+ }
867
+ }
868
+ this.removeInsertIndicator();
869
+ }
870
+ showInsertLine(grid, fields, index) {
871
+ this.removeInsertIndicator();
872
+ const line = document.createElement('div');
873
+ line.className = 'drag-insert-line';
874
+ line.setAttribute('data-insert-line', 'true');
875
+ if (fields.length === 0 || index >= fields.length) {
876
+ // Append at end
877
+ grid.appendChild(line);
878
+ line.style.position = 'relative';
879
+ line.style.marginTop = '4px';
880
+ }
881
+ else {
882
+ // Insert before the field at index
883
+ const targetField = fields[index];
884
+ targetField.style.position = 'relative';
885
+ grid.insertBefore(line, targetField);
886
+ line.style.position = 'relative';
887
+ line.style.marginBottom = '4px';
888
+ }
889
+ }
890
+ removeInsertIndicator() {
891
+ if (!this.shadowRoot)
892
+ return;
893
+ this.shadowRoot.querySelectorAll('[data-insert-line]').forEach(el => el.remove());
894
+ }
895
+ performFieldMove(fieldId, fromSi, toSi, toIndex) {
896
+ if (!this.schema || fromSi < 0)
897
+ return;
898
+ const fromSection = this.schema.sections[fromSi];
899
+ const toSection = this.schema.sections[toSi];
900
+ if (!fromSection || !toSection)
901
+ return;
902
+ const fromFi = fromSection.fields.findIndex(f => f.id === fieldId);
903
+ if (fromFi < 0)
904
+ return;
905
+ const [field] = fromSection.fields.splice(fromFi, 1);
906
+ // Adjust target index if moving within same section and removing shifted indices
907
+ let adjustedIndex = toIndex;
908
+ if (fromSi === toSi && fromFi < toIndex) {
909
+ adjustedIndex = Math.max(0, toIndex - 1);
910
+ }
911
+ adjustedIndex = Math.min(adjustedIndex, toSection.fields.length);
912
+ toSection.fields.splice(adjustedIndex, 0, field);
913
+ this.commitChange();
914
+ }
915
+ addFieldAtIndex(type, sectionIndex, insertIndex) {
916
+ if (!this.schema) {
917
+ this.addField(type, sectionIndex);
918
+ return;
919
+ }
920
+ if (sectionIndex >= this.schema.sections.length)
921
+ sectionIndex = 0;
922
+ const id = `${type}_${Date.now()}`;
923
+ const meta = getAllFields().find(f => f.type === type);
924
+ const newField = {
925
+ id, type, field: id,
926
+ label: meta?.label ?? type,
927
+ props: meta?.defaultProps ? { ...meta.defaultProps } : undefined,
928
+ };
929
+ const idx = Math.min(insertIndex, this.schema.sections[sectionIndex].fields.length);
930
+ this.schema.sections[sectionIndex].fields.splice(idx, 0, newField);
931
+ this.selectedFieldId = id;
932
+ this.commitChange();
548
933
  }
549
934
  // ─── Undo/Redo ────────────────────────────────────
550
935
  pushUndo() {
@@ -700,6 +1085,19 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
700
1085
  this.commitChange();
701
1086
  }}">+ Seccion</button>
702
1087
 
1088
+ <div style="position:relative;display:inline-block;">
1089
+ <button class="tb-btn" @click="${() => { this.showTemplateMenu = !this.showTemplateMenu; }}">📋 Plantillas ▾</button>
1090
+ ${this.showTemplateMenu ? this.renderTemplateMenu() : nothing}
1091
+ </div>
1092
+
1093
+ <button class="tb-btn tb-btn--danger" @click="${() => {
1094
+ this.schema = { id: 'new-form', version: '1.0', title: 'Nuevo Formulario', layout: { type: 'grid', columns: 2 }, sections: [{ id: 'main', title: 'Datos', fields: [] }] };
1095
+ this.undoStack = [JSON.stringify(this.schema)];
1096
+ this.redoStack = [];
1097
+ this.selectedFieldId = null;
1098
+ this.commitChange();
1099
+ }}">🗑 Nuevo</button>
1100
+
703
1101
  <div class="toolbar-sep"></div>
704
1102
 
705
1103
  <button class="tb-btn ${this.viewMode === 'design' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'design'; }}">✏️ Diseño</button>
@@ -750,6 +1148,7 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
750
1148
  ${items.map(f => html `
751
1149
  <div class="toolbox-item"
752
1150
  draggable="true"
1151
+ data-field-type="${f.type}"
753
1152
  @dragstart="${(e) => { this.dragType = f.type; e.dataTransfer?.setData('text/plain', f.type); }}"
754
1153
  @dragend="${() => { this.dragType = null; }}"
755
1154
  @dblclick="${() => this.addField(f.type)}"
@@ -797,7 +1196,7 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
797
1196
  }
798
1197
  renderDesignCanvas() {
799
1198
  if (!this.schema) {
800
- return html `<div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}" style="width:500px;height:200px;display:flex;align-items:center;justify-content:center;"
1199
+ return html `<div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}" data-section-index="0" style="width:500px;height:200px;display:flex;align-items:center;justify-content:center;"
801
1200
  @dragover="${(e) => e.preventDefault()}"
802
1201
  @drop="${(e) => { e.preventDefault(); if (this.dragType) {
803
1202
  this.addField(this.dragType);
@@ -816,10 +1215,12 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
816
1215
  <span style="flex:1;"></span>
817
1216
  <span style="font-size:10px;color:var(--zrd-text-muted);">${section.fields.length} campos</span>
818
1217
  </div>
819
- <div class="canvas-grid" style="grid-template-columns:repeat(${section.columns ?? cols}, 1fr);">
1218
+ <div class="section-drop-zone" data-section-index="${si}" data-position="top"></div>
1219
+ <div class="canvas-grid" data-section-index="${si}" style="grid-template-columns:repeat(${section.columns ?? cols}, 1fr);">
820
1220
  ${section.fields.map((field, fi) => this.renderCanvasField(field, si, fi, section.columns ?? cols))}
821
1221
  </div>
822
1222
  <div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}"
1223
+ data-section-index="${si}"
823
1224
  @dragover="${(e) => e.preventDefault()}"
824
1225
  @drop="${(e) => { e.preventDefault(); if (this.dragType) {
825
1226
  this.addField(this.dragType, si);
@@ -840,6 +1241,8 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
840
1241
  return html `
841
1242
  <div class="canvas-field ${isSelected ? 'canvas-field--selected' : ''}"
842
1243
  style="${gridCol ? `grid-column:${gridCol};` : ''}"
1244
+ data-field-id="${field.id}"
1245
+ data-section-index="${si}"
843
1246
  @click="${(e) => { e.stopPropagation(); this.selectedFieldId = field.id; }}"
844
1247
  >
845
1248
  <!-- Type badge -->
@@ -1180,6 +1583,145 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1180
1583
  </div>
1181
1584
  `;
1182
1585
  }
1586
+ // ─── Template Menu ─────────────────────────────────
1587
+ renderTemplateMenu() {
1588
+ const templates = [
1589
+ {
1590
+ id: 'blank', icon: '📄', title: 'En Blanco', desc: '2 columnas, sin campos',
1591
+ schema: { id: 'blank', version: '1.0', title: 'Nuevo Formulario', layout: { type: 'grid', columns: 2, gap: 16 }, sections: [{ id: 'main', title: 'Datos', fields: [] }] },
1592
+ },
1593
+ {
1594
+ id: 'contact', icon: '👤', title: 'Contacto', desc: 'Nombre, email, telefono, pais',
1595
+ schema: { id: 'contact', version: '1.0', title: 'Ficha de Contacto', layout: { type: 'grid', columns: 2, gap: 16 }, sections: [
1596
+ { id: 's1', title: 'Datos Personales', fields: [
1597
+ { id: 'nombre', type: 'text', field: 'nombre', label: 'Nombre Completo', required: true },
1598
+ { id: 'email', type: 'email', field: 'email', label: 'Email', required: true },
1599
+ { id: 'telefono', type: 'phone', field: 'telefono', label: 'Telefono' },
1600
+ { id: 'pais', type: 'select', field: 'pais', label: 'Pais', props: { options: [{ value: 'VE', label: 'Venezuela' }, { value: 'CO', label: 'Colombia' }, { value: 'MX', label: 'Mexico' }, { value: 'ES', label: 'España' }, { value: 'US', label: 'EEUU' }] } },
1601
+ { id: 'notas', type: 'textarea', field: 'notas', label: 'Notas', colSpan: 2 },
1602
+ ] },
1603
+ ], actions: [{ id: 'save', type: 'submit', label: 'Guardar', variant: 'primary' }, { id: 'cancel', type: 'reset', label: 'Cancelar', variant: 'secondary' }] },
1604
+ },
1605
+ {
1606
+ id: 'client', icon: '🏢', title: 'Cliente Completo', desc: 'Datos personales, direccion, fiscal, tags',
1607
+ schema: { id: 'client', version: '1.0', title: 'Registro de Cliente', layout: { type: 'grid', columns: 2, gap: 16 }, sections: [
1608
+ { id: 's1', title: 'Datos del Cliente', fields: [
1609
+ { id: 'razon', type: 'text', field: 'razonSocial', label: 'Razon Social', required: true },
1610
+ { id: 'rif', type: 'text', field: 'rif', label: 'RIF / NIT', required: true },
1611
+ { id: 'email', type: 'email', field: 'email', label: 'Email' },
1612
+ { id: 'telefono', type: 'phone', field: 'telefono', label: 'Telefono' },
1613
+ { id: 'contacto', type: 'text', field: 'contacto', label: 'Persona de Contacto' },
1614
+ { id: 'tipo', type: 'select', field: 'tipo', label: 'Tipo', props: { options: [{ value: 'empresa', label: 'Empresa' }, { value: 'persona', label: 'Persona Natural' }] } },
1615
+ ] },
1616
+ { id: 's2', title: 'Direccion', fields: [
1617
+ { id: 'direccion', type: 'address', field: 'direccion', label: 'Direccion Fiscal', colSpan: 2 },
1618
+ ] },
1619
+ { id: 's3', title: 'Configuracion', fields: [
1620
+ { id: 'limite', type: 'currency', field: 'limiteCredito', label: 'Limite de Credito' },
1621
+ { id: 'plazo', type: 'number', field: 'plazoPago', label: 'Plazo de Pago (dias)' },
1622
+ { id: 'tags', type: 'chips', field: 'tags', label: 'Etiquetas', colSpan: 2, props: { allowCustom: true, colorMode: 'auto', options: [{ value: 'vip', label: 'VIP' }, { value: 'mayorista', label: 'Mayorista' }, { value: 'credito', label: 'Credito' }] } },
1623
+ { id: 'activo', type: 'switch', field: 'activo', label: 'Cliente Activo' },
1624
+ ] },
1625
+ ], actions: [{ id: 'save', type: 'submit', label: 'Guardar Cliente', variant: 'primary' }, { id: 'cancel', type: 'reset', label: 'Cancelar', variant: 'secondary' }] },
1626
+ },
1627
+ {
1628
+ id: 'employee', icon: '👷', title: 'Empleado', desc: 'Datos personales, laborales, salario',
1629
+ schema: { id: 'employee', version: '1.0', title: 'Ficha de Empleado', layout: { type: 'grid', columns: 3, gap: 16 }, sections: [
1630
+ { id: 's1', title: 'Datos Personales', fields: [
1631
+ { id: 'nombre', type: 'text', field: 'nombre', label: 'Nombre', required: true },
1632
+ { id: 'apellido', type: 'text', field: 'apellido', label: 'Apellido', required: true },
1633
+ { id: 'cedula', type: 'text', field: 'cedula', label: 'Cedula', required: true },
1634
+ { id: 'fechaNac', type: 'date', field: 'fechaNacimiento', label: 'Fecha Nacimiento' },
1635
+ { id: 'genero', type: 'select', field: 'genero', label: 'Genero', props: { options: [{ value: 'M', label: 'Masculino' }, { value: 'F', label: 'Femenino' }] } },
1636
+ { id: 'email', type: 'email', field: 'email', label: 'Email' },
1637
+ ] },
1638
+ { id: 's2', title: 'Datos Laborales', fields: [
1639
+ { id: 'fechaIngreso', type: 'date', field: 'fechaIngreso', label: 'Fecha Ingreso', required: true },
1640
+ { id: 'depto', type: 'text', field: 'departamento', label: 'Departamento' },
1641
+ { id: 'cargo', type: 'text', field: 'cargo', label: 'Cargo', required: true },
1642
+ { id: 'salario', type: 'currency', field: 'salario', label: 'Salario', required: true },
1643
+ { id: 'contrato', type: 'select', field: 'tipoContrato', label: 'Contrato', props: { options: [{ value: 'indefinido', label: 'Indefinido' }, { value: 'temporal', label: 'Temporal' }, { value: 'prueba', label: 'Prueba' }] } },
1644
+ { id: 'activo', type: 'switch', field: 'activo', label: 'Activo' },
1645
+ ] },
1646
+ ], actions: [{ id: 'save', type: 'submit', label: 'Guardar Empleado', variant: 'primary' }, { id: 'cancel', type: 'reset', label: 'Cancelar', variant: 'secondary' }] },
1647
+ },
1648
+ {
1649
+ id: 'invoice', icon: '📄', title: 'Factura', desc: 'Cabecera de factura con campos fiscales',
1650
+ schema: { id: 'invoice', version: '1.0', title: 'Factura de Venta', layout: { type: 'grid', columns: 3, gap: 16 }, sections: [
1651
+ { id: 's1', title: 'Datos de la Factura', fields: [
1652
+ { id: 'numero', type: 'text', field: 'numero', label: 'Numero', required: true, readOnly: true },
1653
+ { id: 'fecha', type: 'date', field: 'fecha', label: 'Fecha', required: true },
1654
+ { id: 'vencimiento', type: 'date', field: 'fechaVencimiento', label: 'Vencimiento' },
1655
+ { id: 'cliente', type: 'lookup', field: 'clienteId', label: 'Cliente', required: true, colSpan: 2, props: { placeholder: 'Buscar cliente...' } },
1656
+ { id: 'vendedor', type: 'select', field: 'vendedorId', label: 'Vendedor' },
1657
+ ] },
1658
+ { id: 's2', title: 'Totales', fields: [
1659
+ { id: 'subtotal', type: 'currency', field: 'subtotal', label: 'Subtotal', readOnly: true },
1660
+ { id: 'iva', type: 'currency', field: 'iva', label: 'IVA', readOnly: true },
1661
+ { id: 'total', type: 'currency', field: 'total', label: 'Total', readOnly: true },
1662
+ { id: 'observaciones', type: 'textarea', field: 'observaciones', label: 'Observaciones', colSpan: 3 },
1663
+ ] },
1664
+ ], actions: [{ id: 'save', type: 'submit', label: 'Guardar Factura', variant: 'primary' }, { id: 'cancel', type: 'reset', label: 'Cancelar', variant: 'secondary' }] },
1665
+ },
1666
+ {
1667
+ id: 'product', icon: '📦', title: 'Producto', desc: 'Articulo con precio, stock, imagen',
1668
+ schema: { id: 'product', version: '1.0', title: 'Ficha de Producto', layout: { type: 'grid', columns: 2, gap: 16 }, sections: [
1669
+ { id: 's1', title: 'Datos del Producto', fields: [
1670
+ { id: 'codigo', type: 'text', field: 'codigo', label: 'Codigo', required: true },
1671
+ { id: 'nombre', type: 'text', field: 'nombre', label: 'Nombre', required: true },
1672
+ { id: 'categoria', type: 'select', field: 'categoria', label: 'Categoria', props: { options: [{ value: 'hardware', label: 'Hardware' }, { value: 'software', label: 'Software' }, { value: 'servicio', label: 'Servicio' }] } },
1673
+ { id: 'precio', type: 'currency', field: 'precioVenta', label: 'Precio Venta', required: true },
1674
+ { id: 'costo', type: 'currency', field: 'costo', label: 'Costo' },
1675
+ { id: 'stock', type: 'number', field: 'stock', label: 'Stock Actual' },
1676
+ { id: 'desc', type: 'textarea', field: 'descripcion', label: 'Descripcion', colSpan: 2 },
1677
+ { id: 'activo', type: 'switch', field: 'activo', label: 'Activo' },
1678
+ ] },
1679
+ ], actions: [{ id: 'save', type: 'submit', label: 'Guardar Producto', variant: 'primary' }, { id: 'cancel', type: 'reset', label: 'Cancelar', variant: 'secondary' }] },
1680
+ },
1681
+ {
1682
+ id: 'survey', icon: '📋', title: 'Encuesta', desc: 'Formulario con rating, checkboxes, radio',
1683
+ schema: { id: 'survey', version: '1.0', title: 'Encuesta de Satisfaccion', layout: { type: 'grid', columns: 1, gap: 20 }, sections: [
1684
+ { id: 's1', title: 'Tu Opinion', fields: [
1685
+ { id: 'nombre', type: 'text', field: 'nombre', label: 'Tu Nombre' },
1686
+ { id: 'satisfaccion', type: 'rating', field: 'satisfaccion', label: 'Nivel de Satisfaccion', required: true, props: { mode: 'rating', maxRating: 5 } },
1687
+ { id: 'recomendaria', type: 'radio', field: 'recomendaria', label: 'Recomendarias nuestro servicio?', props: { mode: 'radio', horizontal: true, options: [{ value: 'si', label: 'Si' }, { value: 'no', label: 'No' }, { value: 'tal_vez', label: 'Tal vez' }] } },
1688
+ { id: 'aspectos', type: 'chips', field: 'aspectos', label: 'Que te gusto?', props: { allowCustom: false, colorMode: 'auto', options: [{ value: 'atencion', label: 'Atencion' }, { value: 'rapidez', label: 'Rapidez' }, { value: 'calidad', label: 'Calidad' }, { value: 'precio', label: 'Precio' }] } },
1689
+ { id: 'comentarios', type: 'textarea', field: 'comentarios', label: 'Comentarios adicionales', placeholder: 'Cuentanos mas...' },
1690
+ ] },
1691
+ ], actions: [{ id: 'send', type: 'submit', label: 'Enviar Encuesta', variant: 'primary' }] },
1692
+ },
1693
+ ];
1694
+ return html `
1695
+ <div style="position:absolute;top:100%;left:0;z-index:100;background:white;border:1px solid #ddd;border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,0.15);width:320px;max-height:400px;overflow-y:auto;margin-top:4px;">
1696
+ <div style="padding:10px 12px;border-bottom:1px solid #eee;font-size:11px;font-weight:600;color:#888;text-transform:uppercase;letter-spacing:0.5px;">
1697
+ Cargar Plantilla
1698
+ </div>
1699
+ ${templates.map(t => html `
1700
+ <div style="display:flex;align-items:flex-start;gap:10px;padding:10px 14px;cursor:pointer;transition:background 0.1s;border-bottom:1px solid #f5f5f5;"
1701
+ @click="${() => {
1702
+ this.schema = structuredClone(t.schema);
1703
+ this.undoStack = [JSON.stringify(this.schema)];
1704
+ this.redoStack = [];
1705
+ this.selectedFieldId = null;
1706
+ this.showTemplateMenu = false;
1707
+ this.commitChange();
1708
+ }}"
1709
+ @mouseenter="${(e) => { e.currentTarget.style.background = '#f0f7ff'; }}"
1710
+ @mouseleave="${(e) => { e.currentTarget.style.background = ''; }}"
1711
+ >
1712
+ <span style="font-size:22px;margin-top:1px;">${t.icon}</span>
1713
+ <div>
1714
+ <div style="font-size:13px;font-weight:600;color:#333;">${t.title}</div>
1715
+ <div style="font-size:11px;color:#999;margin-top:1px;">${t.desc}</div>
1716
+ </div>
1717
+ </div>
1718
+ `)}
1719
+ <div style="padding:8px 14px;border-top:1px solid #eee;">
1720
+ <div style="font-size:10px;color:#bbb;text-align:center;">Clic en una plantilla para cargarla</div>
1721
+ </div>
1722
+ </div>
1723
+ `;
1724
+ }
1183
1725
  renderApiAuth() {
1184
1726
  if (this.apiLoggedIn) {
1185
1727
  return html `
@@ -1456,6 +1998,9 @@ __decorate([
1456
1998
  __decorate([
1457
1999
  state()
1458
2000
  ], ZsPageDesigner.prototype, "zoom", void 0);
2001
+ __decorate([
2002
+ state()
2003
+ ], ZsPageDesigner.prototype, "showTemplateMenu", void 0);
1459
2004
  __decorate([
1460
2005
  state()
1461
2006
  ], ZsPageDesigner.prototype, "apiSources", void 0);