@zentto/studio 0.6.0 → 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.
@@ -101,493 +101,493 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
101
101
  this.apiCompany = '';
102
102
  this.apiBranch = '';
103
103
  }
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
- }
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
591
  `; }
592
592
  // ─── Lifecycle ────────────────────────────────────
593
593
  updated(changed) {
@@ -1015,6 +1015,84 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1015
1015
  this.selectedFieldId = id;
1016
1016
  this.commitChange();
1017
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
+ }
1018
1096
  removeField(si, fi) {
1019
1097
  if (!this.schema)
1020
1098
  return;
@@ -1047,77 +1125,77 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1047
1125
  }
1048
1126
  // ─── Render ───────────────────────────────────────
1049
1127
  render() {
1050
- return html `
1051
- <div class="designer">
1052
- ${this.renderToolbar()}
1053
- ${this.renderLeftPanel()}
1054
- <div class="panel-resize"></div>
1055
- ${this.renderCanvas()}
1056
- <div class="panel-resize"></div>
1057
- ${this.renderRightPanel()}
1058
- </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>
1059
1137
  `;
1060
1138
  }
1061
1139
  // ─── Toolbar ──────────────────────────────────────
1062
1140
  renderToolbar() {
1063
- return html `
1064
- <div class="toolbar">
1141
+ return html `
1142
+ <div class="toolbar">
1065
1143
  ${this.editingTitle
1066
- ? html `<input class="report-name-input" .value="${this.schema?.title ?? ''}"
1144
+ ? html `<input class="report-name-input" .value="${this.schema?.title ?? ''}"
1067
1145
  @blur="${(e) => { if (this.schema)
1068
- this.schema.title = e.target.value; this.editingTitle = false; this.commitChange(); }}"
1146
+ this.schema.title = e.target.value; this.editingTitle = false; this.commitChange(); }}"
1069
1147
  @keydown="${(e) => { if (e.key === 'Enter')
1070
- e.target.blur(); }}"
1148
+ e.target.blur(); }}"
1071
1149
  />`
1072
- : html `<span class="report-name" @click="${() => { this.editingTitle = true; }}">${this.schema?.title || 'Sin titulo'}</span>`}
1073
-
1074
- <div class="toolbar-sep"></div>
1075
-
1076
- <button class="tb-btn" ?disabled="${this.undoStack.length <= 1}" @click="${this.undo}" title="Deshacer (Ctrl+Z)">↩ Deshacer</button>
1077
- <button class="tb-btn" ?disabled="${this.redoStack.length === 0}" @click="${this.redo}" title="Rehacer (Ctrl+Y)">↪ Rehacer</button>
1078
-
1079
- <div class="toolbar-sep"></div>
1080
-
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
+
1081
1159
  <button class="tb-btn" @click="${() => {
1082
1160
  if (!this.schema)
1083
1161
  return;
1084
1162
  this.schema.sections.push({ id: `section_${Date.now()}`, title: 'Nueva Seccion', fields: [] });
1085
1163
  this.commitChange();
1086
- }}">+ Seccion</button>
1087
-
1088
- <div style="position:relative;display:inline-block;">
1089
- <button class="tb-btn" @click="${() => { this.showTemplateMenu = !this.showTemplateMenu; }}">📋 Plantillas ▾</button>
1090
- ${this.showTemplateMenu ? this.renderTemplateMenu() : nothing}
1091
- </div>
1092
-
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
+
1093
1171
  <button class="tb-btn tb-btn--danger" @click="${() => {
1094
1172
  this.schema = { id: 'new-form', version: '1.0', title: 'Nuevo Formulario', layout: { type: 'grid', columns: 2 }, sections: [{ id: 'main', title: 'Datos', fields: [] }] };
1095
1173
  this.undoStack = [JSON.stringify(this.schema)];
1096
1174
  this.redoStack = [];
1097
1175
  this.selectedFieldId = null;
1098
1176
  this.commitChange();
1099
- }}">🗑 Nuevo</button>
1100
-
1101
- <div class="toolbar-sep"></div>
1102
-
1103
- <button class="tb-btn ${this.viewMode === 'design' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'design'; }}">✏️ Diseño</button>
1104
- <button class="tb-btn ${this.viewMode === 'preview' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'preview'; }}">👁 Preview</button>
1105
- <button class="tb-btn ${this.viewMode === 'json' ? 'tb-btn--active' : ''}" @click="${() => { this.viewMode = 'json'; }}">&lt;/&gt; JSON</button>
1106
-
1107
- <span class="tb-spacer"></span>
1108
-
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
+
1109
1187
  <button class="tb-btn" @click="${() => {
