@zentto/studio 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -78,6 +78,16 @@ 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) ──────────────────────
82
92
  this.showTemplateMenu = false;
83
93
  this.apiSources = [];
@@ -91,461 +101,835 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
91
101
  this.apiCompany = '';
92
102
  this.apiBranch = '';
93
103
  }
94
- static { this.styles = css `
95
- :host {
96
- display: block; height: 100%;
97
- --zrd-bg: ${unsafeCSS(BG)};
98
- --zrd-panel-bg: ${unsafeCSS(PANEL_BG)};
99
- --zrd-border: ${unsafeCSS(BORDER)};
100
- --zrd-accent: ${unsafeCSS(ACCENT)};
101
- --zrd-accent-light: ${unsafeCSS(ACCENT_LIGHT)};
102
- --zrd-text: ${unsafeCSS(TEXT)};
103
- --zrd-text-muted: ${unsafeCSS(TEXT_MUTED)};
104
- --zrd-danger: ${unsafeCSS(DANGER)};
105
- font-family: 'Segoe UI', Roboto, Arial, sans-serif;
106
- }
107
-
108
- /* ─── Layout ──────────────────────────────── */
109
- .designer {
110
- display: grid;
111
- grid-template-rows: auto 1fr;
112
- grid-template-columns: 200px 4px 1fr 4px 240px;
113
- height: 100%; background: var(--zrd-bg);
114
- overflow: hidden;
115
- }
116
-
117
- /* ─── Toolbar ─────────────────────────────── */
118
- .toolbar {
119
- grid-column: 1 / -1;
120
- display: flex; align-items: center; gap: 8px;
121
- padding: 6px 12px; min-height: 36px;
122
- background: var(--zrd-panel-bg);
123
- border-bottom: 1px solid var(--zrd-border);
124
- flex-wrap: wrap;
125
- }
126
- .toolbar-sep { width: 1px; height: 20px; background: var(--zrd-border); margin: 0 2px; }
127
- .tb-btn {
128
- background: none; border: 1px solid var(--zrd-border);
129
- border-radius: 4px; padding: 4px 10px;
130
- cursor: pointer; font-size: 12px; color: var(--zrd-text);
131
- font-family: inherit; transition: background 0.15s;
132
- white-space: nowrap; display: flex; align-items: center; gap: 4px;
133
- }
134
- .tb-btn:hover { background: var(--zrd-accent-light); }
135
- .tb-btn:disabled { opacity: 0.4; cursor: default; }
136
- .tb-btn--active { background: var(--zrd-accent); color: white; border-color: var(--zrd-accent); }
137
- .tb-btn--danger:hover { background: #ffebee; color: var(--zrd-danger); border-color: var(--zrd-danger); }
138
- .report-name {
139
- font-weight: 600; font-size: 14px; cursor: pointer;
140
- padding: 2px 6px; border-radius: 3px; border: 1px solid transparent;
141
- color: var(--zrd-text);
142
- }
143
- .report-name:hover { border-color: var(--zrd-border); background: var(--zrd-accent-light); }
144
- .report-name-input {
145
- font-weight: 600; font-size: 14px; border: 1px solid var(--zrd-accent);
146
- border-radius: 3px; padding: 2px 6px; outline: none;
147
- background: white; color: var(--zrd-text); font-family: inherit;
148
- }
149
- .tb-spacer { flex: 1; }
150
- .zoom-controls {
151
- display: flex; align-items: center; gap: 4px;
152
- padding-left: 8px; border-left: 1px solid var(--zrd-border);
153
- }
154
- .zoom-btn { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 3px; font-size: 14px; font-weight: bold; }
155
- .zoom-label { font-size: 11px; min-width: 36px; text-align: center; color: var(--zrd-text-muted); cursor: pointer; }
156
-
157
- /* ─── Resize Handle ───────────────────────── */
158
- .panel-resize {
159
- width: 4px; cursor: col-resize; background: transparent;
160
- transition: background 0.15s; flex-shrink: 0;
161
- }
162
- .panel-resize:hover { background: var(--zrd-accent); }
163
-
164
- /* ─── Left Panel (Toolbox) ────────────────── */
165
- .left-panel {
166
- background: var(--zrd-panel-bg);
167
- border-right: 1px solid var(--zrd-border);
168
- display: flex; flex-direction: column; overflow: hidden;
169
- }
170
- .panel-tabs {
171
- display: flex; border-bottom: 1px solid var(--zrd-border);
172
- }
173
- .panel-tab {
174
- flex: 1; padding: 8px 4px; text-align: center; cursor: pointer;
175
- font-size: 11px; border-bottom: 2px solid transparent;
176
- color: var(--zrd-text-muted); transition: all 0.15s;
177
- background: none; border-top: none; border-left: none; border-right: none;
178
- font-family: inherit;
179
- }
180
- .panel-tab:hover { color: var(--zrd-text); }
181
- .panel-tab--active { border-bottom-color: var(--zrd-accent); color: var(--zrd-accent); font-weight: 600; }
182
- .panel-content { padding: 4px; overflow-y: auto; flex: 1; }
183
- .panel-content::-webkit-scrollbar { width: 6px; }
184
- .panel-content::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
185
-
186
- /* Toolbox grid (3 columns like report-designer) */
187
- .toolbox-section {
188
- font-size: 9px; font-weight: 700; text-transform: uppercase;
189
- letter-spacing: 0.8px; color: #aaa;
190
- padding: 10px 8px 5px; margin-top: 2px;
191
- }
192
- .toolbox-section:first-child { margin-top: 0; padding-top: 6px; }
193
- .toolbox-grid {
194
- display: grid; grid-template-columns: repeat(3, 1fr);
195
- gap: 3px; padding: 0 4px;
196
- }
197
- .toolbox-item {
198
- display: flex; flex-direction: column; align-items: center;
199
- gap: 3px; padding: 7px 2px 5px;
200
- border: 1px solid transparent; border-radius: 5px;
201
- cursor: grab; user-select: none; text-align: center;
202
- transition: all 0.15s; background: transparent;
203
- }
204
- .toolbox-item:hover {
205
- background: var(--zrd-accent-light); border-color: #c5dcf0;
206
- box-shadow: 0 1px 4px rgba(25,118,210,0.1);
207
- }
208
- .toolbox-item:active { cursor: grabbing; opacity: 0.6; transform: scale(0.95); }
209
- .toolbox-icon {
210
- width: 28px; height: 28px; display: flex; align-items: center;
211
- justify-content: center; border-radius: 6px;
212
- font-size: 15px; flex-shrink: 0;
213
- background: #f0f4f8; color: #1976d2;
214
- border: 1px solid #e3e8ee;
215
- transition: all 0.15s;
216
- }
217
- .toolbox-item:hover .toolbox-icon {
218
- background: #1976d2; color: white; border-color: #1976d2;
219
- box-shadow: 0 2px 6px rgba(25,118,210,0.3);
220
- }
221
- .toolbox-label {
222
- font-size: 9px; font-weight: 500; line-height: 1.1;
223
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
224
- max-width: 100%; color: #777;
225
- }
226
- .toolbox-item:hover .toolbox-label { color: #1976d2; }
227
-
228
- /* ─── Canvas ──────────────────────────────── */
229
- .canvas-area {
230
- overflow: auto; padding: 30px; position: relative;
231
- background: #d0d0d0; display: flex; justify-content: center;
232
- }
233
- .canvas {
234
- background: white; position: relative; margin: 0 auto; border-radius: 2px;
235
- box-shadow: 0 4px 20px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.08);
236
- min-width: 600px; min-height: 400px; padding: 24px;
237
- transform-origin: top center;
238
- }
239
- .canvas-section { margin-bottom: 20px; }
240
- .canvas-section-header {
241
- font-size: 13px; font-weight: 600; color: var(--zrd-text);
242
- padding: 6px 8px; margin-bottom: 8px;
243
- background: #f0f0f0; border-radius: 4px; border-left: 3px solid var(--zrd-accent);
244
- display: flex; align-items: center; gap: 8px; cursor: pointer;
245
- }
246
- .canvas-section-header:hover { background: var(--zrd-accent-light); }
247
- .canvas-grid {
248
- display: grid; gap: 8px; padding: 4px;
249
- background: repeating-linear-gradient(0deg, transparent, transparent 19px, rgba(0,0,0,0.03) 19px, rgba(0,0,0,0.03) 20px),
250
- repeating-linear-gradient(90deg, transparent, transparent 19px, rgba(0,0,0,0.03) 19px, rgba(0,0,0,0.03) 20px);
251
- }
252
-
253
- /* Field on canvas */
254
- .canvas-field {
255
- position: relative; border: 1px solid transparent;
256
- border-radius: 4px; cursor: pointer; user-select: none;
257
- transition: border-color 0.15s, box-shadow 0.15s;
258
- padding: 4px;
259
- }
260
- .canvas-field:hover { border-color: rgba(25,118,210,0.5); }
261
- .canvas-field--selected {
262
- border-color: var(--zrd-accent);
263
- box-shadow: 0 0 0 1px var(--zrd-accent);
264
- }
265
-
266
- /* Type badge (above field) */
267
- .field-type-badge {
268
- position: absolute; top: -14px; left: 0;
269
- font-size: 9px; border-radius: 3px 3px 0 0;
270
- padding: 1px 6px; line-height: 13px;
271
- pointer-events: none; z-index: 6; display: none;
272
- white-space: nowrap;
273
- }
274
- .canvas-field:hover .field-type-badge,
275
- .canvas-field--selected .field-type-badge { display: block; }
276
-
277
- /* Resize handles (8-point like report-designer) */
278
- .rh { position: absolute; width: 6px; height: 6px; background: var(--zrd-accent); border: 1px solid white; z-index: 5; display: none; border-radius: 1px; }
279
- .rh:hover { background: #0d47a1; }
280
- .canvas-field--selected .rh { display: block; }
281
- .rh-nw { top: -3px; left: -3px; cursor: nw-resize; }
282
- .rh-n { top: -3px; left: calc(50% - 3px); cursor: n-resize; }
283
- .rh-ne { top: -3px; right: -3px; cursor: ne-resize; }
284
- .rh-e { top: calc(50% - 3px); right: -3px; cursor: e-resize; }
285
- .rh-se { bottom: -3px; right: -3px; cursor: se-resize; }
286
- .rh-s { bottom: -3px; left: calc(50% - 3px); cursor: s-resize; }
287
- .rh-sw { bottom: -3px; left: -3px; cursor: sw-resize; }
288
- .rh-w { top: calc(50% - 3px); left: -3px; cursor: w-resize; }
289
-
290
- /* Action buttons on field */
291
- .field-actions {
292
- position: absolute; top: -14px; right: 4px;
293
- display: flex; gap: 2px; opacity: 0; transition: opacity 0.15s; z-index: 7;
294
- }
295
- .canvas-field:hover .field-actions { opacity: 1; }
296
- .fa-btn {
297
- width: 18px; height: 14px; border-radius: 3px 3px 0 0;
298
- border: none; cursor: pointer; font-size: 9px;
299
- display: flex; align-items: center; justify-content: center;
300
- }
301
- .fa-btn--move { background: var(--zrd-accent); color: white; }
302
- .fa-btn--delete { background: var(--zrd-danger); color: white; }
303
- .fa-btn--copy { background: #7c3aed; color: white; }
304
-
305
- /* Field preview content */
306
- .field-preview {
307
- pointer-events: none;
308
- }
309
- .field-preview-label {
310
- font-size: 11px; font-weight: 500; color: var(--zrd-text-muted);
311
- margin-bottom: 3px;
312
- }
313
- .field-preview-input {
314
- height: 30px; border: 1px solid #e0e0e0; border-radius: 4px;
315
- background: #fafafa; display: flex; align-items: center;
316
- padding: 0 8px; font-size: 12px; color: #999;
317
- }
318
- .field-preview-input--textarea { height: 60px; align-items: flex-start; padding-top: 6px; }
319
- .field-preview-input--switch { height: auto; border: none; background: none; padding: 0; }
320
- .field-preview-input--separator { height: 1px; border: none; background: #ddd; padding: 0; }
321
- .field-preview-input--heading { height: auto; border: none; background: none; padding: 0; font-size: 16px; font-weight: 600; color: var(--zrd-text); }
322
- .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); }
323
- .field-preview-input--report { height: 120px; border: 2px dashed #e65100; background: #fff3e0; justify-content: center; font-weight: 500; color: #e65100; }
324
- .field-preview-input--chart { height: 120px; border: 2px dashed #6a1b9a; background: #f3e5f5; justify-content: center; font-weight: 500; color: #6a1b9a; }
325
-
326
- /* Drop zone */
327
- .drop-zone {
328
- border: 2px dashed var(--zrd-border); border-radius: 6px;
329
- padding: 16px; text-align: center; margin: 4px 0;
330
- color: var(--zrd-text-muted); font-size: 12px; transition: all 0.15s;
331
- }
332
- .drop-zone--active { border-color: var(--zrd-accent); background: var(--zrd-accent-light); color: var(--zrd-accent); }
333
-
334
- /* ─── Right Panel (Properties — Figma-quality) ──── */
335
- .right-panel {
336
- background: var(--zrd-panel-bg);
337
- border-left: 1px solid var(--zrd-border);
338
- overflow-y: auto; font-size: 11px;
339
- }
340
- .right-panel::-webkit-scrollbar { width: 5px; }
341
- .right-panel::-webkit-scrollbar-thumb { background: #d4d4d4; border-radius: 3px; }
342
- .right-panel::-webkit-scrollbar-thumb:hover { background: #bbb; }
343
-
344
- /* ─ Prop Section ─ */
345
- .prop-section { border-bottom: 1px solid #eee; padding: 8px 10px 10px; }
346
- .prop-section:last-child { border-bottom: none; }
347
- .prop-section-header {
348
- display: flex; align-items: center; gap: 6px;
349
- cursor: pointer; user-select: none; margin-bottom: 6px; padding: 2px 0;
350
- }
351
- .prop-section-header h4 {
352
- font-size: 10px; font-weight: 700; text-transform: uppercase;
353
- letter-spacing: 0.6px; margin: 0; flex: 1;
354
- }
355
- .prop-section-header[data-section="general"] h4 { color: #1976d2; }
356
- .prop-section-header[data-section="layout"] h4 { color: #7c3aed; }
357
- .prop-section-header[data-section="behavior"] h4 { color: #0d9488; }
358
- .prop-section-header[data-section="rules"] h4 { color: #ea580c; }
359
- .prop-section-header[data-section="style"] h4 { color: #c2185b; }
360
- .prop-section-header[data-section="form"] h4 { color: #1976d2; }
361
- .collapse-icon {
362
- font-size: 8px; color: #bbb; transition: transform 0.15s;
363
- width: 14px; height: 14px; display: flex; align-items: center;
364
- justify-content: center; border-radius: 3px;
365
- }
366
- .collapse-icon:hover { background: #f0f0f0; color: #666; }
367
- .collapse-icon--collapsed { transform: rotate(-90deg); }
368
-
369
- /* ─ Prop Rows ─ */
370
- .prop-row {
371
- display: grid; grid-template-columns: 62px 1fr;
372
- align-items: center; gap: 4px; min-height: 26px; margin-bottom: 1px;
373
- }
374
- .prop-row-full { grid-template-columns: 1fr; margin-bottom: 3px; }
375
- .prop-label {
376
- font-size: 11px; color: #999; font-weight: 400;
377
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
378
- padding-left: 1px; line-height: 1;
379
- }
380
-
381
- /* ─ Figma-style input ─ */
382
- .prop-input {
383
- width: 100%; border: 1px solid transparent; border-radius: 4px;
384
- padding: 5px 7px; font-size: 11px; background: #f5f5f5;
385
- color: var(--zrd-text); outline: none; min-width: 0;
386
- font-family: inherit; box-sizing: border-box;
387
- transition: border-color 0.12s, background 0.12s, box-shadow 0.12s;
388
- }
389
- .prop-input:hover { background: #efefef; border-color: #ddd; }
390
- .prop-input:focus { border-color: #1976d2; background: white; box-shadow: 0 0 0 2px rgba(25,118,210,0.08); }
391
- select.prop-input {
392
- padding: 4px 20px 4px 7px; cursor: pointer; appearance: none;
393
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23999'/%3E%3C/svg%3E");
394
- background-repeat: no-repeat; background-position: right 6px center;
395
- }
396
- textarea.prop-input {
397
- min-height: 44px; resize: vertical; font-family: 'SF Mono','Consolas','Monaco',monospace;
398
- font-size: 10px; line-height: 1.5; padding: 6px 7px;
399
- }
400
-
401
- /* ─ Numeric stepper (Figma-style) ─ */
402
- .prop-stepper {
403
- display: flex; align-items: center; background: #f5f5f5;
404
- border: 1px solid transparent; border-radius: 4px; overflow: hidden;
405
- transition: border-color 0.12s;
406
- }
407
- .prop-stepper:hover { border-color: #ddd; }
408
- .prop-stepper:focus-within { border-color: #1976d2; background: white; }
409
- .prop-stepper input {
410
- border: none; background: transparent; width: 100%;
411
- font-size: 11px; color: var(--zrd-text); outline: none;
412
- padding: 4px 2px 4px 7px; min-width: 0; font-family: inherit;
413
- -moz-appearance: textfield;
414
- }
415
- .prop-stepper input::-webkit-inner-spin-button { -webkit-appearance: none; }
416
- .prop-stepper-btns {
417
- display: flex; flex-direction: column; border-left: 1px solid #eee;
418
- }
419
- .prop-stepper-btn {
420
- border: none; background: none; cursor: pointer; padding: 0;
421
- width: 18px; height: 12px; display: flex; align-items: center;
422
- justify-content: center; font-size: 7px; color: #999;
423
- transition: all 0.1s;
424
- }
425
- .prop-stepper-btn:hover { background: #e8e8e8; color: #333; }
426
-
427
- /* ─ Position grid (4-cell Figma layout) ─ */
428
- .prop-pos-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3px; }
429
- .prop-pos-cell {
430
- display: flex; align-items: center; background: #f5f5f5;
431
- border: 1px solid transparent; border-radius: 4px; padding: 0 6px;
432
- height: 26px; gap: 3px; transition: border-color 0.12s;
433
- }
434
- .prop-pos-cell:hover { border-color: #ddd; }
435
- .prop-pos-cell:focus-within { border-color: #1976d2; background: white; }
436
- .prop-pos-label {
437
- font-size: 9px; font-weight: 700; width: 10px; text-align: center;
438
- flex-shrink: 0; user-select: none;
439
- }
440
- .prop-pos-label--x { color: #ef4444; }
441
- .prop-pos-label--y { color: #3b82f6; }
442
- .prop-pos-label--w { color: #8b5cf6; }
443
- .prop-pos-label--h { color: #10b981; }
444
- .prop-pos-cell input {
445
- border: none; background: transparent; width: 100%;
446
- font-size: 11px; color: var(--zrd-text); outline: none;
447
- padding: 0; min-width: 0; font-family: inherit;
448
- }
449
-
450
- /* ─ Toggle switches (Figma compact) ─ */
451
- .prop-toggle {
452
- display: flex; align-items: center; gap: 8px;
453
- min-height: 24px; padding: 1px 0;
454
- }
455
- .prop-switch {
456
- position: relative; width: 28px; height: 16px; border-radius: 8px;
457
- background: #d4d4d4; cursor: pointer; transition: background 0.2s;
458
- flex-shrink: 0; border: none; padding: 0;
459
- }
460
- .prop-switch--active { background: #1976d2; }
461
- .prop-switch::after {
462
- content: ''; position: absolute; top: 2px; left: 2px;
463
- width: 12px; height: 12px; border-radius: 50%;
464
- background: white; box-shadow: 0 1px 2px rgba(0,0,0,0.15);
465
- transition: transform 0.2s;
466
- }
467
- .prop-switch--active::after { transform: translateX(12px); }
468
- .prop-toggle-label { font-size: 11px; color: #888; }
469
- .prop-toggle:hover .prop-toggle-label { color: #555; }
470
-
471
- /* ─ Segmented control (alignment, format) ─ */
472
- .prop-segmented {
473
- display: flex; border: 1px solid #e0e0e0; border-radius: 5px;
474
- overflow: hidden; background: #f5f5f5;
475
- }
476
- .prop-seg-btn {
477
- flex: 1; padding: 4px 0; background: transparent; border: none;
478
- border-right: 1px solid #e0e0e0; cursor: pointer;
479
- font-size: 11px; color: #999; transition: all 0.12s; text-align: center;
480
- font-family: inherit;
481
- }
482
- .prop-seg-btn:last-child { border-right: none; }
483
- .prop-seg-btn:hover { background: #eee; color: #555; }
484
- .prop-seg-btn--active { background: #1976d2; color: white; }
485
-
486
- /* ─ Color picker (swatch + hex inline) ─ */
487
- .prop-color-row { display: flex; align-items: center; gap: 6px; }
488
- .prop-color-swatch {
489
- width: 22px; height: 22px; border-radius: 5px;
490
- border: 1px solid #ddd; cursor: pointer; flex-shrink: 0;
491
- position: relative; overflow: hidden;
104
+ static { this.styles = css `
105
+ :host {
106
+ display: block; height: 100%;
107
+ --zrd-bg: ${unsafeCSS(BG)};
108
+ --zrd-panel-bg: ${unsafeCSS(PANEL_BG)};
109
+ --zrd-border: ${unsafeCSS(BORDER)};
110
+ --zrd-accent: ${unsafeCSS(ACCENT)};
111
+ --zrd-accent-light: ${unsafeCSS(ACCENT_LIGHT)};
112
+ --zrd-text: ${unsafeCSS(TEXT)};
113
+ --zrd-text-muted: ${unsafeCSS(TEXT_MUTED)};
114
+ --zrd-danger: ${unsafeCSS(DANGER)};
115
+ font-family: 'Segoe UI', Roboto, Arial, sans-serif;
116
+ }
117
+
118
+ /* ─── Layout ──────────────────────────────── */
119
+ .designer {
120
+ display: grid;
121
+ grid-template-rows: auto 1fr;
122
+ grid-template-columns: 200px 4px 1fr 4px 240px;
123
+ height: 100%; background: var(--zrd-bg);
124
+ overflow: hidden;
125
+ }
126
+
127
+ /* ─── Toolbar ─────────────────────────────── */
128
+ .toolbar {
129
+ grid-column: 1 / -1;
130
+ display: flex; align-items: center; gap: 8px;
131
+ padding: 6px 12px; min-height: 36px;
132
+ background: var(--zrd-panel-bg);
133
+ border-bottom: 1px solid var(--zrd-border);
134
+ flex-wrap: wrap;
135
+ }
136
+ .toolbar-sep { width: 1px; height: 20px; background: var(--zrd-border); margin: 0 2px; }
137
+ .tb-btn {
138
+ background: none; border: 1px solid var(--zrd-border);
139
+ border-radius: 4px; padding: 4px 10px;
140
+ cursor: pointer; font-size: 12px; color: var(--zrd-text);
141
+ font-family: inherit; transition: background 0.15s;
142
+ white-space: nowrap; display: flex; align-items: center; gap: 4px;
143
+ }
144
+ .tb-btn:hover { background: var(--zrd-accent-light); }
145
+ .tb-btn:disabled { opacity: 0.4; cursor: default; }
146
+ .tb-btn--active { background: var(--zrd-accent); color: white; border-color: var(--zrd-accent); }
147
+ .tb-btn--danger:hover { background: #ffebee; color: var(--zrd-danger); border-color: var(--zrd-danger); }
148
+ .report-name {
149
+ font-weight: 600; font-size: 14px; cursor: pointer;
150
+ padding: 2px 6px; border-radius: 3px; border: 1px solid transparent;
151
+ color: var(--zrd-text);
152
+ }
153
+ .report-name:hover { border-color: var(--zrd-border); background: var(--zrd-accent-light); }
154
+ .report-name-input {
155
+ font-weight: 600; font-size: 14px; border: 1px solid var(--zrd-accent);
156
+ border-radius: 3px; padding: 2px 6px; outline: none;
157
+ background: white; color: var(--zrd-text); font-family: inherit;
158
+ }
159
+ .tb-spacer { flex: 1; }
160
+ .zoom-controls {
161
+ display: flex; align-items: center; gap: 4px;
162
+ padding-left: 8px; border-left: 1px solid var(--zrd-border);
163
+ }
164
+ .zoom-btn { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 3px; font-size: 14px; font-weight: bold; }
165
+ .zoom-label { font-size: 11px; min-width: 36px; text-align: center; color: var(--zrd-text-muted); cursor: pointer; }
166
+
167
+ /* ─── Resize Handle ───────────────────────── */
168
+ .panel-resize {
169
+ width: 4px; cursor: col-resize; background: transparent;
170
+ transition: background 0.15s; flex-shrink: 0;
171
+ }
172
+ .panel-resize:hover { background: var(--zrd-accent); }
173
+
174
+ /* ─── Left Panel (Toolbox) ────────────────── */
175
+ .left-panel {
176
+ background: var(--zrd-panel-bg);
177
+ border-right: 1px solid var(--zrd-border);
178
+ display: flex; flex-direction: column; overflow: hidden;
179
+ }
180
+ .panel-tabs {
181
+ display: flex; border-bottom: 1px solid var(--zrd-border);
182
+ }
183
+ .panel-tab {
184
+ flex: 1; padding: 8px 4px; text-align: center; cursor: pointer;
185
+ font-size: 11px; border-bottom: 2px solid transparent;
186
+ color: var(--zrd-text-muted); transition: all 0.15s;
187
+ background: none; border-top: none; border-left: none; border-right: none;
188
+ font-family: inherit;
189
+ }
190
+ .panel-tab:hover { color: var(--zrd-text); }
191
+ .panel-tab--active { border-bottom-color: var(--zrd-accent); color: var(--zrd-accent); font-weight: 600; }
192
+ .panel-content { padding: 4px; overflow-y: auto; flex: 1; }
193
+ .panel-content::-webkit-scrollbar { width: 6px; }
194
+ .panel-content::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
195
+
196
+ /* Toolbox grid (3 columns like report-designer) */
197
+ .toolbox-section {
198
+ font-size: 9px; font-weight: 700; text-transform: uppercase;
199
+ letter-spacing: 0.8px; color: #aaa;
200
+ padding: 10px 8px 5px; margin-top: 2px;
201
+ }
202
+ .toolbox-section:first-child { margin-top: 0; padding-top: 6px; }
203
+ .toolbox-grid {
204
+ display: grid; grid-template-columns: repeat(3, 1fr);
205
+ gap: 3px; padding: 0 4px;
206
+ }
207
+ .toolbox-item {
208
+ display: flex; flex-direction: column; align-items: center;
209
+ gap: 3px; padding: 7px 2px 5px;
210
+ border: 1px solid transparent; border-radius: 5px;
211
+ cursor: grab; user-select: none; text-align: center;
212
+ transition: all 0.15s; background: transparent;
213
+ }
214
+ .toolbox-item:hover {
215
+ background: var(--zrd-accent-light); border-color: #c5dcf0;
216
+ box-shadow: 0 1px 4px rgba(25,118,210,0.1);
217
+ }
218
+ .toolbox-item:active { cursor: grabbing; opacity: 0.6; transform: scale(0.95); }
219
+ .toolbox-icon {
220
+ width: 28px; height: 28px; display: flex; align-items: center;
221
+ justify-content: center; border-radius: 6px;
222
+ font-size: 15px; flex-shrink: 0;
223
+ background: #f0f4f8; color: #1976d2;
224
+ border: 1px solid #e3e8ee;
225
+ transition: all 0.15s;
226
+ }
227
+ .toolbox-item:hover .toolbox-icon {
228
+ background: #1976d2; color: white; border-color: #1976d2;
229
+ box-shadow: 0 2px 6px rgba(25,118,210,0.3);
230
+ }
231
+ .toolbox-label {
232
+ font-size: 9px; font-weight: 500; line-height: 1.1;
233
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
234
+ max-width: 100%; color: #777;
235
+ }
236
+ .toolbox-item:hover .toolbox-label { color: #1976d2; }
237
+
238
+ /* ─── Canvas ──────────────────────────────── */
239
+ .canvas-area {
240
+ overflow: auto; padding: 30px; position: relative;
241
+ background: #d0d0d0; display: flex; justify-content: center;
242
+ }
243
+ .canvas {
244
+ background: white; position: relative; margin: 0 auto; border-radius: 2px;
245
+ box-shadow: 0 4px 20px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.08);
246
+ min-width: 600px; min-height: 400px; padding: 24px;
247
+ transform-origin: top center;
248
+ }
249
+ .canvas-section { margin-bottom: 20px; }
250
+ .canvas-section-header {
251
+ font-size: 13px; font-weight: 600; color: var(--zrd-text);
252
+ padding: 6px 8px; margin-bottom: 8px;
253
+ background: #f0f0f0; border-radius: 4px; border-left: 3px solid var(--zrd-accent);
254
+ display: flex; align-items: center; gap: 8px; cursor: pointer;
255
+ }
256
+ .canvas-section-header:hover { background: var(--zrd-accent-light); }
257
+ .canvas-grid {
258
+ display: grid; gap: 8px; padding: 4px;
259
+ background: repeating-linear-gradient(0deg, transparent, transparent 19px, rgba(0,0,0,0.03) 19px, rgba(0,0,0,0.03) 20px),
260
+ repeating-linear-gradient(90deg, transparent, transparent 19px, rgba(0,0,0,0.03) 19px, rgba(0,0,0,0.03) 20px);
261
+ }
262
+
263
+ /* Field on canvas */
264
+ .canvas-field {
265
+ position: relative; border: 1px solid transparent;
266
+ border-radius: 4px; cursor: pointer; user-select: none;
267
+ transition: border-color 0.15s, box-shadow 0.15s;
268
+ padding: 4px;
269
+ }
270
+ .canvas-field:hover { border-color: rgba(25,118,210,0.5); }
271
+ .canvas-field--selected {
272
+ border-color: var(--zrd-accent);
273
+ box-shadow: 0 0 0 1px var(--zrd-accent);
274
+ }
275
+
276
+ /* Type badge (above field) */
277
+ .field-type-badge {
278
+ position: absolute; top: -14px; left: 0;
279
+ font-size: 9px; border-radius: 3px 3px 0 0;
280
+ padding: 1px 6px; line-height: 13px;
281
+ pointer-events: none; z-index: 6; display: none;
282
+ white-space: nowrap;
283
+ }
284
+ .canvas-field:hover .field-type-badge,
285
+ .canvas-field--selected .field-type-badge { display: block; }
286
+
287
+ /* Resize handles (8-point like report-designer) */
288
+ .rh { position: absolute; width: 6px; height: 6px; background: var(--zrd-accent); border: 1px solid white; z-index: 5; display: none; border-radius: 1px; }
289
+ .rh:hover { background: #0d47a1; }
290
+ .canvas-field--selected .rh { display: block; }
291
+ .rh-nw { top: -3px; left: -3px; cursor: nw-resize; }
292
+ .rh-n { top: -3px; left: calc(50% - 3px); cursor: n-resize; }
293
+ .rh-ne { top: -3px; right: -3px; cursor: ne-resize; }
294
+ .rh-e { top: calc(50% - 3px); right: -3px; cursor: e-resize; }
295
+ .rh-se { bottom: -3px; right: -3px; cursor: se-resize; }
296
+ .rh-s { bottom: -3px; left: calc(50% - 3px); cursor: s-resize; }
297
+ .rh-sw { bottom: -3px; left: -3px; cursor: sw-resize; }
298
+ .rh-w { top: calc(50% - 3px); left: -3px; cursor: w-resize; }
299
+
300
+ /* Action buttons on field */
301
+ .field-actions {
302
+ position: absolute; top: -14px; right: 4px;
303
+ display: flex; gap: 2px; opacity: 0; transition: opacity 0.15s; z-index: 7;
304
+ }
305
+ .canvas-field:hover .field-actions { opacity: 1; }
306
+ .fa-btn {
307
+ width: 18px; height: 14px; border-radius: 3px 3px 0 0;
308
+ border: none; cursor: pointer; font-size: 9px;
309
+ display: flex; align-items: center; justify-content: center;
310
+ }
311
+ .fa-btn--move { background: var(--zrd-accent); color: white; }
312
+ .fa-btn--delete { background: var(--zrd-danger); color: white; }
313
+ .fa-btn--copy { background: #7c3aed; color: white; }
314
+
315
+ /* Field preview content */
316
+ .field-preview {
317
+ pointer-events: none;
318
+ }
319
+ .field-preview-label {
320
+ font-size: 11px; font-weight: 500; color: var(--zrd-text-muted);
321
+ margin-bottom: 3px;
322
+ }
323
+ .field-preview-input {
324
+ height: 30px; border: 1px solid #e0e0e0; border-radius: 4px;
325
+ background: #fafafa; display: flex; align-items: center;
326
+ padding: 0 8px; font-size: 12px; color: #999;
327
+ }
328
+ .field-preview-input--textarea { height: 60px; align-items: flex-start; padding-top: 6px; }
329
+ .field-preview-input--switch { height: auto; border: none; background: none; padding: 0; }
330
+ .field-preview-input--separator { height: 1px; border: none; background: #ddd; padding: 0; }
331
+ .field-preview-input--heading { height: auto; border: none; background: none; padding: 0; font-size: 16px; font-weight: 600; color: var(--zrd-text); }
332
+ .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); }
333
+ .field-preview-input--report { height: 120px; border: 2px dashed #e65100; background: #fff3e0; justify-content: center; font-weight: 500; color: #e65100; }
334
+ .field-preview-input--chart { height: 120px; border: 2px dashed #6a1b9a; background: #f3e5f5; justify-content: center; font-weight: 500; color: #6a1b9a; }
335
+
336
+ /* Drop zone */
337
+ .drop-zone {
338
+ border: 2px dashed var(--zrd-border); border-radius: 6px;
339
+ padding: 16px; text-align: center; margin: 4px 0;
340
+ color: var(--zrd-text-muted); font-size: 12px; transition: all 0.15s;
341
+ }
342
+ .drop-zone--active { border-color: var(--zrd-accent); background: var(--zrd-accent-light); color: var(--zrd-accent); }
343
+
344
+ /* ─── Right Panel (Properties — Figma-quality) ──── */
345
+ .right-panel {
346
+ background: var(--zrd-panel-bg);
347
+ border-left: 1px solid var(--zrd-border);
348
+ overflow-y: auto; font-size: 11px;
349
+ }
350
+ .right-panel::-webkit-scrollbar { width: 5px; }
351
+ .right-panel::-webkit-scrollbar-thumb { background: #d4d4d4; border-radius: 3px; }
352
+ .right-panel::-webkit-scrollbar-thumb:hover { background: #bbb; }
353
+
354
+ /* ─ Prop Section ─ */
355
+ .prop-section { border-bottom: 1px solid #eee; padding: 8px 10px 10px; }
356
+ .prop-section:last-child { border-bottom: none; }
357
+ .prop-section-header {
358
+ display: flex; align-items: center; gap: 6px;
359
+ cursor: pointer; user-select: none; margin-bottom: 6px; padding: 2px 0;
360
+ }
361
+ .prop-section-header h4 {
362
+ font-size: 10px; font-weight: 700; text-transform: uppercase;
363
+ letter-spacing: 0.6px; margin: 0; flex: 1;
364
+ }
365
+ .prop-section-header[data-section="general"] h4 { color: #1976d2; }
366
+ .prop-section-header[data-section="layout"] h4 { color: #7c3aed; }
367
+ .prop-section-header[data-section="behavior"] h4 { color: #0d9488; }
368
+ .prop-section-header[data-section="rules"] h4 { color: #ea580c; }
369
+ .prop-section-header[data-section="style"] h4 { color: #c2185b; }
370
+ .prop-section-header[data-section="form"] h4 { color: #1976d2; }
371
+ .collapse-icon {
372
+ font-size: 8px; color: #bbb; transition: transform 0.15s;
373
+ width: 14px; height: 14px; display: flex; align-items: center;
374
+ justify-content: center; border-radius: 3px;
375
+ }
376
+ .collapse-icon:hover { background: #f0f0f0; color: #666; }
377
+ .collapse-icon--collapsed { transform: rotate(-90deg); }
378
+
379
+ /* ─ Prop Rows ─ */
380
+ .prop-row {
381
+ display: grid; grid-template-columns: 62px 1fr;
382
+ align-items: center; gap: 4px; min-height: 26px; margin-bottom: 1px;
383
+ }
384
+ .prop-row-full { grid-template-columns: 1fr; margin-bottom: 3px; }
385
+ .prop-label {
386
+ font-size: 11px; color: #999; font-weight: 400;
387
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
388
+ padding-left: 1px; line-height: 1;
389
+ }
390
+
391
+ /* ─ Figma-style input ─ */
392
+ .prop-input {
393
+ width: 100%; border: 1px solid transparent; border-radius: 4px;
394
+ padding: 5px 7px; font-size: 11px; background: #f5f5f5;
395
+ color: var(--zrd-text); outline: none; min-width: 0;
396
+ font-family: inherit; box-sizing: border-box;
397
+ transition: border-color 0.12s, background 0.12s, box-shadow 0.12s;
398
+ }
399
+ .prop-input:hover { background: #efefef; border-color: #ddd; }
400
+ .prop-input:focus { border-color: #1976d2; background: white; box-shadow: 0 0 0 2px rgba(25,118,210,0.08); }
401
+ select.prop-input {
402
+ padding: 4px 20px 4px 7px; cursor: pointer; appearance: none;
403
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23999'/%3E%3C/svg%3E");
404
+ background-repeat: no-repeat; background-position: right 6px center;
405
+ }
406
+ textarea.prop-input {
407
+ min-height: 44px; resize: vertical; font-family: 'SF Mono','Consolas','Monaco',monospace;
408
+ font-size: 10px; line-height: 1.5; padding: 6px 7px;
409
+ }
410
+
411
+ /* ─ Numeric stepper (Figma-style) ─ */
412
+ .prop-stepper {
413
+ display: flex; align-items: center; background: #f5f5f5;
414
+ border: 1px solid transparent; border-radius: 4px; overflow: hidden;
415
+ transition: border-color 0.12s;
416
+ }
417
+ .prop-stepper:hover { border-color: #ddd; }
418
+ .prop-stepper:focus-within { border-color: #1976d2; background: white; }
419
+ .prop-stepper input {
420
+ border: none; background: transparent; width: 100%;
421
+ font-size: 11px; color: var(--zrd-text); outline: none;
422
+ padding: 4px 2px 4px 7px; min-width: 0; font-family: inherit;
423
+ -moz-appearance: textfield;
424
+ }
425
+ .prop-stepper input::-webkit-inner-spin-button { -webkit-appearance: none; }
426
+ .prop-stepper-btns {
427
+ display: flex; flex-direction: column; border-left: 1px solid #eee;
428
+ }
429
+ .prop-stepper-btn {
430
+ border: none; background: none; cursor: pointer; padding: 0;
431
+ width: 18px; height: 12px; display: flex; align-items: center;
432
+ justify-content: center; font-size: 7px; color: #999;
433
+ transition: all 0.1s;
434
+ }
435
+ .prop-stepper-btn:hover { background: #e8e8e8; color: #333; }
436
+
437
+ /* ─ Position grid (4-cell Figma layout) ─ */
438
+ .prop-pos-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3px; }
439
+ .prop-pos-cell {
440
+ display: flex; align-items: center; background: #f5f5f5;
441
+ border: 1px solid transparent; border-radius: 4px; padding: 0 6px;
442
+ height: 26px; gap: 3px; transition: border-color 0.12s;
443
+ }
444
+ .prop-pos-cell:hover { border-color: #ddd; }
445
+ .prop-pos-cell:focus-within { border-color: #1976d2; background: white; }
446
+ .prop-pos-label {
447
+ font-size: 9px; font-weight: 700; width: 10px; text-align: center;
448
+ flex-shrink: 0; user-select: none;
449
+ }
450
+ .prop-pos-label--x { color: #ef4444; }
451
+ .prop-pos-label--y { color: #3b82f6; }
452
+ .prop-pos-label--w { color: #8b5cf6; }
453
+ .prop-pos-label--h { color: #10b981; }
454
+ .prop-pos-cell input {
455
+ border: none; background: transparent; width: 100%;
456
+ font-size: 11px; color: var(--zrd-text); outline: none;
457
+ padding: 0; min-width: 0; font-family: inherit;
458
+ }
459
+
460
+ /* ─ Toggle switches (Figma compact) ─ */
461
+ .prop-toggle {
462
+ display: flex; align-items: center; gap: 8px;
463
+ min-height: 24px; padding: 1px 0;
464
+ }
465
+ .prop-switch {
466
+ position: relative; width: 28px; height: 16px; border-radius: 8px;
467
+ background: #d4d4d4; cursor: pointer; transition: background 0.2s;
468
+ flex-shrink: 0; border: none; padding: 0;
469
+ }
470
+ .prop-switch--active { background: #1976d2; }
471
+ .prop-switch::after {
472
+ content: ''; position: absolute; top: 2px; left: 2px;
473
+ width: 12px; height: 12px; border-radius: 50%;
474
+ background: white; box-shadow: 0 1px 2px rgba(0,0,0,0.15);
475
+ transition: transform 0.2s;
476
+ }
477
+ .prop-switch--active::after { transform: translateX(12px); }
478
+ .prop-toggle-label { font-size: 11px; color: #888; }
479
+ .prop-toggle:hover .prop-toggle-label { color: #555; }
480
+
481
+ /* ─ Segmented control (alignment, format) ─ */
482
+ .prop-segmented {
483
+ display: flex; border: 1px solid #e0e0e0; border-radius: 5px;
484
+ overflow: hidden; background: #f5f5f5;
485
+ }
486
+ .prop-seg-btn {
487
+ flex: 1; padding: 4px 0; background: transparent; border: none;
488
+ border-right: 1px solid #e0e0e0; cursor: pointer;
489
+ font-size: 11px; color: #999; transition: all 0.12s; text-align: center;
490
+ font-family: inherit;
491
+ }
492
+ .prop-seg-btn:last-child { border-right: none; }
493
+ .prop-seg-btn:hover { background: #eee; color: #555; }
494
+ .prop-seg-btn--active { background: #1976d2; color: white; }
495
+
496
+ /* ─ Color picker (swatch + hex inline) ─ */
497
+ .prop-color-row { display: flex; align-items: center; gap: 6px; }
498
+ .prop-color-swatch {
499
+ width: 22px; height: 22px; border-radius: 5px;
500
+ border: 1px solid #ddd; cursor: pointer; flex-shrink: 0;
501
+ position: relative; overflow: hidden;
502
+ }
503
+ .prop-color-swatch input[type="color"] {
504
+ position: absolute; inset: -4px; width: calc(100% + 8px);
505
+ height: calc(100% + 8px); cursor: pointer; border: none; padding: 0;
506
+ }
507
+ .prop-color-hex {
508
+ border: 1px solid transparent; border-radius: 4px;
509
+ padding: 4px 6px; font-size: 10px;
510
+ font-family: 'SF Mono','Consolas',monospace;
511
+ background: #f5f5f5; color: var(--zrd-text); width: 70px; outline: none;
512
+ box-sizing: border-box;
513
+ }
514
+ .prop-color-hex:hover { border-color: #ddd; }
515
+ .prop-color-hex:focus { border-color: #1976d2; background: white; }
516
+
517
+ /* ─ Info badge ─ */
518
+ .prop-info {
519
+ display: flex; align-items: center; gap: 4px;
520
+ padding: 5px 8px; border-radius: 4px; background: #f0f7ff;
521
+ font-size: 10px; color: #1976d2; margin-bottom: 4px;
522
+ }
523
+
524
+ /* ─ Divider line ─ */
525
+ .prop-divider { height: 1px; background: #f0f0f0; margin: 4px 0; }
526
+
527
+ /* ─ Empty state ─ */
528
+ .props-empty {
529
+ text-align: center; padding: 32px 16px; color: #ccc;
530
+ }
531
+ .props-empty-icon { font-size: 28px; margin-bottom: 6px; opacity: 0.4; }
532
+ .props-empty-text { font-size: 11px; line-height: 1.5; }
533
+
534
+ /* ─ Type badge ─ */
535
+ .props-type-badge {
536
+ display: inline-flex; align-items: center; gap: 4px;
537
+ padding: 3px 8px; border-radius: 4px;
538
+ font-size: 10px; font-weight: 700; letter-spacing: 0.3px;
539
+ text-transform: uppercase;
540
+ }
541
+ .props-field-id {
542
+ font-size: 10px; color: #bbb; font-family: 'SF Mono','Consolas',monospace;
543
+ margin-top: 2px;
544
+ }
545
+
546
+ /* ─ JSON view ─ */
547
+ .json-panel {
548
+ font-family: 'SF Mono','Consolas','Monaco',monospace; font-size: 11px;
549
+ line-height: 1.5; background: #1e1e2d; color: #a2a3b7;
550
+ padding: 16px; border-radius: 4px; overflow: auto;
551
+ white-space: pre; tab-size: 2;
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
+ }
591
+ `; }
592
+ // ─── Lifecycle ────────────────────────────────────
593
+ updated(changed) {
594
+ if (changed.has('schema') && this.schema && this.undoStack.length === 0) {
595
+ this.undoStack = [JSON.stringify(this.schema)];
596
+ }
597
+ // Initialize InteractJS after render when in design mode
598
+ if (this.viewMode === 'design' && this.schema) {
599
+ this.initInteract();
600
+ }
492
601
  }
493
- .prop-color-swatch input[type="color"] {
494
- position: absolute; inset: -4px; width: calc(100% + 8px);
495
- height: calc(100% + 8px); cursor: pointer; border: none; padding: 0;
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());
496
622
  }
497
- .prop-color-hex {
498
- border: 1px solid transparent; border-radius: 4px;
499
- padding: 4px 6px; font-size: 10px;
500
- font-family: 'SF Mono','Consolas',monospace;
501
- background: #f5f5f5; color: var(--zrd-text); width: 70px; outline: none;
502
- box-sizing: border-box;
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
+ }
503
640
  }
504
- .prop-color-hex:hover { border-color: #ddd; }
505
- .prop-color-hex:focus { border-color: #1976d2; background: white; }
506
-
507
- /* Info badge ─ */
508
- .prop-info {
509
- display: flex; align-items: center; gap: 4px;
510
- padding: 5px 8px; border-radius: 4px; background: #f0f7ff;
511
- font-size: 10px; color: #1976d2; margin-bottom: 4px;
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
+ });
512
823
  }
513
-
514
- /* ─ Divider line ─ */
515
- .prop-divider { height: 1px; background: #f0f0f0; margin: 4px 0; }
516
-
517
- /* Empty state ─ */
518
- .props-empty {
519
- text-align: center; padding: 32px 16px; color: #ccc;
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
+ }
520
839
  }
521
- .props-empty-icon { font-size: 28px; margin-bottom: 6px; opacity: 0.4; }
522
- .props-empty-text { font-size: 11px; line-height: 1.5; }
523
-
524
- /* Type badge ─ */
525
- .props-type-badge {
526
- display: inline-flex; align-items: center; gap: 4px;
527
- padding: 3px 8px; border-radius: 4px;
528
- font-size: 10px; font-weight: 700; letter-spacing: 0.3px;
529
- text-transform: uppercase;
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
+ }
530
889
  }
531
- .props-field-id {
532
- font-size: 10px; color: #bbb; font-family: 'SF Mono','Consolas',monospace;
533
- margin-top: 2px;
890
+ removeInsertIndicator() {
891
+ if (!this.shadowRoot)
892
+ return;
893
+ this.shadowRoot.querySelectorAll('[data-insert-line]').forEach(el => el.remove());
534
894
  }
535
-
536
- /* JSON view */
537
- .json-panel {
538
- font-family: 'SF Mono','Consolas','Monaco',monospace; font-size: 11px;
539
- line-height: 1.5; background: #1e1e2d; color: #a2a3b7;
540
- padding: 16px; border-radius: 4px; overflow: auto;
541
- white-space: pre; tab-size: 2;
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();
542
914
  }
543
- `; }
544
- // ─── Lifecycle ────────────────────────────────────
545
- updated(changed) {
546
- if (changed.has('schema') && this.schema && this.undoStack.length === 0) {
547
- this.undoStack = [JSON.stringify(this.schema)];
915
+ addFieldAtIndex(type, sectionIndex, insertIndex) {
916
+ if (!this.schema) {
917
+ this.addField(type, sectionIndex);
918
+ return;
548
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();
549
933
  }
550
934
  // ─── Undo/Redo ────────────────────────────────────
551
935
  pushUndo() {
@@ -631,6 +1015,84 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
631
1015
  this.selectedFieldId = id;
632
1016
  this.commitChange();
633
1017
  }
1018
+ /** Handle drop from toolbox OR API fields — unified */
1019
+ handleCanvasDrop(e, sectionIndex) {
1020
+ e.preventDefault();
1021
+ const fieldName = e.dataTransfer?.getData('field-name');
1022
+ const dsId = e.dataTransfer?.getData('ds-id');
1023
+ if (fieldName && dsId) {
1024
+ // Drop from API tab — create field with smart type + binding
1025
+ this.addApiField(fieldName, dsId, sectionIndex);
1026
+ }
1027
+ else if (this.dragType) {
1028
+ // Drop from toolbox
1029
+ this.addField(this.dragType, sectionIndex);
1030
+ }
1031
+ this.dragType = null;
1032
+ }
1033
+ /** Add a field from an API data source with smart type detection */
1034
+ addApiField(fieldName, dsId, sectionIndex = 0) {
1035
+ if (!this.schema) {
1036
+ this.schema = {
1037
+ id: 'new-form', version: '1.0', title: 'Nuevo Formulario',
1038
+ layout: { type: 'grid', columns: 2 },
1039
+ sections: [{ id: 'main', title: 'Datos', fields: [] }],
1040
+ };
1041
+ this.undoStack = [JSON.stringify(this.schema)];
1042
+ }
1043
+ if (sectionIndex >= this.schema.sections.length)
1044
+ sectionIndex = 0;
1045
+ const type = this.inferFieldType(fieldName);
1046
+ const id = `${fieldName.toLowerCase().replace(/[^a-z0-9]/g, '_')}_${Date.now()}`;
1047
+ const label = this.humanizeFieldName(fieldName);
1048
+ const meta = getAllFields().find(f => f.type === type);
1049
+ this.schema.sections[sectionIndex].fields.push({
1050
+ id,
1051
+ type,
1052
+ field: fieldName,
1053
+ label,
1054
+ props: { ...meta?.defaultProps, dataSourceId: dsId },
1055
+ });
1056
+ this.selectedFieldId = id;
1057
+ this.commitChange();
1058
+ }
1059
+ /** Infer field type from column/field name */
1060
+ inferFieldType(name) {
1061
+ const lower = name.toLowerCase();
1062
+ if (lower.includes('email') || lower.includes('correo'))
1063
+ return 'email';
1064
+ if (lower.includes('phone') || lower.includes('telefono') || lower.includes('celular'))
1065
+ return 'phone';
1066
+ if (lower.includes('fecha') || lower.includes('date') || lower.includes('nacimiento') || lower.includes('ingreso') || lower.includes('vencimiento'))
1067
+ return 'date';
1068
+ if (lower.includes('precio') || lower.includes('price') || lower.includes('costo') || lower.includes('monto') || lower.includes('total') || lower.includes('saldo') || lower.includes('salario') || lower.includes('limite'))
1069
+ return 'currency';
1070
+ if (lower.includes('cantidad') || lower.includes('qty') || lower.includes('stock') || lower.includes('edad'))
1071
+ return 'number';
1072
+ if (lower.includes('activ') || lower.includes('active') || lower.includes('enabled') || lower.includes('visible'))
1073
+ return 'switch';
1074
+ if (lower.includes('descripcion') || lower.includes('description') || lower.includes('notas') || lower.includes('notes') || lower.includes('observ'))
1075
+ return 'textarea';
1076
+ if (lower.includes('password') || lower.includes('clave'))
1077
+ return 'password';
1078
+ if (lower.includes('url') || lower.includes('website') || lower.includes('link'))
1079
+ return 'url';
1080
+ if (lower.includes('imagen') || lower.includes('image') || lower.includes('foto') || lower.includes('avatar'))
1081
+ return 'image';
1082
+ if (lower.includes('pais') || lower.includes('country') || lower.includes('estado') || lower.includes('tipo') || lower.includes('status') || lower.includes('genero') || lower.includes('categoria'))
1083
+ return 'select';
1084
+ if (lower.includes('direccion') || lower.includes('address'))
1085
+ return 'address';
1086
+ return 'text';
1087
+ }
1088
+ /** Convert FIELD_NAME to "Field Name" */
1089
+ humanizeFieldName(name) {
1090
+ return name
1091
+ .replace(/([A-Z])/g, ' $1')
1092
+ .replace(/[_-]/g, ' ')
1093
+ .replace(/\b\w/g, c => c.toUpperCase())
1094
+ .trim();
1095
+ }
634
1096
  removeField(si, fi) {
635
1097
  if (!this.schema)
636
1098
  return;
@@ -663,77 +1125,77 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
663
1125
  }
664
1126
  // ─── Render ───────────────────────────────────────
665
1127
  render() {
666
- return html `
667
- <div class="designer">
668
- ${this.renderToolbar()}
669
- ${this.renderLeftPanel()}
670
- <div class="panel-resize"></div>
671
- ${this.renderCanvas()}
672
- <div class="panel-resize"></div>
673
- ${this.renderRightPanel()}
674
- </div>
1128
+ return html `
1129
+ <div class="designer">
1130
+ ${this.renderToolbar()}
1131
+ ${this.renderLeftPanel()}
1132
+ <div class="panel-resize"></div>
1133
+ ${this.renderCanvas()}
1134
+ <div class="panel-resize"></div>
1135
+ ${this.renderRightPanel()}
1136
+ </div>
675
1137
  `;
676
1138
  }
677
1139
  // ─── Toolbar ──────────────────────────────────────
678
1140
  renderToolbar() {
679
- return html `
680
- <div class="toolbar">
1141
+ return html `
1142
+ <div class="toolbar">
681
1143
  ${this.editingTitle
682
- ? html `<input class="report-name-input" .value="${this.schema?.title ?? ''}"
1144
+ ? html `<input class="report-name-input" .value="${this.schema?.title ?? ''}"
683
1145
  @blur="${(e) => { if (this.schema)
684
- this.schema.title = e.target.value; this.editingTitle = false; this.commitChange(); }}"
1146
+ this.schema.title = e.target.value; this.editingTitle = false; this.commitChange(); }}"
685
1147
  @keydown="${(e) => { if (e.key === 'Enter')
686
- e.target.blur(); }}"
1148
+ e.target.blur(); }}"
687
1149
  />`
688
- : html `<span class="report-name" @click="${() => { this.editingTitle = true; }}">${this.schema?.title || 'Sin titulo'}</span>`}
689
-
690
- <div class="toolbar-sep"></div>
691
-
692
- <button class="tb-btn" ?disabled="${this.undoStack.length <= 1}" @click="${this.undo}" title="Deshacer (Ctrl+Z)">↩ Deshacer</button>
693
- <button class="tb-btn" ?disabled="${this.redoStack.length === 0}" @click="${this.redo}" title="Rehacer (Ctrl+Y)">↪ Rehacer</button>
694
-
695
- <div class="toolbar-sep"></div>
696
-
1150
+ : html `<span class="report-name" @click="${() => { this.editingTitle = true; }}">${this.schema?.title || 'Sin titulo'}</span>`}
1151
+
1152
+ <div class="toolbar-sep"></div>
1153
+
1154
+ <button class="tb-btn" ?disabled="${this.undoStack.length <= 1}" @click="${this.undo}" title="Deshacer (Ctrl+Z)">↩ Deshacer</button>
1155
+ <button class="tb-btn" ?disabled="${this.redoStack.length === 0}" @click="${this.redo}" title="Rehacer (Ctrl+Y)">↪ Rehacer</button>
1156
+
1157
+ <div class="toolbar-sep"></div>
1158
+
697
1159
  <button class="tb-btn" @click="${() => {
698
1160
  if (!this.schema)
699
1161
  return;
700
1162
  this.schema.sections.push({ id: `section_${Date.now()}`, title: 'Nueva Seccion', fields: [] });
701
1163
  this.commitChange();
702
- }}">+ Seccion</button>
703
-
704
- <div style="position:relative;display:inline-block;">
705
- <button class="tb-btn" @click="${() => { this.showTemplateMenu = !this.showTemplateMenu; }}">📋 Plantillas ▾</button>
706
- ${this.showTemplateMenu ? this.renderTemplateMenu() : nothing}
707
- </div>
708
-
1164
+ }}">+ Seccion</button>
1165
+
1166
+ <div style="position:relative;display:inline-block;">
1167
+ <button class="tb-btn" @click="${() => { this.showTemplateMenu = !this.showTemplateMenu; }}">📋 Plantillas ▾</button>
1168
+ ${this.showTemplateMenu ? this.renderTemplateMenu() : nothing}
1169
+ </div>
1170
+
709
1171
  <button class="tb-btn tb-btn--danger" @click="${() => {
710
1172
  this.schema = { id: 'new-form', version: '1.0', title: 'Nuevo Formulario', layout: { type: 'grid', columns: 2 }, sections: [{ id: 'main', title: 'Datos', fields: [] }] };
711
1173
  this.undoStack = [JSON.stringify(this.schema)];
712
1174
  this.redoStack = [];
713
1175
  this.selectedFieldId = null;
714
1176
  this.commitChange();
715
- }}">🗑 Nuevo</button>
716
-
717
- <div class="toolbar-sep"></div>
718
-
719
- <button class="tb-btn ${this.viewMode === 'design' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'design'; }}">✏️ Diseño</button>
720
- <button class="tb-btn ${this.viewMode === 'preview' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'preview'; }}">👁 Preview</button>
721
- <button class="tb-btn ${this.viewMode === 'json' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'json'; }}">&lt;/&gt; JSON</button>
722
-
723
- <span class="tb-spacer"></span>
724
-
1177
+ }}">🗑 Nuevo</button>
1178
+
1179
+ <div class="toolbar-sep"></div>
1180
+
1181
+ <button class="tb-btn ${this.viewMode === 'design' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'design'; }}">✏️ Diseño</button>
1182
+ <button class="tb-btn ${this.viewMode === 'preview' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'preview'; }}">👁 Preview</button>
1183
+ <button class="tb-btn ${this.viewMode === 'json' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'json'; }}">&lt;/&gt; JSON</button>
1184
+
1185
+ <span class="tb-spacer"></span>
1186
+
725
1187
  <button class="tb-btn" @click="${() => {
726
1188
  if (this.schema) {
727
1189
  navigator.clipboard.writeText(JSON.stringify(this.schema, null, 2));
728
1190
  }
729
- }}">📋 Copiar</button>
730
-
731
- <div class="zoom-controls">
732
- <button class="tb-btn zoom-btn" @click="${() => { this.zoom = Math.max(0.5, this.zoom - 0.1); }}">−</button>
733
- <span class="zoom-label" @click="${() => { this.zoom = 1; }}">${Math.round(this.zoom * 100)}%</span>
734
- <button class="tb-btn zoom-btn" @click="${() => { this.zoom = Math.min(2, this.zoom + 0.1); }}">+</button>
735
- </div>
736
- </div>
1191
+ }}">📋 Copiar</button>
1192
+
1193
+ <div class="zoom-controls">
1194
+ <button class="tb-btn zoom-btn" @click="${() => { this.zoom = Math.max(0.5, this.zoom - 0.1); }}">−</button>
1195
+ <span class="zoom-label" @click="${() => { this.zoom = 1; }}">${Math.round(this.zoom * 100)}%</span>
1196
+ <button class="tb-btn zoom-btn" @click="${() => { this.zoom = Math.min(2, this.zoom + 0.1); }}">+</button>
1197
+ </div>
1198
+ </div>
737
1199
  `;
738
1200
  }
739
1201
  // ─── Left Panel ───────────────────────────────────
@@ -745,104 +1207,103 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
745
1207
  { key: 'media', label: 'Media' },
746
1208
  { key: 'layout', label: 'Layout' },
747
1209
  ];
748
- return html `
749
- <div class="left-panel">
750
- <div class="panel-tabs">
751
- <button class="panel-tab ${this.leftTab === 'fields' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'fields'; }}">Campos</button>
752
- <button class="panel-tab ${this.leftTab === 'sections' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'sections'; }}">Secciones</button>
753
- <button class="panel-tab ${this.leftTab === 'api' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'api'; }}" style="${this.leftTab === 'api' ? 'color:#ea580c;border-bottom-color:#ea580c;' : ''}">API</button>
754
- </div>
755
- <div class="panel-content">
756
- ${this.leftTab === 'fields' ? html `
1210
+ return html `
1211
+ <div class="left-panel">
1212
+ <div class="panel-tabs">
1213
+ <button class="panel-tab ${this.leftTab === 'fields' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'fields'; }}">Campos</button>
1214
+ <button class="panel-tab ${this.leftTab === 'sections' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'sections'; }}">Secciones</button>
1215
+ <button class="panel-tab ${this.leftTab === 'api' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'api'; }}" style="${this.leftTab === 'api' ? 'color:#ea580c;border-bottom-color:#ea580c;' : ''}">API</button>
1216
+ </div>
1217
+ <div class="panel-content">
1218
+ ${this.leftTab === 'fields' ? html `
757
1219
  ${categories.map(cat => {
758
1220
  const items = getFieldsByCategory(cat.key);
759
1221
  if (items.length === 0)
760
1222
  return nothing;
761
- return html `
762
- <div class="toolbox-section">${cat.label}</div>
763
- <div class="toolbox-grid">
764
- ${items.map(f => html `
765
- <div class="toolbox-item"
766
- draggable="true"
767
- @dragstart="${(e) => { this.dragType = f.type; e.dataTransfer?.setData('text/plain', f.type); }}"
768
- @dragend="${() => { this.dragType = null; }}"
769
- @dblclick="${() => this.addField(f.type)}"
770
- title="${f.label}"
771
- >
772
- <span class="toolbox-icon">${unsafeHTML(resolveIcon(f.icon, this.provider))}</span>
773
- <span class="toolbox-label">${f.label}</span>
774
- </div>
775
- `)}
776
- </div>
1223
+ return html `
1224
+ <div class="toolbox-section">${cat.label}</div>
1225
+ <div class="toolbox-grid">
1226
+ ${items.map(f => html `
1227
+ <div class="toolbox-item"
1228
+ draggable="true"
1229
+ data-field-type="${f.type}"
1230
+ @dragstart="${(e) => { this.dragType = f.type; e.dataTransfer?.setData('text/plain', f.type); }}"
1231
+ @dragend="${() => { this.dragType = null; }}"
1232
+ @dblclick="${() => this.addField(f.type)}"
1233
+ title="${f.label}"
1234
+ >
1235
+ <span class="toolbox-icon">${unsafeHTML(resolveIcon(f.icon, this.provider))}</span>
1236
+ <span class="toolbox-label">${f.label}</span>
1237
+ </div>
1238
+ `)}
1239
+ </div>
777
1240
  `;
778
- })}
779
- ` : this.leftTab === 'sections' ? html `
780
- ${this.schema?.sections.map((s, i) => html `
781
- <div style="padding:8px 10px;margin:2px 4px;background:${i % 2 === 0 ? '#f8f9fa' : 'white'};border-radius:6px;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:8px;border:1px solid #eee;transition:all 0.15s;"
782
- @click="${() => { }}"
783
- >
784
- <span style="background:#e3f2fd;color:#1976d2;font-weight:700;font-size:10px;padding:2px 6px;border-radius:4px;">§${i + 1}</span>
785
- <span style="flex:1;font-weight:500;">${s.title ?? 'Sin titulo'}</span>
786
- <span style="color:#aaa;font-size:10px;">${s.fields.length}</span>
787
- </div>
788
- `) ?? nothing}
789
- <button style="margin:8px 4px;padding:8px;width:calc(100% - 8px);border:1px dashed #ccc;border-radius:6px;background:none;cursor:pointer;font-size:11px;color:#888;font-family:inherit;transition:all 0.15s;"
1241
+ })}
1242
+ ` : this.leftTab === 'sections' ? html `
1243
+ ${this.schema?.sections.map((s, i) => html `
1244
+ <div style="padding:8px 10px;margin:2px 4px;background:${i % 2 === 0 ? '#f8f9fa' : 'white'};border-radius:6px;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:8px;border:1px solid #eee;transition:all 0.15s;"
1245
+ @click="${() => { }}"
1246
+ >
1247
+ <span style="background:#e3f2fd;color:#1976d2;font-weight:700;font-size:10px;padding:2px 6px;border-radius:4px;">§${i + 1}</span>
1248
+ <span style="flex:1;font-weight:500;">${s.title ?? 'Sin titulo'}</span>
1249
+ <span style="color:#aaa;font-size:10px;">${s.fields.length}</span>
1250
+ </div>
1251
+ `) ?? nothing}
1252
+ <button style="margin:8px 4px;padding:8px;width:calc(100% - 8px);border:1px dashed #ccc;border-radius:6px;background:none;cursor:pointer;font-size:11px;color:#888;font-family:inherit;transition:all 0.15s;"
790
1253
  @click="${() => { if (this.schema) {
791
1254
  this.schema.sections.push({ id: 'section_' + Date.now(), title: 'Nueva Seccion', fields: [] });
792
1255
  this.commitChange();
793
- } }}"
794
- >+ Agregar Seccion</button>
795
- ` : this.renderApiPanel()}
796
- </div>
797
- </div>
1256
+ } }}"
1257
+ >+ Agregar Seccion</button>
1258
+ ` : this.renderApiPanel()}
1259
+ </div>
1260
+ </div>
798
1261
  `;
799
1262
  }
800
1263
  // ─── Canvas ───────────────────────────────────────
801
1264
  renderCanvas() {
802
- return html `
803
- <div class="canvas-area">
1265
+ return html `
1266
+ <div class="canvas-area">
804
1267
  ${this.viewMode === 'json'
805
1268
  ? html `<div class="json-panel" style="width:100%;max-width:800px;">${JSON.stringify(this.schema, null, 2)}</div>`
806
1269
  : this.viewMode === 'preview'
807
1270
  ? html `<div class="canvas" style="transform:scale(${this.zoom});"><zentto-studio-renderer .schema="${this.schema}" .data="${this.data}"></zentto-studio-renderer></div>`
808
- : this.renderDesignCanvas()}
809
- </div>
1271
+ : this.renderDesignCanvas()}
1272
+ </div>
810
1273
  `;
811
1274
  }
812
1275
  renderDesignCanvas() {
813
1276
  if (!this.schema) {
814
- return html `<div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}" style="width:500px;height:200px;display:flex;align-items:center;justify-content:center;"
815
- @dragover="${(e) => e.preventDefault()}"
816
- @drop="${(e) => { e.preventDefault(); if (this.dragType) {
817
- this.addField(this.dragType);
818
- this.dragType = null;
819
- } }}"
820
- >Arrastra un campo aqui para empezar</div>`;
1277
+ return html `<div class="drop-zone drop-zone--active" data-section-index="0" style="width:500px;height:200px;display:flex;align-items:center;justify-content:center;"
1278
+ @dragover="${(e) => e.preventDefault()}"
1279
+ @drop="${(e) => this.handleCanvasDrop(e, 0)}"
1280
+ >Arrastra campos del toolbox o de la API aqui</div>`;
821
1281
  }
822
1282
  const cols = this.schema.layout.columns ?? 1;
823
- return html `
824
- <div class="canvas" style="transform:scale(${this.zoom});" @click="${() => { this.selectedFieldId = null; }}">
825
- ${this.schema.sections.map((section, si) => html `
826
- <div class="canvas-section">
827
- <div class="canvas-section-header" @click="${(e) => e.stopPropagation()}">
828
- <span style="font-size:10px;color:var(--zrd-accent);">§${si + 1}</span>
829
- ${section.title ?? 'Seccion'}
830
- <span style="flex:1;"></span>
831
- <span style="font-size:10px;color:var(--zrd-text-muted);">${section.fields.length} campos</span>
832
- </div>
833
- <div class="canvas-grid" style="grid-template-columns:repeat(${section.columns ?? cols}, 1fr);">
834
- ${section.fields.map((field, fi) => this.renderCanvasField(field, si, fi, section.columns ?? cols))}
835
- </div>
836
- <div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}"
837
- @dragover="${(e) => e.preventDefault()}"
838
- @drop="${(e) => { e.preventDefault(); if (this.dragType) {
839
- this.addField(this.dragType, si);
840
- this.dragType = null;
841
- } }}"
842
- >${this.dragType ? '↓ Soltar aqui' : '+ Arrastra campos'}</div>
843
- </div>
844
- `)}
845
- </div>
1283
+ return html `
1284
+ <div class="canvas" style="transform:scale(${this.zoom});" @click="${() => { this.selectedFieldId = null; }}">
1285
+ ${this.schema.sections.map((section, si) => html `
1286
+ <div class="canvas-section">
1287
+ <div class="canvas-section-header" @click="${(e) => e.stopPropagation()}">
1288
+ <span style="font-size:10px;color:var(--zrd-accent);">§${si + 1}</span>
1289
+ ${section.title ?? 'Seccion'}
1290
+ <span style="flex:1;"></span>
1291
+ <span style="font-size:10px;color:var(--zrd-text-muted);">${section.fields.length} campos</span>
1292
+ </div>
1293
+ <div class="section-drop-zone" data-section-index="${si}" data-position="top"></div>
1294
+ <div class="canvas-grid" data-section-index="${si}" style="grid-template-columns:repeat(${section.columns ?? cols}, 1fr);">
1295
+ ${section.fields.map((field, fi) => this.renderCanvasField(field, si, fi, section.columns ?? cols))}
1296
+ </div>
1297
+ <div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}"
1298
+ data-section-index="${si}"
1299
+ @dragover="${(e) => { e.preventDefault(); e.currentTarget.classList.add('drop-zone--active'); }}"
1300
+ @dragleave="${(e) => { if (!this.dragType)
1301
+ e.currentTarget.classList.remove('drop-zone--active'); }}"
1302
+ @drop="${(e) => this.handleCanvasDrop(e, si)}"
1303
+ >${this.dragType ? '↓ Soltar aqui' : '+ Arrastra campos o campos de API'}</div>
1304
+ </div>
1305
+ `)}
1306
+ </div>
846
1307
  `;
847
1308
  }
848
1309
  renderCanvasField(field, si, fi, maxCols) {
@@ -851,37 +1312,39 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
851
1312
  const fullWidth = ['separator', 'heading', 'html', 'datagrid', 'report', 'chart'].includes(field.type);
852
1313
  const gridCol = fullWidth ? '1 / -1' : span > 1 ? `span ${span}` : '';
853
1314
  const typeColor = FIELD_TYPE_COLORS[field.type] ?? 'background:#eceff1;color:#546e7a;';
854
- return html `
855
- <div class="canvas-field ${isSelected ? 'canvas-field--selected' : ''}"
856
- style="${gridCol ? `grid-column:${gridCol};` : ''}"
857
- @click="${(e) => { e.stopPropagation(); this.selectedFieldId = field.id; }}"
858
- >
859
- <!-- Type badge -->
860
- <span class="field-type-badge" style="${typeColor}">${field.type}</span>
861
-
862
- <!-- Action buttons -->
863
- <div class="field-actions">
864
- ${fi > 0 ? html `<button class="fa-btn fa-btn--move" @click="${(e) => { e.stopPropagation(); this.moveField(si, fi, -1); }}" title="Subir">↑</button>` : ''}
865
- ${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>` : ''}
866
- <button class="fa-btn fa-btn--copy" @click="${(e) => { e.stopPropagation(); this.duplicateField(si, fi); }}" title="Duplicar">⎘</button>
867
- <button class="fa-btn fa-btn--delete" @click="${(e) => { e.stopPropagation(); this.removeField(si, fi); }}" title="Eliminar">✕</button>
868
- </div>
869
-
870
- <!-- Resize handles -->
871
- ${isSelected ? html `
872
- <div class="rh rh-nw"></div><div class="rh rh-n"></div><div class="rh rh-ne"></div>
873
- <div class="rh rh-e"></div><div class="rh rh-se"></div><div class="rh rh-s"></div>
874
- <div class="rh rh-sw"></div><div class="rh rh-w"></div>
875
- ` : ''}
876
-
877
- <!-- Field preview -->
878
- <div class="field-preview">
879
- <div class="field-preview-label">${field.label ?? field.id}${field.required ? ' *' : ''}</div>
880
- <div class="field-preview-input ${this.getPreviewClass(field.type)}">
881
- ${this.getPreviewContent(field)}
882
- </div>
883
- </div>
884
- </div>
1315
+ return html `
1316
+ <div class="canvas-field ${isSelected ? 'canvas-field--selected' : ''}"
1317
+ style="${gridCol ? `grid-column:${gridCol};` : ''}"
1318
+ data-field-id="${field.id}"
1319
+ data-section-index="${si}"
1320
+ @click="${(e) => { e.stopPropagation(); this.selectedFieldId = field.id; }}"
1321
+ >
1322
+ <!-- Type badge -->
1323
+ <span class="field-type-badge" style="${typeColor}">${field.type}</span>
1324
+
1325
+ <!-- Action buttons -->
1326
+ <div class="field-actions">
1327
+ ${fi > 0 ? html `<button class="fa-btn fa-btn--move" @click="${(e) => { e.stopPropagation(); this.moveField(si, fi, -1); }}" title="Subir">↑</button>` : ''}
1328
+ ${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>` : ''}
1329
+ <button class="fa-btn fa-btn--copy" @click="${(e) => { e.stopPropagation(); this.duplicateField(si, fi); }}" title="Duplicar">⎘</button>
1330
+ <button class="fa-btn fa-btn--delete" @click="${(e) => { e.stopPropagation(); this.removeField(si, fi); }}" title="Eliminar">✕</button>
1331
+ </div>
1332
+
1333
+ <!-- Resize handles -->
1334
+ ${isSelected ? html `
1335
+ <div class="rh rh-nw"></div><div class="rh rh-n"></div><div class="rh rh-ne"></div>
1336
+ <div class="rh rh-e"></div><div class="rh rh-se"></div><div class="rh rh-s"></div>
1337
+ <div class="rh rh-sw"></div><div class="rh rh-w"></div>
1338
+ ` : ''}
1339
+
1340
+ <!-- Field preview -->
1341
+ <div class="field-preview">
1342
+ <div class="field-preview-label">${field.label ?? field.id}${field.required ? ' *' : ''}</div>
1343
+ <div class="field-preview-input ${this.getPreviewClass(field.type)}">
1344
+ ${this.getPreviewContent(field)}
1345
+ </div>
1346
+ </div>
1347
+ </div>
885
1348
  `;
886
1349
  }
887
1350
  getPreviewClass(type) {
@@ -934,227 +1397,215 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
934
1397
  renderRightPanel() {
935
1398
  const field = this.selectedField;
936
1399
  if (!field) {
937
- return html `
938
- <div class="right-panel">
939
- <div class="props-empty">
940
- <div class="props-empty-icon">⬚</div>
941
- <div class="props-empty-text">Selecciona un campo<br/>para editar propiedades</div>
942
- </div>
943
- ${this.schema ? this.renderFormProperties() : ''}
944
- </div>
1400
+ return html `
1401
+ <div class="right-panel">
1402
+ <div class="props-empty">
1403
+ <div class="props-empty-icon">⬚</div>
1404
+ <div class="props-empty-text">Selecciona un campo<br/>para editar propiedades</div>
1405
+ </div>
1406
+ ${this.schema ? this.renderFormProperties() : ''}
1407
+ </div>
945
1408
  `;
946
1409
  }
947
1410
  const typeColor = FIELD_TYPE_COLORS[field.type] ?? 'background:#eceff1;color:#546e7a;';
948
- return html `
949
- <div class="right-panel">
950
- <!-- Header with type badge -->
951
- <div style="padding:8px 10px;border-bottom:1px solid #eee;">
952
- <span class="props-type-badge" style="${typeColor}">${field.type.toUpperCase()}</span>
953
- <div class="props-field-id">#${field.id}</div>
954
- </div>
955
-
956
- <!-- General -->
957
- <div class="prop-section">
958
- <div class="prop-section-header" data-section="general" @click="${() => this.toggleSection('general')}">
959
- <span class="collapse-icon ${this.collapsedSections.has('general') ? 'collapse-icon--collapsed' : ''}">▾</span>
960
- <h4>General</h4>
961
- </div>
962
- ${!this.collapsedSections.has('general') ? html `
963
- <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>
964
- <div class="prop-row"><span class="prop-label">Field</span><input class="prop-input" style="font-family:'SF Mono','Consolas',monospace;font-size:10px;" .value="${field.field}" @change="${(e) => { field.field = e.target.value; this.commitChange(); }}" /></div>
965
- <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>
966
- <div class="prop-row"><span class="prop-label">Ayuda</span><input class="prop-input" .value="${field.helpText ?? ''}" @input="${(e) => { field.helpText = e.target.value; this.emitChange(); }}" /></div>
967
- ` : ''}
968
- </div>
969
-
970
- <!-- Layout (Figma position grid) -->
971
- <div class="prop-section">
972
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('layout')}">
973
- <span class="collapse-icon ${this.collapsedSections.has('layout') ? 'collapse-icon--collapsed' : ''}">▾</span>
974
- <h4>Layout</h4>
975
- </div>
976
- ${!this.collapsedSections.has('layout') ? html `
977
- <div class="prop-pos-grid">
978
- <div class="prop-pos-cell">
979
- <span class="prop-pos-label prop-pos-label--w">W</span>
980
- <input type="number" min="1" max="6" .value="${String(field.colSpan ?? 1)}" @change="${(e) => { field.colSpan = parseInt(e.target.value) || 1; this.commitChange(); }}" />
981
- </div>
982
- <div class="prop-pos-cell">
983
- <span class="prop-pos-label prop-pos-label--h">T</span>
984
- <select .value="${field.type}" @change="${(e) => { field.type = e.target.value; this.commitChange(); }}" style="border:none;background:transparent;font-size:11px;width:100%;outline:none;color:var(--zrd-text);font-family:inherit;">
985
- ${getAllFields().map(f => html `<option value="${f.type}" ?selected="${f.type === field.type}">${f.label}</option>`)}
986
- </select>
987
- </div>
988
- </div>
989
- <div class="prop-divider"></div>
990
- <div class="prop-row"><span class="prop-label">CSS Class</span><input class="prop-input" .value="${field.cssClass ?? ''}" placeholder="mi-clase" @input="${(e) => { field.cssClass = e.target.value; this.emitChange(); }}" /></div>
991
- ` : ''}
992
- </div>
993
-
994
- <!-- Style -->
995
- <div class="prop-section">
996
- <div class="prop-section-header" data-section="style" @click="${() => this.toggleSection('style')}">
997
- <span class="collapse-icon ${this.collapsedSections.has('style') ? 'collapse-icon--collapsed' : ''}">▾</span>
998
- <h4>Estilo</h4>
999
- </div>
1000
- ${!this.collapsedSections.has('style') ? html `
1001
- <div class="prop-row">
1002
- <span class="prop-label">Ancho</span>
1003
- <div class="prop-segmented">
1004
- ${['auto', '100%', '50%'].map(w => html `
1005
- <button class="prop-seg-btn ${(field.width ?? 'auto') === w ? 'prop-seg-btn--active' : ''}"
1006
- @click="${() => { field.width = w === 'auto' ? undefined : w; this.commitChange(); }}"
1007
- >${w}</button>
1008
- `)}
1009
- </div>
1010
- </div>
1011
- ` : ''}
1012
- </div>
1013
-
1014
- <!-- Behavior (toggles) -->
1015
- <div class="prop-section">
1016
- <div class="prop-section-header" data-section="behavior" @click="${() => this.toggleSection('behavior')}">
1017
- <span class="collapse-icon ${this.collapsedSections.has('behavior') ? 'collapse-icon--collapsed' : ''}">▾</span>
1018
- <h4>Comportamiento</h4>
1019
- </div>
1020
- ${!this.collapsedSections.has('behavior') ? html `
1021
- ${this.renderToggle('Requerido', field.required ?? false, (v) => { field.required = v; this.commitChange(); })}
1022
- ${this.renderToggle('Solo lectura', field.readOnly ?? false, (v) => { field.readOnly = v; this.commitChange(); })}
1023
- ${this.renderToggle('Deshabilitado', field.disabled ?? false, (v) => { field.disabled = v; this.commitChange(); })}
1024
- ${this.renderToggle('Oculto', field.hidden ?? false, (v) => { field.hidden = v; this.commitChange(); })}
1025
- ` : ''}
1026
- </div>
1027
-
1028
- <!-- Rules (expressions) -->
1029
- <div class="prop-section">
1030
- <div class="prop-section-header" data-section="rules" @click="${() => this.toggleSection('rules')}">
1031
- <span class="collapse-icon ${this.collapsedSections.has('rules') ? 'collapse-icon--collapsed' : ''}">▾</span>
1032
- <h4>Reglas</h4>
1033
- </div>
1034
- ${!this.collapsedSections.has('rules') ? html `
1035
- <div class="prop-info">💡 Usa {campo} para referencias y expresiones</div>
1036
- <div class="prop-row prop-row-full"><span class="prop-label" style="margin-bottom:2px;">Condicion de visibilidad</span></div>
1037
- <div class="prop-row prop-row-full"><textarea class="prop-input" rows="2" .value="${field.visibilityRule ?? ''}" placeholder='{tipo} == "empresa" AND {activo} == true' @change="${(e) => { field.visibilityRule = e.target.value || undefined; this.commitChange(); }}"></textarea></div>
1038
- <div class="prop-row prop-row-full"><span class="prop-label" style="margin-bottom:2px;">Valor computado</span></div>
1039
- <div class="prop-row prop-row-full"><textarea class="prop-input" rows="2" .value="${field.computedValue ?? ''}" placeholder='{precio} * {cantidad} * (1 + {iva}/100)' @change="${(e) => { field.computedValue = e.target.value || undefined; this.commitChange(); }}"></textarea></div>
1040
- <div class="prop-divider"></div>
1041
- <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>
1042
- ` : ''}
1043
- </div>
1044
-
1045
- <!-- Data Source (API connection like report-designer) -->
1046
- <div class="prop-section">
1047
- <div class="prop-section-header" data-section="datasource" @click="${() => this.toggleSection('datasource')}">
1048
- <span class="collapse-icon ${this.collapsedSections.has('datasource') ? 'collapse-icon--collapsed' : ''}">▾</span>
1049
- <h4>Origen de Datos</h4>
1050
- </div>
1051
- ${!this.collapsedSections.has('datasource') ? html `
1052
- <div class="prop-info">🔌 Conecta campos a APIs y endpoints</div>
1411
+ return html `
1412
+ <div class="right-panel">
1413
+ <!-- Header with type badge -->
1414
+ <div style="padding:8px 10px;border-bottom:1px solid #eee;">
1415
+ <span class="props-type-badge" style="${typeColor}">${field.type.toUpperCase()}</span>
1416
+ <div class="props-field-id">#${field.id}</div>
1417
+ </div>
1418
+
1419
+ <!-- General -->
1420
+ <div class="prop-section">
1421
+ <div class="prop-section-header" data-section="general" @click="${() => this.toggleSection('general')}">
1422
+ <span class="collapse-icon ${this.collapsedSections.has('general') ? 'collapse-icon--collapsed' : ''}">▾</span>
1423
+ <h4>General</h4>
1424
+ </div>
1425
+ ${!this.collapsedSections.has('general') ? html `
1426
+ <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>
1427
+ <div class="prop-row"><span class="prop-label">Field</span><input class="prop-input" style="font-family:'SF Mono','Consolas',monospace;font-size:10px;" .value="${field.field}" @change="${(e) => { field.field = e.target.value; this.commitChange(); }}" /></div>
1428
+ <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>
1429
+ <div class="prop-row"><span class="prop-label">Ayuda</span><input class="prop-input" .value="${field.helpText ?? ''}" @input="${(e) => { field.helpText = e.target.value; this.emitChange(); }}" /></div>
1430
+ ` : ''}
1431
+ </div>
1432
+
1433
+ <!-- Layout (Figma position grid) -->
1434
+ <div class="prop-section">
1435
+ <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('layout')}">
1436
+ <span class="collapse-icon ${this.collapsedSections.has('layout') ? 'collapse-icon--collapsed' : ''}">▾</span>
1437
+ <h4>Layout</h4>
1438
+ </div>
1439
+ ${!this.collapsedSections.has('layout') ? html `
1440
+ <div class="prop-pos-grid">
1441
+ <div class="prop-pos-cell">
1442
+ <span class="prop-pos-label prop-pos-label--w">W</span>
1443
+ <input type="number" min="1" max="6" .value="${String(field.colSpan ?? 1)}" @change="${(e) => { field.colSpan = parseInt(e.target.value) || 1; this.commitChange(); }}" />
1444
+ </div>
1445
+ <div class="prop-pos-cell">
1446
+ <span class="prop-pos-label prop-pos-label--h">T</span>
1447
+ <select .value="${field.type}" @change="${(e) => { field.type = e.target.value; this.commitChange(); }}" style="border:none;background:transparent;font-size:11px;width:100%;outline:none;color:var(--zrd-text);font-family:inherit;">
1448
+ ${getAllFields().map(f => html `<option value="${f.type}" ?selected="${f.type === field.type}">${f.label}</option>`)}
1449
+ </select>
1450
+ </div>
1451
+ </div>
1452
+ <div class="prop-divider"></div>
1453
+ <div class="prop-row"><span class="prop-label">CSS Class</span><input class="prop-input" .value="${field.cssClass ?? ''}" placeholder="mi-clase" @input="${(e) => { field.cssClass = e.target.value; this.emitChange(); }}" /></div>
1454
+ ` : ''}
1455
+ </div>
1456
+
1457
+ <!-- Style -->
1458
+ <div class="prop-section">
1459
+ <div class="prop-section-header" data-section="style" @click="${() => this.toggleSection('style')}">
1460
+ <span class="collapse-icon ${this.collapsedSections.has('style') ? 'collapse-icon--collapsed' : ''}">▾</span>
1461
+ <h4>Estilo</h4>
1462
+ </div>
1463
+ ${!this.collapsedSections.has('style') ? html `
1464
+ <div class="prop-row">
1465
+ <span class="prop-label">Ancho</span>
1466
+ <div class="prop-segmented">
1467
+ ${['auto', '100%', '50%'].map(w => html `
1468
+ <button class="prop-seg-btn ${(field.width ?? 'auto') === w ? 'prop-seg-btn--active' : ''}"
1469
+ @click="${() => { field.width = w === 'auto' ? undefined : w; this.commitChange(); }}"
1470
+ >${w}</button>
1471
+ `)}
1472
+ </div>
1473
+ </div>
1474
+ ` : ''}
1475
+ </div>
1476
+
1477
+ <!-- Behavior (toggles) -->
1478
+ <div class="prop-section">
1479
+ <div class="prop-section-header" data-section="behavior" @click="${() => this.toggleSection('behavior')}">
1480
+ <span class="collapse-icon ${this.collapsedSections.has('behavior') ? 'collapse-icon--collapsed' : ''}">▾</span>
1481
+ <h4>Comportamiento</h4>
1482
+ </div>
1483
+ ${!this.collapsedSections.has('behavior') ? html `
1484
+ ${this.renderToggle('Requerido', field.required ?? false, (v) => { field.required = v; this.commitChange(); })}
1485
+ ${this.renderToggle('Solo lectura', field.readOnly ?? false, (v) => { field.readOnly = v; this.commitChange(); })}
1486
+ ${this.renderToggle('Deshabilitado', field.disabled ?? false, (v) => { field.disabled = v; this.commitChange(); })}
1487
+ ${this.renderToggle('Oculto', field.hidden ?? false, (v) => { field.hidden = v; this.commitChange(); })}
1488
+ ` : ''}
1489
+ </div>
1490
+
1491
+ <!-- Rules (expressions) -->
1492
+ <div class="prop-section">
1493
+ <div class="prop-section-header" data-section="rules" @click="${() => this.toggleSection('rules')}">
1494
+ <span class="collapse-icon ${this.collapsedSections.has('rules') ? 'collapse-icon--collapsed' : ''}">▾</span>
1495
+ <h4>Reglas</h4>
1496
+ </div>
1497
+ ${!this.collapsedSections.has('rules') ? html `
1498
+ <div class="prop-info">💡 Usa {campo} para referencias y expresiones</div>
1499
+ <div class="prop-row prop-row-full"><span class="prop-label" style="margin-bottom:2px;">Condicion de visibilidad</span></div>
1500
+ <div class="prop-row prop-row-full"><textarea class="prop-input" rows="2" .value="${field.visibilityRule ?? ''}" placeholder='{tipo} == "empresa" AND {activo} == true' @change="${(e) => { field.visibilityRule = e.target.value || undefined; this.commitChange(); }}"></textarea></div>
1501
+ <div class="prop-row prop-row-full"><span class="prop-label" style="margin-bottom:2px;">Valor computado</span></div>
1502
+ <div class="prop-row prop-row-full"><textarea class="prop-input" rows="2" .value="${field.computedValue ?? ''}" placeholder='{precio} * {cantidad} * (1 + {iva}/100)' @change="${(e) => { field.computedValue = e.target.value || undefined; this.commitChange(); }}"></textarea></div>
1503
+ <div class="prop-divider"></div>
1504
+ <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>
1505
+ ` : ''}
1506
+ </div>
1507
+
1508
+ <!-- Data Source (API connection like report-designer) -->
1509
+ <div class="prop-section">
1510
+ <div class="prop-section-header" data-section="datasource" @click="${() => this.toggleSection('datasource')}">
1511
+ <span class="collapse-icon ${this.collapsedSections.has('datasource') ? 'collapse-icon--collapsed' : ''}">▾</span>
1512
+ <h4>Origen de Datos</h4>
1513
+ </div>
1514
+ ${!this.collapsedSections.has('datasource') ? html `
1515
+ <div class="prop-info">🔌 Conecta campos a APIs y endpoints</div>
1053
1516
  <div class="prop-row"><span class="prop-label">Endpoint</span><input class="prop-input" style="font-family:'SF Mono','Consolas',monospace;font-size:10px;" .value="${field.props?.endpoint ?? ''}" placeholder="/v1/clientes" @change="${(e) => { if (!field.props)
1054
- field.props = {}; field.props.endpoint = e.target.value || undefined; this.commitChange(); }}" /></div>
1517
+ field.props = {}; field.props.endpoint = e.target.value || undefined; this.commitChange(); }}" /></div>
1055
1518
  <div class="prop-row"><span class="prop-label">Campo valor</span><input class="prop-input" .value="${field.props?.valueField ?? ''}" placeholder="id" @change="${(e) => { if (!field.props)
1056
- field.props = {}; field.props.valueField = e.target.value || undefined; this.commitChange(); }}" /></div>
1519
+ field.props = {}; field.props.valueField = e.target.value || undefined; this.commitChange(); }}" /></div>
1057
1520
  <div class="prop-row"><span class="prop-label">Campo label</span><input class="prop-input" .value="${field.props?.displayField ?? ''}" placeholder="nombre" @change="${(e) => { if (!field.props)
1058
- field.props = {}; field.props.displayField = e.target.value || undefined; this.commitChange(); }}" /></div>
1059
- ${field.type === 'datagrid' || field.type === 'select' || field.type === 'lookup' ? html `
1060
- <div class="prop-divider"></div>
1521
+ field.props = {}; field.props.displayField = e.target.value || undefined; this.commitChange(); }}" /></div>
1522
+ ${field.type === 'datagrid' || field.type === 'select' || field.type === 'lookup' ? html `
1523
+ <div class="prop-divider"></div>
1061
1524
  <div class="prop-row"><span class="prop-label">Data Source</span><input class="prop-input" .value="${field.props?.dataSourceId ?? ''}" placeholder="clientesList" @change="${(e) => { if (!field.props)
1062
- field.props = {}; field.props.dataSourceId = e.target.value || undefined; this.commitChange(); }}" /></div>
1063
- ` : ''}
1064
- ` : ''}
1065
- </div>
1066
- </div>
1525
+ field.props = {}; field.props.dataSourceId = e.target.value || undefined; this.commitChange(); }}" /></div>
1526
+ ` : ''}
1527
+ ` : ''}
1528
+ </div>
1529
+ </div>
1067
1530
  `;
1068
1531
  }
1069
1532
  renderFormProperties() {
1070
1533
  if (!this.schema)
1071
1534
  return nothing;
1072
- return html `
1073
- <div class="prop-section">
1074
- <div class="prop-section-header" data-section="form">
1075
- <span class="collapse-icon">▾</span>
1076
- <h4>Formulario</h4>
1077
- </div>
1078
- <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>
1079
- <div class="prop-divider"></div>
1080
- <div class="prop-pos-grid">
1081
- <div class="prop-pos-cell">
1082
- <span class="prop-pos-label prop-pos-label--w">C</span>
1083
- <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(); }}" />
1084
- </div>
1085
- <div class="prop-pos-cell">
1086
- <span class="prop-pos-label prop-pos-label--y">G</span>
1087
- <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(); }}" />
1088
- </div>
1089
- </div>
1090
- <div style="font-size:9px;color:#bbb;margin-top:4px;display:flex;gap:12px;">
1091
- <span>C = Columnas</span><span>G = Gap (px)</span>
1092
- </div>
1093
- </div>
1535
+ return html `
1536
+ <div class="prop-section">
1537
+ <div class="prop-section-header" data-section="form">
1538
+ <span class="collapse-icon">▾</span>
1539
+ <h4>Formulario</h4>
1540
+ </div>
1541
+ <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>
1542
+ <div class="prop-divider"></div>
1543
+ <div class="prop-pos-grid">
1544
+ <div class="prop-pos-cell">
1545
+ <span class="prop-pos-label prop-pos-label--w">C</span>
1546
+ <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(); }}" />
1547
+ </div>
1548
+ <div class="prop-pos-cell">
1549
+ <span class="prop-pos-label prop-pos-label--y">G</span>
1550
+ <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(); }}" />
1551
+ </div>
1552
+ </div>
1553
+ <div style="font-size:9px;color:#bbb;margin-top:4px;display:flex;gap:12px;">
1554
+ <span>C = Columnas</span><span>G = Gap (px)</span>
1555
+ </div>
1556
+ </div>
1094
1557
  `;
1095
1558
  }
1096
1559
  renderApiPanel() {
1097
- return html `
1098
- <div style="padding:4px;">
1099
- <!-- Auth Section -->
1100
- ${this.renderApiAuth()}
1101
-
1102
- <!-- Quick connect (only when logged in or no auth needed) -->
1103
- <div style="padding:4px 8px;margin-top:4px;">
1104
- <div style="font-size:9px;font-weight:600;color:#aaa;text-transform:uppercase;margin-bottom:4px;">Conectar Endpoint</div>
1105
- <div style="display:flex;gap:4px;margin-bottom:6px;">
1106
- <select id="api-method" style="width:70px;padding:4px 6px;border:1px solid #ddd;border-radius:4px;font-size:10px;background:white;font-family:inherit;">
1107
- <option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option>
1108
- </select>
1109
- <input id="api-url" style="flex:1;padding:4px 8px;border:1px solid #ddd;border-radius:4px;font-size:10px;font-family:'SF Mono','Consolas',monospace;background:#fafafa;" placeholder="/v1/clientes" />
1110
- </div>
1111
- <button style="width:100%;padding:6px;border:1px solid #ea580c;border-radius:5px;background:#fff7ed;color:#ea580c;cursor:pointer;font-size:11px;font-weight:600;font-family:inherit;transition:all 0.15s;"
1112
- @click="${this.fetchApiFields}"
1113
- >${this.apiLoading ? 'Cargando...' : '🔌 Probar Conexion'}</button>
1114
- </div>
1115
-
1116
- <!-- Fetched sources -->
1117
- ${this.apiSources.length > 0 ? html `
1118
- <div style="margin-top:8px;">
1119
- ${this.apiSources.map(src => html `
1120
- <div style="margin:4px;border:1px solid #eee;border-radius:6px;overflow:hidden;">
1121
- <div style="padding:6px 8px;background:#f8f9fa;display:flex;align-items:center;gap:6px;border-bottom:1px solid #eee;">
1122
- <span style="background:#ea580c;color:white;font-size:8px;font-weight:700;padding:1px 5px;border-radius:3px;">${src.method}</span>
1123
- <span style="font-size:10px;font-weight:600;color:#333;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${src.name}</span>
1124
- <span style="font-size:9px;color:#aaa;">${src.fields.length} campos</span>
1125
- </div>
1126
- <div style="padding:2px 4px;max-height:150px;overflow-y:auto;">
1127
- ${src.fields.map(field => html `
1128
- <div style="display:flex;align-items:center;gap:6px;padding:4px 6px;font-size:11px;cursor:grab;border-radius:3px;transition:background 0.1s;"
1129
- draggable="true"
1130
- @dragstart="${(e) => { this.dragType = 'text'; e.dataTransfer?.setData('text/plain', 'text'); e.dataTransfer?.setData('field-name', field); e.dataTransfer?.setData('ds-id', src.id); }}"
1131
- @dblclick="${() => {
1132
- // Auto-create a field bound to this API field
1133
- this.addField('text');
1134
- const lastField = this.schema?.sections[0]?.fields[this.schema.sections[0].fields.length - 1];
1135
- if (lastField) {
1136
- lastField.field = field;
1137
- lastField.label = field.charAt(0).toUpperCase() + field.slice(1).replace(/([A-Z])/g, ' $1');
1138
- if (!lastField.props)
1139
- lastField.props = {};
1140
- lastField.props.dataSourceId = src.id;
1141
- this.commitChange();
1142
- }
1143
- }}"
1144
- >
1145
- <span style="color:#1976d2;font-size:10px;">⬡</span>
1146
- <span style="color:#555;">${field}</span>
1147
- </div>
1148
- `)}
1149
- </div>
1150
- </div>
1151
- `)}
1152
- </div>
1153
- ` : ''}
1154
-
1155
- <!-- Zentto API shortcuts -->
1156
- <div style="margin-top:12px;padding:4px 8px;">
1157
- <div style="font-size:9px;font-weight:600;color:#aaa;text-transform:uppercase;margin-bottom:6px;">Accesos Rapidos Zentto</div>
1560
+ return html `
1561
+ <div style="padding:4px;">
1562
+ <!-- Auth Section -->
1563
+ ${this.renderApiAuth()}
1564
+
1565
+ <!-- Quick connect (only when logged in or no auth needed) -->
1566
+ <div style="padding:4px 8px;margin-top:4px;">
1567
+ <div style="font-size:9px;font-weight:600;color:#aaa;text-transform:uppercase;margin-bottom:4px;">Conectar Endpoint</div>
1568
+ <div style="display:flex;gap:4px;margin-bottom:6px;">
1569
+ <select id="api-method" style="width:70px;padding:4px 6px;border:1px solid #ddd;border-radius:4px;font-size:10px;background:white;font-family:inherit;">
1570
+ <option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option>
1571
+ </select>
1572
+ <input id="api-url" style="flex:1;padding:4px 8px;border:1px solid #ddd;border-radius:4px;font-size:10px;font-family:'SF Mono','Consolas',monospace;background:#fafafa;" placeholder="/v1/clientes" />
1573
+ </div>
1574
+ <button style="width:100%;padding:6px;border:1px solid #ea580c;border-radius:5px;background:#fff7ed;color:#ea580c;cursor:pointer;font-size:11px;font-weight:600;font-family:inherit;transition:all 0.15s;"
1575
+ @click="${this.fetchApiFields}"
1576
+ >${this.apiLoading ? 'Cargando...' : '🔌 Probar Conexion'}</button>
1577
+ </div>
1578
+
1579
+ <!-- Fetched sources -->
1580
+ ${this.apiSources.length > 0 ? html `
1581
+ <div style="margin-top:8px;">
1582
+ ${this.apiSources.map(src => html `
1583
+ <div style="margin:4px;border:1px solid #eee;border-radius:6px;overflow:hidden;">
1584
+ <div style="padding:6px 8px;background:#f8f9fa;display:flex;align-items:center;gap:6px;border-bottom:1px solid #eee;">
1585
+ <span style="background:#ea580c;color:white;font-size:8px;font-weight:700;padding:1px 5px;border-radius:3px;">${src.method}</span>
1586
+ <span style="font-size:10px;font-weight:600;color:#333;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${src.name}</span>
1587
+ <span style="font-size:9px;color:#aaa;">${src.fields.length} campos</span>
1588
+ </div>
1589
+ <div style="padding:2px 4px;max-height:150px;overflow-y:auto;">
1590
+ ${src.fields.map(field => html `
1591
+ <div style="display:flex;align-items:center;gap:6px;padding:4px 6px;font-size:11px;cursor:grab;border-radius:3px;transition:background 0.1s;"
1592
+ draggable="true"
1593
+ @dragstart="${(e) => { this.dragType = 'text'; e.dataTransfer?.setData('text/plain', 'text'); e.dataTransfer?.setData('field-name', field); e.dataTransfer?.setData('ds-id', src.id); }}"
1594
+ @dblclick="${() => this.addApiField(field, src.id)}"
1595
+ >
1596
+ <span style="color:#1976d2;font-size:10px;">⬡</span>
1597
+ <span style="color:#555;">${field}</span>
1598
+ </div>
1599
+ `)}
1600
+ </div>
1601
+ </div>
1602
+ `)}
1603
+ </div>
1604
+ ` : ''}
1605
+
1606
+ <!-- Zentto API shortcuts -->
1607
+ <div style="margin-top:12px;padding:4px 8px;">
1608
+ <div style="font-size:9px;font-weight:600;color:#aaa;text-transform:uppercase;margin-bottom:6px;">Accesos Rapidos Zentto</div>
1158
1609
  ${[
1159
1610
  { label: 'Clientes', endpoint: '/v1/clientes', icon: '👥' },
1160
1611
  { label: 'Articulos', endpoint: '/v1/articulos', icon: '📦' },
@@ -1164,34 +1615,34 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1164
1615
  { label: 'Proveedores', endpoint: '/v1/proveedores', icon: '🏭' },
1165
1616
  { label: 'Bancos', endpoint: '/v1/bancos', icon: '🏦' },
1166
1617
  { label: 'Paises', endpoint: '/v1/config/countries', icon: '🌍' },
1167
- ].map(api => html `
1168
- <div style="display:flex;align-items:center;gap:8px;padding:6px 8px;margin:2px 0;border-radius:5px;cursor:pointer;font-size:11px;transition:all 0.15s;border:1px solid transparent;"
1618
+ ].map(api => html `
1619
+ <div style="display:flex;align-items:center;gap:8px;padding:6px 8px;margin:2px 0;border-radius:5px;cursor:pointer;font-size:11px;transition:all 0.15s;border:1px solid transparent;"
1169
1620
  @click="${() => {
1170
1621
  const urlInput = this.shadowRoot?.querySelector('#api-url');
1171
1622
  if (urlInput)
1172
1623
  urlInput.value = api.endpoint;
1173
- }}"
1174
- @mouseenter="${(e) => { e.target.style.background = '#fff7ed'; e.target.style.borderColor = '#fed7aa'; }}"
1175
- @mouseleave="${(e) => { e.target.style.background = ''; e.target.style.borderColor = 'transparent'; }}"
1176
- >
1177
- <span style="font-size:14px;">${api.icon}</span>
1178
- <span style="flex:1;color:#555;">${api.label}</span>
1179
- <span style="font-size:9px;color:#bbb;font-family:'SF Mono','Consolas',monospace;">${api.endpoint}</span>
1180
- </div>
1181
- `)}
1182
- </div>
1183
-
1184
- <!-- Manual data source -->
1185
- <div style="margin-top:8px;padding:4px 8px;">
1186
- <div style="font-size:9px;font-weight:600;color:#aaa;text-transform:uppercase;margin-bottom:4px;">Datos Estaticos</div>
1187
- <button style="width:100%;padding:6px;border:1px dashed #ccc;border-radius:5px;background:none;cursor:pointer;font-size:11px;color:#888;font-family:inherit;"
1624
+ }}"
1625
+ @mouseenter="${(e) => { e.target.style.background = '#fff7ed'; e.target.style.borderColor = '#fed7aa'; }}"
1626
+ @mouseleave="${(e) => { e.target.style.background = ''; e.target.style.borderColor = 'transparent'; }}"
1627
+ >
1628
+ <span style="font-size:14px;">${api.icon}</span>
1629
+ <span style="flex:1;color:#555;">${api.label}</span>
1630
+ <span style="font-size:9px;color:#bbb;font-family:'SF Mono','Consolas',monospace;">${api.endpoint}</span>
1631
+ </div>
1632
+ `)}
1633
+ </div>
1634
+
1635
+ <!-- Manual data source -->
1636
+ <div style="margin-top:8px;padding:4px 8px;">
1637
+ <div style="font-size:9px;font-weight:600;color:#aaa;text-transform:uppercase;margin-bottom:4px;">Datos Estaticos</div>
1638
+ <button style="width:100%;padding:6px;border:1px dashed #ccc;border-radius:5px;background:none;cursor:pointer;font-size:11px;color:#888;font-family:inherit;"
1188
1639
  @click="${() => {
1189
1640
  const src = { id: 'static_' + Date.now(), name: 'Datos Manuales', endpoint: '', method: 'STATIC', fields: ['campo1', 'campo2', 'campo3'] };
1190
1641
  this.apiSources = [...this.apiSources, src];
1191
- }}"
1192
- >+ Agregar Datos Estaticos</button>
1193
- </div>
1194
- </div>
1642
+ }}"
1643
+ >+ Agregar Datos Estaticos</button>
1644
+ </div>
1645
+ </div>
1195
1646
  `;
1196
1647
  }
1197
1648
  // ─── Template Menu ─────────────────────────────────
@@ -1302,13 +1753,13 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1302
1753
  ], actions: [{ id: 'send', type: 'submit', label: 'Enviar Encuesta', variant: 'primary' }] },
