@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.
- package/dist/designer/zs-app-wizard.d.ts +15 -0
- package/dist/designer/zs-app-wizard.d.ts.map +1 -1
- package/dist/designer/zs-app-wizard.js +470 -16
- package/dist/designer/zs-app-wizard.js.map +1 -1
- package/dist/designer/zs-page-designer.d.ts +21 -0
- package/dist/designer/zs-page-designer.d.ts.map +1 -1
- package/dist/designer/zs-page-designer.js +547 -2
- package/dist/designer/zs-page-designer.js.map +1 -1
- package/dist/fields/zs-field-datagrid.d.ts +42 -0
- package/dist/fields/zs-field-datagrid.d.ts.map +1 -0
- package/dist/fields/zs-field-datagrid.js +206 -0
- package/dist/fields/zs-field-datagrid.js.map +1 -0
- package/dist/fields/zs-field-report.d.ts +36 -0
- package/dist/fields/zs-field-report.d.ts.map +1 -0
- package/dist/fields/zs-field-report.js +168 -0
- package/dist/fields/zs-field-report.js.map +1 -0
- package/dist/zentto-studio-renderer.d.ts +2 -0
- package/dist/zentto-studio-renderer.d.ts.map +1 -1
- package/dist/zentto-studio-renderer.js +15 -0
- package/dist/zentto-studio-renderer.js.map +1 -1
- package/package.json +12 -1
|
@@ -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="
|
|
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);
|