1110
1188
  if (this.schema) {
1111
1189
  navigator.clipboard.writeText(JSON.stringify(this.schema, null, 2));
1112
1190
  }
1113
- }}">📋 Copiar</button>
1114
-
1115
- <div class="zoom-controls">
1116
- <button class="tb-btn zoom-btn" @click="${() => { this.zoom = Math.max(0.5, this.zoom - 0.1); }}">−</button>
1117
- <span class="zoom-label" @click="${() => { this.zoom = 1; }}">${Math.round(this.zoom * 100)}%</span>
1118
- <button class="tb-btn zoom-btn" @click="${() => { this.zoom = Math.min(2, this.zoom + 0.1); }}">+</button>
1119
- </div>
1120
- </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>
1121
1199
  `;
1122
1200
  }
1123
1201
  // ─── Left Panel ───────────────────────────────────
@@ -1129,107 +1207,103 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1129
1207
  { key: 'media', label: 'Media' },
1130
1208
  { key: 'layout', label: 'Layout' },
1131
1209
  ];
1132
- return html `
1133
- <div class="left-panel">
1134
- <div class="panel-tabs">
1135
- <button class="panel-tab ${this.leftTab === 'fields' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'fields'; }}">Campos</button>
1136
- <button class="panel-tab ${this.leftTab === 'sections' ? 'panel-tab--active' : ''}" @click="${() => { this.leftTab = 'sections'; }}">Secciones</button>
1137
- <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>
1138
- </div>
1139
- <div class="panel-content">
1140
- ${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 `
1141
1219
  ${categories.map(cat => {
1142
1220
  const items = getFieldsByCategory(cat.key);
1143
1221
  if (items.length === 0)
1144
1222
  return nothing;
1145
- return html `
1146
- <div class="toolbox-section">${cat.label}</div>
1147
- <div class="toolbox-grid">
1148
- ${items.map(f => html `
1149
- <div class="toolbox-item"
1150
- draggable="true"
1151
- data-field-type="${f.type}"
1152
- @dragstart="${(e) => { this.dragType = f.type; e.dataTransfer?.setData('text/plain', f.type); }}"
1153
- @dragend="${() => { this.dragType = null; }}"
1154
- @dblclick="${() => this.addField(f.type)}"
1155
- title="${f.label}"
1156
- >
1157
- <span class="toolbox-icon">${unsafeHTML(resolveIcon(f.icon, this.provider))}</span>
1158
- <span class="toolbox-label">${f.label}</span>
1159
- </div>
1160
- `)}
1161
- </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>
1162
1240
  `;
1163
- })}
1164
- ` : this.leftTab === 'sections' ? html `
1165
- ${this.schema?.sections.map((s, i) => html `
1166
- <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;"
1167
- @click="${() => { }}"
1168
- >
1169
- <span style="background:#e3f2fd;color:#1976d2;font-weight:700;font-size:10px;padding:2px 6px;border-radius:4px;">§${i + 1}</span>
1170
- <span style="flex:1;font-weight:500;">${s.title ?? 'Sin titulo'}</span>
1171
- <span style="color:#aaa;font-size:10px;">${s.fields.length}</span>
1172
- </div>
1173
- `) ?? nothing}
1174
- <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;"
1175
1253
  @click="${() => { if (this.schema) {
1176
1254
  this.schema.sections.push({ id: 'section_' + Date.now(), title: 'Nueva Seccion', fields: [] });
1177
1255
  this.commitChange();
1178
- } }}"
1179
- >+ Agregar Seccion</button>
1180
- ` : this.renderApiPanel()}
1181
- </div>
1182
- </div>
1256
+ } }}"
1257
+ >+ Agregar Seccion</button>
1258
+ ` : this.renderApiPanel()}
1259
+ </div>
1260
+ </div>
1183
1261
  `;
1184
1262
  }
1185
1263
  // ─── Canvas ───────────────────────────────────────