1303
1754
  },
1304
1755
  ];
1305
- return html `
1306
- <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;">
1307
- <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;">
1308
- Cargar Plantilla
1309
- </div>
1310
- ${templates.map(t => html `
1311
- <div style="display:flex;align-items:flex-start;gap:10px;padding:10px 14px;cursor:pointer;transition:background 0.1s;border-bottom:1px solid #f5f5f5;"
1756
+ return html `
1757
+ <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;">
1758
+ <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;">
1759
+ Cargar Plantilla
1760
+ </div>
1761
+ ${templates.map(t => html `
1762
+ <div style="display:flex;align-items:flex-start;gap:10px;padding:10px 14px;cursor:pointer;transition:background 0.1s;border-bottom:1px solid #f5f5f5;"
1312
1763
  @click="${() => {
1313
1764
  this.schema = structuredClone(t.schema);
1314
1765
  this.undoStack = [JSON.stringify(this.schema)];
@@ -1316,116 +1767,116 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1316
1767
  this.selectedFieldId = null;
1317
1768
  this.showTemplateMenu = false;
1318
1769
  this.commitChange();
1319
- }}"
1320
- @mouseenter="${(e) => { e.currentTarget.style.background = '#f0f7ff'; }}"
1321
- @mouseleave="${(e) => { e.currentTarget.style.background = ''; }}"
1322
- >
1323
- <span style="font-size:22px;margin-top:1px;">${t.icon}</span>
1324
- <div>
1325
- <div style="font-size:13px;font-weight:600;color:#333;">${t.title}</div>
1326
- <div style="font-size:11px;color:#999;margin-top:1px;">${t.desc}</div>
1327
- </div>
1328
- </div>
1329
- `)}
1330
- <div style="padding:8px 14px;border-top:1px solid #eee;">
1331
- <div style="font-size:10px;color:#bbb;text-align:center;">Clic en una plantilla para cargarla</div>
1332
- </div>
1333
- </div>
1770
+ }}"
1771
+ @mouseenter="${(e) => { e.currentTarget.style.background = '#f0f7ff'; }}"
1772
+ @mouseleave="${(e) => { e.currentTarget.style.background = ''; }}"
1773
+ >
1774
+ <span style="font-size:22px;margin-top:1px;">${t.icon}</span>
1775
+ <div>
1776
+ <div style="font-size:13px;font-weight:600;color:#333;">${t.title}</div>
1777
+ <div style="font-size:11px;color:#999;margin-top:1px;">${t.desc}</div>
1778
+ </div>
1779
+ </div>
1780
+ `)}
1781
+ <div style="padding:8px 14px;border-top:1px solid #eee;">
1782
+ <div style="font-size:10px;color:#bbb;text-align:center;">Clic en una plantilla para cargarla</div>
1783
+ </div>
1784
+ </div>
1334
1785
  `;
1335
1786
  }
1336
1787
  renderApiAuth() {
1337
1788
  if (this.apiLoggedIn) {
1338
- return html `
1339
- <div style="margin:4px;padding:10px;background:linear-gradient(135deg,#e8f5e9,#f1f8e9);border:1px solid #c8e6c9;border-radius:8px;">
1340
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
1341
- <span style="width:28px;height:28px;border-radius:50%;background:#27ae60;color:white;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;">✓</span>
1342
- <div style="flex:1;">
1343
- <div style="font-size:11px;font-weight:600;color:#2e7d32;">Conectado</div>
1344
- <div style="font-size:10px;color:#66bb6a;">${this.apiUser}${this.apiCompany ? ` — ${this.apiCompany}` : ''}</div>
1345
- </div>
1346
- <button style="border:none;background:none;cursor:pointer;font-size:14px;color:#999;padding:2px;" title="Cerrar sesion"
1347
- @click="${() => { this.apiLoggedIn = false; this.apiToken = ''; this.apiUser = ''; this.apiCompany = ''; this.apiBranch = ''; }}"
1348
- >✕</button>
1349
- </div>
1350
- <div style="font-size:9px;color:#81c784;font-family:'SF Mono','Consolas',monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${this.apiBaseUrl}">
1351
- 🔗 ${this.apiBaseUrl}
1352
- </div>
1353
- </div>
1789
+ return html `
1790
+ <div style="margin:4px;padding:10px;background:linear-gradient(135deg,#e8f5e9,#f1f8e9);border:1px solid #c8e6c9;border-radius:8px;">
1791
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
1792
+ <span style="width:28px;height:28px;border-radius:50%;background:#27ae60;color:white;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;">✓</span>
1793
+ <div style="flex:1;">
1794
+ <div style="font-size:11px;font-weight:600;color:#2e7d32;">Conectado</div>
1795
+ <div style="font-size:10px;color:#66bb6a;">${this.apiUser}${this.apiCompany ? ` — ${this.apiCompany}` : ''}</div>
1796
+ </div>
1797
+ <button style="border:none;background:none;cursor:pointer;font-size:14px;color:#999;padding:2px;" title="Cerrar sesion"
1798
+ @click="${() => { this.apiLoggedIn = false; this.apiToken = ''; this.apiUser = ''; this.apiCompany = ''; this.apiBranch = ''; }}"
1799
+ >✕</button>
1800
+ </div>
1801
+ <div style="font-size:9px;color:#81c784;font-family:'SF Mono','Consolas',monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${this.apiBaseUrl}">
1802
+ 🔗 ${this.apiBaseUrl}
1803
+ </div>
1804
+ </div>
1354
1805
  `;
