@zentto/studio 0.1.0 → 0.3.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.
@@ -1,6 +1,6 @@
1
- // @zentto/studio — Runtime Page Designer
2
- // Allows users to drag-drop fields, reorder, resize, and edit properties
3
- // in a live preview. Outputs the modified StudioSchema.
1
+ // @zentto/studio — Professional Page Designer
2
+ // Visual form builder with drag-drop, resize, undo/redo, autosave
3
+ // Styled to match @zentto/report-designer (Figma-style properties, Material colors)
4
4
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
5
5
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
6
6
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -9,436 +9,899 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
9
9
  };
10
10
  import { LitElement, html, css, nothing } from 'lit';
11
11
  import { customElement, property, state } from 'lit/decorators.js';
12
- import { studioTokens, fieldBaseStyles } from '../styles/tokens.js';
13
- import { getAllFields } from '@zentto/studio-core';
14
- // Import renderer for live preview
12
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
13
+ import { getAllFields, getFieldsByCategory, resolveIcon } from '@zentto/studio-core';
15
14
  import '../zentto-studio-renderer.js';
15
+ // ─── Design tokens (matching report-designer) ─────────────────────
16
+ const ACCENT = '#1976d2';
17
+ const ACCENT_LIGHT = '#e3f2fd';
18
+ const DANGER = '#d32f2f';
19
+ const TEXT = '#333';
20
+ const TEXT_MUTED = '#888';
21
+ const BORDER = '#ddd';
22
+ const BG = '#f5f5f5';
23
+ const PANEL_BG = '#ffffff';
24
+ const FIELD_TYPE_COLORS = {
25
+ text: 'background:#e3f2fd;color:#1565c0;',
26
+ textarea: 'background:#e3f2fd;color:#1565c0;',
27
+ number: 'background:#e8f5e9;color:#2e7d32;',
28
+ currency: 'background:#e8f5e9;color:#2e7d32;',
29
+ percentage: 'background:#e8f5e9;color:#2e7d32;',
30
+ select: 'background:#f3e5f5;color:#7b1fa2;',
31
+ multiselect: 'background:#f3e5f5;color:#7b1fa2;',
32
+ date: 'background:#fff3e0;color:#e65100;',
33
+ time: 'background:#fff3e0;color:#e65100;',
34
+ datetime: 'background:#fff3e0;color:#e65100;',
35
+ checkbox: 'background:#fce4ec;color:#c62828;',
36
+ radio: 'background:#fce4ec;color:#c62828;',
37
+ switch: 'background:#fce4ec;color:#c62828;',
38
+ email: 'background:#e0f7fa;color:#00695c;',
39
+ phone: 'background:#e0f7fa;color:#00695c;',
40
+ url: 'background:#e0f7fa;color:#00695c;',
41
+ password: 'background:#eceff1;color:#546e7a;',
42
+ file: 'background:#fff8e1;color:#f57f17;',
43
+ image: 'background:#fff8e1;color:#f57f17;',
44
+ signature: 'background:#ede7f6;color:#4527a0;',
45
+ address: 'background:#e0f2f1;color:#004d40;',
46
+ lookup: 'background:#e8eaf6;color:#283593;',
47
+ chips: 'background:#fce4ec;color:#880e4f;',
48
+ treeview: 'background:#e8f5e9;color:#1b5e20;',
49
+ datagrid: 'background:#e3f2fd;color:#0d47a1;',
50
+ report: 'background:#fff3e0;color:#bf360c;',
51
+ chart: 'background:#f3e5f5;color:#6a1b9a;',
52
+ separator: 'background:#eceff1;color:#546e7a;',
53
+ heading: 'background:#eceff1;color:#37474f;',
54
+ html: 'background:#fff3e0;color:#e65100;',
55
+ rating: 'background:#fff8e1;color:#f57f17;',
56
+ slider: 'background:#e8f5e9;color:#2e7d32;',
57
+ media: 'background:#f3e5f5;color:#7b1fa2;',
58
+ custom: 'background:#eceff1;color:#546e7a;',
59
+ };
16
60
  let ZsPageDesigner = class ZsPageDesigner extends LitElement {
17
61
  constructor() {
18
62
  super(...arguments);
63
+ // ─── Properties ───────────────────────────────────
19
64
  this.schema = null;
20
65
  this.data = {};
66
+ this.provider = {};
67
+ this.autoSaveMs = 1000;
68
+ this.gridSnap = 1;
69
+ // ─── State ────────────────────────────────────────
21
70
  this.selectedFieldId = null;
22
71
  this.viewMode = 'design';
72
+ this.leftTab = 'fields';
23
73
  this.dragType = null;
24
- this.renderKey = 0;
74
+ this.editingTitle = false;
75
+ this.collapsedSections = new Set();
76
+ this.zoom = 1;
77
+ // Undo/Redo
78
+ this.undoStack = [];
79
+ this.redoStack = [];
80
+ this.saveTimer = null;
81
+ }
82
+ static { this.styles = css `
83
+ :host {
84
+ display: block; height: 100%;
85
+ --zrd-bg: ${unsafeCSS(BG)};
86
+ --zrd-panel-bg: ${unsafeCSS(PANEL_BG)};
87
+ --zrd-border: ${unsafeCSS(BORDER)};
88
+ --zrd-accent: ${unsafeCSS(ACCENT)};
89
+ --zrd-accent-light: ${unsafeCSS(ACCENT_LIGHT)};
90
+ --zrd-text: ${unsafeCSS(TEXT)};
91
+ --zrd-text-muted: ${unsafeCSS(TEXT_MUTED)};
92
+ --zrd-danger: ${unsafeCSS(DANGER)};
93
+ font-family: 'Segoe UI', Roboto, Arial, sans-serif;
25
94
  }
26
- static { this.styles = [studioTokens, fieldBaseStyles, css `
27
- :host { display: block; font-family: var(--zs-font-family); height: 100%; }
28
95
 
96
+ /* ─── Layout ──────────────────────────────── */
29
97
  .designer {
30
- display: grid; grid-template-columns: 220px 1fr 280px;
31
- height: 100%; overflow: hidden;
32
- border: 1px solid var(--zs-border); border-radius: 8px;
98
+ display: grid;
99
+ grid-template-rows: auto 1fr;
100
+ grid-template-columns: 200px 4px 1fr 4px 240px;
101
+ height: 100%; background: var(--zrd-bg);
102
+ overflow: hidden;
33
103
  }
34
104
 
35
- /* ─── Toolbox (Left) ──────────────────────── */
36
- .toolbox {
37
- background: var(--zs-bg-secondary); border-right: 1px solid var(--zs-border);
38
- overflow-y: auto; padding: 12px;
105
+ /* ─── Toolbar ─────────────────────────────── */
106
+ .toolbar {
107
+ grid-column: 1 / -1;
108
+ display: flex; align-items: center; gap: 8px;
109
+ padding: 6px 12px; min-height: 36px;
110
+ background: var(--zrd-panel-bg);
111
+ border-bottom: 1px solid var(--zrd-border);
112
+ flex-wrap: wrap;
113
+ }
114
+ .toolbar-sep { width: 1px; height: 20px; background: var(--zrd-border); margin: 0 2px; }
115
+ .tb-btn {
116
+ background: none; border: 1px solid var(--zrd-border);
117
+ border-radius: 4px; padding: 4px 10px;
118
+ cursor: pointer; font-size: 12px; color: var(--zrd-text);
119
+ font-family: inherit; transition: background 0.15s;
120
+ white-space: nowrap; display: flex; align-items: center; gap: 4px;
121
+ }
122
+ .tb-btn:hover { background: var(--zrd-accent-light); }
123
+ .tb-btn:disabled { opacity: 0.4; cursor: default; }
124
+ .tb-btn--active { background: var(--zrd-accent); color: white; border-color: var(--zrd-accent); }
125
+ .tb-btn--danger:hover { background: #ffebee; color: var(--zrd-danger); border-color: var(--zrd-danger); }
126
+ .report-name {
127
+ font-weight: 600; font-size: 14px; cursor: pointer;
128
+ padding: 2px 6px; border-radius: 3px; border: 1px solid transparent;
129
+ color: var(--zrd-text);
130
+ }
131
+ .report-name:hover { border-color: var(--zrd-border); background: var(--zrd-accent-light); }
132
+ .report-name-input {
133
+ font-weight: 600; font-size: 14px; border: 1px solid var(--zrd-accent);
134
+ border-radius: 3px; padding: 2px 6px; outline: none;
135
+ background: white; color: var(--zrd-text); font-family: inherit;
136
+ }
137
+ .tb-spacer { flex: 1; }
138
+ .zoom-controls {
139
+ display: flex; align-items: center; gap: 4px;
140
+ padding-left: 8px; border-left: 1px solid var(--zrd-border);
141
+ }
142
+ .zoom-btn { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 3px; font-size: 14px; font-weight: bold; }
143
+ .zoom-label { font-size: 11px; min-width: 36px; text-align: center; color: var(--zrd-text-muted); cursor: pointer; }
144
+
145
+ /* ─── Resize Handle ───────────────────────── */
146
+ .panel-resize {
147
+ width: 4px; cursor: col-resize; background: transparent;
148
+ transition: background 0.15s; flex-shrink: 0;
39
149
  }
40
- .toolbox-title {
41
- font-size: 11px; font-weight: 600; text-transform: uppercase;
42
- letter-spacing: 0.5px; color: var(--zs-text-secondary);
43
- margin: 12px 0 8px; padding: 0 4px;
150
+ .panel-resize:hover { background: var(--zrd-accent); }
151
+
152
+ /* ─── Left Panel (Toolbox) ────────────────── */
153
+ .left-panel {
154
+ background: var(--zrd-panel-bg);
155
+ border-right: 1px solid var(--zrd-border);
156
+ display: flex; flex-direction: column; overflow: hidden;
157
+ }
158
+ .panel-tabs {
159
+ display: flex; border-bottom: 1px solid var(--zrd-border);
160
+ }
161
+ .panel-tab {
162
+ flex: 1; padding: 8px 4px; text-align: center; cursor: pointer;
163
+ font-size: 11px; border-bottom: 2px solid transparent;
164
+ color: var(--zrd-text-muted); transition: all 0.15s;
165
+ background: none; border-top: none; border-left: none; border-right: none;
166
+ font-family: inherit;
167
+ }
168
+ .panel-tab:hover { color: var(--zrd-text); }
169
+ .panel-tab--active { border-bottom-color: var(--zrd-accent); color: var(--zrd-accent); font-weight: 600; }
170
+ .panel-content { padding: 4px; overflow-y: auto; flex: 1; }
171
+ .panel-content::-webkit-scrollbar { width: 6px; }
172
+ .panel-content::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
173
+
174
+ /* Toolbox grid (3 columns like report-designer) */
175
+ .toolbox-section {
176
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
177
+ letter-spacing: 0.5px; color: var(--zrd-text-muted);
178
+ padding: 8px 6px 4px; margin-top: 4px;
179
+ }
180
+ .toolbox-section:first-child { margin-top: 0; }
181
+ .toolbox-grid {
182
+ display: grid; grid-template-columns: repeat(3, 1fr);
183
+ gap: 2px; padding: 2px;
44
184
  }
45
- .toolbox-title:first-child { margin-top: 0; }
46
185
  .toolbox-item {
47
- display: flex; align-items: center; gap: 8px;
48
- padding: 8px 10px; border-radius: 6px;
49
- cursor: grab; font-size: 13px; color: var(--zs-text);
50
- transition: all 100ms; margin-bottom: 2px;
51
- border: 1px solid transparent;
186
+ display: flex; flex-direction: column; align-items: center;
187
+ gap: 1px; padding: 5px 2px 4px;
188
+ border: 1px solid transparent; border-radius: 3px;
189
+ cursor: grab; user-select: none; text-align: center;
190
+ transition: all 0.15s;
191
+ }
192
+ .toolbox-item:hover { background: var(--zrd-accent-light); border-color: var(--zrd-accent); }
193
+ .toolbox-item:active { cursor: grabbing; opacity: 0.7; }
194
+ .toolbox-icon {
195
+ width: 22px; height: 22px; display: flex; align-items: center;
196
+ justify-content: center; border-radius: 3px;
197
+ font-weight: bold; font-size: 14px; color: var(--zrd-accent); flex-shrink: 0;
198
+ }
199
+ .toolbox-label {
200
+ font-size: 8px; font-weight: 500; line-height: 1;
201
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
202
+ max-width: 100%; color: var(--zrd-text-muted);
52
203
  }
53
- .toolbox-item:hover { background: var(--zs-bg-hover); border-color: var(--zs-border); }
54
- .toolbox-item:active { cursor: grabbing; background: var(--zs-primary-light); border-color: var(--zs-primary); }
55
- .toolbox-item-icon { font-size: 16px; width: 24px; text-align: center; }
56
- .toolbox-item-label { flex: 1; }
57
204
 
58
- /* ─── Canvas (Center) ─────────────────────── */
205
+ /* ─── Canvas ──────────────────────────────── */
206
+ .canvas-area {
207
+ overflow: auto; padding: 30px; position: relative;
208
+ background: #d0d0d0; display: flex; justify-content: center;
209
+ }
59
210
  .canvas {
60
- overflow-y: auto; padding: 24px;
61
- background: #f5f5f5; position: relative;
211
+ background: white; position: relative; margin: 0 auto; border-radius: 2px;
212
+ box-shadow: 0 4px 20px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.08);
213
+ min-width: 600px; min-height: 400px; padding: 24px;
214
+ transform-origin: top center;
215
+ }
216
+ .canvas-section { margin-bottom: 20px; }
217
+ .canvas-section-header {
218
+ font-size: 13px; font-weight: 600; color: var(--zrd-text);
219
+ padding: 6px 8px; margin-bottom: 8px;
220
+ background: #f0f0f0; border-radius: 4px; border-left: 3px solid var(--zrd-accent);
221
+ display: flex; align-items: center; gap: 8px; cursor: pointer;
222
+ }
223
+ .canvas-section-header:hover { background: var(--zrd-accent-light); }
224
+ .canvas-grid {
225
+ display: grid; gap: 8px; padding: 4px;
226
+ background: repeating-linear-gradient(0deg, transparent, transparent 19px, rgba(0,0,0,0.03) 19px, rgba(0,0,0,0.03) 20px),
227
+ repeating-linear-gradient(90deg, transparent, transparent 19px, rgba(0,0,0,0.03) 19px, rgba(0,0,0,0.03) 20px);
62
228
  }
63
- .canvas-header {
64
- display: flex; align-items: center; gap: 8px;
65
- margin-bottom: 16px; padding: 0 4px;
66
- }
67
- .canvas-title { font-size: 16px; font-weight: 600; color: var(--zs-text); }
68
- .canvas-actions { margin-left: auto; display: flex; gap: 6px; }
69
- .canvas-btn {
70
- padding: 5px 12px; border: 1px solid var(--zs-border);
71
- border-radius: 4px; background: var(--zs-bg); cursor: pointer;
72
- font-size: 12px; color: var(--zs-text-secondary);
73
- transition: all 100ms;
74
- }
75
- .canvas-btn:hover { background: var(--zs-bg-hover); color: var(--zs-text); }
76
- .canvas-btn--active { background: var(--zs-primary); color: white; border-color: var(--zs-primary); }
77
-
78
- /* Preview wrapper */
79
- .preview-frame {
80
- background: var(--zs-bg); border-radius: 8px;
81
- border: 1px solid var(--zs-border);
82
- box-shadow: 0 2px 8px var(--zs-shadow);
83
- padding: 16px; min-height: 300px;
84
- }
85
-
86
- /* Field overlay (clickable areas on preview) */
87
- .field-overlay {
88
- position: relative; cursor: pointer;
89
- border: 2px solid transparent; border-radius: 4px;
90
- transition: all 100ms; margin: -2px;
91
- padding: 2px;
92
- }
93
- .field-overlay:hover { border-color: var(--zs-accent); background: rgba(52, 152, 219, 0.03); }
94
- .field-overlay--selected { border-color: var(--zs-primary); background: rgba(230, 126, 34, 0.05); }
95
- .field-overlay-label {
96
- position: absolute; top: -10px; left: 8px;
97
- font-size: 10px; background: var(--zs-primary); color: white;
98
- padding: 1px 6px; border-radius: 3px;
99
- opacity: 0; transition: opacity 100ms;
100
- }
101
- .field-overlay:hover .field-overlay-label,
102
- .field-overlay--selected .field-overlay-label { opacity: 1; }
103
- .field-overlay-actions {
104
- position: absolute; top: -10px; right: 4px;
105
- display: flex; gap: 2px;
106
- opacity: 0; transition: opacity 100ms;
107
- }
108
- .field-overlay:hover .field-overlay-actions { opacity: 1; }
109
- .field-overlay-btn {
110
- width: 20px; height: 20px; border-radius: 3px;
111
- border: none; cursor: pointer; font-size: 11px;
229
+
230
+ /* Field on canvas */
231
+ .canvas-field {
232
+ position: relative; border: 1px solid transparent;
233
+ border-radius: 4px; cursor: pointer; user-select: none;
234
+ transition: border-color 0.15s, box-shadow 0.15s;
235
+ padding: 4px;
236
+ }
237
+ .canvas-field:hover { border-color: rgba(25,118,210,0.5); }
238
+ .canvas-field--selected {
239
+ border-color: var(--zrd-accent);
240
+ box-shadow: 0 0 0 1px var(--zrd-accent);
241
+ }
242
+
243
+ /* Type badge (above field) */
244
+ .field-type-badge {
245
+ position: absolute; top: -14px; left: 0;
246
+ font-size: 9px; border-radius: 3px 3px 0 0;
247
+ padding: 1px 6px; line-height: 13px;
248
+ pointer-events: none; z-index: 6; display: none;
249
+ white-space: nowrap;
250
+ }
251
+ .canvas-field:hover .field-type-badge,
252
+ .canvas-field--selected .field-type-badge { display: block; }
253
+
254
+ /* Resize handles (8-point like report-designer) */
255
+ .rh { position: absolute; width: 6px; height: 6px; background: var(--zrd-accent); border: 1px solid white; z-index: 5; display: none; border-radius: 1px; }
256
+ .rh:hover { background: #0d47a1; }
257
+ .canvas-field--selected .rh { display: block; }
258
+ .rh-nw { top: -3px; left: -3px; cursor: nw-resize; }
259
+ .rh-n { top: -3px; left: calc(50% - 3px); cursor: n-resize; }
260
+ .rh-ne { top: -3px; right: -3px; cursor: ne-resize; }
261
+ .rh-e { top: calc(50% - 3px); right: -3px; cursor: e-resize; }
262
+ .rh-se { bottom: -3px; right: -3px; cursor: se-resize; }
263
+ .rh-s { bottom: -3px; left: calc(50% - 3px); cursor: s-resize; }
264
+ .rh-sw { bottom: -3px; left: -3px; cursor: sw-resize; }
265
+ .rh-w { top: calc(50% - 3px); left: -3px; cursor: w-resize; }
266
+
267
+ /* Action buttons on field */
268
+ .field-actions {
269
+ position: absolute; top: -14px; right: 4px;
270
+ display: flex; gap: 2px; opacity: 0; transition: opacity 0.15s; z-index: 7;
271
+ }
272
+ .canvas-field:hover .field-actions { opacity: 1; }
273
+ .fa-btn {
274
+ width: 18px; height: 14px; border-radius: 3px 3px 0 0;
275
+ border: none; cursor: pointer; font-size: 9px;
112
276
  display: flex; align-items: center; justify-content: center;
113
- transition: all 100ms;
114
277
  }
115
- .field-overlay-btn--move { background: var(--zs-accent); color: white; }
116
- .field-overlay-btn--delete { background: var(--zs-danger); color: white; }
278
+ .fa-btn--move { background: var(--zrd-accent); color: white; }
279
+ .fa-btn--delete { background: var(--zrd-danger); color: white; }
280
+ .fa-btn--copy { background: #7c3aed; color: white; }
117
281
 
118
- /* Drop zones */
282
+ /* Field preview content */
283
+ .field-preview {
284
+ pointer-events: none;
285
+ }
286
+ .field-preview-label {
287
+ font-size: 11px; font-weight: 500; color: var(--zrd-text-muted);
288
+ margin-bottom: 3px;
289
+ }
290
+ .field-preview-input {
291
+ height: 30px; border: 1px solid #e0e0e0; border-radius: 4px;
292
+ background: #fafafa; display: flex; align-items: center;
293
+ padding: 0 8px; font-size: 12px; color: #999;
294
+ }
295
+ .field-preview-input--textarea { height: 60px; align-items: flex-start; padding-top: 6px; }
296
+ .field-preview-input--switch { height: auto; border: none; background: none; padding: 0; }
297
+ .field-preview-input--separator { height: 1px; border: none; background: #ddd; padding: 0; }
298
+ .field-preview-input--heading { height: auto; border: none; background: none; padding: 0; font-size: 16px; font-weight: 600; color: var(--zrd-text); }
299
+ .field-preview-input--datagrid { height: 120px; border: 2px dashed var(--zrd-accent); background: var(--zrd-accent-light); justify-content: center; font-weight: 500; color: var(--zrd-accent); }
300
+ .field-preview-input--report { height: 120px; border: 2px dashed #e65100; background: #fff3e0; justify-content: center; font-weight: 500; color: #e65100; }
301
+ .field-preview-input--chart { height: 120px; border: 2px dashed #6a1b9a; background: #f3e5f5; justify-content: center; font-weight: 500; color: #6a1b9a; }
302
+
303
+ /* Drop zone */
119
304
  .drop-zone {
120
- border: 2px dashed var(--zs-border); border-radius: 6px;
121
- padding: 20px; text-align: center; margin: 8px 0;
122
- color: var(--zs-text-muted); font-size: 13px;
123
- transition: all 150ms;
305
+ border: 2px dashed var(--zrd-border); border-radius: 6px;
306
+ padding: 16px; text-align: center; margin: 4px 0;
307
+ color: var(--zrd-text-muted); font-size: 12px; transition: all 0.15s;
124
308
  }
125
- .drop-zone--active { border-color: var(--zs-primary); background: var(--zs-primary-light); color: var(--zs-primary); }
309
+ .drop-zone--active { border-color: var(--zrd-accent); background: var(--zrd-accent-light); color: var(--zrd-accent); }
126
310
 
127
- /* ─── Properties (Right) ──────────────────── */
128
- .properties {
129
- background: var(--zs-bg); border-left: 1px solid var(--zs-border);
130
- overflow-y: auto; padding: 16px;
131
- }
132
- .props-title {
311
+ /* ─── Right Panel (Properties) ────────────── */
312
+ .right-panel {
313
+ background: var(--zrd-panel-bg);
314
+ border-left: 1px solid var(--zrd-border);
315
+ overflow-y: auto;
316
+ }
317
+ .right-panel::-webkit-scrollbar { width: 6px; }
318
+ .right-panel::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
319
+
320
+ /* Property sections (Figma-style) */
321
+ .prop-section { border-bottom: 1px solid var(--zrd-border); padding: 10px 12px; }
322
+ .prop-section:last-child { border-bottom: none; }
323
+ .prop-section-header {
324
+ display: flex; align-items: center; justify-content: space-between;
325
+ cursor: pointer; user-select: none; margin-bottom: 8px;
326
+ }
327
+ .prop-section-header h4 {
133
328
  font-size: 11px; font-weight: 600; text-transform: uppercase;
134
- letter-spacing: 0.5px; color: var(--zs-text-secondary);
135
- margin: 16px 0 8px; padding-bottom: 4px;
136
- border-bottom: 1px solid var(--zs-border);
137
- }
138
- .props-title:first-child { margin-top: 0; }
139
- .props-row { margin-bottom: 12px; }
140
- .props-label {
141
- font-size: 12px; font-weight: 500; color: var(--zs-text-secondary);
142
- margin-bottom: 4px; display: block;
143
- }
144
- .props-input {
145
- width: 100%; padding: 6px 10px; border: 1px solid var(--zs-border);
146
- border-radius: 4px; font-size: 13px; font-family: var(--zs-font-family);
147
- background: var(--zs-bg); color: var(--zs-text);
148
- outline: none; box-sizing: border-box;
149
- }
150
- .props-input:focus { border-color: var(--zs-primary); }
151
- .props-select { height: 32px; }
152
- .props-checkbox {
153
- display: flex; align-items: center; gap: 6px;
154
- font-size: 13px; cursor: pointer;
155
- }
156
- .props-checkbox input { accent-color: var(--zs-primary); }
157
- .props-empty {
158
- text-align: center; padding: 40px 16px;
159
- color: var(--zs-text-muted); font-size: 13px;
160
- }
161
- .props-field-type {
162
- display: inline-block; padding: 2px 8px;
163
- background: var(--zs-primary-light); color: var(--zs-primary);
164
- border-radius: 4px; font-size: 11px; font-weight: 600;
165
- margin-bottom: 12px;
166
- }
167
-
168
- /* JSON panel */
329
+ letter-spacing: 0.5px; margin: 0;
330
+ }
331
+ .prop-section-header[data-section="general"] h4 { color: var(--zrd-accent); }
332
+ .prop-section-header[data-section="layout"] h4 { color: #7c3aed; }
333
+ .prop-section-header[data-section="behavior"] h4 { color: #0d9488; }
334
+ .prop-section-header[data-section="rules"] h4 { color: #ea580c; }
335
+ .prop-section-header[data-section="form"] h4 { color: var(--zrd-accent); }
336
+ .collapse-icon { font-size: 10px; color: var(--zrd-text-muted); transition: transform 0.15s; }
337
+ .collapse-icon--collapsed { transform: rotate(-90deg); }
338
+
339
+ /* Property rows (Figma-style) */
340
+ .prop-row {
341
+ display: grid; grid-template-columns: 68px 1fr;
342
+ align-items: center; gap: 6px; min-height: 28px;
343
+ }
344
+ .prop-row-full { grid-template-columns: 1fr; }
345
+ .prop-label {
346
+ font-size: 11px; color: var(--zrd-text-muted);
347
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-left: 2px;
348
+ }
349
+ .prop-input {
350
+ width: 100%; border: 1px solid transparent; border-radius: 4px;
351
+ padding: 4px 8px; font-size: 12px; background: var(--zrd-bg);
352
+ color: var(--zrd-text); transition: border-color 0.15s, background 0.15s;
353
+ outline: none; min-width: 0; font-family: inherit; box-sizing: border-box;
354
+ }
355
+ .prop-input:hover { border-color: var(--zrd-border); }
356
+ .prop-input:focus { border-color: var(--zrd-accent); background: white; }
357
+ select.prop-input {
358
+ padding: 3px 6px; cursor: pointer; appearance: none;
359
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E");
360
+ background-repeat: no-repeat; background-position: right 6px center; padding-right: 20px;
361
+ }
362
+ textarea.prop-input { min-height: 52px; resize: vertical; font-family: 'Consolas',monospace; font-size: 11px; line-height: 1.5; }
363
+
364
+ /* Position grid (2x2 like report-designer) */
365
+ .prop-grid-2x2 { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
366
+ .prop-grid-item {
367
+ display: flex; align-items: center; background: var(--zrd-bg);
368
+ border: 1px solid transparent; border-radius: 4px; padding: 2px 6px; gap: 4px;
369
+ transition: border-color 0.15s;
370
+ }
371
+ .prop-grid-item:hover { border-color: var(--zrd-border); }
372
+ .prop-grid-item:focus-within { border-color: var(--zrd-accent); }
373
+ .prop-grid-label {
374
+ font-size: 10px; color: var(--zrd-accent); font-weight: 600;
375
+ width: 12px; text-align: center; flex-shrink: 0;
376
+ }
377
+ .prop-grid-item input {
378
+ border: none; background: transparent; width: 100%;
379
+ font-size: 12px; color: var(--zrd-text); outline: none; padding: 2px 0; min-width: 0;
380
+ font-family: inherit;
381
+ }
382
+
383
+ /* Toggle switches (report-designer style) */
384
+ .prop-toggle { display: flex; align-items: center; gap: 8px; min-height: 28px; }
385
+ .prop-switch {
386
+ position: relative; width: 32px; height: 18px; border-radius: 9px;
387
+ background: #ccc; cursor: pointer; transition: background 0.2s; flex-shrink: 0;
388
+ border: none; padding: 0;
389
+ }
390
+ .prop-switch--active { background: var(--zrd-accent); }
391
+ .prop-switch::after {
392
+ content: ''; position: absolute; top: 2px; left: 2px;
393
+ width: 14px; height: 14px; border-radius: 50%;
394
+ background: white; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
395
+ transition: transform 0.2s;
396
+ }
397
+ .prop-switch--active::after { transform: translateX(14px); }
398
+ .prop-toggle-label { font-size: 11px; color: var(--zrd-text-muted); }
399
+
400
+ /* Empty state */
401
+ .props-empty { text-align: center; padding: 40px 16px; color: var(--zrd-text-muted); font-size: 12px; }
402
+ .props-empty-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.3; }
403
+
404
+ /* Type badge in properties */
405
+ .props-type-badge {
406
+ display: inline-block; padding: 3px 10px; border-radius: 4px;
407
+ font-size: 11px; font-weight: 600; margin-bottom: 12px;
408
+ }
409
+
410
+ /* JSON view */
169
411
  .json-panel {
170
- font-family: 'Fira Code', 'Consolas', monospace;
171
- font-size: 12px; line-height: 1.5;
172
- background: #1e1e2d; color: #a2a3b7;
173
- padding: 16px; border-radius: 8px;
174
- overflow: auto; max-height: 400px;
175
- white-space: pre; tab-size: 2;
176
- }
177
- `]; }
412
+ font-family: 'Consolas','Monaco',monospace; font-size: 11px; line-height: 1.5;
413
+ background: #1e1e2d; color: #a2a3b7; padding: 16px; border-radius: 4px;
414
+ overflow: auto; white-space: pre; tab-size: 2;
415
+ }
416
+ `; }
417
+ // ─── Lifecycle ────────────────────────────────────
418
+ updated(changed) {
419
+ if (changed.has('schema') && this.schema && this.undoStack.length === 0) {
420
+ this.undoStack = [JSON.stringify(this.schema)];
421
+ }
422
+ }
423
+ // ─── Undo/Redo ────────────────────────────────────
424
+ pushUndo() {
425
+ if (!this.schema)
426
+ return;
427
+ this.undoStack.push(JSON.stringify(this.schema));
428
+ this.redoStack = [];
429
+ if (this.undoStack.length > 50)
430
+ this.undoStack = this.undoStack.slice(-50);
431
+ }
432
+ undo() {
433
+ if (this.undoStack.length <= 1)
434
+ return;
435
+ const current = this.undoStack.pop();
436
+ this.redoStack.push(current);
437
+ this.schema = JSON.parse(this.undoStack[this.undoStack.length - 1]);
438
+ this.emitChange();
439
+ }
440
+ redo() {
441
+ if (this.redoStack.length === 0)
442
+ return;
443
+ const next = this.redoStack.pop();
444
+ this.undoStack.push(next);
445
+ this.schema = JSON.parse(next);
446
+ this.emitChange();
447
+ }
448
+ commitChange() {
449
+ this.pushUndo();
450
+ this.emitChange();
451
+ this.requestUpdate();
452
+ }
453
+ emitChange() {
454
+ this.dispatchEvent(new CustomEvent('schema-change', {
455
+ detail: { schema: structuredClone(this.schema) },
456
+ bubbles: true, composed: true,
457
+ }));
458
+ if (this.autoSaveMs > 0) {
459
+ if (this.saveTimer)
460
+ clearTimeout(this.saveTimer);
461
+ this.saveTimer = setTimeout(() => {
462
+ this.dispatchEvent(new CustomEvent('auto-save', {
463
+ detail: { schema: structuredClone(this.schema) },
464
+ bubbles: true, composed: true,
465
+ }));
466
+ }, this.autoSaveMs);
467
+ }
468
+ this.requestUpdate();
469
+ }
470
+ // ─── Field helpers ────────────────────────────────
178
471
  get selectedField() {
179
472
  if (!this.schema || !this.selectedFieldId)
180
473
  return null;
181
- for (const section of this.schema.sections) {
182
- const f = section.fields.find(f => f.id === this.selectedFieldId);
474
+ for (const s of this.schema.sections) {
475
+ const f = s.fields.find(f => f.id === this.selectedFieldId);
183
476
  if (f)
184
477
  return f;
185
478
  }
186
479
  return null;
187
480
  }
188
- get selectedSectionIndex() {
189
- if (!this.schema || !this.selectedFieldId)
481
+ findFieldSection(fieldId) {
482
+ if (!this.schema)
190
483
  return -1;
191
- return this.schema.sections.findIndex(s => s.fields.some(f => f.id === this.selectedFieldId));
484
+ return this.schema.sections.findIndex(s => s.fields.some(f => f.id === fieldId));
485
+ }
486
+ addField(type, sectionIndex = 0) {
487
+ if (!this.schema) {
488
+ this.schema = {
489
+ id: 'new-form', version: '1.0', title: 'Nuevo Formulario',
490
+ layout: { type: 'grid', columns: 2 },
491
+ sections: [{ id: 'main', title: 'Datos', fields: [] }],
492
+ };
493
+ this.undoStack = [JSON.stringify(this.schema)];
494
+ }
495
+ if (sectionIndex >= this.schema.sections.length)
496
+ sectionIndex = 0;
497
+ const id = `${type}_${Date.now()}`;
498
+ const meta = getAllFields().find(f => f.type === type);
499
+ this.schema.sections[sectionIndex].fields.push({
500
+ id, type, field: id,
501
+ label: meta?.label ?? type,
502
+ props: meta?.defaultProps ? { ...meta.defaultProps } : undefined,
503
+ });
504
+ this.selectedFieldId = id;
505
+ this.commitChange();
506
+ }
507
+ removeField(si, fi) {
508
+ if (!this.schema)
509
+ return;
510
+ const f = this.schema.sections[si].fields[fi];
511
+ if (this.selectedFieldId === f.id)
512
+ this.selectedFieldId = null;
513
+ this.schema.sections[si].fields.splice(fi, 1);
514
+ this.commitChange();
515
+ }
516
+ moveField(si, fi, dir) {
517
+ if (!this.schema)
518
+ return;
519
+ const fields = this.schema.sections[si].fields;
520
+ const ni = fi + dir;
521
+ if (ni < 0 || ni >= fields.length)
522
+ return;
523
+ [fields[fi], fields[ni]] = [fields[ni], fields[fi]];
524
+ this.commitChange();
525
+ }
526
+ duplicateField(si, fi) {
527
+ if (!this.schema)
528
+ return;
529
+ const original = this.schema.sections[si].fields[fi];
530
+ const clone = structuredClone(original);
531
+ clone.id = `${clone.type}_${Date.now()}`;
532
+ clone.field = clone.id;
533
+ this.schema.sections[si].fields.splice(fi + 1, 0, clone);
534
+ this.selectedFieldId = clone.id;
535
+ this.commitChange();
192
536
  }
193
537
  // ─── Render ───────────────────────────────────────
194
538
  render() {
195
539
  return html `
196
540
  <div class="designer">
197
- ${this.renderToolbox()}
541
+ ${this.renderToolbar()}
542
+ ${this.renderLeftPanel()}
543
+ <div class="panel-resize"></div>
198
544
  ${this.renderCanvas()}
199
- ${this.renderProperties()}
545
+ <div class="panel-resize"></div>
546
+ ${this.renderRightPanel()}
200
547
  </div>
201
548
  `;
202
549
  }
203
- // ─── Toolbox ──────────────────────────────────────
204
- renderToolbox() {
205
- const fields = getAllFields();
206
- const categories = ['basic', 'advanced', 'data', 'media', 'layout'];
207
- const categoryNames = {
208
- basic: 'Basicos', advanced: 'Avanzados',
209
- data: 'Datos', media: 'Media', layout: 'Layout',
210
- };
550
+ // ─── Toolbar ──────────────────────────────────────
551
+ renderToolbar() {
211
552
  return html `
212
- <div class="toolbox">
213
- ${categories.map(cat => {
214
- const items = fields.filter(f => f.category === cat);
553
+ <div class="toolbar">
554
+ ${this.editingTitle
555
+ ? html `<input class="report-name-input" .value="${this.schema?.title ?? ''}"
556
+ @blur="${(e) => { if (this.schema)
557
+ this.schema.title = e.target.value; this.editingTitle = false; this.commitChange(); }}"
558
+ @keydown="${(e) => { if (e.key === 'Enter')
559
+ e.target.blur(); }}"
560
+ />`
561
+ : html `<span class="report-name" @click="${() => { this.editingTitle = true; }}">${this.schema?.title || 'Sin titulo'}</span>`}
562
+
563
+ <div class="toolbar-sep"></div>
564
+
565
+ <button class="tb-btn" ?disabled="${this.undoStack.length <= 1}" @click="${this.undo}" title="Deshacer (Ctrl+Z)">↩ Deshacer</button>
566
+ <button class="tb-btn" ?disabled="${this.redoStack.length === 0}" @click="${this.redo}" title="Rehacer (Ctrl+Y)">↪ Rehacer</button>
567
+
568
+ <div class="toolbar-sep"></div>
569
+
570
+ <button class="tb-btn" @click="${() => {
571
+ if (!this.schema)
572
+ return;
573
+ this.schema.sections.push({ id: `section_${Date.now()}`, title: 'Nueva Seccion', fields: [] });
574
+ this.commitChange();
575
+ }}">+ Seccion</button>
576
+
577
+ <div class="toolbar-sep"></div>
578
+
579
+ <button class="tb-btn ${this.viewMode === 'design' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'design'; }}">✏️ Diseño</button>
580
+ <button class="tb-btn ${this.viewMode === 'preview' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'preview'; }}">👁 Preview</button>
581
+ <button class="tb-btn ${this.viewMode === 'json' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'json'; }}">&lt;/&gt; JSON</button>
582
+
583
+ <span class="tb-spacer"></span>
584
+
585
+ <button class="tb-btn" @click="${() => {
586
+ if (this.schema) {
587
+ navigator.clipboard.writeText(JSON.stringify(this.schema, null, 2));
588
+ }
589
+ }}">📋 Copiar</button>
590
+
591
+ <div class="zoom-controls">
592
+ <button class="tb-btn zoom-btn" @click="${() => { this.zoom = Math.max(0.5, this.zoom - 0.1); }}">−</button>
593
+ <span class="zoom-label" @click="${() => { this.zoom = 1; }}">${Math.round(this.zoom * 100)}%</span>
594
+ <button class="tb-btn zoom-btn" @click="${() => { this.zoom = Math.min(2, this.zoom + 0.1); }}">+</button>
595
+ </div>
596
+ </div>
597
+ `;
598
+ }
599
+ // ─── Left Panel ───────────────────────────────────
600
+ renderLeftPanel() {
601
+ const categories = [
602
+ { key: 'basic', label: 'Basicos' },
603
+ { key: 'advanced', label: 'Avanzados' },
604
+ { key: 'data', label: 'Datos' },
605
+ { key: 'media', label: 'Media' },
606
+ { key: 'layout', label: 'Layout' },
607
+ ];
608
+ return html `
609
+ <div class="left-panel">
610
+ <div class="panel-tabs">
611
+ <button class="panel-tab ${this.leftTab === 'fields' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'fields'; }}">Campos</button>
612
+ <button class="panel-tab ${this.leftTab === 'sections' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'sections'; }}">Secciones</button>
613
+ </div>
614
+ <div class="panel-content">
615
+ ${this.leftTab === 'fields' ? html `
616
+ ${categories.map(cat => {
617
+ const items = getFieldsByCategory(cat.key);
215
618
  if (items.length === 0)
216
619
  return nothing;
217
620
  return html `
218
- <div class="toolbox-title">${categoryNames[cat] ?? cat}</div>
219
- ${items.map(f => html `
220
- <div class="toolbox-item"
221
- draggable="true"
222
- @dragstart="${(e) => { this.dragType = f.type; e.dataTransfer?.setData('text/plain', f.type); }}"
223
- @dragend="${() => { this.dragType = null; }}"
224
- @dblclick="${() => this.addField(f.type)}"
621
+ <div class="toolbox-section">${cat.label}</div>
622
+ <div class="toolbox-grid">
623
+ ${items.map(f => html `
624
+ <div class="toolbox-item"
625
+ draggable="true"
626
+ @dragstart="${(e) => { this.dragType = f.type; e.dataTransfer?.setData('text/plain', f.type); }}"
627
+ @dragend="${() => { this.dragType = null; }}"
628
+ @dblclick="${() => this.addField(f.type)}"
629
+ title="${f.label}"
630
+ >
631
+ <span class="toolbox-icon">${unsafeHTML(resolveIcon(f.icon, this.provider))}</span>
632
+ <span class="toolbox-label">${f.label}</span>
633
+ </div>
634
+ `)}
635
+ </div>
636
+ `;
637
+ })}
638
+ ` : html `
639
+ ${this.schema?.sections.map((s, i) => html `
640
+ <div style="padding:6px;margin:2px 0;background:${i % 2 === 0 ? '#f8f8f8' : 'white'};border-radius:4px;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:6px;"
641
+ @click="${() => { }}"
225
642
  >
226
- <span class="toolbox-item-icon">${f.icon}</span>
227
- <span class="toolbox-item-label">${f.label}</span>
643
+ <span style="color:var(--zrd-accent);font-weight:600;">${i + 1}</span>
644
+ <span style="flex:1;">${s.title ?? 'Sin titulo'}</span>
645
+ <span style="color:var(--zrd-text-muted);font-size:11px;">${s.fields.length} campos</span>
228
646
  </div>
229
- `)}
230
- `;
231
- })}
647
+ `) ?? nothing}
648
+ `}
649
+ </div>
232
650
  </div>
233
651
  `;
234
652
  }
235
653
  // ─── Canvas ───────────────────────────────────────
236
654
  renderCanvas() {
237
655
  return html `
238
- <div class="canvas">
239
- <div class="canvas-header">
240
- <span class="canvas-title">${this.schema?.title || 'Sin titulo'}</span>
241
- <div class="canvas-actions">
242
- <button class="canvas-btn ${this.viewMode === 'design' ? 'canvas-btn--active' : ''}" @click="${() => { this.viewMode = 'design'; }}">Diseño</button>
243
- <button class="canvas-btn ${this.viewMode === 'preview' ? 'canvas-btn--active' : ''}" @click="${() => { this.viewMode = 'preview'; }}">Preview</button>
244
- <button class="canvas-btn ${this.viewMode === 'json' ? 'canvas-btn--active' : ''}" @click="${() => { this.viewMode = 'json'; }}">JSON</button>
245
- </div>
246
- </div>
247
-
656
+ <div class="canvas-area">
248
657
  ${this.viewMode === 'json'
249
- ? html `<div class="json-panel">${JSON.stringify(this.schema, null, 2)}</div>`
658
+ ? html `<div class="json-panel" style="width:100%;max-width:800px;">${JSON.stringify(this.schema, null, 2)}</div>`
250
659
  : this.viewMode === 'preview'
251
- ? html `<div class="preview-frame"><zentto-studio-renderer .schema="${this.schema}" .data="${this.data}"></zentto-studio-renderer></div>`
252
- : this.renderDesignView()}
660
+ ? html `<div class="canvas" style="transform:scale(${this.zoom});"><zentto-studio-renderer .schema="${this.schema}" .data="${this.data}"></zentto-studio-renderer></div>`
661
+ : this.renderDesignCanvas()}
253
662
  </div>
254
663
  `;
255
664
  }
256
- renderDesignView() {
257
- if (!this.schema)
258
- return html `<div class="drop-zone">Arrastra campos del toolbox aqui</div>`;
665
+ renderDesignCanvas() {
666
+ if (!this.schema) {
667
+ return html `<div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}" style="width:500px;height:200px;display:flex;align-items:center;justify-content:center;"
668
+ @dragover="${(e) => e.preventDefault()}"
669
+ @drop="${(e) => { e.preventDefault(); if (this.dragType) {
670
+ this.addField(this.dragType);
671
+ this.dragType = null;
672
+ } }}"
673
+ >Arrastra un campo aqui para empezar</div>`;
674
+ }
675
+ const cols = this.schema.layout.columns ?? 1;
259
676
  return html `
260
- <div class="preview-frame">
677
+ <div class="canvas" style="transform:scale(${this.zoom});" @click="${() => { this.selectedFieldId = null; }}">
261
678
  ${this.schema.sections.map((section, si) => html `
262
- <div style="margin-bottom:24px;">
263
- ${section.title ? html `<h3 style="font-size:16px;font-weight:600;margin:0 0 12px;color:var(--zs-text);border-bottom:2px solid var(--zs-primary-light);padding-bottom:6px;">${section.title}</h3>` : ''}
264
- <div style="display:grid;grid-template-columns:repeat(${section.columns ?? this.schema?.layout.columns ?? 1}, 1fr);gap:12px;">
265
- ${section.fields.map((field, fi) => html `
266
- <div class="field-overlay ${this.selectedFieldId === field.id ? 'field-overlay--selected' : ''}"
267
- style="${field.colSpan ? `grid-column: span ${field.colSpan}` : ''}"
268
- @click="${(e) => { e.stopPropagation(); this.selectedFieldId = field.id; }}"
269
- >
270
- <span class="field-overlay-label">${field.type}: ${field.id}</span>
271
- <div class="field-overlay-actions">
272
- ${fi > 0 ? html `<button class="field-overlay-btn field-overlay-btn--move" @click="${(e) => { e.stopPropagation(); this.moveField(si, fi, -1); }}">↑</button>` : ''}
273
- ${fi < section.fields.length - 1 ? html `<button class="field-overlay-btn field-overlay-btn--move" @click="${(e) => { e.stopPropagation(); this.moveField(si, fi, 1); }}">↓</button>` : ''}
274
- <button class="field-overlay-btn field-overlay-btn--delete" @click="${(e) => { e.stopPropagation(); this.removeField(si, fi); }}">✕</button>
275
- </div>
276
- <!-- Render actual field preview -->
277
- <div style="pointer-events:none;opacity:0.9;">
278
- <div style="font-size:12px;font-weight:500;color:var(--zs-text-secondary);margin-bottom:4px;">${field.label ?? field.id}${field.required ? ' *' : ''}</div>
279
- <div style="height:36px;border:1px solid var(--zs-border);border-radius:4px;background:var(--zs-bg-secondary);display:flex;align-items:center;padding:0 10px;font-size:13px;color:var(--zs-text-muted);">${field.placeholder ?? field.type}</div>
280
- </div>
281
- </div>
282
- `)}
679
+ <div class="canvas-section">
680
+ <div class="canvas-section-header" @click="${(e) => e.stopPropagation()}">
681
+ <span style="font-size:10px;color:var(--zrd-accent);">§${si + 1}</span>
682
+ ${section.title ?? 'Seccion'}
683
+ <span style="flex:1;"></span>
684
+ <span style="font-size:10px;color:var(--zrd-text-muted);">${section.fields.length} campos</span>
685
+ </div>
686
+ <div class="canvas-grid" style="grid-template-columns:repeat(${section.columns ?? cols}, 1fr);">
687
+ ${section.fields.map((field, fi) => this.renderCanvasField(field, si, fi, section.columns ?? cols))}
283
688
  </div>
284
- <!-- Drop zone at end of section -->
285
689
  <div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}"
286
690
  @dragover="${(e) => e.preventDefault()}"
287
691
  @drop="${(e) => { e.preventDefault(); if (this.dragType) {
288
- this.addFieldToSection(si, this.dragType);
692
+ this.addField(this.dragType, si);
289
693
  this.dragType = null;
290
694
  } }}"
291
- >
292
- ${this.dragType ? 'Soltar aqui' : 'Arrastra un campo aqui'}
293
- </div>
695
+ >${this.dragType ? '↓ Soltar aqui' : '+ Arrastra campos'}</div>
294
696
  </div>
295
697
  `)}
296
698
  </div>
297
699
  `;
298
700
  }
299
- // ─── Properties Panel ─────────────────────────────
300
- renderProperties() {
701
+ renderCanvasField(field, si, fi, maxCols) {
702
+ const isSelected = this.selectedFieldId === field.id;
703
+ const span = Math.min(field.colSpan ?? 1, maxCols);
704
+ const fullWidth = ['separator', 'heading', 'html', 'datagrid', 'report', 'chart'].includes(field.type);
705
+ const gridCol = fullWidth ? '1 / -1' : span > 1 ? `span ${span}` : '';
706
+ const typeColor = FIELD_TYPE_COLORS[field.type] ?? 'background:#eceff1;color:#546e7a;';
707
+ return html `
708
+ <div class="canvas-field ${isSelected ? 'canvas-field--selected' : ''}"
709
+ style="${gridCol ? `grid-column:${gridCol};` : ''}"
710
+ @click="${(e) => { e.stopPropagation(); this.selectedFieldId = field.id; }}"
711
+ >
712
+ <!-- Type badge -->
713
+ <span class="field-type-badge" style="${typeColor}">${field.type}</span>
714
+
715
+ <!-- Action buttons -->
716
+ <div class="field-actions">
717
+ ${fi > 0 ? html `<button class="fa-btn fa-btn--move" @click="${(e) => { e.stopPropagation(); this.moveField(si, fi, -1); }}" title="Subir">↑</button>` : ''}
718
+ ${fi < (this.schema?.sections[si].fields.length ?? 0) - 1 ? html `<button class="fa-btn fa-btn--move" @click="${(e) => { e.stopPropagation(); this.moveField(si, fi, 1); }}" title="Bajar">↓</button>` : ''}
719
+ <button class="fa-btn fa-btn--copy" @click="${(e) => { e.stopPropagation(); this.duplicateField(si, fi); }}" title="Duplicar">⎘</button>
720
+ <button class="fa-btn fa-btn--delete" @click="${(e) => { e.stopPropagation(); this.removeField(si, fi); }}" title="Eliminar">✕</button>
721
+ </div>
722
+
723
+ <!-- Resize handles -->
724
+ ${isSelected ? html `
725
+ <div class="rh rh-nw"></div><div class="rh rh-n"></div><div class="rh rh-ne"></div>
726
+ <div class="rh rh-e"></div><div class="rh rh-se"></div><div class="rh rh-s"></div>
727
+ <div class="rh rh-sw"></div><div class="rh rh-w"></div>
728
+ ` : ''}
729
+
730
+ <!-- Field preview -->
731
+ <div class="field-preview">
732
+ <div class="field-preview-label">${field.label ?? field.id}${field.required ? ' *' : ''}</div>
733
+ <div class="field-preview-input ${this.getPreviewClass(field.type)}">
734
+ ${this.getPreviewContent(field)}
735
+ </div>
736
+ </div>
737
+ </div>
738
+ `;
739
+ }
740
+ getPreviewClass(type) {
741
+ if (type === 'textarea')
742
+ return 'field-preview-input--textarea';
743
+ if (type === 'switch')
744
+ return 'field-preview-input--switch';
745
+ if (type === 'separator')
746
+ return 'field-preview-input--separator';
747
+ if (type === 'heading')
748
+ return 'field-preview-input--heading';
749
+ if (type === 'datagrid')
750
+ return 'field-preview-input--datagrid';
751
+ if (type === 'report')
752
+ return 'field-preview-input--report';
753
+ if (type === 'chart')
754
+ return 'field-preview-input--chart';
755
+ return '';
756
+ }
757
+ getPreviewContent(field) {
758
+ if (field.type === 'switch')
759
+ return '⬤───────';
760
+ if (field.type === 'separator')
761
+ return '';
762
+ if (field.type === 'heading')
763
+ return field.label ?? 'Titulo';
764
+ if (field.type === 'datagrid')
765
+ return '◫ ZenttoDataGrid';
766
+ if (field.type === 'report')
767
+ return '◫ ZenttoReportViewer';
768
+ if (field.type === 'chart')
769
+ return '◫ Chart SVG';
770
+ if (field.type === 'checkbox' || field.type === 'radio')
771
+ return '☐ ' + (field.label ?? '');
772
+ if (field.type === 'rating')
773
+ return '★ ★ ★ ★ ☆';
774
+ if (field.type === 'signature')
775
+ return '✍ Firma';
776
+ if (field.type === 'file' || field.type === 'image')
777
+ return '📁 Arrastra archivos';
778
+ if (field.type === 'address')
779
+ return '📍 Calle, Ciudad, Estado, CP';
780
+ if (field.type === 'chips' || field.type === 'tags')
781
+ return '🏷 tag1 × tag2 ×';
782
+ if (field.type === 'treeview')
783
+ return '▸ Nodo 1\n ▸ Nodo 2';
784
+ return field.placeholder ?? field.type;
785
+ }
786
+ // ─── Right Panel (Properties) ─────────────────────
787
+ renderRightPanel() {
301
788
  const field = this.selectedField;
302
789
  if (!field) {
303
790
  return html `
304
- <div class="properties">
791
+ <div class="right-panel">
305
792
  <div class="props-empty">
306
- <div style="font-size:32px;margin-bottom:8px;">👆</div>
307
- Selecciona un campo para editar sus propiedades
793
+ <div class="props-empty-icon">👆</div>
794
+ <div>Selecciona un campo<br/>para editar propiedades</div>
308
795
  </div>
309
- ${this.schema ? html `
310
- <div class="props-title">FORMULARIO</div>
311
- <div class="props-row">
312
- <label class="props-label">Titulo</label>
313
- <input class="props-input" .value="${this.schema.title}" @input="${(e) => { this.schema.title = e.target.value; this.emitChange(); }}" />
314
- </div>
315
- <div class="props-row">
316
- <label class="props-label">Columnas</label>
317
- <input class="props-input" type="number" min="1" max="6" .value="${String(this.schema.layout.columns ?? 1)}" @input="${(e) => { this.schema.layout.columns = parseInt(e.target.value) || 1; this.emitChange(); }}" />
318
- </div>
319
- ` : ''}
796
+ ${this.schema ? this.renderFormProperties() : ''}
320
797
  </div>
321
798
  `;
322
799
  }
800
+ const typeColor = FIELD_TYPE_COLORS[field.type] ?? 'background:#eceff1;color:#546e7a;';
323
801
  return html `
324
- <div class="properties">
325
- <span class="props-field-type">${field.type}</span>
326
-
327
- <div class="props-title">GENERAL</div>
328
- <div class="props-row">
329
- <label class="props-label">ID</label>
330
- <input class="props-input" .value="${field.id}" @input="${(e) => { field.id = e.target.value; this.emitChange(); }}" />
331
- </div>
332
- <div class="props-row">
333
- <label class="props-label">Label</label>
334
- <input class="props-input" .value="${field.label ?? ''}" @input="${(e) => { field.label = e.target.value; this.emitChange(); }}" />
335
- </div>
336
- <div class="props-row">
337
- <label class="props-label">Field (binding)</label>
338
- <input class="props-input" .value="${field.field}" @input="${(e) => { field.field = e.target.value; this.emitChange(); }}" />
339
- </div>
340
- <div class="props-row">
341
- <label class="props-label">Placeholder</label>
342
- <input class="props-input" .value="${field.placeholder ?? ''}" @input="${(e) => { field.placeholder = e.target.value; this.emitChange(); }}" />
343
- </div>
344
- <div class="props-row">
345
- <label class="props-label">Help Text</label>
346
- <input class="props-input" .value="${field.helpText ?? ''}" @input="${(e) => { field.helpText = e.target.value; this.emitChange(); }}" />
802
+ <div class="right-panel">
803
+ <div style="padding:10px 12px;border-bottom:1px solid var(--zrd-border);">
804
+ <span class="props-type-badge" style="${typeColor}">${field.type.toUpperCase()}</span>
347
805
  </div>
348
806
 
349
- <div class="props-title">LAYOUT</div>
350
- <div class="props-row">
351
- <label class="props-label">Col Span</label>
352
- <input class="props-input" type="number" min="1" max="6" .value="${String(field.colSpan ?? 1)}" @input="${(e) => { field.colSpan = parseInt(e.target.value) || 1; this.emitChange(); }}" />
353
- </div>
354
- <div class="props-row">
355
- <label class="props-label">CSS Class</label>
356
- <input class="props-input" .value="${field.cssClass ?? ''}" @input="${(e) => { field.cssClass = e.target.value; this.emitChange(); }}" />
807
+ <!-- General -->
808
+ <div class="prop-section">
809
+ <div class="prop-section-header" data-section="general" @click="${() => this.toggleSection('general')}">
810
+ <h4>General</h4>
811
+ <span class="collapse-icon ${this.collapsedSections.has('general') ? 'collapse-icon--collapsed' : ''}">▾</span>
812
+ </div>
813
+ ${!this.collapsedSections.has('general') ? html `
814
+ <div class="prop-row"><span class="prop-label">ID</span><input class="prop-input" .value="${field.id}" @change="${(e) => { field.id = e.target.value; this.commitChange(); }}" /></div>
815
+ <div class="prop-row"><span class="prop-label">Label</span><input class="prop-input" .value="${field.label ?? ''}" @input="${(e) => { field.label = e.target.value; this.emitChange(); }}" /></div>
816
+ <div class="prop-row"><span class="prop-label">Field</span><input class="prop-input" .value="${field.field}" @change="${(e) => { field.field = e.target.value; this.commitChange(); }}" /></div>
817
+ <div class="prop-row"><span class="prop-label">Placeholder</span><input class="prop-input" .value="${field.placeholder ?? ''}" @input="${(e) => { field.placeholder = e.target.value; this.emitChange(); }}" /></div>
818
+ <div class="prop-row"><span class="prop-label">Help</span><input class="prop-input" .value="${field.helpText ?? ''}" @input="${(e) => { field.helpText = e.target.value; this.emitChange(); }}" /></div>
819
+ ` : ''}
357
820
  </div>
358
821
 
359
- <div class="props-title">COMPORTAMIENTO</div>
360
- <div class="props-row">
361
- <label class="props-checkbox"><input type="checkbox" .checked="${field.required ?? false}" @change="${(e) => { field.required = e.target.checked; this.emitChange(); }}" /> Requerido</label>
362
- </div>
363
- <div class="props-row">
364
- <label class="props-checkbox"><input type="checkbox" .checked="${field.readOnly ?? false}" @change="${(e) => { field.readOnly = e.target.checked; this.emitChange(); }}" /> Solo lectura</label>
365
- </div>
366
- <div class="props-row">
367
- <label class="props-checkbox"><input type="checkbox" .checked="${field.disabled ?? false}" @change="${(e) => { field.disabled = e.target.checked; this.emitChange(); }}" /> Deshabilitado</label>
368
- </div>
369
- <div class="props-row">
370
- <label class="props-checkbox"><input type="checkbox" .checked="${field.hidden ?? false}" @change="${(e) => { field.hidden = e.target.checked; this.emitChange(); }}" /> Oculto</label>
822
+ <!-- Layout -->
823
+ <div class="prop-section">
824
+ <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('layout')}">
825
+ <h4>Layout</h4>
826
+ <span class="collapse-icon ${this.collapsedSections.has('layout') ? 'collapse-icon--collapsed' : ''}">▾</span>
827
+ </div>
828
+ ${!this.collapsedSections.has('layout') ? html `
829
+ <div class="prop-grid-2x2">
830
+ <div class="prop-grid-item">
831
+ <span class="prop-grid-label">W</span>
832
+ <input type="number" min="1" max="6" .value="${String(field.colSpan ?? 1)}" @change="${(e) => { field.colSpan = parseInt(e.target.value) || 1; this.commitChange(); }}" />
833
+ </div>
834
+ <div class="prop-grid-item">
835
+ <span class="prop-grid-label" style="color:#7c3aed;">T</span>
836
+ <select .value="${field.type}" @change="${(e) => { field.type = e.target.value; this.commitChange(); }}" style="border:none;background:transparent;font-size:12px;width:100%;outline:none;">
837
+ ${getAllFields().map(f => html `<option value="${f.type}" ?selected="${f.type === field.type}">${f.label}</option>`)}
838
+ </select>
839
+ </div>
840
+ </div>
841
+ <div class="prop-row"><span class="prop-label">CSS Class</span><input class="prop-input" .value="${field.cssClass ?? ''}" @input="${(e) => { field.cssClass = e.target.value; this.emitChange(); }}" /></div>
842
+ ` : ''}
371
843
  </div>
372
844
 
373
- <div class="props-title">REGLAS</div>
374
- <div class="props-row">
375
- <label class="props-label">Visibilidad (expresion)</label>
376
- <input class="props-input" .value="${field.visibilityRule ?? ''}" placeholder='{role} == "admin"' @input="${(e) => { field.visibilityRule = e.target.value || undefined; this.emitChange(); }}" />
377
- </div>
378
- <div class="props-row">
379
- <label class="props-label">Valor Computado</label>
380
- <input class="props-input" .value="${field.computedValue ?? ''}" placeholder='{precio} * {cantidad}' @input="${(e) => { field.computedValue = e.target.value || undefined; this.emitChange(); }}" />
845
+ <!-- Behavior -->
846
+ <div class="prop-section">
847
+ <div class="prop-section-header" data-section="behavior" @click="${() => this.toggleSection('behavior')}">
848
+ <h4>Comportamiento</h4>
849
+ <span class="collapse-icon ${this.collapsedSections.has('behavior') ? 'collapse-icon--collapsed' : ''}">▾</span>
850
+ </div>
851
+ ${!this.collapsedSections.has('behavior') ? html `
852
+ ${this.renderToggle('Requerido', field.required ?? false, (v) => { field.required = v; this.commitChange(); })}
853
+ ${this.renderToggle('Solo lectura', field.readOnly ?? false, (v) => { field.readOnly = v; this.commitChange(); })}
854
+ ${this.renderToggle('Deshabilitado', field.disabled ?? false, (v) => { field.disabled = v; this.commitChange(); })}
855
+ ${this.renderToggle('Oculto', field.hidden ?? false, (v) => { field.hidden = v; this.commitChange(); })}
856
+ ` : ''}
381
857
  </div>
382
- <div class="props-row">
383
- <label class="props-label">Default Value</label>
384
- <input class="props-input" .value="${String(field.defaultValue ?? '')}" @input="${(e) => { field.defaultValue = e.target.value || undefined; this.emitChange(); }}" />
858
+
859
+ <!-- Rules -->
860
+ <div class="prop-section">
861
+ <div class="prop-section-header" data-section="rules" @click="${() => this.toggleSection('rules')}">
862
+ <h4>Reglas</h4>
863
+ <span class="collapse-icon ${this.collapsedSections.has('rules') ? 'collapse-icon--collapsed' : ''}">▾</span>
864
+ </div>
865
+ ${!this.collapsedSections.has('rules') ? html `
866
+ <div class="prop-row prop-row-full"><span class="prop-label">Visibilidad</span></div>
867
+ <div class="prop-row prop-row-full"><textarea class="prop-input" rows="2" .value="${field.visibilityRule ?? ''}" placeholder='{role} == "admin"' @change="${(e) => { field.visibilityRule = e.target.value || undefined; this.commitChange(); }}"></textarea></div>
868
+ <div class="prop-row prop-row-full"><span class="prop-label">Valor computado</span></div>
869
+ <div class="prop-row prop-row-full"><textarea class="prop-input" rows="2" .value="${field.computedValue ?? ''}" placeholder='{precio} * {cantidad}' @change="${(e) => { field.computedValue = e.target.value || undefined; this.commitChange(); }}"></textarea></div>
870
+ <div class="prop-row"><span class="prop-label">Default</span><input class="prop-input" .value="${String(field.defaultValue ?? '')}" @change="${(e) => { field.defaultValue = e.target.value || undefined; this.commitChange(); }}" /></div>
871
+ ` : ''}
385
872
  </div>
386
873
  </div>
387
874
  `;
388
875
  }
389
- // ─── Actions ──────────────────────────────────────
390
- addField(type) {
391
- if (!this.schema) {
392
- this.schema = {
393
- id: 'new-form', version: '1.0', title: 'Nuevo Formulario',
394
- layout: { type: 'grid', columns: 2 },
395
- sections: [{ id: 'main', title: 'Datos', fields: [] }],
396
- };
397
- }
398
- this.addFieldToSection(0, type);
399
- }
400
- addFieldToSection(sectionIndex, type) {
876
+ renderFormProperties() {
401
877
  if (!this.schema)
402
- return;
403
- const id = `${type}_${Date.now()}`;
404
- const meta = getAllFields().find(f => f.type === type);
405
- const field = {
406
- id,
407
- type,
408
- field: id,
409
- label: meta?.label ?? type,
410
- props: meta?.defaultProps ? { ...meta.defaultProps } : undefined,
411
- };
412
- this.schema.sections[sectionIndex].fields.push(field);
413
- this.selectedFieldId = id;
414
- this.emitChange();
415
- }
416
- removeField(sectionIndex, fieldIndex) {
417
- if (!this.schema)
418
- return;
419
- const field = this.schema.sections[sectionIndex].fields[fieldIndex];
420
- if (this.selectedFieldId === field.id)
421
- this.selectedFieldId = null;
422
- this.schema.sections[sectionIndex].fields.splice(fieldIndex, 1);
423
- this.emitChange();
878
+ return nothing;
879
+ return html `
880
+ <div class="prop-section">
881
+ <div class="prop-section-header" data-section="form"><h4>Formulario</h4></div>
882
+ <div class="prop-row"><span class="prop-label">Titulo</span><input class="prop-input" .value="${this.schema.title}" @input="${(e) => { this.schema.title = e.target.value; this.emitChange(); }}" /></div>
883
+ <div class="prop-row"><span class="prop-label">Columnas</span><input class="prop-input" type="number" min="1" max="6" .value="${String(this.schema.layout.columns ?? 1)}" @change="${(e) => { this.schema.layout.columns = parseInt(e.target.value) || 1; this.commitChange(); }}" /></div>
884
+ <div class="prop-row"><span class="prop-label">Gap (px)</span><input class="prop-input" type="number" min="0" .value="${String(this.schema.layout.gap ?? 16)}" @change="${(e) => { this.schema.layout.gap = parseInt(e.target.value) || 16; this.commitChange(); }}" /></div>
885
+ </div>
886
+ `;
424
887
  }
425
- moveField(sectionIndex, fieldIndex, dir) {
426
- if (!this.schema)
427
- return;
428
- const fields = this.schema.sections[sectionIndex].fields;
429
- const newIndex = fieldIndex + dir;
430
- if (newIndex < 0 || newIndex >= fields.length)
431
- return;
432
- [fields[fieldIndex], fields[newIndex]] = [fields[newIndex], fields[fieldIndex]];
433
- this.emitChange();
888
+ renderToggle(label, value, onChange) {
889
+ return html `
890
+ <div class="prop-toggle">
891
+ <button class="prop-switch ${value ? 'prop-switch--active' : ''}"
892
+ @click="${() => onChange(!value)}"
893
+ ></button>
894
+ <span class="prop-toggle-label">${label}</span>
895
+ </div>
896
+ `;
434
897
  }
435
- emitChange() {
436
- this.renderKey++;
437
- this.requestUpdate();
438
- this.dispatchEvent(new CustomEvent('schema-change', {
439
- detail: { schema: structuredClone(this.schema) },
440
- bubbles: true, composed: true,
441
- }));
898
+ toggleSection(id) {
899
+ const next = new Set(this.collapsedSections);
900
+ if (next.has(id))
901
+ next.delete(id);
902
+ else
903
+ next.add(id);
904
+ this.collapsedSections = next;
442
905
  }
443
906
  };
444
907
  __decorate([
@@ -447,20 +910,40 @@ __decorate([
447
910
  __decorate([
448
911
  property({ type: Object })
449
912
  ], ZsPageDesigner.prototype, "data", void 0);
913
+ __decorate([
914
+ property({ type: Object })
915
+ ], ZsPageDesigner.prototype, "provider", void 0);
916
+ __decorate([
917
+ property({ type: Number, attribute: 'auto-save-ms' })
918
+ ], ZsPageDesigner.prototype, "autoSaveMs", void 0);
919
+ __decorate([
920
+ property({ type: Number, attribute: 'grid-snap' })
921
+ ], ZsPageDesigner.prototype, "gridSnap", void 0);
450
922
  __decorate([
451
923
  state()
452
924
  ], ZsPageDesigner.prototype, "selectedFieldId", void 0);
453
925
  __decorate([
454
926
  state()
455
927
  ], ZsPageDesigner.prototype, "viewMode", void 0);
928
+ __decorate([
929
+ state()
930
+ ], ZsPageDesigner.prototype, "leftTab", void 0);
456
931
  __decorate([
457
932
  state()
458
933
  ], ZsPageDesigner.prototype, "dragType", void 0);
459
934
  __decorate([
460
935
  state()
461
- ], ZsPageDesigner.prototype, "renderKey", void 0);
936
+ ], ZsPageDesigner.prototype, "editingTitle", void 0);
937
+ __decorate([
938
+ state()
939
+ ], ZsPageDesigner.prototype, "collapsedSections", void 0);
940
+ __decorate([
941
+ state()
942
+ ], ZsPageDesigner.prototype, "zoom", void 0);
462
943
  ZsPageDesigner = __decorate([
463
944
  customElement('zs-page-designer')
464
945
  ], ZsPageDesigner);
465
946
  export { ZsPageDesigner };
947
+ // Helper for CSS template literals with variables
948
+ function unsafeCSS(str) { return str; }
466
949
  //# sourceMappingURL=zs-page-designer.js.map