1186
1264
  renderCanvas() {
1187
- return html `
1188
- <div class="canvas-area">
1265
+ return html `
1266
+ <div class="canvas-area">
1189
1267
  ${this.viewMode === 'json'
1190
1268
  ? html `<div class="json-panel" style="width:100%;max-width:800px;">${JSON.stringify(this.schema, null, 2)}</div>`
1191
1269
  : this.viewMode === 'preview'
1192
1270
  ? html `<div class="canvas" style="transform:scale(${this.zoom});"><zentto-studio-renderer .schema="${this.schema}" .data="${this.data}"></zentto-studio-renderer></div>`
1193
- : this.renderDesignCanvas()}
1194
- </div>
1271
+ : this.renderDesignCanvas()}
1272
+ </div>
1195
1273
  `;
1196
1274
  }
1197
1275
  renderDesignCanvas() {
1198
1276
  if (!this.schema) {
1199
- return html `<div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}" data-section-index="0" style="width:500px;height:200px;display:flex;align-items:center;justify-content:center;"
1200
- @dragover="${(e) => e.preventDefault()}"
1201
- @drop="${(e) => { e.preventDefault(); if (this.dragType) {
1202
- this.addField(this.dragType);
1203
- this.dragType = null;
1204
- } }}"
1205
- >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>`;
1206
1281
  }
1207
1282
  const cols = this.schema.layout.columns ?? 1;
1208
- return html `
1209
- <div class="canvas" style="transform:scale(${this.zoom});" @click="${() => { this.selectedFieldId = null; }}">
1210
- ${this.schema.sections.map((section, si) => html `
1211
- <div class="canvas-section">
1212
- <div class="canvas-section-header" @click="${(e) => e.stopPropagation()}">
1213
- <span style="font-size:10px;color:var(--zrd-accent);">§${si + 1}</span>
1214
- ${section.title ?? 'Seccion'}
1215
- <span style="flex:1;"></span>
1216
- <span style="font-size:10px;color:var(--zrd-text-muted);">${section.fields.length} campos</span>
1217
- </div>
1218
- <div class="section-drop-zone" data-section-index="${si}" data-position="top"></div>
1219
- <div class="canvas-grid" data-section-index="${si}" style="grid-template-columns:repeat(${section.columns ?? cols}, 1fr);">
1220
- ${section.fields.map((field, fi) => this.renderCanvasField(field, si, fi, section.columns ?? cols))}
1221
- </div>
1222
- <div class="drop-zone ${this.dragType ? 'drop-zone--active' : ''}"
1223
- data-section-index="${si}"
1224
- @dragover="${(e) => e.preventDefault()}"
1225
- @drop="${(e) => { e.preventDefault(); if (this.dragType) {
1226
- this.addField(this.dragType, si);
1227
- this.dragType = null;
1228
- } }}"
1229
- >${this.dragType ? '↓ Soltar aqui' : '+ Arrastra campos'}</div>
1230
- </div>
1231
- `)}
1232
- </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>
1233
1307
  `;
1234
1308
  }
1235
1309
  renderCanvasField(field, si, fi, maxCols) {
@@ -1238,39 +1312,39 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1238
1312
  const fullWidth = ['separator', 'heading', 'html', 'datagrid', 'report', 'chart'].includes(field.type);
1239
1313
  const gridCol = fullWidth ? '1 / -1' : span > 1 ? `span ${span}` : '';
1240
1314
  const typeColor = FIELD_TYPE_COLORS[field.type] ?? 'background:#eceff1;color:#546e7a;';
1241
- return html `
1242
- <div class="canvas-field ${isSelected ? 'canvas-field--selected' : ''}"
1243
- style="${gridCol ? `grid-column:${gridCol};` : ''}"
1244
- data-field-id="${field.id}"
1245
- data-section-index="${si}"
1246
- @click="${(e) => { e.stopPropagation(); this.selectedFieldId = field.id; }}"
1247
- >
1248
- <!-- Type badge -->
1249
- <span class="field-type-badge" style="${typeColor}">${field.type}</span>
1250
-
1251
- <!-- Action buttons -->
1252
- <div class="field-actions">
1253
- ${fi > 0 ? html `<button class="fa-btn fa-btn--move" @click="${(e) => { e.stopPropagation(); this.moveField(si, fi, -1); }}" title="Subir">↑</button>` : ''}
1254
- ${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>` : ''}
1255
- <button class="fa-btn fa-btn--copy" @click="${(e) => { e.stopPropagation(); this.duplicateField(si, fi); }}" title="Duplicar">⎘</button>
1256
- <button class="fa-btn fa-btn--delete" @click="${(e) => { e.stopPropagation(); this.removeField(si, fi); }}" title="Eliminar">✕</button>
1257
- </div>
1258
-
1259
- <!-- Resize handles -->
1260
- ${isSelected ? html `
1261
- <div class="rh rh-nw"></div><div class="rh rh-n"></div><div class="rh rh-ne"></div>
1262
- <div class="rh rh-e"></div><div class="rh rh-se"></div><div class="rh rh-s"></div>
1263
- <div class="rh rh-sw"></div><div class="rh rh-w"></div>
1264
- ` : ''}
1265
-
1266
- <!-- Field preview -->
1267
- <div class="field-preview">
1268
- <div class="field-preview-label">${field.label ?? field.id}${field.required ? ' *' : ''}</div>
1269
- <div class="field-preview-input ${this.getPreviewClass(field.type)}">
1270
- ${this.getPreviewContent(field)}
1271
- </div>
1272
- </div>
1273
- </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>
1274
1348
  `;
1275
1349
  }