1355
1806
  }
1356
- return html `
1357
- <div style="margin:4px;padding:10px;background:#fafafa;border:1px solid #eee;border-radius:8px;">
1358
- <div style="display:flex;align-items:center;gap:6px;margin-bottom:10px;">
1359
- <span style="font-size:14px;">🔐</span>
1360
- <span style="font-size:11px;font-weight:600;color:#333;">Iniciar Sesion</span>
1361
- </div>
1362
-
1363
- ${this.apiLoginError ? html `
1364
- <div style="padding:6px 8px;background:#fde8e8;border:1px solid #f5c6cb;border-radius:5px;margin-bottom:8px;font-size:10px;color:#c62828;">
1365
- ⚠ ${this.apiLoginError}
1366
- </div>
1367
- ` : ''}
1368
-
1369
- <div style="margin-bottom:6px;">
1370
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">URL Base</label>
1371
- <input id="api-base-url" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:10px;font-family:'SF Mono','Consolas',monospace;background:white;box-sizing:border-box;outline:none;transition:border-color 0.15s;"
1372
- .value="${this.apiBaseUrl}" placeholder="Vacio = proxy local (recomendado)"
1373
- @input="${(e) => { this.apiBaseUrl = e.target.value; }}"
1374
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1375
- @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1376
- />
1377
- </div>
1378
-
1379
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:6px;">
1380
- <div>
1381
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Usuario</label>
1382
- <input id="api-login-user" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:11px;background:white;box-sizing:border-box;outline:none;font-family:inherit;"
1383
- placeholder="SUP"
1384
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1385
- @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1386
- />
1387
- </div>
1388
- <div>
1389
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Clave</label>
1390
- <input id="api-login-pass" type="password" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:11px;background:white;box-sizing:border-box;outline:none;font-family:inherit;"
1391
- placeholder="••••"
1392
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1393
- @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1807
+ return html `
1808
+ <div style="margin:4px;padding:10px;background:#fafafa;border:1px solid #eee;border-radius:8px;">
1809
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:10px;">
1810
+ <span style="font-size:14px;">🔐</span>
1811
+ <span style="font-size:11px;font-weight:600;color:#333;">Iniciar Sesion</span>
1812
+ </div>
1813
+
1814
+ ${this.apiLoginError ? html `
1815
+ <div style="padding:6px 8px;background:#fde8e8;border:1px solid #f5c6cb;border-radius:5px;margin-bottom:8px;font-size:10px;color:#c62828;">
1816
+ ⚠ ${this.apiLoginError}
1817
+ </div>
1818
+ ` : ''}
1819
+
1820
+ <div style="margin-bottom:6px;">
1821
+ <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">URL Base</label>
1822
+ <input id="api-base-url" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:10px;font-family:'SF Mono','Consolas',monospace;background:white;box-sizing:border-box;outline:none;transition:border-color 0.15s;"
1823
+ .value="${this.apiBaseUrl}" placeholder="Vacio = proxy local (recomendado)"
1824
+ @input="${(e) => { this.apiBaseUrl = e.target.value; }}"
1825
+ @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1826
+ @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1827
+ />
1828
+ </div>
1829
+
1830
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:6px;">
1831
+ <div>
1832
+ <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Usuario</label>
1833
+ <input id="api-login-user" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:11px;background:white;box-sizing:border-box;outline:none;font-family:inherit;"
1834
+ placeholder="SUP"
1835
+ @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1836
+ @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1837
+ />
1838
+ </div>
1839
+ <div>
1840
+ <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Clave</label>
1841
+ <input id="api-login-pass" type="password" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:11px;background:white;box-sizing:border-box;outline:none;font-family:inherit;"
1842
+ placeholder="••••"
1843
+ @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1844
+ @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1394
1845
  @keydown="${(e) => { if (e.key === 'Enter')