1276
1350
  getPreviewClass(type) {
@@ -1323,227 +1397,215 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1323
1397
  renderRightPanel() {
1324
1398
  const field = this.selectedField;
1325
1399
  if (!field) {
1326
- return html `
1327
- <div class="right-panel">
1328
- <div class="props-empty">
1329
- <div class="props-empty-icon">⬚</div>
1330
- <div class="props-empty-text">Selecciona un campo<br/>para editar propiedades</div>
1331
- </div>
1332
- ${this.schema ? this.renderFormProperties() : ''}
1333
- </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>
1334
1408
  `;
1335
1409
  }
1336
1410
  const typeColor = FIELD_TYPE_COLORS[field.type] ?? 'background:#eceff1;color:#546e7a;';
1337
- return html `
1338
- <div class="right-panel">
1339
- <!-- Header with type badge -->
1340
- <div style="padding:8px 10px;border-bottom:1px solid #eee;">
1341
- <span class="props-type-badge" style="${typeColor}">${field.type.toUpperCase()}</span>
1342
- <div class="props-field-id">#${field.id}</div>
1343
- </div>
1344
-
1345
- <!-- General -->
1346
- <div class="prop-section">
1347
- <div class="prop-section-header" data-section="general" @click="${() => this.toggleSection('general')}">
1348
- <span class="collapse-icon ${this.collapsedSections.has('general') ? 'collapse-icon--collapsed' : ''}">▾</span>
1349
- <h4>General</h4>
1350
- </div>
1351
- ${!this.collapsedSections.has('general') ? html `
1352
- <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>
1353
- <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>
1354
- <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>
1355
- <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>
1356
- ` : ''}
1357
- </div>
1358
-
1359
- <!-- Layout (Figma position grid) -->
1360
- <div class="prop-section">
1361
- <div class="prop-section-header" data-section="layout" @click="${() => this.toggleSection('layout')}">
1362
- <span class="collapse-icon ${this.collapsedSections.has('layout') ? 'collapse-icon--collapsed' : ''}">▾</span>
1363
- <h4>Layout</h4>
1364
- </div>
1365
- ${!this.collapsedSections.has('layout') ? html `
1366
- <div class="prop-pos-grid">
1367
- <div class="prop-pos-cell">
1368
- <span class="prop-pos-label prop-pos-label--w">W</span>
1369
- <input type="number" min="1" max="6" .value="${String(field.colSpan ?? 1)}" @change="${(e) => { field.colSpan = parseInt(e.target.value) || 1; this.commitChange(); }}" />
1370
- </div>
1371
- <div class="prop-pos-cell">
1372
- <span class="prop-pos-label prop-pos-label--h">T</span>
1373
- <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;">
1374
- ${getAllFields().map(f => html `<option value="${f.type}" ?selected="${f.type === field.type}">${f.label}</option>`)}
1375
- </select>
1376
- </div>
1377
- </div>
1378
- <div class="prop-divider"></div>
1379
- <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>
1380
- ` : ''}
1381
- </div>
1382
-
1383
- <!-- Style -->
1384
- <div class="prop-section">
1385
- <div class="prop-section-header" data-section="style" @click="${() => this.toggleSection('style')}">
1386
- <span class="collapse-icon ${this.collapsedSections.has('style') ? 'collapse-icon--collapsed' : ''}">▾</span>
1387
- <h4>Estilo</h4>
1388
- </div>
1389
- ${!this.collapsedSections.has('style') ? html `
1390
- <div class="prop-row">
1391
- <span class="prop-label">Ancho</span>
1392
- <div class="prop-segmented">
1393
- ${['auto', '100%', '50%'].map(w => html `
1394
- <button class="prop-seg-btn ${(field.width ?? 'auto') === w ? 'prop-seg-btn--active' : ''}"
1395
- @click="${() => { field.width = w === 'auto' ? undefined : w; this.commitChange(); }}"
1396
- >${w}</button>
1397
- `)}
1398
- </div>
1399
- </div>
1400
- ` : ''}
1401
- </div>
1402
-
1403
- <!-- Behavior (toggles) -->
1404
- <div class="prop-section">
1405
- <div class="prop-section-header" data-section="behavior" @click="${() => this.toggleSection('behavior')}">
1406
- <span class="collapse-icon ${this.collapsedSections.has('behavior') ? 'collapse-icon--collapsed' : ''}">▾</span>
1407
- <h4>Comportamiento</h4>
1408
- </div>
1409
- ${!this.collapsedSections.has('behavior') ? html `
1410
- ${this.renderToggle('Requerido', field.required ?? false, (v) => { field.required = v; this.commitChange(); })}
1411
- ${this.renderToggle('Solo lectura', field.readOnly ?? false, (v) => { field.readOnly = v; this.commitChange(); })}
1412
- ${this.renderToggle('Deshabilitado', field.disabled ?? false, (v) => { field.disabled = v; this.commitChange(); })}
1413
- ${this.renderToggle('Oculto', field.hidden ?? false, (v) => { field.hidden = v; this.commitChange(); })}
1414
- ` : ''}
1415
- </div>
1416
-
1417
- <!-- Rules (expressions) -->
1418
- <div class="prop-section">
1419
- <div class="prop-section-header" data-section="rules" @click="${() => this.toggleSection('rules')}">
1420
- <span class="collapse-icon ${this.collapsedSections.has('rules') ? 'collapse-icon--collapsed' : ''}">▾</span>
1421
- <h4>Reglas</h4>
1422
- </div>
1423
- ${!this.collapsedSections.has('rules') ? html `
1424
- <div class="prop-info">💡 Usa {campo} para referencias y expresiones</div>
1425
- <div class="prop-row prop-row-full"><span class="prop-label" style="margin-bottom:2px;">Condicion de visibilidad</span></div>
1426
- <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>
1427
- <div class="prop-row prop-row-full"><span class="prop-label" style="margin-bottom:2px;">Valor computado</span></div>
1428
- <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>
1429
- <div class="prop-divider"></div>
1430
- <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>
1431
- ` : ''}
1432
- </div>
1433
-
1434
- <!-- Data Source (API connection like report-designer) -->
1435
- <div class="prop-section">
1436
- <div class="prop-section-header" data-section="datasource" @click="${() => this.toggleSection('datasource')}">
1437
- <span class="collapse-icon ${this.collapsedSections.has('datasource') ? 'collapse-icon--collapsed' : ''}">▾</span>
1438
- <h4>Origen de Datos</h4>
1439
- </div>
1440
- ${!this.collapsedSections.has('datasource') ? html `
1441
- <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>
1442
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)
1443
- 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>
1444
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)
1445
- 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>
1446
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)
1447
- field.props = {}; field.props.displayField = e.target.value || undefined; this.commitChange(); }}" /></div>
1448
- ${field.type === 'datagrid' || field.type === 'select' || field.type === 'lookup' ? html `
1449
- <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>
1450
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)
1451
- field.props = {}; field.props.dataSourceId = e.target.value || undefined; this.commitChange(); }}" /></div>
1452
- ` : ''}
1453
- ` : ''}
1454
- </div>
1455
- </div>
1525
+ field.props = {}; field.props.dataSourceId = e.target.value || undefined; this.commitChange(); }}" /></div>
1526
+ ` : ''}
1527
+ ` : ''}
1528
+ </div>
1529
+ </div>
1456
1530
  `;