1395
- this.doApiLogin(); }}"
1396
- />
1397
- </div>
1398
- </div>
1399
-
1400
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:8px;">
1401
- <div>
1402
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Empresa (cod)</label>
1403
- <input id="api-login-company" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:11px;background:white;box-sizing:border-box;outline:none;font-family:inherit;"
1404
- placeholder="01" value="01"
1405
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1406
- @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1407
- />
1408
- </div>
1409
- <div>
1410
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Sucursal</label>
1411
- <input id="api-login-branch" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:11px;background:white;box-sizing:border-box;outline:none;font-family:inherit;"
1412
- placeholder="01" value="01"
1413
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1414
- @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1415
- />
1416
- </div>
1417
- </div>
1418
-
1419
- <button style="width:100%;padding:8px;border:none;border-radius:6px;background:#1976d2;color:white;cursor:pointer;font-size:12px;font-weight:600;font-family:inherit;transition:all 0.15s;${this.apiLoginLoading ? 'opacity:0.7;pointer-events:none;' : ''}"
1420
- @click="${this.doApiLogin}"
1421
- @mouseenter="${(e) => { e.target.style.background = '#1565c0'; }}"
1422
- @mouseleave="${(e) => { e.target.style.background = '#1976d2'; }}"
1423
- >${this.apiLoginLoading ? '⏳ Conectando...' : '🔑 Iniciar Sesion'}</button>
1424
-
1425
- <div style="margin-top:6px;font-size:9px;color:#bbb;text-align:center;">
1426
- Usa las mismas credenciales de Zentto ERP
1427
- </div>
1428
- </div>
1846
+ this.doApiLogin(); }}"
1847
+ />
1848
+ </div>
1849
+ </div>
1850
+
1851
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:8px;">
1852
+ <div>
1853
+ <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Empresa (cod)</label>
1854
+ <input id="api-login-company" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:11px;background:white;box-sizing:border-box;outline:none;font-family:inherit;"
1855
+ placeholder="01" value="01"
1856
+ @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1857
+ @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1858
+ />
1859
+ </div>
1860
+ <div>
1861
+ <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Sucursal</label>
1862
+ <input id="api-login-branch" style="width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:5px;font-size:11px;background:white;box-sizing:border-box;outline:none;font-family:inherit;"
1863
+ placeholder="01" value="01"
1864
+ @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1865
+ @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1866
+ />
1867
+ </div>
1868
+ </div>
1869
+
1870
+ <button style="width:100%;padding:8px;border:none;border-radius:6px;background:#1976d2;color:white;cursor:pointer;font-size:12px;font-weight:600;font-family:inherit;transition:all 0.15s;${this.apiLoginLoading ? 'opacity:0.7;pointer-events:none;' : ''}"
1871
+ @click="${this.doApiLogin}"
1872
+ @mouseenter="${(e) => { e.target.style.background = '#1565c0'; }}"
1873
+ @mouseleave="${(e) => { e.target.style.background = '#1976d2'; }}"
1874
+ >${this.apiLoginLoading ? '⏳ Conectando...' : '🔑 Iniciar Sesion'}</button>
1875
+
1876
+ <div style="margin-top:6px;font-size:9px;color:#bbb;text-align:center;">
1877
+ Usa las mismas credenciales de Zentto ERP
1878
+ </div>
1879
+ </div>
1429
1880
  `;
1430
1881
  }
1431
1882
  async doApiLogin() {
@@ -1555,13 +2006,13 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1555
2006
  return Object.keys(sample).filter(k => !k.startsWith('_'));
1556
2007
  }
1557
2008
  renderToggle(label, value, onChange) {
1558
- return html `
1559
- <div class="prop-toggle">
1560
- <button class="prop-switch ${value ? 'prop-switch--active' : ''}"
1561
- @click="${() => onChange(!value)}"
1562
- ></button>
1563
- <span class="prop-toggle-label">${label}</span>
1564
- </div>
2009
+ return html `
2010
+ <div class="prop-toggle">
2011
+ <button class="prop-switch ${value ? 'prop-switch--active' : ''}"
2012
+ @click="${() => onChange(!value)}"
2013
+ ></button>
2014
+ <span class="prop-toggle-label">${label}</span>
2015
+ </div>
1565
2016
  `;
1566
2017
  }
1567
2018
  toggleSection(id) {