1457
1531
  }
1458
1532
  renderFormProperties() {
1459
1533
  if (!this.schema)
1460
1534
  return nothing;
1461
- return html `
1462
- <div class="prop-section">
1463
- <div class="prop-section-header" data-section="form">
1464
- <span class="collapse-icon">▾</span>
1465
- <h4>Formulario</h4>
1466
- </div>
1467
- <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>
1468
- <div class="prop-divider"></div>
1469
- <div class="prop-pos-grid">
1470
- <div class="prop-pos-cell">
1471
- <span class="prop-pos-label prop-pos-label--w">C</span>
1472
- <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(); }}" />
1473
- </div>
1474
- <div class="prop-pos-cell">
1475
- <span class="prop-pos-label prop-pos-label--y">G</span>
1476
- <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(); }}" />
1477
- </div>
1478
- </div>
1479
- <div style="font-size:9px;color:#bbb;margin-top:4px;display:flex;gap:12px;">
1480
- <span>C = Columnas</span><span>G = Gap (px)</span>
1481
- </div>
1482
- </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>
1483
1557
  `;
1484
1558
  }
1485
1559
  renderApiPanel() {
1486
- return html `
1487
- <div style="padding:4px;">
1488
- <!-- Auth Section -->
1489
- ${this.renderApiAuth()}
1490
-
1491
- <!-- Quick connect (only when logged in or no auth needed) -->
1492
- <div style="padding:4px 8px;margin-top:4px;">
1493
- <div style="font-size:9px;font-weight:600;color:#aaa;text-transform:uppercase;margin-bottom:4px;">Conectar Endpoint</div>
1494
- <div style="display:flex;gap:4px;margin-bottom:6px;">
1495
- <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;">
1496
- <option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option>
1497
- </select>
1498
- <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" />
1499
- </div>
1500
- <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;"
1501
- @click="${this.fetchApiFields}"
1502
- >${this.apiLoading ? 'Cargando...' : '🔌 Probar Conexion'}</button>
1503
- </div>
1504
-
1505
- <!-- Fetched sources -->
1506
- ${this.apiSources.length > 0 ? html `
1507
- <div style="margin-top:8px;">
1508
- ${this.apiSources.map(src => html `
1509
- <div style="margin:4px;border:1px solid #eee;border-radius:6px;overflow:hidden;">
1510
- <div style="padding:6px 8px;background:#f8f9fa;display:flex;align-items:center;gap:6px;border-bottom:1px solid #eee;">
1511
- <span style="background:#ea580c;color:white;font-size:8px;font-weight:700;padding:1px 5px;border-radius:3px;">${src.method}</span>
1512
- <span style="font-size:10px;font-weight:600;color:#333;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${src.name}</span>
1513
- <span style="font-size:9px;color:#aaa;">${src.fields.length} campos</span>
1514
- </div>
1515
- <div style="padding:2px 4px;max-height:150px;overflow-y:auto;">
1516
- ${src.fields.map(field => html `
1517
- <div style="display:flex;align-items:center;gap:6px;padding:4px 6px;font-size:11px;cursor:grab;border-radius:3px;transition:background 0.1s;"
1518
- draggable="true"
1519
- @dragstart="${(e) => { this.dragType = 'text'; e.dataTransfer?.setData('text/plain', 'text'); e.dataTransfer?.setData('field-name', field); e.dataTransfer?.setData('ds-id', src.id); }}"
1520
- @dblclick="${() => {
1521
- // Auto-create a field bound to this API field
1522
- this.addField('text');
1523
- const lastField = this.schema?.sections[0]?.fields[this.schema.sections[0].fields.length - 1];
1524
- if (lastField) {
1525
- lastField.field = field;
1526
- lastField.label = field.charAt(0).toUpperCase() + field.slice(1).replace(/([A-Z])/g, ' $1');
1527
- if (!lastField.props)
1528
- lastField.props = {};
1529
- lastField.props.dataSourceId = src.id;
1530
- this.commitChange();
1531
- }
1532
- }}"
1533
- >
1534
- <span style="color:#1976d2;font-size:10px;">⬡</span>
1535
- <span style="color:#555;">${field}</span>
1536
- </div>
1537
- `)}
1538
- </div>
1539
- </div>
1540
- `)}
1541
- </div>
1542
- ` : ''}
1543
-
1544
- <!-- Zentto API shortcuts -->
1545
- <div style="margin-top:12px;padding:4px 8px;">
1546
- <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>
1547
1609
  ${[
1548
1610
  { label: 'Clientes', endpoint: '/v1/clientes', icon: '👥' },
1549
1611
  { label: 'Articulos', endpoint: '/v1/articulos', icon: '📦' },
@@ -1553,34 +1615,34 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1553
1615
  { label: 'Proveedores', endpoint: '/v1/proveedores', icon: '🏭' },
1554
1616
  { label: 'Bancos', endpoint: '/v1/bancos', icon: '🏦' },
1555
1617
  { label: 'Paises', endpoint: '/v1/config/countries', icon: '🌍' },
1556
- ].map(api => html `
1557
- <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;"
1558
1620
  @click="${() => {
1559
1621
  const urlInput = this.shadowRoot?.querySelector('#api-url');
1560
1622
  if (urlInput)
1561
1623
  urlInput.value = api.endpoint;
1562
- }}"
1563
- @mouseenter="${(e) => { e.target.style.background = '#fff7ed'; e.target.style.borderColor = '#fed7aa'; }}"
1564
- @mouseleave="${(e) => { e.target.style.background = ''; e.target.style.borderColor = 'transparent'; }}"
1565
- >
1566
- <span style="font-size:14px;">${api.icon}</span>
1567
- <span style="flex:1;color:#555;">${api.label}</span>
1568
- <span style="font-size:9px;color:#bbb;font-family:'SF Mono','Consolas',monospace;">${api.endpoint}</span>
1569
- </div>
1570
- `)}
1571
- </div>
1572
-
1573
- <!-- Manual data source -->
1574
- <div style="margin-top:8px;padding:4px 8px;">
1575
- <div style="font-size:9px;font-weight:600;color:#aaa;text-transform:uppercase;margin-bottom:4px;">Datos Estaticos</div>
1576
- <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;"
1577
1639
  @click="${() => {
1578
1640
  const src = { id: 'static_' + Date.now(), name: 'Datos Manuales', endpoint: '', method: 'STATIC', fields: ['campo1', 'campo2', 'campo3'] };
1579
1641
  this.apiSources = [...this.apiSources, src];
1580
- }}"
1581
- >+ Agregar Datos Estaticos</button>
1582
- </div>
1583
- </div>
1642
+ }}"
1643
+ >+ Agregar Datos Estaticos</button>
1644
+ </div>
1645
+ </div>
1584
1646
  `;
1585
1647
  }
1586
1648
  // ─── Template Menu ─────────────────────────────────
@@ -1691,13 +1753,13 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1691
1753
  ], actions: [{ id: 'send', type: 'submit', label: 'Enviar Encuesta', variant: 'primary' }] },
1692
1754
  },
1693
1755
  ];
1694
- return html `
1695
- <div style="position:absolute;top:100%;left:0;z-index:100;background:white;border:1px solid #ddd;border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,0.15);width:320px;max-height:400px;overflow-y:auto;margin-top:4px;">
1696
- <div style="padding:10px 12px;border-bottom:1px solid #eee;font-size:11px;font-weight:600;color:#888;text-transform:uppercase;letter-spacing:0.5px;">
1697
- Cargar Plantilla
1698
- </div>
1699
- ${templates.map(t => html `
1700
- <div style="display:flex;align-items:flex-start;gap:10px;padding:10px 14px;cursor:pointer;transition:background 0.1s;border-bottom:1px solid #f5f5f5;"
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;"
1701
1763
  @click="${() => {
1702
1764
  this.schema = structuredClone(t.schema);
1703
1765
  this.undoStack = [JSON.stringify(this.schema)];
@@ -1705,116 +1767,116 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1705
1767
  this.selectedFieldId = null;
1706
1768
  this.showTemplateMenu = false;
1707
1769
  this.commitChange();
1708
- }}"
1709
- @mouseenter="${(e) => { e.currentTarget.style.background = '#f0f7ff'; }}"
1710
- @mouseleave="${(e) => { e.currentTarget.style.background = ''; }}"
1711
- >
1712
- <span style="font-size:22px;margin-top:1px;">${t.icon}</span>
1713
- <div>
1714
- <div style="font-size:13px;font-weight:600;color:#333;">${t.title}</div>
1715
- <div style="font-size:11px;color:#999;margin-top:1px;">${t.desc}</div>
1716
- </div>
1717
- </div>
1718
- `)}
1719
- <div style="padding:8px 14px;border-top:1px solid #eee;">
1720
- <div style="font-size:10px;color:#bbb;text-align:center;">Clic en una plantilla para cargarla</div>
1721
- </div>
1722
- </div>
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>
1723
1785
  `;
1724
1786
  }
1725
1787
  renderApiAuth() {
1726
1788
  if (this.apiLoggedIn) {
1727
- return html `
1728
- <div style="margin:4px;padding:10px;background:linear-gradient(135deg,#e8f5e9,#f1f8e9);border:1px solid #c8e6c9;border-radius:8px;">
1729
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
1730
- <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>
1731
- <div style="flex:1;">
1732
- <div style="font-size:11px;font-weight:600;color:#2e7d32;">Conectado</div>
1733
- <div style="font-size:10px;color:#66bb6a;">${this.apiUser}${this.apiCompany ? ` — ${this.apiCompany}` : ''}</div>
1734
- </div>
1735
- <button style="border:none;background:none;cursor:pointer;font-size:14px;color:#999;padding:2px;" title="Cerrar sesion"
1736
- @click="${() => { this.apiLoggedIn = false; this.apiToken = ''; this.apiUser = ''; this.apiCompany = ''; this.apiBranch = ''; }}"
1737
- >✕</button>
1738
- </div>
1739
- <div style="font-size:9px;color:#81c784;font-family:'SF Mono','Consolas',monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${this.apiBaseUrl}">
1740
- 🔗 ${this.apiBaseUrl}
1741
- </div>
1742
- </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>
1743
1805
  `;
1744
1806
  }
1745
- return html `
1746
- <div style="margin:4px;padding:10px;background:#fafafa;border:1px solid #eee;border-radius:8px;">
1747
- <div style="display:flex;align-items:center;gap:6px;margin-bottom:10px;">
1748
- <span style="font-size:14px;">🔐</span>
1749
- <span style="font-size:11px;font-weight:600;color:#333;">Iniciar Sesion</span>
1750
- </div>
1751
-
1752
- ${this.apiLoginError ? html `
1753
- <div style="padding:6px 8px;background:#fde8e8;border:1px solid #f5c6cb;border-radius:5px;margin-bottom:8px;font-size:10px;color:#c62828;">
1754
- ⚠ ${this.apiLoginError}
1755
- </div>
1756
- ` : ''}
1757
-
1758
- <div style="margin-bottom:6px;">
1759
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">URL Base</label>
1760
- <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;"
1761
- .value="${this.apiBaseUrl}" placeholder="Vacio = proxy local (recomendado)"
1762
- @input="${(e) => { this.apiBaseUrl = e.target.value; }}"
1763
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1764
- @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1765
- />
1766
- </div>
1767
-
1768
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:6px;">
1769
- <div>
1770
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Usuario</label>
1771
- <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;"
1772
- placeholder="SUP"
1773
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1774
- @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1775
- />
1776
- </div>
1777
- <div>
1778
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Clave</label>
1779
- <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;"
1780
- placeholder="••••"
1781
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1782
- @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'; }}"
1783
1845
  @keydown="${(e) => { if (e.key === 'Enter')
1784
- this.doApiLogin(); }}"
1785
- />
1786
- </div>
1787
- </div>
1788
-
1789
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:8px;">
1790
- <div>
1791
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Empresa (cod)</label>
1792
- <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;"
1793
- placeholder="01" value="01"
1794
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1795
- @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1796
- />
1797
- </div>
1798
- <div>
1799
- <label style="font-size:9px;font-weight:600;color:#999;display:block;margin-bottom:2px;">Sucursal</label>
1800
- <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;"
1801
- placeholder="01" value="01"
1802
- @focus="${(e) => { e.target.style.borderColor = '#1976d2'; }}"
1803
- @blur="${(e) => { e.target.style.borderColor = '#ddd'; }}"
1804
- />
1805
- </div>
1806
- </div>
1807
-
1808
- <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;' : ''}"
1809
- @click="${this.doApiLogin}"
1810
- @mouseenter="${(e) => { e.target.style.background = '#1565c0'; }}"
1811
- @mouseleave="${(e) => { e.target.style.background = '#1976d2'; }}"
1812
- >${this.apiLoginLoading ? '⏳ Conectando...' : '🔑 Iniciar Sesion'}</button>
1813
-
1814
- <div style="margin-top:6px;font-size:9px;color:#bbb;text-align:center;">
1815
- Usa las mismas credenciales de Zentto ERP
1816
- </div>
1817
- </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>
1818
1880
  `;
1819
1881
  }
1820
1882
  async doApiLogin() {
@@ -1944,13 +2006,13 @@ let ZsPageDesigner = class ZsPageDesigner extends LitElement {
1944
2006
  return Object.keys(sample).filter(k => !k.startsWith('_'));
1945
2007
  }
1946
2008
  renderToggle(label, value, onChange) {
1947
- return html `
1948
- <div class="prop-toggle">
1949
- <button class="prop-switch ${value ? 'prop-switch--active' : ''}"
1950
- @click="${() => onChange(!value)}"
1951
- ></button>
1952
- <span class="prop-toggle-label">${label}</span>
1953
- </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>
1954
2016
  `;
1955
2017
  }
1956
2018
  toggleSection(id) {