@vectoriox/iox-builder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4357 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, EventEmitter, Output, Input, Directive, ChangeDetectionStrategy, Component, ViewChild, Optional, Inject, InjectionToken, ContentChildren, NO_ERRORS_SCHEMA, NgModule } from '@angular/core';
3
+ import * as i2 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import * as i3 from '@angular/forms';
6
+ import { FormsModule } from '@angular/forms';
7
+ import * as i4 from '@angular/cdk/drag-drop';
8
+ import { DragDropModule } from '@angular/cdk/drag-drop';
9
+ import { AccordionModule } from 'primeng/accordion';
10
+ import * as i10 from 'primeng/button';
11
+ import { ButtonModule } from 'primeng/button';
12
+ import * as i2$1 from 'primeng/popover';
13
+ import { PopoverModule } from 'primeng/popover';
14
+ import { InputTextModule } from 'primeng/inputtext';
15
+ import { SelectModule } from 'primeng/select';
16
+ import { TooltipModule } from 'primeng/tooltip';
17
+ import { DialogModule } from 'primeng/dialog';
18
+ import * as i11 from '@vectoriox/iox-ui';
19
+ import { IS_PREVIEW, IoxPageModule } from '@vectoriox/iox-ui';
20
+ import Lenis from 'lenis';
21
+ import { Subject, BehaviorSubject } from 'rxjs';
22
+ import interact from 'interactjs';
23
+ import * as i5 from '@angular/router';
24
+
25
+ var PanelEventTypes;
26
+ (function (PanelEventTypes) {
27
+ PanelEventTypes["ELEMENT_SELECT"] = "elementselect";
28
+ PanelEventTypes["ELEMENT_DESELECT"] = "elementdeselect";
29
+ PanelEventTypes["PANEL_SELECTED"] = "panelselect";
30
+ PanelEventTypes["PANEL_OPEN"] = "panelopen";
31
+ PanelEventTypes["PANEL_CLOSE"] = "panelclose";
32
+ PanelEventTypes["BINDING_CHANGED"] = "bindingchanged";
33
+ PanelEventTypes["NODE_RENDERED"] = "noderendered";
34
+ })(PanelEventTypes || (PanelEventTypes = {}));
35
+ class PanelEventService {
36
+ constructor() {
37
+ this.panelEventSubject = new Subject();
38
+ }
39
+ subscribe(next) {
40
+ return this.panelEventSubject.asObservable().subscribe(next);
41
+ }
42
+ emit(type, data) {
43
+ this.panelEventSubject.next({ type, data });
44
+ }
45
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: PanelEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
46
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: PanelEventService }); }
47
+ }
48
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: PanelEventService, decorators: [{
49
+ type: Injectable
50
+ }] });
51
+
52
+ var PanelTypes;
53
+ (function (PanelTypes) {
54
+ PanelTypes["BINDINGS"] = "Bindings";
55
+ PanelTypes["STYLES"] = "Styles";
56
+ PanelTypes["PAGE"] = "Page";
57
+ })(PanelTypes || (PanelTypes = {}));
58
+ const ROUTE_ANIMATION_OPTIONS = [
59
+ { value: 'none', label: 'None', description: 'No animation' },
60
+ { value: 'fade', label: 'Fade', description: 'Opacity cross-dissolve' },
61
+ { value: 'slideUp', label: 'Slide Up', description: 'Rise from below + fade' },
62
+ { value: 'slideDown', label: 'Slide Down', description: 'Drop from above + fade' },
63
+ { value: 'zoomIn', label: 'Zoom In', description: 'Scale up from 96% + fade' },
64
+ { value: 'zoomOut', label: 'Zoom Out', description: 'Scale down from 104% + fade' },
65
+ { value: 'blurIn', label: 'Blur', description: 'Unblur + fade' },
66
+ { value: 'flip', label: 'Flip', description: 'Perspective tilt + fade' },
67
+ ];
68
+ const EASING_OPTIONS$1 = [
69
+ { label: 'Ease', value: 'ease' },
70
+ { label: 'Ease In', value: 'ease-in' },
71
+ { label: 'Ease Out', value: 'ease-out' },
72
+ { label: 'Ease In Out', value: 'ease-in-out' },
73
+ { label: 'Linear', value: 'linear' },
74
+ { label: 'Spring', value: 'cubic-bezier(0.34, 1.56, 0.64, 1)' },
75
+ ];
76
+ function defaultPageSettings() {
77
+ return {
78
+ style: {
79
+ backgroundColor: '#ffffff',
80
+ backgroundType: 'solid',
81
+ backgroundGradient: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',
82
+ backgroundImage: '',
83
+ fontFamily: 'Poppins, sans-serif',
84
+ textColor: '#000000',
85
+ maxWidth: '1440px',
86
+ },
87
+ scroll: {
88
+ enabled: true,
89
+ lerp: 0.1,
90
+ wheelMultiplier: 1,
91
+ touchMultiplier: 1,
92
+ direction: 'vertical',
93
+ infinite: false,
94
+ },
95
+ routeAnimation: {
96
+ enter: 'fade',
97
+ leave: 'fade',
98
+ duration: 400,
99
+ easing: 'ease',
100
+ },
101
+ };
102
+ }
103
+ var ToolbarAction;
104
+ (function (ToolbarAction) {
105
+ ToolbarAction["SelectParent"] = "select-parent";
106
+ ToolbarAction["Duplicate"] = "duplicate";
107
+ ToolbarAction["Delete"] = "delete";
108
+ ToolbarAction["Play"] = "play";
109
+ ToolbarAction["SaveAsBlock"] = "save-as-block";
110
+ })(ToolbarAction || (ToolbarAction = {}));
111
+ var NodeAction;
112
+ (function (NodeAction) {
113
+ NodeAction["Delete"] = "delete";
114
+ NodeAction["Duplicate"] = "duplicate";
115
+ })(NodeAction || (NodeAction = {}));
116
+ var BuilderMode;
117
+ (function (BuilderMode) {
118
+ BuilderMode["Select"] = "select";
119
+ BuilderMode["Style"] = "style";
120
+ BuilderMode["Pan"] = "pan";
121
+ })(BuilderMode || (BuilderMode = {}));
122
+ var DeviceMode;
123
+ (function (DeviceMode) {
124
+ DeviceMode["Desktop"] = "desktop";
125
+ DeviceMode["Tablet"] = "tablet";
126
+ DeviceMode["Mobile"] = "mobile";
127
+ })(DeviceMode || (DeviceMode = {}));
128
+ const DEVICE_OPTIONS = [
129
+ { mode: DeviceMode.Desktop, label: 'Desktop', icon: 'ph-thin ph-desktop', width: 1440 },
130
+ { mode: DeviceMode.Tablet, label: 'Tablet', icon: 'ph-thin ph-device-tablet', width: 768 },
131
+ { mode: DeviceMode.Mobile, label: 'Mobile', icon: 'ph-thin ph-device-mobile', width: 375 },
132
+ ];
133
+ const ZOOM_OPTIONS = [
134
+ { label: '25%', value: 25 },
135
+ { label: '50%', value: 50 },
136
+ { label: '75%', value: 75 },
137
+ { label: '100%', value: 100 },
138
+ { label: 'Fit', value: 'fit' },
139
+ ];
140
+
141
+ class ComponentRegistryService {
142
+ constructor() {
143
+ this.components = {};
144
+ }
145
+ getComponent(type) {
146
+ return this.components[type]?.component ?? null;
147
+ }
148
+ hasComponent(type) {
149
+ return !!this.components[type];
150
+ }
151
+ createConfig(type) {
152
+ const configType = this.components[type]?.config;
153
+ return configType ? new configType() : null;
154
+ }
155
+ register(type, component, config) {
156
+ this.components[type] = { type, component, config };
157
+ }
158
+ getAll() {
159
+ return Object.values(this.components);
160
+ }
161
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ComponentRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
162
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ComponentRegistryService }); }
163
+ }
164
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ComponentRegistryService, decorators: [{
165
+ type: Injectable
166
+ }] });
167
+
168
+ class OverlayService {
169
+ constructor() {
170
+ this.hoverSubject = new BehaviorSubject(null);
171
+ this.selectSubject = new BehaviorSubject(null);
172
+ this.nodeMap = new Map();
173
+ this.hover$ = this.hoverSubject.asObservable();
174
+ this.select$ = this.selectSubject.asObservable();
175
+ this.containerElement = null;
176
+ this.scrollContainerElement = null;
177
+ /** Emits the scroll container element whenever it is registered. */
178
+ this.scrollContainer$ = new Subject();
179
+ }
180
+ setContainer(container) {
181
+ this.containerElement = container;
182
+ }
183
+ getContainer() {
184
+ return this.containerElement;
185
+ }
186
+ setScrollContainer(el) {
187
+ this.scrollContainerElement = el;
188
+ if (el) {
189
+ this.scrollContainer$.next(el);
190
+ }
191
+ }
192
+ getScrollContainer() {
193
+ return this.scrollContainerElement;
194
+ }
195
+ /**
196
+ * Resolve the inner styled element.
197
+ * Builder components apply [ngStyle] on their first child element,
198
+ * so computed styles (padding, margin) live there — not on the host.
199
+ */
200
+ getStyledElement(host) {
201
+ return host.firstElementChild || host;
202
+ }
203
+ // ── Node → Element registry ──────────────────────────────
204
+ registerNode(node, element, componentRef) {
205
+ this.nodeMap.set(node, { element, componentRef });
206
+ }
207
+ unregisterNode(node) {
208
+ this.nodeMap.delete(node);
209
+ }
210
+ getNodeRef(node) {
211
+ return this.nodeMap.get(node);
212
+ }
213
+ /** Return all registered node → ref entries. */
214
+ getAllNodeEntries() {
215
+ return this.nodeMap.entries();
216
+ }
217
+ /** Find a registered node whose host element matches the given DOM element. */
218
+ findNodeByElement(element) {
219
+ for (const [node, ref] of this.nodeMap) {
220
+ if (ref.element === element) {
221
+ return { node, ref };
222
+ }
223
+ }
224
+ return undefined;
225
+ }
226
+ setHover(element, componentName, mode) {
227
+ if (this.selectSubject.getValue()?.element === element)
228
+ return;
229
+ this.hoverSubject.next({ element, componentName, boxModel: this.buildBoxModel(element), mode });
230
+ }
231
+ clearHover() {
232
+ this.hoverSubject.next(null);
233
+ }
234
+ setSelect(element, componentName, mode, node, componentRef) {
235
+ this.hoverSubject.next(null);
236
+ this.selectSubject.next({ element, componentName, mode, node, componentRef });
237
+ }
238
+ clearSelect() {
239
+ this.selectSubject.next(null);
240
+ }
241
+ refreshSelect() {
242
+ const current = this.selectSubject.getValue();
243
+ if (!current)
244
+ return;
245
+ this.selectSubject.next({ ...current });
246
+ }
247
+ /** Lightweight swap for drag-preview tracking. */
248
+ updateSelectElement(element) {
249
+ const current = this.selectSubject.getValue();
250
+ if (!current)
251
+ return;
252
+ this.selectSubject.next({ ...current, element });
253
+ }
254
+ buildBoxModel(host) {
255
+ const s = window.getComputedStyle(this.getStyledElement(host));
256
+ const px = (v) => { const n = Number.parseFloat(v); return Number.isFinite(n) ? n : 0; };
257
+ return {
258
+ margin: { top: px(s.marginTop), right: px(s.marginRight), bottom: px(s.marginBottom), left: px(s.marginLeft) },
259
+ padding: { top: px(s.paddingTop), right: px(s.paddingRight), bottom: px(s.paddingBottom), left: px(s.paddingLeft) },
260
+ border: { top: px(s.borderTopWidth), right: px(s.borderRightWidth), bottom: px(s.borderBottomWidth), left: px(s.borderLeftWidth) },
261
+ };
262
+ }
263
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: OverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
264
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: OverlayService }); }
265
+ }
266
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: OverlayService, decorators: [{
267
+ type: Injectable
268
+ }] });
269
+
270
+ class DragEngineService {
271
+ constructor(registry) {
272
+ this.registry = registry;
273
+ // ── Drop-zone registry ────────────────────────────────────
274
+ this.ids = [];
275
+ this.elMap = new Map();
276
+ this.dataMap = new Map();
277
+ this.cdrMap = new Map();
278
+ this.dropCbMap = new Map();
279
+ this.postDropMap = new Map();
280
+ this.ids$ = new BehaviorSubject([]);
281
+ this.isDragging$ = new BehaviorSubject(false);
282
+ this.deepTarget$ = new BehaviorSubject(null);
283
+ // ── Active drag state ─────────────────────────────────────
284
+ this._isDragging = false;
285
+ this._payload = null;
286
+ this._sourceEl = null;
287
+ /**
288
+ * For internal drags: a DOM clone of the dragged element appended to <body>.
289
+ * Lives outside any CSS transform, so position:fixed works in viewport coords.
290
+ * For external drags: the label chip overlay.
291
+ */
292
+ this._overlay = null;
293
+ /** Where on the element the user grabbed it (for accurate ghost positioning). */
294
+ this._grabOffsetX = 0;
295
+ this._grabOffsetY = 0;
296
+ // ── Pointer tracking ──────────────────────────────────────
297
+ this._lastPointerX = 0;
298
+ this._lastPointerY = 0;
299
+ // ── Deep-target tracking ──────────────────────────────────
300
+ this._deepTargetId = null;
301
+ /** Injected overlay div marking the active drop target. */
302
+ this._targetOverlay = null;
303
+ // ── Insertion indicator ───────────────────────────────────
304
+ this._indicator = null;
305
+ this._indicatorContainerId = null;
306
+ this._indicatorIndex = -1;
307
+ this._indicatorRaf = 0;
308
+ /** Elements that have temporary FLIP inline styles — cleared on drag end. */
309
+ this._flipEls = new Set();
310
+ // ── Viewport scale ────────────────────────────────────────
311
+ this._scale = 1;
312
+ // ── Root-escape ───────────────────────────────────────────
313
+ this.ROOT_ESCAPE_PX = 14;
314
+ this._rootEscapeActiveFor = null;
315
+ }
316
+ // ── Shared drop handler — used by all layout components ───────────────────
317
+ handleDrop(children, event, dropListId, cdr, afterDrop) {
318
+ const { payload, insertIndex } = event;
319
+ if (payload.type === 'external') {
320
+ const config = this.registry.createConfig(payload.data);
321
+ if (config) {
322
+ children.splice(insertIndex, 0, config);
323
+ cdr.detectChanges();
324
+ }
325
+ afterDrop?.();
326
+ return;
327
+ }
328
+ const item = payload.data;
329
+ if (payload.sourceId !== dropListId) {
330
+ const sourceData = payload.sourceId ? this.getData(payload.sourceId) : null;
331
+ if (sourceData) {
332
+ const srcIdx = sourceData.indexOf(item);
333
+ if (srcIdx !== -1) {
334
+ sourceData.splice(srcIdx, 1);
335
+ this.getCdr(payload.sourceId)?.detectChanges();
336
+ }
337
+ }
338
+ children.splice(insertIndex, 0, item);
339
+ cdr.detectChanges();
340
+ afterDrop?.();
341
+ return;
342
+ }
343
+ const currentIdx = children.indexOf(item);
344
+ if (currentIdx !== -1) {
345
+ const adjustedIdx = insertIndex > currentIdx ? insertIndex - 1 : insertIndex;
346
+ children.splice(currentIdx, 1);
347
+ children.splice(adjustedIdx, 0, item);
348
+ cdr.detectChanges();
349
+ }
350
+ afterDrop?.();
351
+ }
352
+ /** Called by BuilderComponent whenever the viewport scale changes. */
353
+ setScale(scale) { this._scale = scale; }
354
+ // ─────────────────────────────────────────────────────────
355
+ // Element lookup — elMap first, DOM id fallback
356
+ // ─────────────────────────────────────────────────────────
357
+ _getEl(id) {
358
+ return this.elMap.get(id) ?? document.getElementById(id);
359
+ }
360
+ // ─────────────────────────────────────────────────────────
361
+ // Drop-zone registration
362
+ // ─────────────────────────────────────────────────────────
363
+ registerDropzone(id, el, data, cdr, dropCb, postDrop) {
364
+ if (!id)
365
+ return;
366
+ // Stamp id on element so DOM-walk helpers (_isRootContainer) find it.
367
+ el.id = id;
368
+ if (!this.ids.includes(id)) {
369
+ this.ids = [...this.ids, id];
370
+ this.ids$.next(this.ids);
371
+ }
372
+ this.elMap.set(id, el);
373
+ this.dataMap.set(id, data);
374
+ this.cdrMap.set(id, cdr);
375
+ this.dropCbMap.set(id, dropCb);
376
+ if (postDrop)
377
+ this.postDropMap.set(id, postDrop);
378
+ interact(el).dropzone({
379
+ accept: '[data-iox-draggable]',
380
+ overlap: 'pointer',
381
+ });
382
+ }
383
+ unregisterDropzone(id, el) {
384
+ this.ids = this.ids.filter(i => i !== id);
385
+ this.elMap.delete(id);
386
+ this.dataMap.delete(id);
387
+ this.cdrMap.delete(id);
388
+ this.dropCbMap.delete(id);
389
+ this.postDropMap.delete(id);
390
+ this.ids$.next(this.ids);
391
+ try {
392
+ interact(el).unset();
393
+ }
394
+ catch { /* already unset */ }
395
+ }
396
+ // ─────────────────────────────────────────────────────────
397
+ // Draggable registration
398
+ // ─────────────────────────────────────────────────────────
399
+ registerDraggable(el, getPayload) {
400
+ el.setAttribute('data-iox-draggable', '');
401
+ interact(el).draggable({
402
+ listeners: {
403
+ start: (event) => this._onDragStart(event, el, getPayload),
404
+ move: (event) => this._onDragMove(event),
405
+ end: (event) => this._onDragEnd(event),
406
+ },
407
+ modifiers: [],
408
+ });
409
+ }
410
+ unregisterDraggable(el) {
411
+ el.removeAttribute('data-iox-draggable');
412
+ try {
413
+ interact(el).unset();
414
+ }
415
+ catch { /* already unset */ }
416
+ }
417
+ getData(id) { return this.dataMap.get(id); }
418
+ getCdr(id) { return this.cdrMap.get(id); }
419
+ invokePostDrop(id) { this.postDropMap.get(id)?.(); }
420
+ consumeDeepTarget() {
421
+ const v = this._deepTargetId;
422
+ this._deepTargetId = null;
423
+ return v;
424
+ }
425
+ // ─────────────────────────────────────────────────────────
426
+ // Drag lifecycle
427
+ // ─────────────────────────────────────────────────────────
428
+ _onDragStart(event, el, getPayload) {
429
+ this._payload = getPayload();
430
+ this._sourceEl = el;
431
+ this._isDragging = true;
432
+ this._rootEscapeActiveFor = null;
433
+ this._lastPointerX = event.client.x;
434
+ this._lastPointerY = event.client.y;
435
+ this.isDragging$.next(true);
436
+ if (this._payload.type === 'internal') {
437
+ // ── Clone the element and append to <body> ────────────────────────────
438
+ //
439
+ // DOM structure for a canvas item:
440
+ // div[ioxDraggable] (el — the drag wrapper, may be wider than content)
441
+ // └─ <app-card> / <app-section> … (Angular component host)
442
+ // └─ div.card-root / div.section-root … (visual root with styles)
443
+ //
444
+ // el.getBoundingClientRect() gives the wrapper's rect, which can be
445
+ // wider than the visual element when the wrapper is in a flex container
446
+ // that stretches it. This causes the ghost to have wrong dimensions
447
+ // and the grab offset to be calculated from the wrong origin.
448
+ //
449
+ // Fix: use the Angular component host (el.firstElementChild) for both
450
+ // the rect measurement and as the clone source.
451
+ // - Rect from host → correct visual dimensions and true grab offset.
452
+ // - Clone from host → carries _ngcontent-* attrs so encapsulated CSS works.
453
+ //
454
+ // The wrapper el still gets iox-drag-active-source (visibility:hidden)
455
+ // to keep its layout space stable while dragging.
456
+ const hostEl = el.firstElementChild ?? el;
457
+ const rect = hostEl.getBoundingClientRect();
458
+ this._grabOffsetX = event.client.x - rect.left;
459
+ this._grabOffsetY = event.client.y - rect.top;
460
+ // Scale factor: getBoundingClientRect() returns scaled viewport px,
461
+ // but the clone renders at natural CSS px (no transform parent on body).
462
+ // Apply the same scale transform so the clone matches the visual size
463
+ // exactly — no explicit width/height, no overflow clipping.
464
+ const scale = this._scale;
465
+ // Cancel fill:forwards animations before cloning so the ghost shows
466
+ // the element at its natural (un-animated) state, and so the original
467
+ // element is clean after a same-position drop (where Angular won't
468
+ // destroy/recreate the DOM node and InteractionEngine.detach() won't fire).
469
+ hostEl.getAnimations().forEach(a => a.cancel());
470
+ const clone = hostEl.cloneNode(true);
471
+ // getBoundingClientRect() returns scaled viewport px. Divide by scale
472
+ // to get the natural CSS px — this constrains the clone to match the
473
+ // original's width when it's reparented to <body> (no canvas parent).
474
+ // overflow:hidden suppresses scrollbars that can appear when the clone's
475
+ // children lose their layout context (e.g. percentage-width children).
476
+ const cssWidth = Math.round(rect.width / scale);
477
+ const cssHeight = Math.round(rect.height / scale);
478
+ clone.style.cssText = [
479
+ 'position:fixed',
480
+ `left:${rect.left}px`,
481
+ `top:${rect.top}px`,
482
+ `width:${cssWidth}px`,
483
+ `height:${cssHeight}px`,
484
+ `transform:scale(${scale})`,
485
+ 'transform-origin:top left',
486
+ 'pointer-events:none',
487
+ 'overflow:hidden',
488
+ 'z-index:9999',
489
+ 'margin:0',
490
+ ].join(';');
491
+ clone.classList.add('iox-drag-moving');
492
+ document.body.appendChild(clone);
493
+ this._overlay = clone;
494
+ // Hide original wrapper in-place (keeps layout space so container size is stable).
495
+ el.classList.add('iox-drag-active-source');
496
+ }
497
+ else {
498
+ // ── Sidebar external drag — show a label chip ─────────────────────────
499
+ this._createExternalChip(this._payload, event.client.x, event.client.y);
500
+ el.classList.add('iox-drag-source');
501
+ }
502
+ }
503
+ _onDragMove(event) {
504
+ this._lastPointerX = event.client.x;
505
+ this._lastPointerY = event.client.y;
506
+ // Move the clone (or chip) under the cursor.
507
+ if (this._overlay) {
508
+ this._overlay.style.left = (event.client.x - this._grabOffsetX) + 'px';
509
+ this._overlay.style.top = (event.client.y - this._grabOffsetY) + 'px';
510
+ }
511
+ this._updateDeepTarget();
512
+ this._updateIndicator();
513
+ }
514
+ _onDragEnd(event) {
515
+ // Restore original element visibility BEFORE Angular re-renders on drop.
516
+ if (this._sourceEl) {
517
+ this._sourceEl.classList.remove('iox-drag-active-source');
518
+ this._sourceEl.classList.remove('iox-drag-source');
519
+ }
520
+ document.querySelectorAll('.iox-drag-active-source')
521
+ .forEach(el => el.classList.remove('iox-drag-active-source'));
522
+ // Remove clone/chip overlay.
523
+ this._removeOverlay();
524
+ // Remove indicator BEFORE committing so calcInsertIndex sees a clean DOM.
525
+ this._removeIndicator();
526
+ document.body.classList.remove('iox-drag-has-indicator');
527
+ // Clean up any FLIP transform/transition inline styles left on container children.
528
+ for (const el of this._flipEls) {
529
+ el.style.transform = '';
530
+ el.style.transition = '';
531
+ }
532
+ this._flipEls.clear();
533
+ this._commitDrop();
534
+ this._removeTargetOverlay();
535
+ document.querySelectorAll('.iox-drag-over').forEach(el => el.classList.remove('iox-drag-over'));
536
+ this._rootEscapeActiveFor = null;
537
+ this._isDragging = false;
538
+ this._payload = null;
539
+ this._sourceEl = null;
540
+ this.isDragging$.next(false);
541
+ this.deepTarget$.next(null);
542
+ setTimeout(() => { this._deepTargetId = null; });
543
+ }
544
+ // ─────────────────────────────────────────────────────────
545
+ // Drop commit
546
+ // ─────────────────────────────────────────────────────────
547
+ _commitDrop() {
548
+ if (!this._payload)
549
+ return;
550
+ const targetId = this._deepTargetId ?? 'canvas-preview';
551
+ const insertIndex = this.calcInsertIndex(targetId);
552
+ const dropCb = this.dropCbMap.get(targetId);
553
+ if (!dropCb)
554
+ return;
555
+ dropCb({ payload: this._payload, targetId, insertIndex });
556
+ }
557
+ // ─────────────────────────────────────────────────────────
558
+ // External-drag chip (sidebar items only)
559
+ // ─────────────────────────────────────────────────────────
560
+ _createExternalChip(payload, pointerX, pointerY) {
561
+ this._removeOverlay();
562
+ const label = typeof payload.data === 'string' ? payload.data : 'Component';
563
+ const chip = document.createElement('div');
564
+ chip.className = 'iox-drag-overlay iox-drag-overlay-chip';
565
+ chip.textContent = label;
566
+ // Chip follows 14px below-right of cursor.
567
+ this._grabOffsetX = -14;
568
+ this._grabOffsetY = -14;
569
+ chip.style.left = (pointerX + 14) + 'px';
570
+ chip.style.top = (pointerY + 14) + 'px';
571
+ document.body.appendChild(chip);
572
+ this._overlay = chip;
573
+ }
574
+ _removeOverlay() {
575
+ this._overlay?.remove();
576
+ this._overlay = null;
577
+ this._grabOffsetX = 0;
578
+ this._grabOffsetY = 0;
579
+ }
580
+ // ─────────────────────────────────────────────────────────
581
+ // Deep-target detection (smallest bounding-rect area)
582
+ // ─────────────────────────────────────────────────────────
583
+ _updateDeepTarget() {
584
+ const px = this._lastPointerX;
585
+ const py = this._lastPointerY;
586
+ let bestId = null;
587
+ let bestArea = Infinity;
588
+ for (const id of this.ids) {
589
+ const el = this._getEl(id);
590
+ if (!el)
591
+ continue;
592
+ // Never drop an element into its own subtree: skip any dropzone
593
+ // whose DOM element lives inside the dragged source wrapper.
594
+ if (this._sourceEl?.contains(el))
595
+ continue;
596
+ const rect = el.getBoundingClientRect();
597
+ if (px >= rect.left && px <= rect.right && py >= rect.top && py <= rect.bottom) {
598
+ const area = rect.width * rect.height;
599
+ if (area < bestArea) {
600
+ bestArea = area;
601
+ bestId = id;
602
+ }
603
+ }
604
+ }
605
+ // Root-escape with hysteresis.
606
+ // Purpose: when the cursor is near the top/bottom edge of a root-level
607
+ // section, treat the target as canvas-preview so the user can insert
608
+ // between root sections.
609
+ //
610
+ // IMPORTANT: only activate if the cursor was ALREADY inside this section
611
+ // (i.e. deepTarget is the section itself or a nested child of it).
612
+ // Without this guard, crossing from Section A into the top 14px of
613
+ // Section C immediately locks the target to canvas-preview and prevents
614
+ // the user from ever reaching Section C's nested containers.
615
+ if (this._rootEscapeActiveFor) {
616
+ const escEl = this._getEl(this._rootEscapeActiveFor);
617
+ if (escEl) {
618
+ const r = escEl.getBoundingClientRect();
619
+ const inside = px >= r.left && px <= r.right && py >= r.top && py <= r.bottom;
620
+ if (inside) {
621
+ bestId = 'canvas-preview';
622
+ }
623
+ else {
624
+ this._rootEscapeActiveFor = null;
625
+ }
626
+ }
627
+ else {
628
+ this._rootEscapeActiveFor = null;
629
+ }
630
+ }
631
+ if (!this._rootEscapeActiveFor &&
632
+ bestId && bestId !== 'canvas-preview' && this._isRootContainer(bestId)) {
633
+ const el = this._getEl(bestId);
634
+ if (el) {
635
+ const rect = el.getBoundingClientRect();
636
+ const nearEdge = py <= rect.top + this.ROOT_ESCAPE_PX ||
637
+ py >= rect.bottom - this.ROOT_ESCAPE_PX;
638
+ // Only escape if we were already targeting this section or
639
+ // something nested inside it — not when crossing into it fresh.
640
+ const wasInsideThisSection = this._deepTargetId === bestId ||
641
+ this._isDescendantDropzone(this._deepTargetId, bestId);
642
+ if (nearEdge && wasInsideThisSection) {
643
+ this._rootEscapeActiveFor = bestId;
644
+ bestId = 'canvas-preview';
645
+ }
646
+ }
647
+ }
648
+ if (bestId !== this._deepTargetId) {
649
+ if (this._deepTargetId) {
650
+ const old = this._getEl(this._deepTargetId);
651
+ old?.classList.remove('iox-drag-over');
652
+ }
653
+ this._deepTargetId = bestId;
654
+ if (bestId) {
655
+ this._getEl(bestId)?.classList.add('iox-drag-over');
656
+ }
657
+ // Move injected overlay to the new target container.
658
+ this._moveTargetOverlay(bestId);
659
+ this.deepTarget$.next(bestId);
660
+ // Reset indicator position state when container changes.
661
+ this._indicatorIndex = -1;
662
+ this._indicatorContainerId = null;
663
+ }
664
+ }
665
+ // ─────────────────────────────────────────────────────────
666
+ // Target overlay — fixed to <body>, sized from getBoundingClientRect
667
+ // so it is always above all canvas content regardless of stacking
668
+ // contexts or background colors inside the target container.
669
+ // ─────────────────────────────────────────────────────────
670
+ /** Call on every canvas scroll tick to keep the fixed-position drop-target
671
+ * overlay aligned with the container element that moved in the viewport. */
672
+ refreshDragOverlays() {
673
+ if (!this._isDragging)
674
+ return;
675
+ this._moveTargetOverlay(this._deepTargetId);
676
+ }
677
+ _moveTargetOverlay(targetId) {
678
+ if (!targetId || targetId === 'canvas-preview') {
679
+ this._removeTargetOverlay();
680
+ return;
681
+ }
682
+ const el = this._getEl(targetId);
683
+ if (!el) {
684
+ this._removeTargetOverlay();
685
+ return;
686
+ }
687
+ if (!this._targetOverlay) {
688
+ this._targetOverlay = document.createElement('div');
689
+ this._targetOverlay.className = 'iox-drop-target-overlay';
690
+ document.body.appendChild(this._targetOverlay);
691
+ }
692
+ const rect = el.getBoundingClientRect();
693
+ this._targetOverlay.style.cssText = [
694
+ 'position:fixed',
695
+ `left:${rect.left}px`,
696
+ `top:${rect.top}px`,
697
+ `width:${rect.width}px`,
698
+ `height:${rect.height}px`,
699
+ 'pointer-events:none',
700
+ 'z-index:9998',
701
+ 'box-sizing:border-box',
702
+ ].join(';');
703
+ }
704
+ _removeTargetOverlay() {
705
+ this._targetOverlay?.remove();
706
+ this._targetOverlay = null;
707
+ }
708
+ _isRootContainer(id) {
709
+ const el = this._getEl(id);
710
+ if (!el)
711
+ return false;
712
+ let parent = el.parentElement;
713
+ while (parent) {
714
+ if (parent.id === 'canvas-preview')
715
+ return true;
716
+ if (parent.id && this.ids.includes(parent.id))
717
+ return false;
718
+ parent = parent.parentElement;
719
+ }
720
+ return false;
721
+ }
722
+ /**
723
+ * Returns true if the dropzone with `childId` is a DOM descendant of the
724
+ * dropzone with `parentId`. Used to decide whether the cursor was already
725
+ * "inside" a root section before triggering root-escape.
726
+ */
727
+ _isDescendantDropzone(childId, parentId) {
728
+ if (!childId)
729
+ return false;
730
+ const childEl = this._getEl(childId);
731
+ const parentEl = this._getEl(parentId);
732
+ if (!childEl || !parentEl)
733
+ return false;
734
+ return parentEl.contains(childEl);
735
+ }
736
+ // ─────────────────────────────────────────────────────────
737
+ // Insertion indicator (animates open so siblings shift smoothly)
738
+ // ─────────────────────────────────────────────────────────
739
+ _updateIndicator() {
740
+ const targetId = this._deepTargetId ?? 'canvas-preview';
741
+ document.body.classList.add('iox-drag-has-indicator');
742
+ const idx = this.calcInsertIndex(targetId);
743
+ if (idx !== this._indicatorIndex || targetId !== this._indicatorContainerId) {
744
+ this._indicatorIndex = idx;
745
+ this._positionIndicator(targetId, idx);
746
+ }
747
+ }
748
+ _positionIndicator(containerId, index) {
749
+ const container = this._getEl(containerId);
750
+ if (!container)
751
+ return;
752
+ const cs = window.getComputedStyle(container);
753
+ const isRow = (cs.display === 'flex' || cs.display === 'inline-flex') &&
754
+ (!cs.flexDirection || cs.flexDirection === 'row' || cs.flexDirection === 'row-reverse');
755
+ const containerChanged = this._indicatorContainerId !== containerId;
756
+ if (containerChanged) {
757
+ cancelAnimationFrame(this._indicatorRaf);
758
+ this._indicator?.remove();
759
+ this._indicator = null;
760
+ }
761
+ const needsCreate = !this._indicator;
762
+ if (needsCreate) {
763
+ this._indicator = document.createElement('div');
764
+ this._indicator.className = 'iox-insert-indicator';
765
+ if (isRow) {
766
+ // Start at width:0 with align-self:stretch so it spans the full
767
+ // container height once visible. overflow:hidden keeps it invisible
768
+ // at width:0 — no painted pixels, no phantom height like min-height
769
+ // would cause. Animates width 0→3px on first appearance.
770
+ this._indicator.style.cssText =
771
+ 'flex-shrink:0;width:0;align-self:stretch;' +
772
+ 'background:#6366f1;border-radius:2px;pointer-events:none;margin:0;' +
773
+ 'transition:width 140ms ease,margin 140ms ease;overflow:hidden;';
774
+ }
775
+ else {
776
+ this._indicator.style.cssText =
777
+ 'height:0;width:100%;background:#6366f1;border-radius:2px;' +
778
+ 'pointer-events:none;margin:0;display:block;' +
779
+ 'transition:height 140ms ease,margin 140ms ease;overflow:hidden;';
780
+ }
781
+ }
782
+ this._indicatorContainerId = containerId;
783
+ const skipClasses = [
784
+ 'section-placeholder', 'container-placeholder', 'repeater-placeholder',
785
+ 'repeater-badge', 'repeater-preview-row', 'iox-insert-indicator',
786
+ 'iox-drag-active-source',
787
+ ];
788
+ const children = Array.from(container.children).filter(el => !skipClasses.some(cls => el.classList.contains(cls)));
789
+ // ── FLIP: snapshot sibling rects BEFORE DOM change (reposition case only) ──
790
+ let beforeRects = null;
791
+ if (!needsCreate) {
792
+ beforeRects = new Map();
793
+ Array.from(container.children).forEach(child => {
794
+ if (child !== this._indicator) {
795
+ beforeRects.set(child, child.getBoundingClientRect());
796
+ }
797
+ });
798
+ }
799
+ // Reposition indicator in DOM.
800
+ if (!this._indicator)
801
+ return;
802
+ if (index >= children.length) {
803
+ container.appendChild(this._indicator);
804
+ }
805
+ else {
806
+ container.insertBefore(this._indicator, children[index]);
807
+ }
808
+ // ── FLIP: animate siblings from their old positions to their new positions ──
809
+ if (beforeRects) {
810
+ Array.from(container.children).forEach(child => {
811
+ const before = beforeRects.get(child);
812
+ if (!before)
813
+ return;
814
+ const after = child.getBoundingClientRect();
815
+ const dx = before.left - after.left;
816
+ const dy = before.top - after.top;
817
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5)
818
+ return;
819
+ const el = child;
820
+ this._flipEls.add(el);
821
+ // Invert: snap element back to where it was, then animate to new pos.
822
+ el.style.transition = 'none';
823
+ el.style.transform = `translate(${dx}px,${dy}px)`;
824
+ requestAnimationFrame(() => {
825
+ el.style.transition = 'transform 140ms ease';
826
+ el.style.transform = '';
827
+ });
828
+ });
829
+ }
830
+ // Animate indicator open (first appearance in this container only).
831
+ if (needsCreate) {
832
+ cancelAnimationFrame(this._indicatorRaf);
833
+ this._indicatorRaf = requestAnimationFrame(() => {
834
+ if (!this._indicator)
835
+ return;
836
+ if (isRow) {
837
+ this._indicator.style.width = '3px';
838
+ this._indicator.style.margin = '0 3px';
839
+ }
840
+ else {
841
+ this._indicator.style.height = '4px';
842
+ this._indicator.style.margin = '3px 0';
843
+ }
844
+ });
845
+ }
846
+ }
847
+ _removeIndicator() {
848
+ cancelAnimationFrame(this._indicatorRaf);
849
+ this._indicator?.remove();
850
+ this._indicator = null;
851
+ this._indicatorContainerId = null;
852
+ this._indicatorIndex = -1;
853
+ }
854
+ // ─────────────────────────────────────────────────────────
855
+ // calcInsertIndex
856
+ // ─────────────────────────────────────────────────────────
857
+ calcInsertIndex(containerId) {
858
+ const container = this._getEl(containerId);
859
+ if (!container)
860
+ return 0;
861
+ const skipClasses = [
862
+ 'section-placeholder', 'container-placeholder', 'repeater-placeholder',
863
+ 'repeater-badge', 'repeater-preview-row', 'iox-insert-indicator',
864
+ 'iox-drag-active-source',
865
+ ];
866
+ const children = Array.from(container.children).filter(el => !skipClasses.some(cls => el.classList.contains(cls)));
867
+ if (!children.length)
868
+ return 0;
869
+ const cs = window.getComputedStyle(container);
870
+ const isHorizontal = (cs.display === 'flex' || cs.display === 'inline-flex') &&
871
+ (!cs.flexDirection || cs.flexDirection === 'row' || cs.flexDirection === 'row-reverse');
872
+ for (let i = 0; i < children.length; i++) {
873
+ const rect = children[i].getBoundingClientRect();
874
+ if (isHorizontal) {
875
+ if (this._lastPointerX < rect.left + rect.width / 2)
876
+ return i;
877
+ }
878
+ else {
879
+ if (this._lastPointerY < rect.top + rect.height / 2)
880
+ return i;
881
+ }
882
+ }
883
+ return children.length;
884
+ }
885
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DragEngineService, deps: [{ token: ComponentRegistryService }], target: i0.ɵɵFactoryTarget.Injectable }); }
886
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DragEngineService }); }
887
+ }
888
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DragEngineService, decorators: [{
889
+ type: Injectable
890
+ }], ctorParameters: () => [{ type: ComponentRegistryService }] });
891
+
892
+ /**
893
+ * Page-scoped registry that holds the current page's data sources.
894
+ * Scoped to PageUiComponent.providers[] so every builder service
895
+ * (BindingsComponent, BuilderRepeaterComponent) shares the same instance.
896
+ *
897
+ * Seeded by PageUiComponent.loadPage() from saved page data, then kept
898
+ * up to date by BindingsComponent whenever the user adds/removes a source.
899
+ */
900
+ class DataSourceRegistryService {
901
+ constructor() {
902
+ this._ds$ = new BehaviorSubject([]);
903
+ this.dataSources$ = this._ds$.asObservable();
904
+ }
905
+ setDataSources(ds) {
906
+ this._ds$.next(ds);
907
+ }
908
+ getDataSources() {
909
+ return this._ds$.value;
910
+ }
911
+ findByAlias(alias) {
912
+ return this._ds$.value.find(ds => ds.alias === alias);
913
+ }
914
+ getAliasOptions() {
915
+ return this._ds$.value.map(ds => ({ label: ds.alias, value: ds.alias }));
916
+ }
917
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DataSourceRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
918
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DataSourceRegistryService }); }
919
+ }
920
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DataSourceRegistryService, decorators: [{
921
+ type: Injectable
922
+ }] });
923
+
924
+ class ViewportService {
925
+ constructor() {
926
+ this.state = {
927
+ device: DeviceMode.Desktop,
928
+ width: DEVICE_OPTIONS[0].width,
929
+ scale: 0.75,
930
+ };
931
+ this.stateSubject = new BehaviorSubject(this.state);
932
+ this.state$ = this.stateSubject.asObservable();
933
+ }
934
+ getState() {
935
+ return this.state;
936
+ }
937
+ getScale() {
938
+ return this.state.scale;
939
+ }
940
+ setDevice(device) {
941
+ const option = DEVICE_OPTIONS.find(d => d.mode === device);
942
+ if (!option)
943
+ return;
944
+ this.state = { ...this.state, device, width: option.width };
945
+ this.stateSubject.next(this.state);
946
+ }
947
+ setScale(scale) {
948
+ this.state = { ...this.state, scale: Math.max(0.1, Math.min(2, scale)) };
949
+ this.stateSubject.next(this.state);
950
+ }
951
+ /**
952
+ * Calculate the scale that makes the canvas fit inside the available editor width.
953
+ * @param availableWidth The pixel width of the editor area that holds the canvas.
954
+ * @param padding Optional horizontal padding to subtract (default 20px each side).
955
+ */
956
+ fitToWidth(availableWidth, padding = 40) {
957
+ const usable = availableWidth - padding;
958
+ if (usable <= 0)
959
+ return;
960
+ const scale = Math.min(1, usable / this.state.width);
961
+ this.setScale(scale);
962
+ }
963
+ /**
964
+ * Convert a screen-space coordinate to canvas-space (unscaled).
965
+ * Call this when translating pointer events into canvas-relative positions.
966
+ */
967
+ screenToCanvas(screenX, screenY, canvasRect) {
968
+ return {
969
+ x: (screenX - canvasRect.left) / this.state.scale,
970
+ y: (screenY - canvasRect.top) / this.state.scale,
971
+ };
972
+ }
973
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ViewportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
974
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ViewportService }); }
975
+ }
976
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ViewportService, decorators: [{
977
+ type: Injectable
978
+ }] });
979
+
980
+ /**
981
+ * Wires ComponentNode interactions to live DOM elements via the
982
+ * OverlayService node-element registry.
983
+ *
984
+ * Call `attach(node)` after the node is rendered and registered.
985
+ * Call `detach(node)` on destroy to tear down listeners/observers.
986
+ */
987
+ class InteractionEngineService {
988
+ constructor(overlayService) {
989
+ this.overlayService = overlayService;
990
+ this.attached = new Map();
991
+ }
992
+ /** Wire all interactions for a node to its rendered DOM element. */
993
+ attach(node) {
994
+ if (!node.interactions?.length)
995
+ return;
996
+ const ref = this.overlayService.getNodeRef(node);
997
+ if (!ref)
998
+ return;
999
+ const cleanups = [];
1000
+ for (const ix of node.interactions) {
1001
+ const cleanup = this.attachInteraction(node, ix, ref.element);
1002
+ cleanups.push({ node, interaction: ix, cleanup });
1003
+ }
1004
+ this.attached.set(node, cleanups);
1005
+ }
1006
+ /** Tear down all interaction listeners for a node and reset its animated state. */
1007
+ detach(node) {
1008
+ const entries = this.attached.get(node);
1009
+ if (!entries)
1010
+ return;
1011
+ for (const entry of entries) {
1012
+ entry.cleanup();
1013
+ }
1014
+ this.attached.delete(node);
1015
+ // Cancelling Web Animations removes fill:'forwards' committed styles,
1016
+ // returning the element to its natural CSS state.
1017
+ const ref = this.overlayService.getNodeRef(node);
1018
+ if (ref) {
1019
+ ref.element.getAnimations().forEach(a => a.cancel());
1020
+ }
1021
+ }
1022
+ /** Re-attach interactions after they have been edited in the panel. */
1023
+ refresh(node) {
1024
+ this.detach(node);
1025
+ this.attach(node);
1026
+ }
1027
+ /**
1028
+ * Replay all interactions on a node immediately, regardless of their trigger.
1029
+ * Used by the builder overlay play button for in-editor animation preview.
1030
+ * Uses fill:'none' so the element returns to its natural state after the preview.
1031
+ */
1032
+ replay(node) {
1033
+ if (!node.interactions?.length)
1034
+ return;
1035
+ // Cancel any in-progress animations first to reset the element cleanly.
1036
+ const ref = this.overlayService.getNodeRef(node);
1037
+ if (ref) {
1038
+ ref.element.getAnimations().forEach(a => a.cancel());
1039
+ }
1040
+ for (const ix of node.interactions) {
1041
+ this.runActions(node, ix.actions);
1042
+ }
1043
+ }
1044
+ // ── Private ──────────────────────────────────────────────
1045
+ attachInteraction(node, ix, element) {
1046
+ switch (ix.trigger) {
1047
+ case 'pageLoad':
1048
+ return this.attachPageLoad(node, ix, element);
1049
+ case 'viewportEnter':
1050
+ return this.attachViewportEnter(node, ix, element);
1051
+ case 'click':
1052
+ return this.attachClick(node, ix, element);
1053
+ case 'hover':
1054
+ return this.attachHover(node, ix, element);
1055
+ case 'scrollProgress':
1056
+ // Scroll-progress is complex — deferred.
1057
+ return () => { };
1058
+ default:
1059
+ return () => { };
1060
+ }
1061
+ }
1062
+ // ── Triggers ─────────────────────────────────────────────
1063
+ attachPageLoad(node, ix, element) {
1064
+ // Execute immediately (next microtask to let rendering settle)
1065
+ const timer = setTimeout(() => {
1066
+ this.runActions(node, ix.actions);
1067
+ }, 50);
1068
+ return () => clearTimeout(timer);
1069
+ }
1070
+ attachViewportEnter(node, ix, element) {
1071
+ const firedSet = new Set();
1072
+ const observer = new IntersectionObserver((entries) => {
1073
+ for (const entry of entries) {
1074
+ if (entry.isIntersecting) {
1075
+ const key = ix.id;
1076
+ if (ix.actions.some(a => a.once) && firedSet.has(key))
1077
+ continue;
1078
+ firedSet.add(key);
1079
+ this.runActions(node, ix.actions);
1080
+ }
1081
+ }
1082
+ }, { threshold: 0.1 });
1083
+ observer.observe(element);
1084
+ return () => observer.disconnect();
1085
+ }
1086
+ attachClick(node, ix, element) {
1087
+ const handler = (e) => {
1088
+ // Don't trigger during builder editing clicks — only in preview mode.
1089
+ // For now, we always run it. In the future, check BuilderMode.
1090
+ this.runActions(node, ix.actions);
1091
+ };
1092
+ element.addEventListener('click', handler);
1093
+ return () => element.removeEventListener('click', handler);
1094
+ }
1095
+ attachHover(node, ix, element) {
1096
+ const enterHandler = () => this.runActions(node, ix.actions);
1097
+ const leaveHandler = () => {
1098
+ // Reverse animations on leave for hover triggers
1099
+ for (const action of ix.actions) {
1100
+ const target = this.resolveTarget(node, action);
1101
+ if (!target)
1102
+ continue;
1103
+ this.reverseAction(target, action);
1104
+ }
1105
+ };
1106
+ element.addEventListener('mouseenter', enterHandler);
1107
+ element.addEventListener('mouseleave', leaveHandler);
1108
+ return () => {
1109
+ element.removeEventListener('mouseenter', enterHandler);
1110
+ element.removeEventListener('mouseleave', leaveHandler);
1111
+ };
1112
+ }
1113
+ // ── Actions ──────────────────────────────────────────────
1114
+ runActions(node, actions) {
1115
+ for (const action of actions) {
1116
+ const target = this.resolveTarget(node, action);
1117
+ if (!target)
1118
+ continue;
1119
+ this.executeAction(target, action);
1120
+ }
1121
+ }
1122
+ resolveTarget(ownerNode, action) {
1123
+ if (action.target === 'self') {
1124
+ const ref = this.overlayService.getNodeRef(ownerNode);
1125
+ return ref?.element ?? null;
1126
+ }
1127
+ // Find by node ID
1128
+ for (const [n, ref] of this.overlayService.getAllNodeEntries()) {
1129
+ if (n.id === action.target)
1130
+ return ref.element;
1131
+ }
1132
+ return null;
1133
+ }
1134
+ executeAction(element, action) {
1135
+ const { keyframes } = this.buildAnimation(action);
1136
+ if (!keyframes.length)
1137
+ return;
1138
+ element.animate(keyframes, {
1139
+ duration: action.duration,
1140
+ delay: action.delay,
1141
+ easing: action.easing,
1142
+ fill: 'forwards',
1143
+ });
1144
+ }
1145
+ reverseAction(element, action) {
1146
+ const { keyframes } = this.buildAnimation(action);
1147
+ if (!keyframes.length)
1148
+ return;
1149
+ // Reverse: play the keyframes backwards
1150
+ element.animate([...keyframes].reverse(), {
1151
+ duration: action.duration,
1152
+ easing: action.easing,
1153
+ fill: 'forwards',
1154
+ });
1155
+ }
1156
+ buildAnimation(action) {
1157
+ const dist = action.params?.['distance'] ?? 20;
1158
+ let keyframes = [];
1159
+ switch (action.type) {
1160
+ case 'fadeIn':
1161
+ keyframes = [{ opacity: 0 }, { opacity: 1 }];
1162
+ break;
1163
+ case 'fadeOut':
1164
+ keyframes = [{ opacity: 1 }, { opacity: 0 }];
1165
+ break;
1166
+ case 'moveUp':
1167
+ keyframes = [{ transform: `translateY(${dist}px)`, opacity: 0 }, { transform: 'translateY(0)', opacity: 1 }];
1168
+ break;
1169
+ case 'moveDown':
1170
+ keyframes = [{ transform: `translateY(-${dist}px)`, opacity: 0 }, { transform: 'translateY(0)', opacity: 1 }];
1171
+ break;
1172
+ case 'moveLeft':
1173
+ keyframes = [{ transform: `translateX(${dist}px)`, opacity: 0 }, { transform: 'translateX(0)', opacity: 1 }];
1174
+ break;
1175
+ case 'moveRight':
1176
+ keyframes = [{ transform: `translateX(-${dist}px)`, opacity: 0 }, { transform: 'translateX(0)', opacity: 1 }];
1177
+ break;
1178
+ case 'scaleIn':
1179
+ keyframes = [{ transform: 'scale(0.8)', opacity: 0 }, { transform: 'scale(1)', opacity: 1 }];
1180
+ break;
1181
+ case 'scaleOut':
1182
+ keyframes = [{ transform: 'scale(1)', opacity: 1 }, { transform: 'scale(0.8)', opacity: 0 }];
1183
+ break;
1184
+ case 'rotate':
1185
+ const deg = action.params?.['degrees'] ?? 360;
1186
+ keyframes = [{ transform: `rotate(0deg)` }, { transform: `rotate(${deg}deg)` }];
1187
+ break;
1188
+ case 'show':
1189
+ keyframes = [{ opacity: 0, visibility: 'hidden' }, { opacity: 1, visibility: 'visible' }];
1190
+ break;
1191
+ case 'hide':
1192
+ keyframes = [{ opacity: 1, visibility: 'visible' }, { opacity: 0, visibility: 'hidden' }];
1193
+ break;
1194
+ case 'toggleVisibility':
1195
+ // Toggle is stateful — handled separately via element style
1196
+ keyframes = [{ opacity: 0 }, { opacity: 1 }];
1197
+ break;
1198
+ }
1199
+ return {
1200
+ keyframes,
1201
+ options: {
1202
+ duration: action.duration,
1203
+ delay: action.delay,
1204
+ easing: action.easing,
1205
+ fill: 'forwards',
1206
+ },
1207
+ };
1208
+ }
1209
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InteractionEngineService, deps: [{ token: OverlayService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1210
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InteractionEngineService }); }
1211
+ }
1212
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InteractionEngineService, decorators: [{
1213
+ type: Injectable
1214
+ }], ctorParameters: () => [{ type: OverlayService }] });
1215
+
1216
+ /**
1217
+ * StyleRegistryService — manages a single <style> tag for the builder canvas.
1218
+ *
1219
+ * Each builder component gets a unique CSS class `.iox-node-{id}` applied to its
1220
+ * first inner element. onUpdate() in the style panel calls upsert() to compile
1221
+ * the flat style-trait map into a CSS rule and flush it to the stylesheet.
1222
+ *
1223
+ * This replaces the old [ngStyle] binding, enabling future support for
1224
+ * hover states, breakpoints, and pseudo-selectors without DOM modifications.
1225
+ *
1226
+ * Scoped to PageUiComponent.providers[] — one stylesheet per builder instance.
1227
+ */
1228
+ class StyleRegistryService {
1229
+ constructor() {
1230
+ this.rules = new Map();
1231
+ this.styleEl = null;
1232
+ }
1233
+ init() {
1234
+ this.styleEl = document.createElement('style');
1235
+ this.styleEl.id = 'iox-runtime-styles';
1236
+ document.head.appendChild(this.styleEl);
1237
+ }
1238
+ upsert(nodeId, styles) {
1239
+ if (!nodeId)
1240
+ return;
1241
+ const css = this.compile(nodeId, styles);
1242
+ if (css) {
1243
+ this.rules.set(nodeId, css);
1244
+ }
1245
+ else {
1246
+ this.rules.delete(nodeId);
1247
+ }
1248
+ this.flush();
1249
+ }
1250
+ remove(nodeId) {
1251
+ if (!nodeId)
1252
+ return;
1253
+ this.rules.delete(nodeId);
1254
+ this.flush();
1255
+ }
1256
+ destroy() {
1257
+ this.rules.clear();
1258
+ if (this.styleEl) {
1259
+ this.styleEl.remove();
1260
+ this.styleEl = null;
1261
+ }
1262
+ }
1263
+ compile(nodeId, styles) {
1264
+ const entries = Object.entries(styles)
1265
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
1266
+ .map(([k, v]) => ` ${this.toKebabCase(k)}: ${v};`);
1267
+ if (!entries.length)
1268
+ return '';
1269
+ return `.iox-node-${nodeId} {\n${entries.join('\n')}\n}`;
1270
+ }
1271
+ toKebabCase(str) {
1272
+ return str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
1273
+ }
1274
+ flush() {
1275
+ if (!this.styleEl)
1276
+ return;
1277
+ this.styleEl.textContent = Array.from(this.rules.values()).join('\n');
1278
+ }
1279
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1280
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService }); }
1281
+ }
1282
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, decorators: [{
1283
+ type: Injectable
1284
+ }] });
1285
+
1286
+ // Module-level counter — unique drop-list ID per Section instance across the whole app session.
1287
+ let dropListCounter = 0;
1288
+ /**
1289
+ * RenderDirective — applied to <ng-container> to dynamically instantiate a
1290
+ * builder component with zero wrapper elements in the DOM.
1291
+ *
1292
+ * A directive's ViewContainerRef inserts the created component's host element as
1293
+ * a sibling of the ng-container comment node — directly inside the parent element
1294
+ * — so the resulting DOM has no extra <app-renderer> wrapper.
1295
+ *
1296
+ * Usage:
1297
+ * <ng-container [ioxRender]="node"
1298
+ * (onClick)="handleClick($event)"
1299
+ * (onMouseEnter)="handleMouseEnter($event)"
1300
+ * (onMouseLeave)="handleMouseLeave()">
1301
+ * </ng-container>
1302
+ */
1303
+ class RenderDirective {
1304
+ constructor(vcr, injector, registryService, overlayService, styleRegistry, interactionEngine, cdr, panelEventService) {
1305
+ this.vcr = vcr;
1306
+ this.injector = injector;
1307
+ this.registryService = registryService;
1308
+ this.overlayService = overlayService;
1309
+ this.styleRegistry = styleRegistry;
1310
+ this.interactionEngine = interactionEngine;
1311
+ this.cdr = cdr;
1312
+ this.panelEventService = panelEventService;
1313
+ this.ioxRender = null;
1314
+ this.onClick = new EventEmitter();
1315
+ this.onMouseEnter = new EventEmitter();
1316
+ this.onMouseLeave = new EventEmitter();
1317
+ this.childSubs = [];
1318
+ }
1319
+ ngOnInit() {
1320
+ this.render(this.ioxRender);
1321
+ }
1322
+ ngOnDestroy() {
1323
+ this.childSubs.forEach(s => s.unsubscribe());
1324
+ if (this.ioxRender) {
1325
+ this.interactionEngine.detach(this.ioxRender);
1326
+ this.overlayService.unregisterNode(this.ioxRender);
1327
+ // Only remove the CSS rule if this node OWNS it (i.e. it is not a clone).
1328
+ // Clones set styleId to point at the template node's id — removing would
1329
+ // destroy the shared rule and break all other rows.
1330
+ const isClone = !!this.ioxRender.styleId;
1331
+ if (!isClone && this.ioxRender.id) {
1332
+ this.styleRegistry.remove(this.ioxRender.id);
1333
+ }
1334
+ }
1335
+ }
1336
+ /**
1337
+ * Walk up from `target` to find a registered parent component element.
1338
+ * Returns { element, node } if found, or null if the target is outside
1339
+ * all known builder components (e.g. the canvas background).
1340
+ */
1341
+ findParentNodeElement(target) {
1342
+ let el = target;
1343
+ while (el) {
1344
+ const match = this.overlayService.findNodeByElement(el);
1345
+ if (match) {
1346
+ return { element: el, node: match.node };
1347
+ }
1348
+ el = el.parentElement;
1349
+ }
1350
+ return null;
1351
+ }
1352
+ render(node) {
1353
+ if (!node)
1354
+ return;
1355
+ const componentClass = this.registryService.getComponent(node.type);
1356
+ if (!componentClass)
1357
+ return;
1358
+ // vcr.createComponent inserts the new component's host element after the ng-container
1359
+ // comment anchor — directly inside the parent element. No wrapper div is created.
1360
+ const componentRef = this.vcr.createComponent(componentClass, { injector: this.injector });
1361
+ // Apply regular trait defaults (title, image, description, etc.)
1362
+ // Use setInput() (Angular 14+) — it marks the view dirty and
1363
+ // integrates with Angular's CD tracking, unlike direct assignment.
1364
+ Object.values(node.traits || {}).forEach((trait) => {
1365
+ componentRef.setInput(trait.name, trait.default);
1366
+ });
1367
+ // Build initial style map from styleTraits defaults.
1368
+ const initialStyle = {};
1369
+ if (node.styleTraits?.length) {
1370
+ for (const group of node.styleTraits) {
1371
+ for (const trait of group.traits) {
1372
+ if (trait.default !== undefined) {
1373
+ initialStyle[trait.name] = trait.default;
1374
+ }
1375
+ }
1376
+ }
1377
+ }
1378
+ // Use styleId when present (clone nodes share the template node's CSS rule).
1379
+ const cssId = node.styleId ?? node.id;
1380
+ // Register CSS rule in the central stylesheet.
1381
+ if (cssId) {
1382
+ this.styleRegistry.upsert(cssId, initialStyle);
1383
+ }
1384
+ // Set nodeId — declared as @Input() on every builder component.
1385
+ if (cssId) {
1386
+ componentRef.setInput('nodeId', cssId);
1387
+ }
1388
+ // Pass the node reference. 'node' is NOT declared as @Input on leaf
1389
+ // components (Card, Text, etc.) — always use direct assignment to avoid
1390
+ // Angular NG0303 warnings that setInput() would trigger.
1391
+ componentRef.instance['node'] = node;
1392
+ // Layout components (Section/Container) read this.style internally for
1393
+ // CDK drop list orientation. 'style' is not declared as @Input so use
1394
+ // direct assignment; children and dropListId ARE @Input on all layouts.
1395
+ // Guard with Array.isArray so leaf nodes whose serialized data happens
1396
+ // to have a stale/empty children key don't trigger NG0303.
1397
+ const instance = componentRef.instance;
1398
+ if (Array.isArray(node.children) && 'dropListId' in instance) {
1399
+ instance['style'] = { ...initialStyle };
1400
+ componentRef.setInput('children', node.children);
1401
+ componentRef.setInput('dropListId', `section-${++dropListCounter}`);
1402
+ }
1403
+ // Forward child events from layout components (Section → builder)
1404
+ const childClick = instance['childClick'];
1405
+ const childMouseEnter = instance['childMouseEnter'];
1406
+ const childMouseLeave = instance['childMouseLeave'];
1407
+ if (childClick)
1408
+ this.childSubs.push(childClick.subscribe((e) => this.onClick.emit(e)));
1409
+ if (childMouseEnter)
1410
+ this.childSubs.push(childMouseEnter.subscribe((e) => this.onMouseEnter.emit(e)));
1411
+ if (childMouseLeave)
1412
+ this.childSubs.push(childMouseLeave.subscribe(() => this.onMouseLeave.emit()));
1413
+ componentRef.changeDetectorRef.detectChanges();
1414
+ // Attach overlay-driven DOM event listeners directly on the component host element.
1415
+ // For layout components the host element is <app-section>; for leaf components
1416
+ // it is e.g. <app-card>. Either way this is the correct inspectable element.
1417
+ const nativeEl = componentRef.location?.nativeElement ?? null;
1418
+ // Custom element host defaults to display:inline. Set it to block so it
1419
+ // participates properly in layout and CDK can capture its bounding rect.
1420
+ if (nativeEl) {
1421
+ nativeEl.style.display = 'block';
1422
+ }
1423
+ if (nativeEl) {
1424
+ // Register node → element mapping for parent lookups
1425
+ this.overlayService.registerNode(node, nativeEl, componentRef);
1426
+ // Notify BindingsComponent so it re-applies bindings on the new
1427
+ // ComponentRef — needed after cross-container drag where Angular
1428
+ // creates a fresh instance that hasn't had bindings pushed to it yet.
1429
+ this.panelEventService.emit(PanelEventTypes.NODE_RENDERED, { node });
1430
+ nativeEl.addEventListener('mouseenter', (event) => {
1431
+ if (event._fromClick)
1432
+ return;
1433
+ this.onMouseEnter.emit({ element: nativeEl, node });
1434
+ });
1435
+ nativeEl.addEventListener('mouseleave', (event) => {
1436
+ // When leaving a child component, check if the mouse moved to a
1437
+ // parent component (e.g. card → section). If so, emit mouseenter
1438
+ // for that parent instead of clearing hover entirely.
1439
+ const related = event.relatedTarget;
1440
+ if (related) {
1441
+ const parentEntry = this.findParentNodeElement(related);
1442
+ if (parentEntry) {
1443
+ this.onMouseEnter.emit(parentEntry);
1444
+ return;
1445
+ }
1446
+ }
1447
+ this.onMouseLeave.emit();
1448
+ });
1449
+ nativeEl.addEventListener('click', (event) => {
1450
+ event.stopPropagation();
1451
+ this.onClick.emit({ element: nativeEl, node, componentRef });
1452
+ });
1453
+ }
1454
+ // Wire up interactions (animations) after the element is in the DOM.
1455
+ this.interactionEngine.attach(node);
1456
+ }
1457
+ /** Try setInput(); fall back to direct assignment for non-@Input properties. */
1458
+ safeSetInput(ref, name, value) {
1459
+ try {
1460
+ ref.setInput(name, value);
1461
+ }
1462
+ catch {
1463
+ ref.instance[name] = value;
1464
+ }
1465
+ }
1466
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: RenderDirective, deps: [{ token: i0.ViewContainerRef }, { token: i0.Injector }, { token: ComponentRegistryService }, { token: OverlayService }, { token: StyleRegistryService }, { token: InteractionEngineService }, { token: i0.ChangeDetectorRef }, { token: PanelEventService }], target: i0.ɵɵFactoryTarget.Directive }); }
1467
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.11", type: RenderDirective, isStandalone: false, selector: "[ioxRender]", inputs: { ioxRender: "ioxRender" }, outputs: { onClick: "onClick", onMouseEnter: "onMouseEnter", onMouseLeave: "onMouseLeave" }, ngImport: i0 }); }
1468
+ }
1469
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: RenderDirective, decorators: [{
1470
+ type: Directive,
1471
+ args: [{
1472
+ selector: '[ioxRender]',
1473
+ standalone: false,
1474
+ }]
1475
+ }], ctorParameters: () => [{ type: i0.ViewContainerRef }, { type: i0.Injector }, { type: ComponentRegistryService }, { type: OverlayService }, { type: StyleRegistryService }, { type: InteractionEngineService }, { type: i0.ChangeDetectorRef }, { type: PanelEventService }], propDecorators: { ioxRender: [{
1476
+ type: Input
1477
+ }], onClick: [{
1478
+ type: Output
1479
+ }], onMouseEnter: [{
1480
+ type: Output
1481
+ }], onMouseLeave: [{
1482
+ type: Output
1483
+ }] } });
1484
+
1485
+ class IoxDraggableDirective {
1486
+ constructor(el, dragEngine) {
1487
+ this.el = el;
1488
+ this.dragEngine = dragEngine;
1489
+ /** The node (for canvas items) or component type string (for sidebar items). */
1490
+ this.ioxDragData = '';
1491
+ /** Drop-zone id the item lives in. Null for sidebar items. */
1492
+ this.ioxDragSourceId = null;
1493
+ }
1494
+ ngOnInit() {
1495
+ this.dragEngine.registerDraggable(this.el.nativeElement, () => {
1496
+ const data = this.ioxDragData;
1497
+ const type = typeof data === 'string' ? 'external' : 'internal';
1498
+ const sourceIndex = type === 'internal' && this.ioxDragSourceId
1499
+ ? (this.dragEngine.getData(this.ioxDragSourceId)?.indexOf(data) ?? -1)
1500
+ : -1;
1501
+ return { type, data, sourceId: this.ioxDragSourceId, sourceIndex };
1502
+ });
1503
+ }
1504
+ ngOnDestroy() {
1505
+ this.dragEngine.unregisterDraggable(this.el.nativeElement);
1506
+ }
1507
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxDraggableDirective, deps: [{ token: i0.ElementRef }, { token: DragEngineService }], target: i0.ɵɵFactoryTarget.Directive }); }
1508
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.11", type: IoxDraggableDirective, isStandalone: false, selector: "[ioxDraggable]", inputs: { ioxDragData: "ioxDragData", ioxDragSourceId: "ioxDragSourceId" }, ngImport: i0 }); }
1509
+ }
1510
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxDraggableDirective, decorators: [{
1511
+ type: Directive,
1512
+ args: [{
1513
+ selector: '[ioxDraggable]',
1514
+ standalone: false,
1515
+ }]
1516
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: DragEngineService }], propDecorators: { ioxDragData: [{
1517
+ type: Input
1518
+ }], ioxDragSourceId: [{
1519
+ type: Input
1520
+ }] } });
1521
+
1522
+ class OverlayComponent {
1523
+ constructor(overlayService, viewportService, cdr) {
1524
+ this.overlayService = overlayService;
1525
+ this.viewportService = viewportService;
1526
+ this.cdr = cdr;
1527
+ this.toolbarAction = new EventEmitter();
1528
+ this.hoverEntry = null;
1529
+ this.selectEntry = null;
1530
+ // Hover: Chrome DevTools-style ring layers (clip-path punches out content area)
1531
+ this.hiOutlineStyle = {};
1532
+ this.hiMarginStyle = {};
1533
+ this.hiPaddingStyle = {};
1534
+ // Select: simple IOX pink outline
1535
+ this.selectOutlineStyle = {};
1536
+ // More dropdown (toolbar ⋯ button)
1537
+ this.moreMenuVisible = false;
1538
+ // Right-click context menu
1539
+ this.contextMenuVisible = false;
1540
+ this.contextMenuStyle = {};
1541
+ this.subs = [];
1542
+ this.boundUpdate = () => this.updateAll();
1543
+ this.activeScrollContainer = null;
1544
+ }
1545
+ ngOnInit() {
1546
+ this.subs.push(this.overlayService.hover$.subscribe(entry => {
1547
+ this.hoverEntry = entry;
1548
+ this.updateHover();
1549
+ this.cdr.markForCheck();
1550
+ }), this.overlayService.select$.subscribe(entry => {
1551
+ this.selectEntry = entry;
1552
+ this.observeSelectElement();
1553
+ this.updateSelect();
1554
+ this.cdr.markForCheck();
1555
+ }));
1556
+ window.addEventListener('resize', this.boundUpdate, { passive: true });
1557
+ // Re-calculate overlay positions whenever zoom / device changes.
1558
+ this.subs.push(this.viewportService.state$.subscribe(() => {
1559
+ // Delay one frame so the DOM has applied the new transform/width.
1560
+ requestAnimationFrame(() => this.updateAll());
1561
+ }));
1562
+ // Watch the canvas container for size changes (e.g. right panel open/close
1563
+ // transition changes canvas width, shifting element positions).
1564
+ const container = this.overlayService.getContainer();
1565
+ if (container) {
1566
+ if (typeof ResizeObserver !== 'undefined') {
1567
+ this.containerResizeObserver = new ResizeObserver(() => this.updateAll());
1568
+ this.containerResizeObserver.observe(container);
1569
+ }
1570
+ }
1571
+ // iox-page-component's Lenis dispatches a synthetic 'scroll' event on
1572
+ // its wrapper element after each raf tick — listen to that to reposition
1573
+ // overlay boxes as the canvas scrolls.
1574
+ this.subs.push(this.overlayService.scrollContainer$.subscribe(el => {
1575
+ this.activeScrollContainer?.removeEventListener('scroll', this.boundUpdate);
1576
+ this.activeScrollContainer = el;
1577
+ el.addEventListener('scroll', this.boundUpdate, { passive: true });
1578
+ }));
1579
+ }
1580
+ ngOnDestroy() {
1581
+ this.subs.forEach(s => s.unsubscribe());
1582
+ this.resizeObserver?.disconnect();
1583
+ this.containerResizeObserver?.disconnect();
1584
+ this.mutationObserver?.disconnect();
1585
+ window.removeEventListener('resize', this.boundUpdate);
1586
+ this.activeScrollContainer?.removeEventListener('scroll', this.boundUpdate);
1587
+ this.activeScrollContainer = null;
1588
+ }
1589
+ // ── Toolbar actions ──────────────────────────────────────
1590
+ onSelectParent(event) {
1591
+ event.stopPropagation();
1592
+ if (this.selectEntry)
1593
+ this.toolbarAction.emit({ action: ToolbarAction.SelectParent, entry: this.selectEntry });
1594
+ }
1595
+ onDuplicate(event) {
1596
+ event.stopPropagation();
1597
+ if (this.selectEntry)
1598
+ this.toolbarAction.emit({ action: ToolbarAction.Duplicate, entry: this.selectEntry });
1599
+ }
1600
+ onDelete(event) {
1601
+ event.stopPropagation();
1602
+ if (this.selectEntry)
1603
+ this.toolbarAction.emit({ action: ToolbarAction.Delete, entry: this.selectEntry });
1604
+ }
1605
+ onPlay(event) {
1606
+ event.stopPropagation();
1607
+ if (this.selectEntry)
1608
+ this.toolbarAction.emit({ action: ToolbarAction.Play, entry: this.selectEntry });
1609
+ }
1610
+ onSaveAsBlock(event) {
1611
+ event.stopPropagation();
1612
+ if (this.selectEntry)
1613
+ this.toolbarAction.emit({ action: ToolbarAction.SaveAsBlock, entry: this.selectEntry });
1614
+ }
1615
+ onToggleMore(event) {
1616
+ event.stopPropagation();
1617
+ this.moreMenuVisible = !this.moreMenuVisible;
1618
+ this.cdr.markForCheck();
1619
+ }
1620
+ closeMoreMenu() {
1621
+ this.moreMenuVisible = false;
1622
+ this.cdr.markForCheck();
1623
+ }
1624
+ onContextMenu(event) {
1625
+ event.preventDefault();
1626
+ event.stopPropagation();
1627
+ this.contextMenuStyle = {
1628
+ position: 'fixed',
1629
+ top: `${event.clientY}px`,
1630
+ left: `${event.clientX}px`,
1631
+ };
1632
+ this.contextMenuVisible = true;
1633
+ this.cdr.markForCheck();
1634
+ }
1635
+ onContextSaveAsBlock(event) {
1636
+ event.stopPropagation();
1637
+ this.closeContextMenu();
1638
+ if (this.selectEntry)
1639
+ this.toolbarAction.emit({ action: ToolbarAction.SaveAsBlock, entry: this.selectEntry });
1640
+ }
1641
+ closeContextMenu() {
1642
+ this.contextMenuVisible = false;
1643
+ this.cdr.markForCheck();
1644
+ }
1645
+ get hasInteractions() {
1646
+ return !!this.selectEntry?.node?.interactions?.length;
1647
+ }
1648
+ observeSelectElement() {
1649
+ this.resizeObserver?.disconnect();
1650
+ this.mutationObserver?.disconnect();
1651
+ // Scroll listener is on the container permanently (attached in ngOnInit).
1652
+ // No need to re-attach it here.
1653
+ if (!this.selectEntry?.element)
1654
+ return;
1655
+ // Observe the inner styled element — that's where [ngStyle] applies CSS.
1656
+ const styledEl = this.overlayService.getStyledElement(this.selectEntry.element);
1657
+ // ResizeObserver — catches size changes.
1658
+ // Skip the first callback: it fires synchronously on observe() in the same
1659
+ // microtask as the select$ subscription which already calls updateAll().
1660
+ // Painting twice in one frame causes a visible flicker.
1661
+ if (typeof ResizeObserver !== 'undefined') {
1662
+ let skipFirst = true;
1663
+ this.resizeObserver = new ResizeObserver(() => {
1664
+ if (skipFirst) {
1665
+ skipFirst = false;
1666
+ return;
1667
+ }
1668
+ this.updateAll();
1669
+ });
1670
+ this.resizeObserver.observe(styledEl);
1671
+ }
1672
+ // MutationObserver — catches style attribute changes (margin, padding etc.)
1673
+ this.mutationObserver = new MutationObserver(() => this.updateAll());
1674
+ this.mutationObserver.observe(styledEl, {
1675
+ attributes: true,
1676
+ attributeFilter: ['style', 'class'],
1677
+ });
1678
+ }
1679
+ updateAll() {
1680
+ this.updateHover();
1681
+ this.updateSelect();
1682
+ this.cdr.markForCheck();
1683
+ }
1684
+ updateHover() {
1685
+ if (!this.hoverEntry?.boxModel) {
1686
+ this.hiOutlineStyle = {};
1687
+ this.hiMarginStyle = {};
1688
+ this.hiPaddingStyle = {};
1689
+ return;
1690
+ }
1691
+ const styledEl = this.overlayService.getStyledElement(this.hoverEntry.element);
1692
+ const rect = styledEl.getBoundingClientRect();
1693
+ const { margin, border, padding } = this.hoverEntry.boxModel;
1694
+ // All overlay boxes use position:fixed with raw viewport coords from
1695
+ // getBoundingClientRect() — no container offset subtraction.
1696
+ // This matches the approach used by the drop-target overlay and the drag
1697
+ // clone, both of which position correctly at any zoom level.
1698
+ // Using cRect subtraction + position:absolute inside .preview (which has
1699
+ // contain:layout paint) caused sub-pixel accumulation errors at zoom != 100%.
1700
+ const scale = this.viewportService.getScale();
1701
+ // Scale the spacing values: getComputedStyle returns unscaled CSS px,
1702
+ // getBoundingClientRect returns scaled viewport px. Both must match.
1703
+ const m = {
1704
+ top: margin.top * scale,
1705
+ right: margin.right * scale,
1706
+ bottom: margin.bottom * scale,
1707
+ left: margin.left * scale,
1708
+ };
1709
+ const mW = rect.width + m.left + m.right;
1710
+ const mH = rect.height + m.top + m.bottom;
1711
+ this.hiOutlineStyle = this.createBoxStyle(rect.top, rect.left, rect.width, rect.height);
1712
+ // Orange ring: margin-box with border-box punched out
1713
+ this.hiMarginStyle = {
1714
+ ...this.createBoxStyle(rect.top - m.top, rect.left - m.left, mW, mH),
1715
+ 'clip-path': this.createRingClipPath(mW, mH, m.top, m.right, m.bottom, m.left),
1716
+ };
1717
+ // Green ring: border-box with content-box punched out
1718
+ const bpTop = (border.top + padding.top) * scale;
1719
+ const bpRight = (border.right + padding.right) * scale;
1720
+ const bpBottom = (border.bottom + padding.bottom) * scale;
1721
+ const bpLeft = (border.left + padding.left) * scale;
1722
+ this.hiPaddingStyle = {
1723
+ ...this.createBoxStyle(rect.top, rect.left, rect.width, rect.height),
1724
+ 'clip-path': this.createRingClipPath(rect.width, rect.height, bpTop, bpRight, bpBottom, bpLeft),
1725
+ };
1726
+ }
1727
+ updateSelect() {
1728
+ if (!this.selectEntry) {
1729
+ this.selectOutlineStyle = {};
1730
+ return;
1731
+ }
1732
+ const styledEl = this.overlayService.getStyledElement(this.selectEntry.element);
1733
+ const rect = styledEl.getBoundingClientRect();
1734
+ // position:fixed with raw viewport coords — same reasoning as updateHover.
1735
+ this.selectOutlineStyle = this.createBoxStyle(rect.top, rect.left, rect.width, rect.height);
1736
+ }
1737
+ /** Build position/size styles for a position:fixed overlay box. */
1738
+ createBoxStyle(top, left, width, height) {
1739
+ return {
1740
+ top: `${top}px`,
1741
+ left: `${left}px`,
1742
+ width: `${Math.max(width, 0)}px`,
1743
+ height: `${Math.max(height, 0)}px`,
1744
+ };
1745
+ }
1746
+ createRingClipPath(w, h, inTop, inRight, inBottom, inLeft) {
1747
+ const x1 = Math.max(inLeft, 0);
1748
+ const y1 = Math.max(inTop, 0);
1749
+ const x2 = Math.max(w - inRight, x1);
1750
+ const y2 = Math.max(h - inBottom, y1);
1751
+ // Each sub-rect must be explicitly closed so the bridge between them
1752
+ // is a zero-area line — prevents diagonal edge triangle artifacts.
1753
+ return `polygon(evenodd, 0px 0px, ${w}px 0px, ${w}px ${h}px, 0px ${h}px, 0px 0px, ${x1}px ${y1}px, ${x2}px ${y1}px, ${x2}px ${y2}px, ${x1}px ${y2}px, ${x1}px ${y1}px)`;
1754
+ }
1755
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: OverlayComponent, deps: [{ token: OverlayService }, { token: ViewportService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
1756
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: OverlayComponent, isStandalone: false, selector: "app-overlay", outputs: { toolbarAction: "toolbarAction" }, ngImport: i0, template: "<div class=\"overlay-root\">\n\n <!-- HOVER \u2014 Chrome DevTools-style rings (content area stays transparent) -->\n <ng-container *ngIf=\"hoverEntry\">\n <div class=\"hi-outline\" [ngStyle]=\"hiOutlineStyle\"></div>\n <div class=\"hi-margin\" [ngStyle]=\"hiMarginStyle\"></div>\n <div class=\"hi-padding\" [ngStyle]=\"hiPaddingStyle\"></div>\n </ng-container>\n\n <!-- SELECT \u2014 IOX pink outline + toolbar -->\n <div class=\"select-outline\" *ngIf=\"selectEntry\" [ngStyle]=\"selectOutlineStyle\"\n (contextmenu)=\"onContextMenu($event)\">\n\n <div class=\"select-toolbar\" (click)=\"$event.stopPropagation()\" (mousedown)=\"$event.stopPropagation()\">\n\n <!-- Component name -->\n <span class=\"toolbar-name\">{{ selectEntry?.componentName }}</span>\n <span class=\"toolbar-sep\"></span>\n\n <!-- Quick actions -->\n <button class=\"toolbar-btn\" title=\"Duplicate\" (mousedown)=\"onDuplicate($event)\">\n <i class=\"ph-thin ph-copy\"></i>\n </button>\n <button *ngIf=\"hasInteractions\" class=\"toolbar-btn toolbar-btn--play\" title=\"Play animations\" (mousedown)=\"onPlay($event)\">\n <i class=\"ph-thin ph-play\"></i>\n </button>\n <button class=\"toolbar-btn toolbar-btn--danger\" title=\"Delete\" (mousedown)=\"onDelete($event)\">\n <i class=\"ph-thin ph-trash\"></i>\n </button>\n <span class=\"toolbar-sep\"></span>\n <button class=\"toolbar-btn\" title=\"Select parent\" (mousedown)=\"onSelectParent($event)\">\n <i class=\"ph-thin ph-arrow-up\"></i>\n </button>\n\n <!-- More menu trigger \u2014 last -->\n <button class=\"toolbar-btn toolbar-btn--more\" title=\"More actions\" (mousedown)=\"onToggleMore($event)\">\n <i class=\"ph-thin ph-dots-three\"></i>\n </button>\n\n <!-- More dropdown (inline, positioned from toolbar) -->\n <div *ngIf=\"moreMenuVisible\" class=\"toolbar-more-dropdown\" (click)=\"$event.stopPropagation()\">\n <button class=\"more-item\" (mousedown)=\"onSelectParent($event); closeMoreMenu()\">\n <i class=\"ph-thin ph-arrow-up\"></i>\n Select parent\n </button>\n <button class=\"more-item\" (mousedown)=\"onDuplicate($event); closeMoreMenu()\">\n <i class=\"ph-thin ph-copy\"></i>\n Duplicate\n </button>\n <button class=\"more-item more-item--danger\" (mousedown)=\"onDelete($event); closeMoreMenu()\">\n <i class=\"ph-thin ph-trash\"></i>\n Delete\n </button>\n <div class=\"more-divider\"></div>\n <button class=\"more-item\" (mousedown)=\"onContextSaveAsBlock($event)\">\n <i class=\"ph-thin ph-bookmark\"></i>\n Save as reusable block\n </button>\n <button *ngIf=\"hasInteractions\" class=\"more-item more-item--play\" (mousedown)=\"onPlay($event); closeMoreMenu()\">\n <i class=\"ph-thin ph-play\"></i>\n Play animations\n </button>\n </div>\n </div>\n </div>\n\n <!-- Backdrop to close more menu or context menu -->\n <div *ngIf=\"moreMenuVisible || contextMenuVisible\" class=\"overlay-context-backdrop\"\n (mousedown)=\"closeMoreMenu(); closeContextMenu()\">\n </div>\n\n <!-- Right-click context menu -->\n <div *ngIf=\"contextMenuVisible\" class=\"overlay-context-menu\"\n [ngStyle]=\"contextMenuStyle\"\n (click)=\"$event.stopPropagation()\">\n <button class=\"context-menu-item\" (mousedown)=\"onContextSaveAsBlock($event)\">\n <i class=\"ph-thin ph-bookmark\"></i>\n Save as reusable block\n </button>\n </div>\n\n</div>\n", styles: [":host{display:block;position:absolute;inset:0;pointer-events:none;z-index:12;overflow:visible}.hi-outline,.hi-margin,.hi-padding{position:fixed;pointer-events:none;transition:none;box-sizing:border-box}.hi-outline{box-shadow:inset 0 0 0 1.5px #cb9090}.hi-margin{background:#f6b23361}.hi-padding{background:#60c38366}.select-outline{position:fixed;z-index:999;box-shadow:inset 0 0 0 1px #cb9090;pointer-events:none;transition:none;overflow:visible;will-change:transform;box-sizing:border-box}.select-toolbar{position:absolute;top:0;left:0;transform:translateY(-100%);display:flex;align-items:center;background:#cb9090;border-radius:3px 3px 0 0;pointer-events:all;white-space:nowrap;overflow:visible}.select-toolbar .toolbar-name{padding:0 8px;font-size:11px;font-weight:500;color:#fff;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:22px}.select-toolbar .toolbar-sep{width:1px;height:14px;background:#ffffff59;margin:0 2px;flex-shrink:0}.select-toolbar .toolbar-btn{display:flex;align-items:center;justify-content:center;width:24px;height:22px;padding:0;border:none;background:transparent;color:#fff;font-size:13px;cursor:pointer;transition:background .15s;flex-shrink:0}.select-toolbar .toolbar-btn:hover{background:#ffffff2e}.select-toolbar .toolbar-btn--danger:hover{background:#c8323259}.select-toolbar .toolbar-btn--more{border-radius:0 3px 0 0}.select-toolbar .toolbar-more-dropdown{position:absolute;top:100%;right:0;left:auto;z-index:1000;background:#fff;border:1px solid var(--p-surface-200, #e5e7eb);border-radius:6px;box-shadow:0 4px 16px #0000001f;padding:4px 0;min-width:200px;pointer-events:all;margin-top:2px}.select-toolbar .toolbar-more-dropdown .more-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;border:none;background:transparent;font-size:13px;color:var(--p-text-color, #333);cursor:pointer;text-align:left;white-space:nowrap}.select-toolbar .toolbar-more-dropdown .more-item i{font-size:14px}.select-toolbar .toolbar-more-dropdown .more-item:hover{background:var(--p-surface-100, #f5f5f5)}.select-toolbar .toolbar-more-dropdown .more-item--danger{color:#c84040}.select-toolbar .toolbar-more-dropdown .more-item--danger:hover{background:#c8404014}.select-toolbar .toolbar-more-dropdown .more-item--play{color:var(--p-text-color, #333)}.select-toolbar .toolbar-more-dropdown .more-divider{height:1px;background:var(--p-surface-200, #e5e7eb);margin:4px 0}.overlay-context-backdrop{position:fixed;inset:0;z-index:998;pointer-events:all}.overlay-context-menu{position:fixed;z-index:999;background:#fff;border:1px solid var(--p-surface-200);border-radius:6px;box-shadow:0 4px 16px #0000001f;padding:4px 0;min-width:200px;pointer-events:all}.overlay-context-menu .context-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;border:none;background:transparent;font-size:13px;color:var(--p-text-color, #333);cursor:pointer;text-align:left}.overlay-context-menu .context-menu-item i{font-size:14px}.overlay-context-menu .context-menu-item:hover{background:var(--p-surface-100, #f5f5f5)}\n"], dependencies: [{ kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1757
+ }
1758
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: OverlayComponent, decorators: [{
1759
+ type: Component,
1760
+ args: [{ selector: 'app-overlay', standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"overlay-root\">\n\n <!-- HOVER \u2014 Chrome DevTools-style rings (content area stays transparent) -->\n <ng-container *ngIf=\"hoverEntry\">\n <div class=\"hi-outline\" [ngStyle]=\"hiOutlineStyle\"></div>\n <div class=\"hi-margin\" [ngStyle]=\"hiMarginStyle\"></div>\n <div class=\"hi-padding\" [ngStyle]=\"hiPaddingStyle\"></div>\n </ng-container>\n\n <!-- SELECT \u2014 IOX pink outline + toolbar -->\n <div class=\"select-outline\" *ngIf=\"selectEntry\" [ngStyle]=\"selectOutlineStyle\"\n (contextmenu)=\"onContextMenu($event)\">\n\n <div class=\"select-toolbar\" (click)=\"$event.stopPropagation()\" (mousedown)=\"$event.stopPropagation()\">\n\n <!-- Component name -->\n <span class=\"toolbar-name\">{{ selectEntry?.componentName }}</span>\n <span class=\"toolbar-sep\"></span>\n\n <!-- Quick actions -->\n <button class=\"toolbar-btn\" title=\"Duplicate\" (mousedown)=\"onDuplicate($event)\">\n <i class=\"ph-thin ph-copy\"></i>\n </button>\n <button *ngIf=\"hasInteractions\" class=\"toolbar-btn toolbar-btn--play\" title=\"Play animations\" (mousedown)=\"onPlay($event)\">\n <i class=\"ph-thin ph-play\"></i>\n </button>\n <button class=\"toolbar-btn toolbar-btn--danger\" title=\"Delete\" (mousedown)=\"onDelete($event)\">\n <i class=\"ph-thin ph-trash\"></i>\n </button>\n <span class=\"toolbar-sep\"></span>\n <button class=\"toolbar-btn\" title=\"Select parent\" (mousedown)=\"onSelectParent($event)\">\n <i class=\"ph-thin ph-arrow-up\"></i>\n </button>\n\n <!-- More menu trigger \u2014 last -->\n <button class=\"toolbar-btn toolbar-btn--more\" title=\"More actions\" (mousedown)=\"onToggleMore($event)\">\n <i class=\"ph-thin ph-dots-three\"></i>\n </button>\n\n <!-- More dropdown (inline, positioned from toolbar) -->\n <div *ngIf=\"moreMenuVisible\" class=\"toolbar-more-dropdown\" (click)=\"$event.stopPropagation()\">\n <button class=\"more-item\" (mousedown)=\"onSelectParent($event); closeMoreMenu()\">\n <i class=\"ph-thin ph-arrow-up\"></i>\n Select parent\n </button>\n <button class=\"more-item\" (mousedown)=\"onDuplicate($event); closeMoreMenu()\">\n <i class=\"ph-thin ph-copy\"></i>\n Duplicate\n </button>\n <button class=\"more-item more-item--danger\" (mousedown)=\"onDelete($event); closeMoreMenu()\">\n <i class=\"ph-thin ph-trash\"></i>\n Delete\n </button>\n <div class=\"more-divider\"></div>\n <button class=\"more-item\" (mousedown)=\"onContextSaveAsBlock($event)\">\n <i class=\"ph-thin ph-bookmark\"></i>\n Save as reusable block\n </button>\n <button *ngIf=\"hasInteractions\" class=\"more-item more-item--play\" (mousedown)=\"onPlay($event); closeMoreMenu()\">\n <i class=\"ph-thin ph-play\"></i>\n Play animations\n </button>\n </div>\n </div>\n </div>\n\n <!-- Backdrop to close more menu or context menu -->\n <div *ngIf=\"moreMenuVisible || contextMenuVisible\" class=\"overlay-context-backdrop\"\n (mousedown)=\"closeMoreMenu(); closeContextMenu()\">\n </div>\n\n <!-- Right-click context menu -->\n <div *ngIf=\"contextMenuVisible\" class=\"overlay-context-menu\"\n [ngStyle]=\"contextMenuStyle\"\n (click)=\"$event.stopPropagation()\">\n <button class=\"context-menu-item\" (mousedown)=\"onContextSaveAsBlock($event)\">\n <i class=\"ph-thin ph-bookmark\"></i>\n Save as reusable block\n </button>\n </div>\n\n</div>\n", styles: [":host{display:block;position:absolute;inset:0;pointer-events:none;z-index:12;overflow:visible}.hi-outline,.hi-margin,.hi-padding{position:fixed;pointer-events:none;transition:none;box-sizing:border-box}.hi-outline{box-shadow:inset 0 0 0 1.5px #cb9090}.hi-margin{background:#f6b23361}.hi-padding{background:#60c38366}.select-outline{position:fixed;z-index:999;box-shadow:inset 0 0 0 1px #cb9090;pointer-events:none;transition:none;overflow:visible;will-change:transform;box-sizing:border-box}.select-toolbar{position:absolute;top:0;left:0;transform:translateY(-100%);display:flex;align-items:center;background:#cb9090;border-radius:3px 3px 0 0;pointer-events:all;white-space:nowrap;overflow:visible}.select-toolbar .toolbar-name{padding:0 8px;font-size:11px;font-weight:500;color:#fff;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:22px}.select-toolbar .toolbar-sep{width:1px;height:14px;background:#ffffff59;margin:0 2px;flex-shrink:0}.select-toolbar .toolbar-btn{display:flex;align-items:center;justify-content:center;width:24px;height:22px;padding:0;border:none;background:transparent;color:#fff;font-size:13px;cursor:pointer;transition:background .15s;flex-shrink:0}.select-toolbar .toolbar-btn:hover{background:#ffffff2e}.select-toolbar .toolbar-btn--danger:hover{background:#c8323259}.select-toolbar .toolbar-btn--more{border-radius:0 3px 0 0}.select-toolbar .toolbar-more-dropdown{position:absolute;top:100%;right:0;left:auto;z-index:1000;background:#fff;border:1px solid var(--p-surface-200, #e5e7eb);border-radius:6px;box-shadow:0 4px 16px #0000001f;padding:4px 0;min-width:200px;pointer-events:all;margin-top:2px}.select-toolbar .toolbar-more-dropdown .more-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;border:none;background:transparent;font-size:13px;color:var(--p-text-color, #333);cursor:pointer;text-align:left;white-space:nowrap}.select-toolbar .toolbar-more-dropdown .more-item i{font-size:14px}.select-toolbar .toolbar-more-dropdown .more-item:hover{background:var(--p-surface-100, #f5f5f5)}.select-toolbar .toolbar-more-dropdown .more-item--danger{color:#c84040}.select-toolbar .toolbar-more-dropdown .more-item--danger:hover{background:#c8404014}.select-toolbar .toolbar-more-dropdown .more-item--play{color:var(--p-text-color, #333)}.select-toolbar .toolbar-more-dropdown .more-divider{height:1px;background:var(--p-surface-200, #e5e7eb);margin:4px 0}.overlay-context-backdrop{position:fixed;inset:0;z-index:998;pointer-events:all}.overlay-context-menu{position:fixed;z-index:999;background:#fff;border:1px solid var(--p-surface-200);border-radius:6px;box-shadow:0 4px 16px #0000001f;padding:4px 0;min-width:200px;pointer-events:all}.overlay-context-menu .context-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;border:none;background:transparent;font-size:13px;color:var(--p-text-color, #333);cursor:pointer;text-align:left}.overlay-context-menu .context-menu-item i{font-size:14px}.overlay-context-menu .context-menu-item:hover{background:var(--p-surface-100, #f5f5f5)}\n"] }]
1761
+ }], ctorParameters: () => [{ type: OverlayService }, { type: ViewportService }, { type: i0.ChangeDetectorRef }], propDecorators: { toolbarAction: [{
1762
+ type: Output
1763
+ }] } });
1764
+
1765
+ class ToolbarComponent {
1766
+ constructor() {
1767
+ this.activeMode = BuilderMode.Select;
1768
+ this.activeDevice = DeviceMode.Desktop;
1769
+ this.activeZoom = 100;
1770
+ this.canUndo = false;
1771
+ this.canRedo = false;
1772
+ this.isSaving = false;
1773
+ this.pageStatus = 'draft';
1774
+ this.modeChange = new EventEmitter();
1775
+ this.deviceChange = new EventEmitter();
1776
+ this.zoomChange = new EventEmitter();
1777
+ this.undo = new EventEmitter();
1778
+ this.redo = new EventEmitter();
1779
+ this.save = new EventEmitter();
1780
+ this.preview = new EventEmitter();
1781
+ this.publishToggle = new EventEmitter();
1782
+ this.modes = BuilderMode;
1783
+ this.deviceOptions = DEVICE_OPTIONS;
1784
+ this.zoomOptions = ZOOM_OPTIONS;
1785
+ }
1786
+ get isPublished() {
1787
+ return this.pageStatus === 'published';
1788
+ }
1789
+ get activeModeIcon() {
1790
+ switch (this.activeMode) {
1791
+ case BuilderMode.Select: return 'ph-thin ph-cursor';
1792
+ case BuilderMode.Style: return 'ph-thin ph-paint-brush-broad';
1793
+ case BuilderMode.Pan: return 'ph-thin ph-hand';
1794
+ default: return 'ph-thin ph-cursor';
1795
+ }
1796
+ }
1797
+ get activeModeLabel() {
1798
+ switch (this.activeMode) {
1799
+ case BuilderMode.Select: return 'Select';
1800
+ case BuilderMode.Style: return 'Style';
1801
+ case BuilderMode.Pan: return 'Pan';
1802
+ default: return 'Select';
1803
+ }
1804
+ }
1805
+ get activeDeviceOption() {
1806
+ return this.deviceOptions.find(d => d.mode === this.activeDevice) || this.deviceOptions[0];
1807
+ }
1808
+ get zoomLabel() {
1809
+ return `${this.activeZoom}%`;
1810
+ }
1811
+ // ── Mode ────────────────────────────────────────────────
1812
+ toggleModePopover(event) {
1813
+ this.modePopover.toggle(event);
1814
+ }
1815
+ selectMode(mode) {
1816
+ if (this.activeMode !== mode) {
1817
+ this.modeChange.emit(mode);
1818
+ }
1819
+ this.modePopover.hide();
1820
+ }
1821
+ // ── Device ──────────────────────────────────────────────
1822
+ toggleDevicePopover(event) {
1823
+ this.devicePopover.toggle(event);
1824
+ }
1825
+ selectDevice(mode) {
1826
+ if (this.activeDevice !== mode) {
1827
+ this.deviceChange.emit(mode);
1828
+ }
1829
+ this.devicePopover.hide();
1830
+ }
1831
+ // ── Zoom ────────────────────────────────────────────────
1832
+ toggleZoomPopover(event) {
1833
+ this.zoomPopover.toggle(event);
1834
+ }
1835
+ selectZoom(value) {
1836
+ this.zoomChange.emit(value);
1837
+ this.zoomPopover.hide();
1838
+ }
1839
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1840
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: ToolbarComponent, isStandalone: false, selector: "app-toolbar", inputs: { activeMode: "activeMode", activeDevice: "activeDevice", activeZoom: "activeZoom", canUndo: "canUndo", canRedo: "canRedo", isSaving: "isSaving", pageStatus: "pageStatus" }, outputs: { modeChange: "modeChange", deviceChange: "deviceChange", zoomChange: "zoomChange", undo: "undo", redo: "redo", save: "save", preview: "preview", publishToggle: "publishToggle" }, viewQueries: [{ propertyName: "modePopover", first: true, predicate: ["modePopover"], descendants: true }, { propertyName: "devicePopover", first: true, predicate: ["devicePopover"], descendants: true }, { propertyName: "zoomPopover", first: true, predicate: ["zoomPopover"], descendants: true }], ngImport: i0, template: "<div class=\"builder-toolbar\">\n <!-- \u2500\u2500 Mode switcher (stateful) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn stateful\"\n (click)=\"toggleModePopover($event)\"\n [title]=\"activeModeLabel\">\n <i [class]=\"activeModeIcon\"></i>\n </button>\n\n <p-popover #modePopover [appendTo]=\"'body'\" styleClass=\"toolbar-popover\">\n <div class=\"popover-strip\">\n <button class=\"popover-option\"\n [class.is-active]=\"activeMode === modes.Select\"\n (click)=\"selectMode(modes.Select)\">\n <i class=\"ph-thin ph-cursor\"></i>\n <span>Select</span>\n </button>\n <button class=\"popover-option\"\n [class.is-active]=\"activeMode === modes.Pan\"\n (click)=\"selectMode(modes.Pan)\">\n <i class=\"ph-thin ph-hand\"></i>\n <span>Pan</span>\n </button>\n <button class=\"popover-option\"\n [class.is-active]=\"activeMode === modes.Style\"\n (click)=\"selectMode(modes.Style)\">\n <i class=\"ph-thin ph-paint-brush-broad\"></i>\n <span>Style</span>\n </button>\n </div>\n </p-popover>\n\n <span class=\"divider\"></span>\n\n <!-- \u2500\u2500 Undo / Redo (action) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn\" title=\"Undo\" [disabled]=\"!canUndo\" (click)=\"undo.emit()\">\n <i class=\"ph-thin ph-arrow-counter-clockwise\"></i>\n </button>\n <button class=\"tool-btn\" title=\"Redo\" [disabled]=\"!canRedo\" (click)=\"redo.emit()\">\n <i class=\"ph-thin ph-arrow-clockwise\"></i>\n </button>\n\n <span class=\"divider\"></span>\n\n <!-- \u2500\u2500 Device switcher (stateful) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn stateful\"\n (click)=\"toggleDevicePopover($event)\"\n [title]=\"activeDeviceOption.label\">\n <i [class]=\"activeDeviceOption.icon\"></i>\n </button>\n\n <p-popover #devicePopover [appendTo]=\"'body'\" styleClass=\"toolbar-popover\">\n <div class=\"popover-strip\">\n <button class=\"popover-option\"\n *ngFor=\"let d of deviceOptions\"\n [class.is-active]=\"activeDevice === d.mode\"\n (click)=\"selectDevice(d.mode)\">\n <i [class]=\"d.icon\"></i>\n <span>{{ d.label }}</span>\n </button>\n </div>\n </p-popover>\n\n <!-- \u2500\u2500 Zoom (stateful) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn stateful zoom-btn\"\n (click)=\"toggleZoomPopover($event)\"\n title=\"Zoom\">\n <span class=\"zoom-label\">{{ zoomLabel }}</span>\n </button>\n\n <p-popover #zoomPopover [appendTo]=\"'body'\" styleClass=\"toolbar-popover\">\n <div class=\"popover-strip\">\n <button class=\"popover-option\"\n *ngFor=\"let z of zoomOptions\"\n [class.is-active]=\"z.value === activeZoom\"\n (click)=\"selectZoom(z.value)\">\n <span>{{ z.label }}</span>\n </button>\n </div>\n </p-popover>\n\n <span class=\"divider\"></span>\n\n <!-- \u2500\u2500 Save \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn save-btn\"\n [class.is-saving]=\"isSaving\"\n [disabled]=\"isSaving\"\n title=\"Save\"\n (click)=\"save.emit()\">\n <i class=\"ph-thin ph-floppy-disk\"></i>\n </button>\n\n <!-- \u2500\u2500 Preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn preview-btn\"\n title=\"Preview\"\n (click)=\"preview.emit()\">\n <i class=\"ph-thin ph-eye\"></i>\n </button>\n\n <span class=\"divider\"></span>\n\n <!-- \u2500\u2500 Publish / Unpublish \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn publish-btn\"\n [class.is-published]=\"isPublished\"\n [title]=\"isPublished ? 'Unpublish' : 'Publish'\"\n (click)=\"publishToggle.emit()\">\n <i [class]=\"isPublished ? 'ph-thin ph-cloud-slash' : 'ph-thin ph-cloud-arrow-up'\"></i>\n <span class=\"publish-label\">{{ isPublished ? 'Unpublish' : 'Publish' }}</span>\n </button>\n</div>\n", styles: [".builder-toolbar{display:flex;align-items:center;gap:.25rem;background:#0f172ae0;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border:1px solid rgba(148,163,184,.2);border-radius:999px;padding:.3rem .5rem;box-shadow:0 8px 24px #0f172a47}.divider{width:1px;height:1.25rem;background:#94a3b833;margin:0 .2rem;flex-shrink:0}.tool-btn{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:50%;border:none;background:transparent;color:#ffffffbf;cursor:pointer;font-size:1.1rem;transition:background .15s,color .15s}.tool-btn:hover:not(:disabled){background:#ffffff1f;color:#fff}.tool-btn:disabled{opacity:.3;cursor:default}.tool-btn.stateful{position:relative}.tool-btn.stateful:after{content:\"\";position:absolute;inset-block-end:2px;width:4px;height:4px;border-radius:50%;background:#cb9090;opacity:.6}.zoom-btn{width:auto;border-radius:999px;padding:0 .5rem;min-width:2.5rem}.zoom-btn .zoom-label{font-size:.7rem;font-weight:500;letter-spacing:.02em}.save-btn{background:#cb9090;color:#fff}.save-btn:hover:not(:disabled){background:color-mix(in srgb,#cb9090 85%,#fff);color:#fff}.save-btn.is-saving{opacity:.7}.publish-btn{width:auto;border-radius:999px;padding:0 .65rem;gap:.35rem;font-size:.75rem;font-weight:500;background:#22c55e;color:#fff;border:none}.publish-btn .publish-label{font-size:.72rem;letter-spacing:.02em}.publish-btn:hover:not(:disabled){background:color-mix(in srgb,#22c55e 85%,#fff);color:#fff}.publish-btn.is-published{background:transparent;color:#94a3b8;border:1px solid rgba(255,255,255,.15)}.publish-btn.is-published:hover:not(:disabled){background:#ef444426;color:#f87171;border-color:#ef44444d}.preview-btn{color:#ffffffbf;border:1px solid rgba(255,255,255,.15)}.preview-btn:hover:not(:disabled){background:#ffffff1a;color:#fff;border-color:#ffffff40}::ng-deep .toolbar-popover .p-popover-content{padding:.3rem!important;background:#0f172ae0!important;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid rgba(148,163,184,.2)!important;border-radius:12px!important;box-shadow:0 12px 32px #0f172a59!important}.popover-strip{display:flex;flex-direction:column;gap:2px;min-width:120px}.popover-option{display:flex;align-items:center;gap:.5rem;width:100%;padding:.4rem .65rem;border:none;background:transparent;color:#ffffffbf;cursor:pointer;font-size:.8rem;border-radius:8px;transition:background .12s,color .12s;white-space:nowrap}.popover-option i{font-size:1rem}.popover-option:hover{background:#ffffff1a;color:#fff}.popover-option.is-active{background:#cb9090;color:#fff}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: i2$1.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }] }); }
1841
+ }
1842
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ToolbarComponent, decorators: [{
1843
+ type: Component,
1844
+ args: [{ selector: 'app-toolbar', standalone: false, template: "<div class=\"builder-toolbar\">\n <!-- \u2500\u2500 Mode switcher (stateful) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn stateful\"\n (click)=\"toggleModePopover($event)\"\n [title]=\"activeModeLabel\">\n <i [class]=\"activeModeIcon\"></i>\n </button>\n\n <p-popover #modePopover [appendTo]=\"'body'\" styleClass=\"toolbar-popover\">\n <div class=\"popover-strip\">\n <button class=\"popover-option\"\n [class.is-active]=\"activeMode === modes.Select\"\n (click)=\"selectMode(modes.Select)\">\n <i class=\"ph-thin ph-cursor\"></i>\n <span>Select</span>\n </button>\n <button class=\"popover-option\"\n [class.is-active]=\"activeMode === modes.Pan\"\n (click)=\"selectMode(modes.Pan)\">\n <i class=\"ph-thin ph-hand\"></i>\n <span>Pan</span>\n </button>\n <button class=\"popover-option\"\n [class.is-active]=\"activeMode === modes.Style\"\n (click)=\"selectMode(modes.Style)\">\n <i class=\"ph-thin ph-paint-brush-broad\"></i>\n <span>Style</span>\n </button>\n </div>\n </p-popover>\n\n <span class=\"divider\"></span>\n\n <!-- \u2500\u2500 Undo / Redo (action) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn\" title=\"Undo\" [disabled]=\"!canUndo\" (click)=\"undo.emit()\">\n <i class=\"ph-thin ph-arrow-counter-clockwise\"></i>\n </button>\n <button class=\"tool-btn\" title=\"Redo\" [disabled]=\"!canRedo\" (click)=\"redo.emit()\">\n <i class=\"ph-thin ph-arrow-clockwise\"></i>\n </button>\n\n <span class=\"divider\"></span>\n\n <!-- \u2500\u2500 Device switcher (stateful) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn stateful\"\n (click)=\"toggleDevicePopover($event)\"\n [title]=\"activeDeviceOption.label\">\n <i [class]=\"activeDeviceOption.icon\"></i>\n </button>\n\n <p-popover #devicePopover [appendTo]=\"'body'\" styleClass=\"toolbar-popover\">\n <div class=\"popover-strip\">\n <button class=\"popover-option\"\n *ngFor=\"let d of deviceOptions\"\n [class.is-active]=\"activeDevice === d.mode\"\n (click)=\"selectDevice(d.mode)\">\n <i [class]=\"d.icon\"></i>\n <span>{{ d.label }}</span>\n </button>\n </div>\n </p-popover>\n\n <!-- \u2500\u2500 Zoom (stateful) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn stateful zoom-btn\"\n (click)=\"toggleZoomPopover($event)\"\n title=\"Zoom\">\n <span class=\"zoom-label\">{{ zoomLabel }}</span>\n </button>\n\n <p-popover #zoomPopover [appendTo]=\"'body'\" styleClass=\"toolbar-popover\">\n <div class=\"popover-strip\">\n <button class=\"popover-option\"\n *ngFor=\"let z of zoomOptions\"\n [class.is-active]=\"z.value === activeZoom\"\n (click)=\"selectZoom(z.value)\">\n <span>{{ z.label }}</span>\n </button>\n </div>\n </p-popover>\n\n <span class=\"divider\"></span>\n\n <!-- \u2500\u2500 Save \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn save-btn\"\n [class.is-saving]=\"isSaving\"\n [disabled]=\"isSaving\"\n title=\"Save\"\n (click)=\"save.emit()\">\n <i class=\"ph-thin ph-floppy-disk\"></i>\n </button>\n\n <!-- \u2500\u2500 Preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn preview-btn\"\n title=\"Preview\"\n (click)=\"preview.emit()\">\n <i class=\"ph-thin ph-eye\"></i>\n </button>\n\n <span class=\"divider\"></span>\n\n <!-- \u2500\u2500 Publish / Unpublish \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button class=\"tool-btn publish-btn\"\n [class.is-published]=\"isPublished\"\n [title]=\"isPublished ? 'Unpublish' : 'Publish'\"\n (click)=\"publishToggle.emit()\">\n <i [class]=\"isPublished ? 'ph-thin ph-cloud-slash' : 'ph-thin ph-cloud-arrow-up'\"></i>\n <span class=\"publish-label\">{{ isPublished ? 'Unpublish' : 'Publish' }}</span>\n </button>\n</div>\n", styles: [".builder-toolbar{display:flex;align-items:center;gap:.25rem;background:#0f172ae0;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border:1px solid rgba(148,163,184,.2);border-radius:999px;padding:.3rem .5rem;box-shadow:0 8px 24px #0f172a47}.divider{width:1px;height:1.25rem;background:#94a3b833;margin:0 .2rem;flex-shrink:0}.tool-btn{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:50%;border:none;background:transparent;color:#ffffffbf;cursor:pointer;font-size:1.1rem;transition:background .15s,color .15s}.tool-btn:hover:not(:disabled){background:#ffffff1f;color:#fff}.tool-btn:disabled{opacity:.3;cursor:default}.tool-btn.stateful{position:relative}.tool-btn.stateful:after{content:\"\";position:absolute;inset-block-end:2px;width:4px;height:4px;border-radius:50%;background:#cb9090;opacity:.6}.zoom-btn{width:auto;border-radius:999px;padding:0 .5rem;min-width:2.5rem}.zoom-btn .zoom-label{font-size:.7rem;font-weight:500;letter-spacing:.02em}.save-btn{background:#cb9090;color:#fff}.save-btn:hover:not(:disabled){background:color-mix(in srgb,#cb9090 85%,#fff);color:#fff}.save-btn.is-saving{opacity:.7}.publish-btn{width:auto;border-radius:999px;padding:0 .65rem;gap:.35rem;font-size:.75rem;font-weight:500;background:#22c55e;color:#fff;border:none}.publish-btn .publish-label{font-size:.72rem;letter-spacing:.02em}.publish-btn:hover:not(:disabled){background:color-mix(in srgb,#22c55e 85%,#fff);color:#fff}.publish-btn.is-published{background:transparent;color:#94a3b8;border:1px solid rgba(255,255,255,.15)}.publish-btn.is-published:hover:not(:disabled){background:#ef444426;color:#f87171;border-color:#ef44444d}.preview-btn{color:#ffffffbf;border:1px solid rgba(255,255,255,.15)}.preview-btn:hover:not(:disabled){background:#ffffff1a;color:#fff;border-color:#ffffff40}::ng-deep .toolbar-popover .p-popover-content{padding:.3rem!important;background:#0f172ae0!important;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid rgba(148,163,184,.2)!important;border-radius:12px!important;box-shadow:0 12px 32px #0f172a59!important}.popover-strip{display:flex;flex-direction:column;gap:2px;min-width:120px}.popover-option{display:flex;align-items:center;gap:.5rem;width:100%;padding:.4rem .65rem;border:none;background:transparent;color:#ffffffbf;cursor:pointer;font-size:.8rem;border-radius:8px;transition:background .12s,color .12s;white-space:nowrap}.popover-option i{font-size:1rem}.popover-option:hover{background:#ffffff1a;color:#fff}.popover-option.is-active{background:#cb9090;color:#fff}\n"] }]
1845
+ }], propDecorators: { activeMode: [{
1846
+ type: Input
1847
+ }], activeDevice: [{
1848
+ type: Input
1849
+ }], activeZoom: [{
1850
+ type: Input
1851
+ }], canUndo: [{
1852
+ type: Input
1853
+ }], canRedo: [{
1854
+ type: Input
1855
+ }], isSaving: [{
1856
+ type: Input
1857
+ }], pageStatus: [{
1858
+ type: Input
1859
+ }], modeChange: [{
1860
+ type: Output
1861
+ }], deviceChange: [{
1862
+ type: Output
1863
+ }], zoomChange: [{
1864
+ type: Output
1865
+ }], undo: [{
1866
+ type: Output
1867
+ }], redo: [{
1868
+ type: Output
1869
+ }], save: [{
1870
+ type: Output
1871
+ }], preview: [{
1872
+ type: Output
1873
+ }], publishToggle: [{
1874
+ type: Output
1875
+ }], modePopover: [{
1876
+ type: ViewChild,
1877
+ args: ['modePopover']
1878
+ }], devicePopover: [{
1879
+ type: ViewChild,
1880
+ args: ['devicePopover']
1881
+ }], zoomPopover: [{
1882
+ type: ViewChild,
1883
+ args: ['zoomPopover']
1884
+ }] } });
1885
+
1886
+ class LayerTreeComponent {
1887
+ constructor(registry, overlayService) {
1888
+ this.registry = registry;
1889
+ this.overlayService = overlayService;
1890
+ this.layout = [];
1891
+ this.nodeSelect = new EventEmitter();
1892
+ this.nodeAction = new EventEmitter();
1893
+ this.nodeMove = new EventEmitter();
1894
+ this.flatTree = [];
1895
+ this.selectedNodeId = null;
1896
+ this.hoveredNodeId = null;
1897
+ /** Cache: component type → icon class */
1898
+ this.iconMap = new Map();
1899
+ /** Last-seen structural signature — used by ngDoCheck to skip no-op rebuilds. */
1900
+ this._layoutSig = '';
1901
+ this.expandedState = new Map();
1902
+ }
1903
+ ngOnInit() {
1904
+ this.buildIconMap();
1905
+ this.rebuild();
1906
+ this.selectSub = this.overlayService.select$.subscribe(entry => {
1907
+ this.selectedNodeId = entry?.node?.id ?? null;
1908
+ });
1909
+ }
1910
+ ngOnDestroy() {
1911
+ this.selectSub?.unsubscribe();
1912
+ }
1913
+ // Runs on every CD cycle — exit immediately if structure hasn't changed.
1914
+ ngDoCheck() {
1915
+ const sig = this.sig(this.layout);
1916
+ if (sig !== this._layoutSig) {
1917
+ this._layoutSig = sig;
1918
+ this.rebuild();
1919
+ }
1920
+ }
1921
+ /** Cheap structural signature: node IDs + nesting depth only. */
1922
+ sig(nodes) {
1923
+ return nodes.map(n => (n.id ?? n.type) + (n.children?.length ? '{' + this.sig(n.children) + '}' : '')).join(',');
1924
+ }
1925
+ onNodeClick(treeNode) {
1926
+ const node = treeNode.node;
1927
+ this.selectedNodeId = node.id ?? null;
1928
+ const ref = this.overlayService.getNodeRef(node);
1929
+ if (ref) {
1930
+ this.overlayService.setSelect(ref.element, node.type, BuilderMode.Select, node, ref.componentRef);
1931
+ // Scroll the element into view on the canvas
1932
+ ref.element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1933
+ }
1934
+ this.nodeSelect.emit(node);
1935
+ }
1936
+ onNodeMouseEnter(treeNode) {
1937
+ this.hoveredNodeId = treeNode.node.id ?? null;
1938
+ const ref = this.overlayService.getNodeRef(treeNode.node);
1939
+ if (ref) {
1940
+ this.overlayService.setHover(ref.element, treeNode.node.type, BuilderMode.Select);
1941
+ }
1942
+ }
1943
+ onNodeMouseLeave() {
1944
+ this.hoveredNodeId = null;
1945
+ this.overlayService.clearHover();
1946
+ }
1947
+ toggleExpand(treeNode, event) {
1948
+ event.stopPropagation();
1949
+ treeNode.expanded = !treeNode.expanded;
1950
+ }
1951
+ onDeleteNode(treeNode, event) {
1952
+ event.stopPropagation();
1953
+ this.nodeAction.emit({ action: NodeAction.Delete, node: treeNode.node });
1954
+ }
1955
+ onDuplicateNode(treeNode, event) {
1956
+ event.stopPropagation();
1957
+ this.nodeAction.emit({ action: NodeAction.Duplicate, node: treeNode.node });
1958
+ }
1959
+ /**
1960
+ * CDK drop handler for tree reorder.
1961
+ * Works on the flat tree indices — maps back to parent arrays to do the move.
1962
+ */
1963
+ onTreeDrop(event) {
1964
+ if (event.previousIndex === event.currentIndex)
1965
+ return;
1966
+ const draggedTreeNode = this.flatTree[event.previousIndex];
1967
+ if (!draggedTreeNode)
1968
+ return;
1969
+ const targetTreeNode = this.flatTree[event.currentIndex];
1970
+ if (!targetTreeNode)
1971
+ return;
1972
+ // Remove from source parent
1973
+ const srcArray = draggedTreeNode.parentArray;
1974
+ const srcIdx = draggedTreeNode.indexInParent;
1975
+ srcArray.splice(srcIdx, 1);
1976
+ // Determine destination: insert at the same position as the target node in its parent.
1977
+ // If dropping after the target, adjust index.
1978
+ const destArray = targetTreeNode.parentArray;
1979
+ let destIdx = targetTreeNode.indexInParent;
1980
+ // If dragging downward (previousIndex < currentIndex), the target index
1981
+ // may have shifted because we already removed the item from source.
1982
+ // If source and destination are the same array and srcIdx < destIdx, adjust.
1983
+ if (srcArray === destArray && srcIdx < destIdx) {
1984
+ destIdx--;
1985
+ }
1986
+ destArray.splice(destIdx, 0, draggedTreeNode.node);
1987
+ // Rebuild tree and notify builder
1988
+ this.rebuild();
1989
+ this.nodeMove.emit();
1990
+ }
1991
+ // ── Internals ────────────────────────────────────────────
1992
+ /** Build a type→icon lookup from the registry so we don't instantiate configs per node. */
1993
+ buildIconMap() {
1994
+ for (const entry of this.registry.getAll()) {
1995
+ const instance = new entry.config();
1996
+ this.iconMap.set(entry.type, instance.icon || 'ph-thin ph-cube');
1997
+ }
1998
+ }
1999
+ rebuild() {
2000
+ // Save expanded state
2001
+ for (const tn of this.flatTree) {
2002
+ if (tn.node.id) {
2003
+ this.expandedState.set(tn.node.id, tn.expanded);
2004
+ }
2005
+ }
2006
+ const next = [];
2007
+ this.flattenTree(this.layout, 0, next);
2008
+ this.flatTree = next;
2009
+ }
2010
+ flattenTree(nodes, depth, out, parentArr) {
2011
+ const arr = parentArr ?? nodes;
2012
+ for (let i = 0; i < nodes.length; i++) {
2013
+ const node = nodes[i];
2014
+ const hasChildren = !!(node.children && node.children.length);
2015
+ const expanded = node.id ? (this.expandedState.get(node.id) ?? true) : true;
2016
+ const treeNode = {
2017
+ node,
2018
+ icon: this.iconMap.get(node.type) || 'ph-thin ph-cube',
2019
+ label: node.type,
2020
+ depth,
2021
+ expanded,
2022
+ hasChildren,
2023
+ parentArray: arr,
2024
+ indexInParent: i,
2025
+ };
2026
+ out.push(treeNode);
2027
+ if (node.children && expanded) {
2028
+ this.flattenTree(node.children, depth + 1, out, node.children);
2029
+ }
2030
+ }
2031
+ }
2032
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LayerTreeComponent, deps: [{ token: ComponentRegistryService }, { token: OverlayService }], target: i0.ɵɵFactoryTarget.Component }); }
2033
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: LayerTreeComponent, isStandalone: false, selector: "app-layer-tree", inputs: { layout: "layout" }, outputs: { nodeSelect: "nodeSelect", nodeAction: "nodeAction", nodeMove: "nodeMove" }, ngImport: i0, template: "<div class=\"layer-tree\">\n <div class=\"tree-header\">\n <h3 class=\"panel-title\">Layers</h3>\n </div>\n\n <div class=\"tree-body\"\n cdkDropList\n [cdkDropListData]=\"flatTree\"\n (cdkDropListDropped)=\"onTreeDrop($event)\">\n <div *ngIf=\"!flatTree.length\" class=\"tree-empty\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n <span>No layers yet</span>\n </div>\n\n <div *ngFor=\"let tn of flatTree\"\n class=\"tree-row\"\n cdkDrag\n [cdkDragData]=\"tn\"\n [class.is-selected]=\"tn.node.id === selectedNodeId\"\n [class.is-hovered]=\"tn.node.id === hoveredNodeId\"\n [style.padding-inline-start.px]=\"12 + tn.depth * 16\"\n (click)=\"onNodeClick(tn)\"\n (mouseenter)=\"onNodeMouseEnter(tn)\"\n (mouseleave)=\"onNodeMouseLeave()\">\n\n <!-- Drag handle indicator -->\n <i class=\"ph-thin ph-dots-six-vertical drag-handle\" cdkDragHandle></i>\n\n <!-- Expand/collapse toggle -->\n <button *ngIf=\"tn.hasChildren\"\n class=\"expand-btn\"\n (click)=\"toggleExpand(tn, $event)\">\n <i class=\"ph-thin\" [ngClass]=\"tn.expanded ? 'ph-caret-down' : 'ph-caret-right'\"></i>\n </button>\n <span *ngIf=\"!tn.hasChildren\" class=\"expand-spacer\"></span>\n\n <!-- Icon + Label -->\n <i [class]=\"tn.icon\" class=\"node-icon\"></i>\n <span class=\"node-label\">{{ tn.label }}</span>\n\n <!-- Actions (visible on hover) -->\n <span class=\"node-actions\">\n <button class=\"action-btn\" title=\"Duplicate\" (click)=\"onDuplicateNode(tn, $event)\">\n <i class=\"ph-thin ph-copy\"></i>\n </button>\n <button class=\"action-btn action-btn--delete\" title=\"Delete\" (click)=\"onDeleteNode(tn, $event)\">\n <i class=\"ph-thin ph-trash\"></i>\n </button>\n </span>\n </div>\n </div>\n</div>\n", styles: [".layer-tree{display:flex;flex-direction:column;height:100%;overflow:hidden}.layer-tree .tree-header{padding:.75rem 1rem;border-bottom:1px solid var(--p-surface-200);flex-shrink:0}.layer-tree .tree-header .panel-title{margin:0;font-size:13px;font-weight:600;text-transform:capitalize}.layer-tree .tree-body{flex:1 1 0;overflow-y:auto;overflow-x:hidden;padding-block:4px}.layer-tree .tree-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:2rem 1rem;color:var(--p-text-muted-color);font-size:12px}.layer-tree .tree-empty i{font-size:22px}.layer-tree .tree-row{display:flex;align-items:center;gap:6px;height:28px;padding-inline-end:8px;cursor:pointer;font-size:12px;color:var(--p-text-color);border-inline-start:2px solid transparent;transition:background .12s,border-color .12s;-webkit-user-select:none;user-select:none}.layer-tree .tree-row:hover,.layer-tree .tree-row.is-hovered{background:var(--p-surface-100)}.layer-tree .tree-row.is-selected{background:#cb90901a;border-inline-start-color:#cb9090;font-weight:500}.layer-tree .drag-handle{font-size:10px;color:var(--p-text-muted-color);opacity:0;cursor:grab;flex-shrink:0;transition:opacity .12s}.layer-tree .tree-row:hover .drag-handle{opacity:.5}.layer-tree .drag-handle:hover{opacity:1!important}.layer-tree .cdk-drag-preview{box-sizing:border-box;display:flex;align-items:center;gap:6px;height:28px;padding:0 8px 0 12px;border-radius:6px;background:var(--p-surface-0);border:1px solid #cb9090;box-shadow:0 4px 12px #0000001f;font-size:12px}.layer-tree .cdk-drag-placeholder{height:2px;background:#cb9090;border-radius:1px;opacity:.6}.layer-tree .cdk-drag-placeholder>*{visibility:hidden}.layer-tree .cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.layer-tree .expand-btn{display:flex;align-items:center;justify-content:center;width:16px;height:16px;padding:0;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;flex-shrink:0;border-radius:2px}.layer-tree .expand-btn:hover{background:var(--p-surface-200);color:var(--p-text-color)}.layer-tree .expand-btn i{font-size:10px}.layer-tree .expand-spacer{width:16px;flex-shrink:0}.layer-tree .node-icon{font-size:14px;color:var(--p-text-muted-color);flex-shrink:0}.layer-tree .node-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}.layer-tree .node-actions{display:flex;gap:2px;margin-inline-start:auto;opacity:0;transition:opacity .12s;flex-shrink:0}.layer-tree .tree-row:hover .node-actions,.layer-tree .tree-row.is-selected .node-actions{opacity:1}.layer-tree .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;border-radius:4px;font-size:12px;transition:background .12s,color .12s}.layer-tree .action-btn:hover{background:var(--p-surface-200);color:var(--p-text-color)}.layer-tree .action-btn--delete:hover{background:#dc26261a;color:#dc2626}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i4.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i4.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i4.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }] }); }
2034
+ }
2035
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LayerTreeComponent, decorators: [{
2036
+ type: Component,
2037
+ args: [{ selector: 'app-layer-tree', standalone: false, template: "<div class=\"layer-tree\">\n <div class=\"tree-header\">\n <h3 class=\"panel-title\">Layers</h3>\n </div>\n\n <div class=\"tree-body\"\n cdkDropList\n [cdkDropListData]=\"flatTree\"\n (cdkDropListDropped)=\"onTreeDrop($event)\">\n <div *ngIf=\"!flatTree.length\" class=\"tree-empty\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n <span>No layers yet</span>\n </div>\n\n <div *ngFor=\"let tn of flatTree\"\n class=\"tree-row\"\n cdkDrag\n [cdkDragData]=\"tn\"\n [class.is-selected]=\"tn.node.id === selectedNodeId\"\n [class.is-hovered]=\"tn.node.id === hoveredNodeId\"\n [style.padding-inline-start.px]=\"12 + tn.depth * 16\"\n (click)=\"onNodeClick(tn)\"\n (mouseenter)=\"onNodeMouseEnter(tn)\"\n (mouseleave)=\"onNodeMouseLeave()\">\n\n <!-- Drag handle indicator -->\n <i class=\"ph-thin ph-dots-six-vertical drag-handle\" cdkDragHandle></i>\n\n <!-- Expand/collapse toggle -->\n <button *ngIf=\"tn.hasChildren\"\n class=\"expand-btn\"\n (click)=\"toggleExpand(tn, $event)\">\n <i class=\"ph-thin\" [ngClass]=\"tn.expanded ? 'ph-caret-down' : 'ph-caret-right'\"></i>\n </button>\n <span *ngIf=\"!tn.hasChildren\" class=\"expand-spacer\"></span>\n\n <!-- Icon + Label -->\n <i [class]=\"tn.icon\" class=\"node-icon\"></i>\n <span class=\"node-label\">{{ tn.label }}</span>\n\n <!-- Actions (visible on hover) -->\n <span class=\"node-actions\">\n <button class=\"action-btn\" title=\"Duplicate\" (click)=\"onDuplicateNode(tn, $event)\">\n <i class=\"ph-thin ph-copy\"></i>\n </button>\n <button class=\"action-btn action-btn--delete\" title=\"Delete\" (click)=\"onDeleteNode(tn, $event)\">\n <i class=\"ph-thin ph-trash\"></i>\n </button>\n </span>\n </div>\n </div>\n</div>\n", styles: [".layer-tree{display:flex;flex-direction:column;height:100%;overflow:hidden}.layer-tree .tree-header{padding:.75rem 1rem;border-bottom:1px solid var(--p-surface-200);flex-shrink:0}.layer-tree .tree-header .panel-title{margin:0;font-size:13px;font-weight:600;text-transform:capitalize}.layer-tree .tree-body{flex:1 1 0;overflow-y:auto;overflow-x:hidden;padding-block:4px}.layer-tree .tree-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:2rem 1rem;color:var(--p-text-muted-color);font-size:12px}.layer-tree .tree-empty i{font-size:22px}.layer-tree .tree-row{display:flex;align-items:center;gap:6px;height:28px;padding-inline-end:8px;cursor:pointer;font-size:12px;color:var(--p-text-color);border-inline-start:2px solid transparent;transition:background .12s,border-color .12s;-webkit-user-select:none;user-select:none}.layer-tree .tree-row:hover,.layer-tree .tree-row.is-hovered{background:var(--p-surface-100)}.layer-tree .tree-row.is-selected{background:#cb90901a;border-inline-start-color:#cb9090;font-weight:500}.layer-tree .drag-handle{font-size:10px;color:var(--p-text-muted-color);opacity:0;cursor:grab;flex-shrink:0;transition:opacity .12s}.layer-tree .tree-row:hover .drag-handle{opacity:.5}.layer-tree .drag-handle:hover{opacity:1!important}.layer-tree .cdk-drag-preview{box-sizing:border-box;display:flex;align-items:center;gap:6px;height:28px;padding:0 8px 0 12px;border-radius:6px;background:var(--p-surface-0);border:1px solid #cb9090;box-shadow:0 4px 12px #0000001f;font-size:12px}.layer-tree .cdk-drag-placeholder{height:2px;background:#cb9090;border-radius:1px;opacity:.6}.layer-tree .cdk-drag-placeholder>*{visibility:hidden}.layer-tree .cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.layer-tree .expand-btn{display:flex;align-items:center;justify-content:center;width:16px;height:16px;padding:0;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;flex-shrink:0;border-radius:2px}.layer-tree .expand-btn:hover{background:var(--p-surface-200);color:var(--p-text-color)}.layer-tree .expand-btn i{font-size:10px}.layer-tree .expand-spacer{width:16px;flex-shrink:0}.layer-tree .node-icon{font-size:14px;color:var(--p-text-muted-color);flex-shrink:0}.layer-tree .node-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}.layer-tree .node-actions{display:flex;gap:2px;margin-inline-start:auto;opacity:0;transition:opacity .12s;flex-shrink:0}.layer-tree .tree-row:hover .node-actions,.layer-tree .tree-row.is-selected .node-actions{opacity:1}.layer-tree .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;border-radius:4px;font-size:12px;transition:background .12s,color .12s}.layer-tree .action-btn:hover{background:var(--p-surface-200);color:var(--p-text-color)}.layer-tree .action-btn--delete:hover{background:#dc26261a;color:#dc2626}\n"] }]
2038
+ }], ctorParameters: () => [{ type: ComponentRegistryService }, { type: OverlayService }], propDecorators: { layout: [{
2039
+ type: Input
2040
+ }], nodeSelect: [{
2041
+ type: Output
2042
+ }], nodeAction: [{
2043
+ type: Output
2044
+ }], nodeMove: [{
2045
+ type: Output
2046
+ }] } });
2047
+
2048
+ class BuilderComponent {
2049
+ // Type-safe event forwarding for toolbar outputs
2050
+ handleToolbarModeChange(mode) {
2051
+ this.onModeChange(mode);
2052
+ }
2053
+ handleToolbarDeviceChange(device) {
2054
+ this.onDeviceChange(device);
2055
+ }
2056
+ handleToolbarZoomChange(value) {
2057
+ this.onZoomChange(value);
2058
+ }
2059
+ /** Canvas width in CSS pixels (driven by ViewportService). */
2060
+ get viewportWidth() {
2061
+ return this.viewportService.getState().width;
2062
+ }
2063
+ /** Visual (scaled) canvas width — used to size the scroll container so the
2064
+ * scrollbar aligns with the right edge of the canvas, not the CMS shell. */
2065
+ get viewportScaledWidth() {
2066
+ const s = this.viewportService.getState().scale;
2067
+ return Math.round(this.viewportWidth * s);
2068
+ }
2069
+ /** CSS transform for the visual canvas.
2070
+ * Includes translateX(-50%) for centering because iox-page-component is
2071
+ * position:absolute; left:50% inside .preview-lenis-content. */
2072
+ get viewportTransform() {
2073
+ const s = this.viewportService.getState().scale;
2074
+ const parts = ['translateX(-50%)'];
2075
+ if (this.panX !== 0 || this.panY !== 0)
2076
+ parts.push(`translate(${this.panX}px, ${this.panY}px)`);
2077
+ if (s !== 1)
2078
+ parts.push(`scale(${s})`);
2079
+ return parts.join(' ');
2080
+ }
2081
+ /**
2082
+ * Height of the Lenis content proxy div (realContentHeight × scale).
2083
+ * Lenis scrolls against this value so scroll range is always in visual px,
2084
+ * not unscaled CSS px — correct at any zoom level.
2085
+ */
2086
+ get scaledContentHeight() {
2087
+ const s = this.viewportService.getState().scale;
2088
+ return Math.round(this.canvasContentHeight * s);
2089
+ }
2090
+ /**
2091
+ * The canvas min-height simulates a real browser viewport.
2092
+ * We subtract the builder header (~49px) and some padding from window.innerHeight.
2093
+ */
2094
+ get canvasMinHeight() {
2095
+ return Math.max(400, window.innerHeight - 80);
2096
+ }
2097
+ constructor(registry, overlayService, panelEventService, dragEngine, dsRegistry, viewportService, interactionEngine, cdr, appRef) {
2098
+ this.registry = registry;
2099
+ this.overlayService = overlayService;
2100
+ this.panelEventService = panelEventService;
2101
+ this.dragEngine = dragEngine;
2102
+ this.dsRegistry = dsRegistry;
2103
+ this.viewportService = viewportService;
2104
+ this.interactionEngine = interactionEngine;
2105
+ this.cdr = cdr;
2106
+ this.appRef = appRef;
2107
+ this.layout = [];
2108
+ this.dataSources = [];
2109
+ this.orgId = 'default';
2110
+ this.routeParams = [];
2111
+ this.pageName = '';
2112
+ this.isSaving = false;
2113
+ this.pageStatus = 'draft';
2114
+ this.pageSettings = null;
2115
+ this.save = new EventEmitter();
2116
+ this.back = new EventEmitter();
2117
+ this.preview = new EventEmitter();
2118
+ this.publishToggle = new EventEmitter();
2119
+ this.pageSettingsChange = new EventEmitter();
2120
+ this.applyAnimationToAll = new EventEmitter();
2121
+ this.saveBlock = new EventEmitter();
2122
+ this.components = [];
2123
+ this.activePanel = PanelTypes.STYLES;
2124
+ this.isPanelOpen = false;
2125
+ this.panelTypes = PanelTypes;
2126
+ this.activeMode = BuilderMode.Select;
2127
+ this.builderModes = BuilderMode;
2128
+ this.activeDevice = DeviceMode.Desktop;
2129
+ this.activeZoom = 100;
2130
+ this.selectedItem = null;
2131
+ this.isDragging = false;
2132
+ this.scrollThumbTop = 0; // px from top of track
2133
+ this.scrollThumbHeight = 0; // px height of thumb
2134
+ this.isScrollbarVisible = false;
2135
+ this.scrollbarTranslate = 0; // counteracts Lenis wrapper scrollTop shifting position:absolute children
2136
+ this.activeSidebar = 'components';
2137
+ this.showSaveBlockDialog = false;
2138
+ this.saveBlockName = '';
2139
+ this.pendingBlockNode = null;
2140
+ this.isPanning = false;
2141
+ this.panX = 0;
2142
+ this.panY = 0;
2143
+ this.panStartX = 0;
2144
+ this.panStartY = 0;
2145
+ this.panOriginX = 0;
2146
+ this.panOriginY = 0;
2147
+ this.hoverHideRaf = 0;
2148
+ this.canvasContentHeight = 0;
2149
+ this._scrollbarTimer = null;
2150
+ }
2151
+ ngOnInit() {
2152
+ this.components = this.registry.getAll();
2153
+ this.dsRegistry.setDataSources(this.dataSources);
2154
+ this.dragEngine.setScale(this.viewportService.getScale());
2155
+ this.sub = this.panelEventService.subscribe((panelEvent) => this.handlePanelEvents(panelEvent));
2156
+ this.panelEventService.emit(PanelEventTypes.PANEL_OPEN, PanelTypes.STYLES);
2157
+ this.dragEngine.isDragging$.subscribe((v) => {
2158
+ this.isDragging = v;
2159
+ this.cdr.markForCheck();
2160
+ });
2161
+ this.viewportSub = this.viewportService.state$.subscribe(state => {
2162
+ this.activeDevice = state.device;
2163
+ this.activeZoom = Math.round(state.scale * 100);
2164
+ this.dragEngine.setScale(state.scale);
2165
+ // scaledContentHeight changes with scale — flush DOM first so
2166
+ // .preview-lenis-content has its new height before Lenis reads it.
2167
+ this.cdr.detectChanges();
2168
+ this.lenis?.resize();
2169
+ // iox-page-component has transition:0.25s — overlay positions are stale
2170
+ // until the transform animation completes. Re-fire after the transition.
2171
+ setTimeout(() => this.overlayService.refreshSelect(), 270);
2172
+ });
2173
+ }
2174
+ ngOnChanges(changes) {
2175
+ if (changes['pageSettings']) {
2176
+ // Reinit Lenis with updated scroll options (lerp, wheelMultiplier, etc.)
2177
+ // so the builder preview reflects the live-page scroll feel in real time.
2178
+ this.initLenis();
2179
+ }
2180
+ }
2181
+ ngAfterViewInit() {
2182
+ this.overlayService.setContainer(this.previewCanvas?.nativeElement || null);
2183
+ // Builder dispatches synthetic 'scroll' events on .preview-lenis-content from
2184
+ // the Lenis callback — point the overlay scroll listener at that element.
2185
+ // (Not previewScrollRef which is the Lenis eventsTarget — dispatching there
2186
+ // would cause Lenis to re-fire its scroll handler → infinite loop.)
2187
+ this.overlayService.setScrollContainer(this.lenisContentRef?.nativeElement || null);
2188
+ // Register the root canvas as a dropzone.
2189
+ const canvasEl = document.getElementById('canvas-preview');
2190
+ if (canvasEl) {
2191
+ this.dragEngine.registerDropzone('canvas-preview', canvasEl, this.layout, this.cdr, (evt) => this.onDrop(evt));
2192
+ // Track canvas content height so scaledContentHeight (= height × scale)
2193
+ // keeps the Lenis content proxy correctly sized at any zoom level.
2194
+ this.canvasRo = new ResizeObserver(entries => {
2195
+ const h = entries[0]?.contentRect.height ?? 0;
2196
+ if (h !== this.canvasContentHeight) {
2197
+ this.canvasContentHeight = h;
2198
+ this.cdr.detectChanges();
2199
+ this.lenis?.resize();
2200
+ }
2201
+ });
2202
+ this.canvasRo.observe(canvasEl);
2203
+ }
2204
+ this.initLenis();
2205
+ setTimeout(() => this.appRef.tick());
2206
+ }
2207
+ ngOnDestroy() {
2208
+ this.sub?.unsubscribe();
2209
+ this.registrySub?.unsubscribe();
2210
+ this.viewportSub?.unsubscribe();
2211
+ this.destroyLenis();
2212
+ this.canvasRo?.disconnect();
2213
+ this.overlayService.setContainer(null);
2214
+ this.overlayService.setScrollContainer(null);
2215
+ const canvasEl = document.getElementById('canvas-preview');
2216
+ if (canvasEl) {
2217
+ this.dragEngine.unregisterDropzone('canvas-preview', canvasEl);
2218
+ }
2219
+ }
2220
+ initLenis() {
2221
+ this.destroyLenis();
2222
+ if (!this.previewScrollRef?.nativeElement || !this.lenisContentRef?.nativeElement)
2223
+ return;
2224
+ const s = this.pageSettings?.scroll;
2225
+ this.lenis = new Lenis({
2226
+ wrapper: this.previewScrollRef.nativeElement,
2227
+ content: this.lenisContentRef.nativeElement,
2228
+ eventsTarget: this.previewScrollRef.nativeElement,
2229
+ lerp: s?.lerp ?? 0.1,
2230
+ wheelMultiplier: s?.wheelMultiplier ?? 1,
2231
+ touchMultiplier: s?.touchMultiplier ?? 1,
2232
+ orientation: s?.direction ?? 'vertical',
2233
+ infinite: s?.infinite ?? false,
2234
+ autoResize: false, // we drive resize() manually via canvasRo + viewportSub
2235
+ });
2236
+ this.lenis.on('scroll', (instance) => {
2237
+ const clientH = this.previewScrollRef.nativeElement.clientHeight;
2238
+ this.onPageScroll({
2239
+ scrollTop: instance.scroll,
2240
+ scrollHeight: instance.limit + clientH,
2241
+ clientHeight: clientH,
2242
+ scrollPercentage: instance.progress * 100,
2243
+ });
2244
+ // Notify overlay to reposition boxes on each scroll tick.
2245
+ // Dispatch on lenisContent (not previewScrollRef/eventsTarget) to avoid
2246
+ // Lenis re-consuming the event and causing infinite recursion.
2247
+ this.lenisContentRef.nativeElement.dispatchEvent(new Event('scroll'));
2248
+ // Keep the fixed-position drop-target overlay aligned when canvas scrolls
2249
+ // during an active drag (pointer doesn't move, so _onDragMove never fires).
2250
+ this.dragEngine.refreshDragOverlays();
2251
+ });
2252
+ const raf = (time) => {
2253
+ this.lenis?.raf(time);
2254
+ this.lenisRafId = requestAnimationFrame(raf);
2255
+ };
2256
+ this.lenisRafId = requestAnimationFrame(raf);
2257
+ }
2258
+ destroyLenis() {
2259
+ if (this.lenisRafId != null)
2260
+ cancelAnimationFrame(this.lenisRafId);
2261
+ this.lenis?.destroy();
2262
+ this.lenis = undefined;
2263
+ this.lenisRafId = undefined;
2264
+ }
2265
+ addComponent(name) {
2266
+ const config = this.registry.createConfig(name);
2267
+ if (!config) {
2268
+ return;
2269
+ }
2270
+ this.layout.push(config);
2271
+ }
2272
+ onDrop(event) {
2273
+ const { payload, insertIndex } = event;
2274
+ if (payload.type === 'external') {
2275
+ if (typeof payload.data === 'string') {
2276
+ const config = this.registry.createConfig(payload.data);
2277
+ if (!config)
2278
+ return;
2279
+ this.layout.splice(insertIndex, 0, config);
2280
+ }
2281
+ else {
2282
+ const clone = this.deepCloneWithNewIds(payload.data);
2283
+ this.layout.splice(insertIndex, 0, clone);
2284
+ }
2285
+ this.cdr.markForCheck();
2286
+ return;
2287
+ }
2288
+ const item = payload.data;
2289
+ if (payload.sourceId === 'blocks-panel') {
2290
+ // Reusable block dragged from blocks panel — deep clone with new IDs.
2291
+ const clone = this.deepCloneWithNewIds(item);
2292
+ this.layout.splice(insertIndex, 0, clone);
2293
+ this.cdr.markForCheck();
2294
+ return;
2295
+ }
2296
+ if (payload.sourceId && payload.sourceId !== 'canvas-preview') {
2297
+ // Cross-list move from a nested container into canvas root.
2298
+ const sourceData = this.dragEngine.getData(payload.sourceId);
2299
+ if (sourceData) {
2300
+ const srcIdx = sourceData.indexOf(item);
2301
+ if (srcIdx !== -1) {
2302
+ sourceData.splice(srcIdx, 1);
2303
+ this.dragEngine.getCdr(payload.sourceId)?.detectChanges();
2304
+ }
2305
+ }
2306
+ this.layout.splice(insertIndex, 0, item);
2307
+ this.cdr.markForCheck();
2308
+ return;
2309
+ }
2310
+ // Same-container reorder (canvas root).
2311
+ const currentIdx = this.layout.indexOf(item);
2312
+ if (currentIdx !== -1) {
2313
+ const adjustedIdx = insertIndex > currentIdx ? insertIndex - 1 : insertIndex;
2314
+ this.layout.splice(currentIdx, 1);
2315
+ this.layout.splice(adjustedIdx, 0, item);
2316
+ this.cdr.markForCheck();
2317
+ }
2318
+ }
2319
+ /** Resolve the width style-trait value for a node so the cdkDrag wrapper
2320
+ * matches the component's actual width (prevents full-canvas-width clones). */
2321
+ getNodeWidth(node) {
2322
+ if (!node.styleTraits)
2323
+ return undefined;
2324
+ for (const group of node.styleTraits) {
2325
+ for (const trait of group.traits) {
2326
+ if (trait.name === 'width' && trait.default !== undefined) {
2327
+ return trait.default;
2328
+ }
2329
+ }
2330
+ }
2331
+ return undefined;
2332
+ }
2333
+ onModeChange(mode) {
2334
+ this.activeMode = mode;
2335
+ if (mode === BuilderMode.Pan) {
2336
+ this.selectedItem = null;
2337
+ this.overlayService.clearSelect();
2338
+ this.overlayService.clearHover();
2339
+ }
2340
+ else {
2341
+ // Returning to Select/Style — reset pan offset so canvas re-centers
2342
+ this.panX = 0;
2343
+ this.panY = 0;
2344
+ }
2345
+ }
2346
+ onDeviceChange(device) {
2347
+ this.viewportService.setDevice(device);
2348
+ this.panX = 0;
2349
+ this.panY = 0;
2350
+ this.autoFitViewport();
2351
+ }
2352
+ onZoomChange(value) {
2353
+ if (value === 'fit') {
2354
+ this.panX = 0;
2355
+ this.panY = 0;
2356
+ this.autoFitViewport();
2357
+ }
2358
+ else {
2359
+ this.viewportService.setScale(value / 100);
2360
+ }
2361
+ }
2362
+ // ── Board pan ────────────────────────────────────────────
2363
+ onBoardMouseDown(event) {
2364
+ if (this.activeMode !== BuilderMode.Pan)
2365
+ return;
2366
+ if (event.button !== 0)
2367
+ return;
2368
+ this.isPanning = true;
2369
+ this.panStartX = event.clientX;
2370
+ this.panStartY = event.clientY;
2371
+ this.panOriginX = this.panX;
2372
+ this.panOriginY = this.panY;
2373
+ event.preventDefault();
2374
+ }
2375
+ onBoardMouseMove(event) {
2376
+ if (!this.isPanning)
2377
+ return;
2378
+ this.panX = this.panOriginX + (event.clientX - this.panStartX);
2379
+ this.panY = this.panOriginY + (event.clientY - this.panStartY);
2380
+ }
2381
+ onBoardMouseUp() {
2382
+ this.isPanning = false;
2383
+ }
2384
+ autoFitViewport() {
2385
+ const container = this.previewCanvas?.nativeElement;
2386
+ if (!container)
2387
+ return;
2388
+ // Available width = container width minus horizontal padding (10px × 2)
2389
+ this.viewportService.fitToWidth(container.clientWidth, 20);
2390
+ }
2391
+ selectPanel(panel) {
2392
+ this.activePanel = panel;
2393
+ this.panelEventService.emit(this.isPanelOpen ? PanelEventTypes.PANEL_SELECTED : PanelEventTypes.PANEL_OPEN, panel);
2394
+ }
2395
+ handleClick(event) {
2396
+ if (this.activeMode === BuilderMode.Pan)
2397
+ return;
2398
+ this.selectedItem = event.node;
2399
+ this.overlayService.setSelect(event.element, event.node.type, this.activeMode, event.node, event.componentRef);
2400
+ this.selectPanel(PanelTypes.STYLES);
2401
+ this.panelEventService.emit(PanelEventTypes.ELEMENT_SELECT, { node: event.node, componentRef: event.componentRef });
2402
+ }
2403
+ handleMouseEnter(event) {
2404
+ if (this.isDragging || this.activeMode === BuilderMode.Pan)
2405
+ return;
2406
+ cancelAnimationFrame(this.hoverHideRaf);
2407
+ this.overlayService.setHover(event.element, event.node.type, this.activeMode);
2408
+ }
2409
+ handleMouseLeave() {
2410
+ if (this.isDragging || this.activeMode === BuilderMode.Pan)
2411
+ return;
2412
+ cancelAnimationFrame(this.hoverHideRaf);
2413
+ this.hoverHideRaf = requestAnimationFrame(() => this.overlayService.clearHover());
2414
+ }
2415
+ handleCanvasClick() {
2416
+ // Only fires when clicking empty canvas space (renderers call stopPropagation)
2417
+ this.selectedItem = null;
2418
+ this.overlayService.clearSelect();
2419
+ }
2420
+ onTreeNodeSelect(node) {
2421
+ this.selectedItem = node;
2422
+ const ref = this.overlayService.getNodeRef(node);
2423
+ if (ref) {
2424
+ this.panelEventService.emit(PanelEventTypes.ELEMENT_SELECT, { node, componentRef: ref.componentRef });
2425
+ }
2426
+ this.selectPanel(PanelTypes.STYLES);
2427
+ }
2428
+ onTreeNodeAction(event) {
2429
+ const { action, node } = event;
2430
+ const result = this.findNodeInTree(node, this.layout);
2431
+ if (!result)
2432
+ return;
2433
+ if (action === NodeAction.Delete) {
2434
+ result.parent.splice(result.index, 1);
2435
+ this.selectedItem = null;
2436
+ this.overlayService.clearSelect();
2437
+ }
2438
+ else if (action === NodeAction.Duplicate) {
2439
+ const clone = JSON.parse(JSON.stringify(node));
2440
+ result.parent.splice(result.index + 1, 0, clone);
2441
+ }
2442
+ }
2443
+ onTreeNodeMove() {
2444
+ // Tree drag moved a node — canvas will re-render via change detection.
2445
+ // Clear selection since the moved node's DOM element may be recreated.
2446
+ this.selectedItem = null;
2447
+ this.overlayService.clearSelect();
2448
+ }
2449
+ // ── Toolbar actions ──────────────────────────────────────
2450
+ onToolbarAction(event) {
2451
+ const { action, entry } = event;
2452
+ const node = entry.node;
2453
+ if (!node)
2454
+ return;
2455
+ switch (action) {
2456
+ case ToolbarAction.Delete: {
2457
+ const result = this.findNodeInTree(node, this.layout);
2458
+ if (result) {
2459
+ result.parent.splice(result.index, 1);
2460
+ this.selectedItem = null;
2461
+ this.overlayService.clearSelect();
2462
+ }
2463
+ break;
2464
+ }
2465
+ case ToolbarAction.Duplicate: {
2466
+ const result = this.findNodeInTree(node, this.layout);
2467
+ if (result) {
2468
+ const clone = JSON.parse(JSON.stringify(node));
2469
+ result.parent.splice(result.index + 1, 0, clone);
2470
+ }
2471
+ break;
2472
+ }
2473
+ case ToolbarAction.Play: {
2474
+ this.interactionEngine.replay(node);
2475
+ break;
2476
+ }
2477
+ case ToolbarAction.SaveAsBlock: {
2478
+ this.pendingBlockNode = node;
2479
+ this.saveBlockName = '';
2480
+ this.showSaveBlockDialog = true;
2481
+ break;
2482
+ }
2483
+ case ToolbarAction.SelectParent: {
2484
+ const parentNode = this.findParentNode(node, this.layout);
2485
+ if (parentNode) {
2486
+ const ref = this.overlayService.getNodeRef(parentNode);
2487
+ if (ref) {
2488
+ this.selectedItem = parentNode;
2489
+ this.overlayService.setSelect(ref.element, parentNode.type, this.activeMode, parentNode, ref.componentRef);
2490
+ this.panelEventService.emit(PanelEventTypes.ELEMENT_SELECT, { node: parentNode, componentRef: ref.componentRef });
2491
+ }
2492
+ }
2493
+ else {
2494
+ // Already at root level — deselect
2495
+ this.selectedItem = null;
2496
+ this.overlayService.clearSelect();
2497
+ }
2498
+ break;
2499
+ }
2500
+ }
2501
+ }
2502
+ confirmSaveBlock() {
2503
+ if (!this.saveBlockName.trim() || !this.pendingBlockNode)
2504
+ return;
2505
+ this.saveBlock.emit({ name: this.saveBlockName.trim(), node: this.pendingBlockNode });
2506
+ this.showSaveBlockDialog = false;
2507
+ this.pendingBlockNode = null;
2508
+ }
2509
+ cancelSaveBlock() {
2510
+ this.showSaveBlockDialog = false;
2511
+ this.pendingBlockNode = null;
2512
+ }
2513
+ deepCloneWithNewIds(node) {
2514
+ const clone = JSON.parse(JSON.stringify(node));
2515
+ this.assignNewIds(clone);
2516
+ return clone;
2517
+ }
2518
+ assignNewIds(node) {
2519
+ node.id = Math.random().toString(36).slice(2, 10);
2520
+ if (node.styleId)
2521
+ node.styleId = Math.random().toString(36).slice(2, 10);
2522
+ node.children?.forEach(child => this.assignNewIds(child));
2523
+ }
2524
+ onPageScroll(event) {
2525
+ const contentH = event.scrollHeight;
2526
+ const wrapperH = event.clientHeight;
2527
+ if (contentH <= wrapperH) {
2528
+ this.isScrollbarVisible = false;
2529
+ return;
2530
+ }
2531
+ // .canvas-scrollbar has top:20px; bottom:20px — visible track is 40px shorter.
2532
+ const trackH = wrapperH - 40;
2533
+ const thumbH = Math.max((trackH / contentH) * trackH, 32);
2534
+ // Use Lenis progress (0–1) when available — more accurate than recomputing from scroll/limit.
2535
+ const progress = event.scrollPercentage != null ? event.scrollPercentage / 100
2536
+ : event.scrollTop / (contentH - wrapperH);
2537
+ const thumbTop = progress * (trackH - thumbH);
2538
+ this.scrollThumbHeight = thumbH;
2539
+ this.scrollThumbTop = thumbTop;
2540
+ this.isScrollbarVisible = true;
2541
+ // .canvas-scrollbar is position:absolute inside the Lenis wrapper.
2542
+ // When Lenis calls wrapper.scrollTo(), scrollTop shifts all absolute children.
2543
+ // Counteract with translateY(scrollTop) so the track stays visually anchored.
2544
+ this.scrollbarTranslate = event.scrollTop;
2545
+ this.cdr.markForCheck();
2546
+ if (this._scrollbarTimer)
2547
+ clearTimeout(this._scrollbarTimer);
2548
+ this._scrollbarTimer = setTimeout(() => {
2549
+ this.isScrollbarVisible = false;
2550
+ this.cdr.markForCheck();
2551
+ }, 1200);
2552
+ }
2553
+ findNodeInTree(target, children) {
2554
+ for (let i = 0; i < children.length; i++) {
2555
+ if (children[i] === target) {
2556
+ return { parent: children, index: i };
2557
+ }
2558
+ if (children[i].children) {
2559
+ const found = this.findNodeInTree(target, children[i].children);
2560
+ if (found)
2561
+ return found;
2562
+ }
2563
+ }
2564
+ return null;
2565
+ }
2566
+ findParentNode(target, children, parent) {
2567
+ for (const child of children) {
2568
+ if (child === target)
2569
+ return parent || null;
2570
+ if (child.children) {
2571
+ const found = this.findParentNode(target, child.children, child);
2572
+ if (found)
2573
+ return found;
2574
+ }
2575
+ }
2576
+ return null;
2577
+ }
2578
+ handlePanelEvents(panelEvent) {
2579
+ if (panelEvent.type === PanelEventTypes.PANEL_CLOSE) {
2580
+ this.isPanelOpen = false;
2581
+ return;
2582
+ }
2583
+ if (panelEvent.type === PanelEventTypes.PANEL_OPEN) {
2584
+ this.isPanelOpen = true;
2585
+ }
2586
+ }
2587
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderComponent, deps: [{ token: ComponentRegistryService }, { token: OverlayService }, { token: PanelEventService }, { token: DragEngineService }, { token: DataSourceRegistryService }, { token: ViewportService }, { token: InteractionEngineService }, { token: i0.ChangeDetectorRef }, { token: i0.ApplicationRef }], target: i0.ɵɵFactoryTarget.Component }); }
2588
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: BuilderComponent, isStandalone: false, selector: "app-builder", inputs: { layout: "layout", dataSources: "dataSources", orgId: "orgId", routeParams: "routeParams", pageName: "pageName", isSaving: "isSaving", pageStatus: "pageStatus", pageSettings: "pageSettings" }, outputs: { save: "save", back: "back", preview: "preview", publishToggle: "publishToggle", pageSettingsChange: "pageSettingsChange", applyAnimationToAll: "applyAnimationToAll", saveBlock: "saveBlock" }, viewQueries: [{ propertyName: "previewCanvas", first: true, predicate: ["previewCanvas"], descendants: true }, { propertyName: "previewScrollRef", first: true, predicate: ["previewScroll"], descendants: true }, { propertyName: "lenisContentRef", first: true, predicate: ["lenisContent"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"builder\">\n <header class=\"builder-header\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n </div>\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\">\n <i class=\"ph-thin ph-file\"></i>\n </button>\n </div>\n </header>\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <aside class=\"sidebar\">\n <div *ngIf=\"activeSidebar === 'components'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\" [layout]=\"layout\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n </aside>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Custom Lenis scrollbar \u2014 inside .preview-scroll (Lenis wrapper).\n position:absolute shifts with scrollTop, so we counteract with\n transform:translateY(scrollTop) to keep it visually anchored. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\"\n [style.transform]=\"'translateY(' + scrollbarTranslate + 'px)'\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <iox-page-component\n [style.width.px]=\"viewportWidth\"\n [style.transform]=\"viewportTransform\">\n\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <div *ngFor=\"let item of layout\"\n class=\"preview-node\"\n [class.is-selected]=\"selectedItem === item\"\n [style.width]=\"getNodeWidth(item)\"\n ioxDraggable\n [ioxDragData]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\">\n <ng-container ioxRender=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </div>\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeDevice]=\"activeDevice\"\n [activeZoom]=\"activeZoom\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (deviceChange)=\"handleToolbarDeviceChange($event)\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\">\n </app-toolbar>\n\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n <div *ngIf=\"showSaveBlockDialog\" class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div *ngIf=\"showSaveBlockDialog\" class=\"save-block-dialog\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .builder-header{background:#fff;padding:.5rem 1rem;border-bottom:1px solid var(--p-surface-200);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}.builder .builder-header .header-back{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;background:transparent;border-radius:6px;cursor:pointer;color:var(--p-text-muted-color);font-size:16px;transition:background .15s;margin-inline-end:.25rem}.builder .builder-header .header-back:hover{background:var(--p-surface-100, #f1f5f9);color:var(--p-text-color)}.builder .builder-header .header-page-name{flex:1;text-align:center;font-size:.8rem;font-weight:500;color:var(--p-text-muted-color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-inline:1rem}.builder .sidebar-switcher,.builder .panel-switcher{display:flex;gap:.5rem}.builder .sidebar-switcher p-button ::ng-deep .p-button,.builder .panel-switcher p-button ::ng-deep .p-button{color:#000;background:transparent;border:1px solid transparent;transition:border-color .2s}.builder .sidebar-switcher p-button ::ng-deep .p-button:hover,.builder .panel-switcher p-button ::ng-deep .p-button:hover{border-color:#cb9090}.builder .sidebar-switcher p-button.active ::ng-deep .p-button,.builder .panel-switcher p-button.active ::ng-deep .p-button{background:#cb9090;color:#fff;border-color:#cb9090}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden}.builder .builder-main app-toolbar{position:fixed;bottom:1.25rem;left:50%;transform:translate(-50%);z-index:1000;pointer-events:auto}.builder .sidebar{width:220px;min-width:220px;background:var(--p-surface-0);border-right:1px solid var(--p-surface-200);overflow-y:auto;overflow-x:hidden;flex-shrink:0}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;background:#fff;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:hidden!important}.builder .preview-node{position:relative}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i10.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i11.IoxPageComponent, selector: "iox-page-component", inputs: ["class"], outputs: ["scrollEvent"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId"] }, { kind: "component", type: OverlayComponent, selector: "app-overlay", outputs: ["toolbarAction"] }, { kind: "component", type: ToolbarComponent, selector: "app-toolbar", inputs: ["activeMode", "activeDevice", "activeZoom", "canUndo", "canRedo", "isSaving", "pageStatus"], outputs: ["modeChange", "deviceChange", "zoomChange", "undo", "redo", "save", "preview", "publishToggle"] }, { kind: "component", type: LayerTreeComponent, selector: "app-layer-tree", inputs: ["layout"], outputs: ["nodeSelect", "nodeAction", "nodeMove"] }] }); }
2589
+ }
2590
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderComponent, decorators: [{
2591
+ type: Component,
2592
+ args: [{ selector: 'app-builder', standalone: false, template: "<div class=\"builder\">\n <header class=\"builder-header\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n </div>\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\">\n <i class=\"ph-thin ph-file\"></i>\n </button>\n </div>\n </header>\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <aside class=\"sidebar\">\n <div *ngIf=\"activeSidebar === 'components'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\" [layout]=\"layout\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n </aside>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Custom Lenis scrollbar \u2014 inside .preview-scroll (Lenis wrapper).\n position:absolute shifts with scrollTop, so we counteract with\n transform:translateY(scrollTop) to keep it visually anchored. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\"\n [style.transform]=\"'translateY(' + scrollbarTranslate + 'px)'\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <iox-page-component\n [style.width.px]=\"viewportWidth\"\n [style.transform]=\"viewportTransform\">\n\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <div *ngFor=\"let item of layout\"\n class=\"preview-node\"\n [class.is-selected]=\"selectedItem === item\"\n [style.width]=\"getNodeWidth(item)\"\n ioxDraggable\n [ioxDragData]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\">\n <ng-container ioxRender=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </div>\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeDevice]=\"activeDevice\"\n [activeZoom]=\"activeZoom\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (deviceChange)=\"handleToolbarDeviceChange($event)\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\">\n </app-toolbar>\n\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n <div *ngIf=\"showSaveBlockDialog\" class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div *ngIf=\"showSaveBlockDialog\" class=\"save-block-dialog\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .builder-header{background:#fff;padding:.5rem 1rem;border-bottom:1px solid var(--p-surface-200);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}.builder .builder-header .header-back{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;background:transparent;border-radius:6px;cursor:pointer;color:var(--p-text-muted-color);font-size:16px;transition:background .15s;margin-inline-end:.25rem}.builder .builder-header .header-back:hover{background:var(--p-surface-100, #f1f5f9);color:var(--p-text-color)}.builder .builder-header .header-page-name{flex:1;text-align:center;font-size:.8rem;font-weight:500;color:var(--p-text-muted-color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-inline:1rem}.builder .sidebar-switcher,.builder .panel-switcher{display:flex;gap:.5rem}.builder .sidebar-switcher p-button ::ng-deep .p-button,.builder .panel-switcher p-button ::ng-deep .p-button{color:#000;background:transparent;border:1px solid transparent;transition:border-color .2s}.builder .sidebar-switcher p-button ::ng-deep .p-button:hover,.builder .panel-switcher p-button ::ng-deep .p-button:hover{border-color:#cb9090}.builder .sidebar-switcher p-button.active ::ng-deep .p-button,.builder .panel-switcher p-button.active ::ng-deep .p-button{background:#cb9090;color:#fff;border-color:#cb9090}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden}.builder .builder-main app-toolbar{position:fixed;bottom:1.25rem;left:50%;transform:translate(-50%);z-index:1000;pointer-events:auto}.builder .sidebar{width:220px;min-width:220px;background:var(--p-surface-0);border-right:1px solid var(--p-surface-200);overflow-y:auto;overflow-x:hidden;flex-shrink:0}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;background:#fff;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:hidden!important}.builder .preview-node{position:relative}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}\n"] }]
2593
+ }], ctorParameters: () => [{ type: ComponentRegistryService }, { type: OverlayService }, { type: PanelEventService }, { type: DragEngineService }, { type: DataSourceRegistryService }, { type: ViewportService }, { type: InteractionEngineService }, { type: i0.ChangeDetectorRef }, { type: i0.ApplicationRef }], propDecorators: { layout: [{
2594
+ type: Input
2595
+ }], dataSources: [{
2596
+ type: Input
2597
+ }], orgId: [{
2598
+ type: Input
2599
+ }], routeParams: [{
2600
+ type: Input
2601
+ }], pageName: [{
2602
+ type: Input
2603
+ }], isSaving: [{
2604
+ type: Input
2605
+ }], pageStatus: [{
2606
+ type: Input
2607
+ }], pageSettings: [{
2608
+ type: Input
2609
+ }], save: [{
2610
+ type: Output
2611
+ }], back: [{
2612
+ type: Output
2613
+ }], preview: [{
2614
+ type: Output
2615
+ }], publishToggle: [{
2616
+ type: Output
2617
+ }], pageSettingsChange: [{
2618
+ type: Output
2619
+ }], applyAnimationToAll: [{
2620
+ type: Output
2621
+ }], saveBlock: [{
2622
+ type: Output
2623
+ }], previewCanvas: [{
2624
+ type: ViewChild,
2625
+ args: ['previewCanvas']
2626
+ }], previewScrollRef: [{
2627
+ type: ViewChild,
2628
+ args: ['previewScroll']
2629
+ }], lenisContentRef: [{
2630
+ type: ViewChild,
2631
+ args: ['lenisContent']
2632
+ }] } });
2633
+
2634
+ class IoxDropzoneDirective {
2635
+ constructor(el, dragEngine, cdr) {
2636
+ this.el = el;
2637
+ this.dragEngine = dragEngine;
2638
+ this.cdr = cdr;
2639
+ this.ioxDropzoneId = '';
2640
+ this.ioxDropzoneData = [];
2641
+ this.ioxDrop = new EventEmitter();
2642
+ }
2643
+ ngOnInit() {
2644
+ this.dragEngine.registerDropzone(this.ioxDropzoneId, this.el.nativeElement, this.ioxDropzoneData, this.cdr, (evt) => this.ioxDrop.emit(evt), this.ioxDropzonePostDrop);
2645
+ }
2646
+ ngOnDestroy() {
2647
+ this.dragEngine.unregisterDropzone(this.ioxDropzoneId, this.el.nativeElement);
2648
+ }
2649
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxDropzoneDirective, deps: [{ token: i0.ElementRef }, { token: DragEngineService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive }); }
2650
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.11", type: IoxDropzoneDirective, isStandalone: false, selector: "[ioxDropzone]", inputs: { ioxDropzoneId: "ioxDropzoneId", ioxDropzoneData: "ioxDropzoneData", ioxDropzonePostDrop: "ioxDropzonePostDrop" }, outputs: { ioxDrop: "ioxDrop" }, ngImport: i0 }); }
2651
+ }
2652
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxDropzoneDirective, decorators: [{
2653
+ type: Directive,
2654
+ args: [{
2655
+ selector: '[ioxDropzone]',
2656
+ standalone: false,
2657
+ }]
2658
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: DragEngineService }, { type: i0.ChangeDetectorRef }], propDecorators: { ioxDropzoneId: [{
2659
+ type: Input
2660
+ }], ioxDropzoneData: [{
2661
+ type: Input
2662
+ }], ioxDropzonePostDrop: [{
2663
+ type: Input
2664
+ }], ioxDrop: [{
2665
+ type: Output
2666
+ }] } });
2667
+
2668
+ class SectionComponent {
2669
+ constructor(dragEngine, cdr) {
2670
+ this.dragEngine = dragEngine;
2671
+ this.cdr = cdr;
2672
+ this.children = [];
2673
+ this.style = {};
2674
+ this.nodeId = '';
2675
+ this.dropListId = '';
2676
+ this.childClick = new EventEmitter();
2677
+ this.childMouseEnter = new EventEmitter();
2678
+ this.childMouseLeave = new EventEmitter();
2679
+ }
2680
+ ngOnInit() { }
2681
+ get listOrientation() {
2682
+ const d = this.style?.['display'];
2683
+ if (d === 'flex' || d === 'inline-flex') {
2684
+ const dir = this.style?.['flexDirection'];
2685
+ if (!dir || dir === 'row' || dir === 'row-reverse')
2686
+ return 'horizontal';
2687
+ }
2688
+ return 'vertical';
2689
+ }
2690
+ getChildWidth(node) {
2691
+ if (!node.styleTraits)
2692
+ return undefined;
2693
+ for (const group of node.styleTraits) {
2694
+ for (const trait of group.traits) {
2695
+ if (trait.name === 'width' && trait.default !== undefined)
2696
+ return trait.default;
2697
+ }
2698
+ }
2699
+ return undefined;
2700
+ }
2701
+ ngOnDestroy() { }
2702
+ onDrop(event) {
2703
+ this.dragEngine.handleDrop(this.children, event, this.dropListId, this.cdr);
2704
+ }
2705
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: SectionComponent, deps: [{ token: DragEngineService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
2706
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: SectionComponent, isStandalone: false, selector: "app-section", inputs: { children: "children", style: "style", nodeId: "nodeId", dropListId: "dropListId" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
2707
+ <div class="section-root"
2708
+ [ngClass]="['iox-node-' + nodeId, listOrientation === 'horizontal' ? 'is-horizontal' : '']"
2709
+ ioxDropzone
2710
+ [ioxDropzoneId]="dropListId"
2711
+ [ioxDropzoneData]="children"
2712
+ (ioxDrop)="onDrop($event)">
2713
+
2714
+ <div *ngIf="!children.length" class="section-placeholder">
2715
+ <i class="ph-thin ph-rows"></i>
2716
+ <span>Drop components here</span>
2717
+ </div>
2718
+
2719
+ <div *ngFor="let child of children"
2720
+ class="section-child"
2721
+ [style.width]="getChildWidth(child)"
2722
+ ioxDraggable
2723
+ [ioxDragData]="child"
2724
+ [ioxDragSourceId]="dropListId">
2725
+ <ng-container
2726
+ [ioxRender]="child"
2727
+ (onClick)="childClick.emit($event)"
2728
+ (onMouseEnter)="childMouseEnter.emit($event)"
2729
+ (onMouseLeave)="childMouseLeave.emit()">
2730
+ </ng-container>
2731
+ </div>
2732
+ </div>
2733
+ `, isInline: true, styles: [".section-root{position:relative;box-sizing:border-box}.section-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:0}.section-placeholder i{font-size:22px}.section-child{min-width:0}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
2734
+ }
2735
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: SectionComponent, decorators: [{
2736
+ type: Component,
2737
+ args: [{ selector: 'app-section', template: `
2738
+ <div class="section-root"
2739
+ [ngClass]="['iox-node-' + nodeId, listOrientation === 'horizontal' ? 'is-horizontal' : '']"
2740
+ ioxDropzone
2741
+ [ioxDropzoneId]="dropListId"
2742
+ [ioxDropzoneData]="children"
2743
+ (ioxDrop)="onDrop($event)">
2744
+
2745
+ <div *ngIf="!children.length" class="section-placeholder">
2746
+ <i class="ph-thin ph-rows"></i>
2747
+ <span>Drop components here</span>
2748
+ </div>
2749
+
2750
+ <div *ngFor="let child of children"
2751
+ class="section-child"
2752
+ [style.width]="getChildWidth(child)"
2753
+ ioxDraggable
2754
+ [ioxDragData]="child"
2755
+ [ioxDragSourceId]="dropListId">
2756
+ <ng-container
2757
+ [ioxRender]="child"
2758
+ (onClick)="childClick.emit($event)"
2759
+ (onMouseEnter)="childMouseEnter.emit($event)"
2760
+ (onMouseLeave)="childMouseLeave.emit()">
2761
+ </ng-container>
2762
+ </div>
2763
+ </div>
2764
+ `, standalone: false, styles: [".section-root{position:relative;box-sizing:border-box}.section-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:0}.section-placeholder i{font-size:22px}.section-child{min-width:0}\n"] }]
2765
+ }], ctorParameters: () => [{ type: DragEngineService }, { type: i0.ChangeDetectorRef }], propDecorators: { children: [{
2766
+ type: Input
2767
+ }], style: [{
2768
+ type: Input
2769
+ }], nodeId: [{
2770
+ type: Input
2771
+ }], dropListId: [{
2772
+ type: Input
2773
+ }], childClick: [{
2774
+ type: Output
2775
+ }], childMouseEnter: [{
2776
+ type: Output
2777
+ }], childMouseLeave: [{
2778
+ type: Output
2779
+ }] } });
2780
+
2781
+ class BuilderContainerComponent {
2782
+ constructor(dragEngine, cdr) {
2783
+ this.dragEngine = dragEngine;
2784
+ this.cdr = cdr;
2785
+ this.children = [];
2786
+ this.style = {};
2787
+ this.nodeId = '';
2788
+ this.dropListId = '';
2789
+ this.childClick = new EventEmitter();
2790
+ this.childMouseEnter = new EventEmitter();
2791
+ this.childMouseLeave = new EventEmitter();
2792
+ }
2793
+ ngOnInit() { }
2794
+ ngOnDestroy() { }
2795
+ get listOrientation() {
2796
+ const d = this.style?.['display'];
2797
+ if (d === 'flex' || d === 'inline-flex') {
2798
+ const dir = this.style?.['flexDirection'];
2799
+ if (!dir || dir === 'row' || dir === 'row-reverse')
2800
+ return 'horizontal';
2801
+ }
2802
+ return 'vertical';
2803
+ }
2804
+ getChildWidth(node) {
2805
+ if (!node.styleTraits)
2806
+ return undefined;
2807
+ for (const group of node.styleTraits) {
2808
+ for (const trait of group.traits) {
2809
+ if (trait.name === 'width' && trait.default !== undefined)
2810
+ return trait.default;
2811
+ }
2812
+ }
2813
+ return undefined;
2814
+ }
2815
+ onDrop(event) {
2816
+ this.dragEngine.handleDrop(this.children, event, this.dropListId, this.cdr);
2817
+ }
2818
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderContainerComponent, deps: [{ token: DragEngineService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
2819
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: BuilderContainerComponent, isStandalone: false, selector: "app-builder-container", inputs: { children: "children", style: "style", nodeId: "nodeId", dropListId: "dropListId" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
2820
+ <div class="container-root"
2821
+ [ngClass]="['iox-node-' + nodeId, listOrientation === 'horizontal' ? 'is-horizontal' : '']"
2822
+ ioxDropzone
2823
+ [ioxDropzoneId]="dropListId"
2824
+ [ioxDropzoneData]="children"
2825
+ (ioxDrop)="onDrop($event)">
2826
+
2827
+ <div *ngIf="!children.length" class="container-placeholder">
2828
+ <i class="ph-thin ph-square"></i>
2829
+ <span>Drop components here</span>
2830
+ </div>
2831
+
2832
+ <div *ngFor="let child of children"
2833
+ class="container-child"
2834
+ [style.width]="getChildWidth(child)"
2835
+ ioxDraggable
2836
+ [ioxDragData]="child"
2837
+ [ioxDragSourceId]="dropListId">
2838
+ <ng-container
2839
+ [ioxRender]="child"
2840
+ (onClick)="childClick.emit($event)"
2841
+ (onMouseEnter)="childMouseEnter.emit($event)"
2842
+ (onMouseLeave)="childMouseLeave.emit()">
2843
+ </ng-container>
2844
+ </div>
2845
+ </div>
2846
+ `, isInline: true, styles: [".container-root{position:relative;box-sizing:border-box}.container-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:0}.container-placeholder i{font-size:18px}.container-child{min-width:0}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
2847
+ }
2848
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderContainerComponent, decorators: [{
2849
+ type: Component,
2850
+ args: [{ selector: 'app-builder-container', template: `
2851
+ <div class="container-root"
2852
+ [ngClass]="['iox-node-' + nodeId, listOrientation === 'horizontal' ? 'is-horizontal' : '']"
2853
+ ioxDropzone
2854
+ [ioxDropzoneId]="dropListId"
2855
+ [ioxDropzoneData]="children"
2856
+ (ioxDrop)="onDrop($event)">
2857
+
2858
+ <div *ngIf="!children.length" class="container-placeholder">
2859
+ <i class="ph-thin ph-square"></i>
2860
+ <span>Drop components here</span>
2861
+ </div>
2862
+
2863
+ <div *ngFor="let child of children"
2864
+ class="container-child"
2865
+ [style.width]="getChildWidth(child)"
2866
+ ioxDraggable
2867
+ [ioxDragData]="child"
2868
+ [ioxDragSourceId]="dropListId">
2869
+ <ng-container
2870
+ [ioxRender]="child"
2871
+ (onClick)="childClick.emit($event)"
2872
+ (onMouseEnter)="childMouseEnter.emit($event)"
2873
+ (onMouseLeave)="childMouseLeave.emit()">
2874
+ </ng-container>
2875
+ </div>
2876
+ </div>
2877
+ `, standalone: false, styles: [".container-root{position:relative;box-sizing:border-box}.container-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:0}.container-placeholder i{font-size:18px}.container-child{min-width:0}\n"] }]
2878
+ }], ctorParameters: () => [{ type: DragEngineService }, { type: i0.ChangeDetectorRef }], propDecorators: { children: [{
2879
+ type: Input
2880
+ }], style: [{
2881
+ type: Input
2882
+ }], nodeId: [{
2883
+ type: Input
2884
+ }], dropListId: [{
2885
+ type: Input
2886
+ }], childClick: [{
2887
+ type: Output
2888
+ }], childMouseEnter: [{
2889
+ type: Output
2890
+ }], childMouseLeave: [{
2891
+ type: Output
2892
+ }] } });
2893
+
2894
+ class BuilderLinkedContainerComponent {
2895
+ constructor(dragEngine, cdr, elRef, isPreview) {
2896
+ this.dragEngine = dragEngine;
2897
+ this.cdr = cdr;
2898
+ this.elRef = elRef;
2899
+ this.isPreview = isPreview;
2900
+ this.children = [];
2901
+ this.style = {};
2902
+ this.nodeId = '';
2903
+ this.dropListId = '';
2904
+ this.linkType = 'external';
2905
+ this.url = 'https://';
2906
+ this.pageRoute = '';
2907
+ this.target = '_self';
2908
+ this.childClick = new EventEmitter();
2909
+ this.childMouseEnter = new EventEmitter();
2910
+ this.childMouseLeave = new EventEmitter();
2911
+ this.capturePreventDefault = (e) => e.preventDefault();
2912
+ }
2913
+ ngOnInit() {
2914
+ if (!this.isPreview) {
2915
+ this.elRef.nativeElement.addEventListener('click', this.capturePreventDefault, true);
2916
+ }
2917
+ }
2918
+ ngOnDestroy() {
2919
+ if (!this.isPreview) {
2920
+ this.elRef.nativeElement.removeEventListener('click', this.capturePreventDefault, true);
2921
+ }
2922
+ }
2923
+ get resolvedHref() {
2924
+ if (this.linkType !== 'page')
2925
+ return this.url || '#';
2926
+ return this.pageRoute || '#';
2927
+ }
2928
+ get listOrientation() {
2929
+ const d = this.style?.['display'];
2930
+ if (d === 'flex' || d === 'inline-flex') {
2931
+ const dir = this.style?.['flexDirection'];
2932
+ if (!dir || dir === 'row' || dir === 'row-reverse')
2933
+ return 'horizontal';
2934
+ }
2935
+ return 'vertical';
2936
+ }
2937
+ getChildWidth(node) {
2938
+ for (const group of (node.styleTraits || [])) {
2939
+ for (const trait of group.traits) {
2940
+ if (trait.name === 'width' && trait.default !== undefined)
2941
+ return trait.default;
2942
+ }
2943
+ }
2944
+ return undefined;
2945
+ }
2946
+ onDrop(event) {
2947
+ this.dragEngine.handleDrop(this.children, event, this.dropListId, this.cdr);
2948
+ }
2949
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderLinkedContainerComponent, deps: [{ token: DragEngineService }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: IS_PREVIEW, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
2950
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: BuilderLinkedContainerComponent, isStandalone: false, selector: "app-builder-linked-container", inputs: { children: "children", style: "style", nodeId: "nodeId", dropListId: "dropListId", linkType: "linkType", url: "url", pageRoute: "pageRoute", target: "target" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
2951
+ <a [href]="resolvedHref"
2952
+ [target]="target"
2953
+ [ngStyle]="style"
2954
+ [ngClass]="['iox-node-' + nodeId, listOrientation === 'horizontal' ? 'is-horizontal' : '']"
2955
+ class="linked-container-root"
2956
+ ioxDropzone
2957
+ [ioxDropzoneId]="dropListId"
2958
+ [ioxDropzoneData]="children"
2959
+ (ioxDrop)="onDrop($event)">
2960
+
2961
+ <div *ngIf="!children.length" class="linked-container-placeholder">
2962
+ <i class="ph-thin ph-link"></i>
2963
+ <span>Drop components here</span>
2964
+ </div>
2965
+
2966
+ <div *ngFor="let child of children"
2967
+ class="linked-container-child"
2968
+ [style.width]="getChildWidth(child)"
2969
+ ioxDraggable
2970
+ [ioxDragData]="child"
2971
+ [ioxDragSourceId]="dropListId">
2972
+ <ng-container
2973
+ [ioxRender]="child"
2974
+ (onClick)="childClick.emit($event)"
2975
+ (onMouseEnter)="childMouseEnter.emit($event)"
2976
+ (onMouseLeave)="childMouseLeave.emit()">
2977
+ </ng-container>
2978
+ </div>
2979
+ </a>
2980
+ `, isInline: true, styles: [".linked-container-root{display:block;position:relative;box-sizing:border-box;text-decoration:none;color:inherit;cursor:pointer}.linked-container-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.linked-container-placeholder i{font-size:18px}.linked-container-child{min-width:0}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
2981
+ }
2982
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderLinkedContainerComponent, decorators: [{
2983
+ type: Component,
2984
+ args: [{ selector: 'app-builder-linked-container', template: `
2985
+ <a [href]="resolvedHref"
2986
+ [target]="target"
2987
+ [ngStyle]="style"
2988
+ [ngClass]="['iox-node-' + nodeId, listOrientation === 'horizontal' ? 'is-horizontal' : '']"
2989
+ class="linked-container-root"
2990
+ ioxDropzone
2991
+ [ioxDropzoneId]="dropListId"
2992
+ [ioxDropzoneData]="children"
2993
+ (ioxDrop)="onDrop($event)">
2994
+
2995
+ <div *ngIf="!children.length" class="linked-container-placeholder">
2996
+ <i class="ph-thin ph-link"></i>
2997
+ <span>Drop components here</span>
2998
+ </div>
2999
+
3000
+ <div *ngFor="let child of children"
3001
+ class="linked-container-child"
3002
+ [style.width]="getChildWidth(child)"
3003
+ ioxDraggable
3004
+ [ioxDragData]="child"
3005
+ [ioxDragSourceId]="dropListId">
3006
+ <ng-container
3007
+ [ioxRender]="child"
3008
+ (onClick)="childClick.emit($event)"
3009
+ (onMouseEnter)="childMouseEnter.emit($event)"
3010
+ (onMouseLeave)="childMouseLeave.emit()">
3011
+ </ng-container>
3012
+ </div>
3013
+ </a>
3014
+ `, standalone: false, styles: [".linked-container-root{display:block;position:relative;box-sizing:border-box;text-decoration:none;color:inherit;cursor:pointer}.linked-container-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.linked-container-placeholder i{font-size:18px}.linked-container-child{min-width:0}\n"] }]
3015
+ }], ctorParameters: () => [{ type: DragEngineService }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: undefined, decorators: [{
3016
+ type: Optional
3017
+ }, {
3018
+ type: Inject,
3019
+ args: [IS_PREVIEW]
3020
+ }] }], propDecorators: { children: [{
3021
+ type: Input
3022
+ }], style: [{
3023
+ type: Input
3024
+ }], nodeId: [{
3025
+ type: Input
3026
+ }], dropListId: [{
3027
+ type: Input
3028
+ }], linkType: [{
3029
+ type: Input
3030
+ }], url: [{
3031
+ type: Input
3032
+ }], pageRoute: [{
3033
+ type: Input
3034
+ }], target: [{
3035
+ type: Input
3036
+ }], childClick: [{
3037
+ type: Output
3038
+ }], childMouseEnter: [{
3039
+ type: Output
3040
+ }], childMouseLeave: [{
3041
+ type: Output
3042
+ }] } });
3043
+
3044
+ /** Optional CMS content fetcher. Provide from the host app to enable Repeater data preview. */
3045
+ const IOX_CONTENT_SERVICE = new InjectionToken('IOX_CONTENT_SERVICE');
3046
+ /** Optional font manager. Provide from the host app to populate the fontFamily dropdown. */
3047
+ const IOX_FONT_MANAGER = new InjectionToken('IOX_FONT_MANAGER');
3048
+
3049
+ /**
3050
+ * Resolves a dot-notation path (with optional array bracket notation) against an object.
3051
+ * Examples: 'title', 'author.name', 'tags[0]', 'items[2].title'
3052
+ */
3053
+ function resolvePath(obj, path) {
3054
+ return path.split('.').reduce((current, key) => {
3055
+ if (current == null)
3056
+ return undefined;
3057
+ const bracketMatch = key.match(/^(\w+)\[(\d+)]$/);
3058
+ if (bracketMatch) {
3059
+ const arr = current[bracketMatch[1]];
3060
+ return Array.isArray(arr) ? arr[parseInt(bracketMatch[2], 10)] : undefined;
3061
+ }
3062
+ return current[key];
3063
+ }, obj);
3064
+ }
3065
+ /**
3066
+ * Deep-clones a ComponentNode tree and overrides trait defaults
3067
+ * according to each node's bindings resolved against `item`.
3068
+ *
3069
+ * Each clone gets a new `id` (for Angular tracking / overlay registration)
3070
+ * but shares the original node's CSS class by setting `styleId = originalId`.
3071
+ * This means all repeated preview rows point to the same CSS rule block as the
3072
+ * template node — the style engine only needs one rule per logical slot.
3073
+ */
3074
+ function cloneNodesForItem(nodes, item) {
3075
+ return nodes.map(node => {
3076
+ // Capture the CSS-owning id before clone
3077
+ const cssId = node.styleId ?? node.id;
3078
+ const clone = JSON.parse(JSON.stringify(node));
3079
+ clone.id = Math.random().toString(36).substr(2, 9);
3080
+ // Point this clone and all its descendants at the original CSS rule.
3081
+ clone.styleId = cssId;
3082
+ if (clone.bindings?.length && item) {
3083
+ for (const binding of clone.bindings) {
3084
+ const value = resolvePath(item, binding.path);
3085
+ if (value !== undefined) {
3086
+ for (const trait of clone.traits || []) {
3087
+ if (trait.name === binding.prop) {
3088
+ trait.default = value;
3089
+ }
3090
+ }
3091
+ }
3092
+ }
3093
+ }
3094
+ if (clone.children?.length) {
3095
+ clone.children = cloneNodesForItem(clone.children, item);
3096
+ }
3097
+ return clone;
3098
+ });
3099
+ }
3100
+
3101
+ class BuilderRepeaterComponent {
3102
+ get source() { return this._source; }
3103
+ set source(value) {
3104
+ if (value === this._source)
3105
+ return;
3106
+ this._source = value;
3107
+ if (value) {
3108
+ this.fetchData();
3109
+ }
3110
+ else {
3111
+ this.items = [];
3112
+ this.previewRows = [];
3113
+ }
3114
+ }
3115
+ get limit() { return this._limit; }
3116
+ set limit(value) {
3117
+ if (value === this._limit)
3118
+ return;
3119
+ this._limit = value;
3120
+ if (this._source) {
3121
+ this.fetchData();
3122
+ }
3123
+ }
3124
+ constructor(dragEngine, overlayService, contentService, dsRegistry, panelEventService, activatedRoute, cdr) {
3125
+ this.dragEngine = dragEngine;
3126
+ this.overlayService = overlayService;
3127
+ this.contentService = contentService;
3128
+ this.dsRegistry = dsRegistry;
3129
+ this.panelEventService = panelEventService;
3130
+ this.activatedRoute = activatedRoute;
3131
+ this.cdr = cdr;
3132
+ this.children = [];
3133
+ this.style = {};
3134
+ this.nodeId = '';
3135
+ this.dropListId = '';
3136
+ this.childClick = new EventEmitter();
3137
+ this.childMouseEnter = new EventEmitter();
3138
+ this.childMouseLeave = new EventEmitter();
3139
+ this.items = [];
3140
+ this.previewRows = [];
3141
+ this.isLoading = false;
3142
+ /** Bound reference passed to [ioxDropzonePostDrop] so the directive can call it. */
3143
+ this.refreshPreviewsBound = () => { setTimeout(() => this.refreshPreviews()); };
3144
+ this._source = '';
3145
+ this._limit = 10;
3146
+ this.orgId = 'default';
3147
+ }
3148
+ ngOnInit() {
3149
+ this.orgId = this.activatedRoute.snapshot.paramMap.get('orgId') || 'default';
3150
+ // When a binding is added/removed on any node, regenerate the preview rows.
3151
+ this.bindingSub = this.panelEventService.subscribe(event => {
3152
+ if (event.type !== PanelEventTypes.BINDING_CHANGED)
3153
+ return;
3154
+ const changedNode = event.data?.node;
3155
+ if (!changedNode || !this.items.length)
3156
+ return;
3157
+ if (this.children.some(c => c === changedNode || c.id === changedNode.id)) {
3158
+ this.refreshPreviews();
3159
+ }
3160
+ });
3161
+ }
3162
+ ngOnDestroy() {
3163
+ this.bindingSub?.unsubscribe();
3164
+ }
3165
+ onDrop(event) {
3166
+ this.dragEngine.handleDrop(this.children, event, this.dropListId, this.cdr, () => { if (this.items.length > 0)
3167
+ setTimeout(() => this.refreshPreviews()); });
3168
+ }
3169
+ fetchData() {
3170
+ if (!this._source)
3171
+ return;
3172
+ const ds = this.dsRegistry.findByAlias(this._source);
3173
+ const contentType = ds?.request?.cms?.contentType;
3174
+ if (!contentType || !this.contentService)
3175
+ return;
3176
+ this.isLoading = true;
3177
+ this.cdr.markForCheck();
3178
+ this.contentService.getContent(contentType, this.orgId).subscribe({
3179
+ next: (data) => {
3180
+ const all = Array.isArray(data) ? data : (data?.items || data?.data || []);
3181
+ this.items = all.slice(0, this._limit);
3182
+ this.refreshPreviews();
3183
+ this.isLoading = false;
3184
+ this.cdr.markForCheck();
3185
+ },
3186
+ error: () => {
3187
+ this.isLoading = false;
3188
+ this.cdr.markForCheck();
3189
+ }
3190
+ });
3191
+ }
3192
+ refreshPreviews() {
3193
+ if (!this.items.length || !this.children.length) {
3194
+ this.previewRows = [];
3195
+ this.cdr.markForCheck();
3196
+ return;
3197
+ }
3198
+ this.applyFirstItemToTemplate();
3199
+ this.previewRows = this.items.slice(1).map(item => cloneNodesForItem(this.children, item));
3200
+ this.cdr.markForCheck();
3201
+ }
3202
+ applyFirstItemToTemplate() {
3203
+ const item = this.items[0];
3204
+ if (!item || !this.children.length)
3205
+ return;
3206
+ for (const child of this.children) {
3207
+ if (!child.bindings?.length)
3208
+ continue;
3209
+ const ref = this.overlayService.getNodeRef(child);
3210
+ if (!ref?.componentRef)
3211
+ continue;
3212
+ for (const binding of child.bindings) {
3213
+ const value = resolvePath(item, binding.path);
3214
+ if (value !== undefined) {
3215
+ ref.componentRef.instance[binding.prop] = value;
3216
+ }
3217
+ }
3218
+ ref.componentRef.changeDetectorRef.detectChanges();
3219
+ }
3220
+ }
3221
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderRepeaterComponent, deps: [{ token: DragEngineService }, { token: OverlayService }, { token: IOX_CONTENT_SERVICE, optional: true }, { token: DataSourceRegistryService }, { token: PanelEventService }, { token: i5.ActivatedRoute }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
3222
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: BuilderRepeaterComponent, isStandalone: false, selector: "app-builder-repeater", inputs: { children: "children", style: "style", nodeId: "nodeId", dropListId: "dropListId", source: "source", limit: "limit" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
3223
+ <div class="repeater-root" [ngClass]="['iox-node-' + nodeId, children.length ? 'has-children' : '']">
3224
+
3225
+ <!-- Badge: always visible when empty, absolute hover-only when has children -->
3226
+ <div class="repeater-badge">
3227
+ <i class="ph-thin ph-repeat"></i>
3228
+ <span *ngIf="source">{{ source }}</span>
3229
+ <span *ngIf="!source" class="repeater-badge-empty">No source</span>
3230
+ <span *ngIf="items.length" class="repeater-badge-count">&times; {{ items.length }}</span>
3231
+ <span *ngIf="isLoading" class="repeater-badge-loading"><i class="ph-thin ph-circle-notch"></i></span>
3232
+ </div>
3233
+
3234
+ <!-- Template drop zone — user designs one item here -->
3235
+ <div class="repeater-template"
3236
+ ioxDropzone
3237
+ [ioxDropzoneId]="dropListId"
3238
+ [ioxDropzoneData]="children"
3239
+ [ioxDropzonePostDrop]="refreshPreviewsBound"
3240
+ (ioxDrop)="onDrop($event)">
3241
+
3242
+ <div *ngIf="!children.length" class="repeater-placeholder">
3243
+ <i class="ph-thin ph-rows"></i>
3244
+ <span>Drop a component here as the item template</span>
3245
+ </div>
3246
+
3247
+ <div *ngFor="let child of children"
3248
+ class="repeater-template-child"
3249
+ ioxDraggable
3250
+ [ioxDragData]="child"
3251
+ [ioxDragSourceId]="dropListId">
3252
+ <ng-container
3253
+ [ioxRender]="child"
3254
+ (onClick)="childClick.emit($event)"
3255
+ (onMouseEnter)="childMouseEnter.emit($event)"
3256
+ (onMouseLeave)="childMouseLeave.emit()">
3257
+ </ng-container>
3258
+ </div>
3259
+ </div>
3260
+
3261
+ <!-- Preview rows for items 2..N — read-only clones with real data -->
3262
+ <div *ngFor="let row of previewRows" class="repeater-preview-row">
3263
+ <ng-container *ngFor="let child of row">
3264
+ <ng-container
3265
+ [ioxRender]="child"
3266
+ (onClick)="$event"
3267
+ (onMouseEnter)="$event"
3268
+ (onMouseLeave)="undefined">
3269
+ </ng-container>
3270
+ </ng-container>
3271
+ </div>
3272
+ </div>
3273
+ `, isInline: true, styles: [".repeater-root{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-root.has-children .repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-root.has-children:hover .repeater-badge{opacity:1}.repeater-root.has-children .repeater-template{border:none;border-radius:0;padding:0;min-height:unset}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-template{border:1px dashed rgba(203,144,144,.4);border-radius:0 0 4px 4px;padding:4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-template-child{min-width:0}.repeater-preview-row{opacity:.55;pointer-events:none}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
3274
+ }
3275
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderRepeaterComponent, decorators: [{
3276
+ type: Component,
3277
+ args: [{ selector: 'app-builder-repeater', template: `
3278
+ <div class="repeater-root" [ngClass]="['iox-node-' + nodeId, children.length ? 'has-children' : '']">
3279
+
3280
+ <!-- Badge: always visible when empty, absolute hover-only when has children -->
3281
+ <div class="repeater-badge">
3282
+ <i class="ph-thin ph-repeat"></i>
3283
+ <span *ngIf="source">{{ source }}</span>
3284
+ <span *ngIf="!source" class="repeater-badge-empty">No source</span>
3285
+ <span *ngIf="items.length" class="repeater-badge-count">&times; {{ items.length }}</span>
3286
+ <span *ngIf="isLoading" class="repeater-badge-loading"><i class="ph-thin ph-circle-notch"></i></span>
3287
+ </div>
3288
+
3289
+ <!-- Template drop zone — user designs one item here -->
3290
+ <div class="repeater-template"
3291
+ ioxDropzone
3292
+ [ioxDropzoneId]="dropListId"
3293
+ [ioxDropzoneData]="children"
3294
+ [ioxDropzonePostDrop]="refreshPreviewsBound"
3295
+ (ioxDrop)="onDrop($event)">
3296
+
3297
+ <div *ngIf="!children.length" class="repeater-placeholder">
3298
+ <i class="ph-thin ph-rows"></i>
3299
+ <span>Drop a component here as the item template</span>
3300
+ </div>
3301
+
3302
+ <div *ngFor="let child of children"
3303
+ class="repeater-template-child"
3304
+ ioxDraggable
3305
+ [ioxDragData]="child"
3306
+ [ioxDragSourceId]="dropListId">
3307
+ <ng-container
3308
+ [ioxRender]="child"
3309
+ (onClick)="childClick.emit($event)"
3310
+ (onMouseEnter)="childMouseEnter.emit($event)"
3311
+ (onMouseLeave)="childMouseLeave.emit()">
3312
+ </ng-container>
3313
+ </div>
3314
+ </div>
3315
+
3316
+ <!-- Preview rows for items 2..N — read-only clones with real data -->
3317
+ <div *ngFor="let row of previewRows" class="repeater-preview-row">
3318
+ <ng-container *ngFor="let child of row">
3319
+ <ng-container
3320
+ [ioxRender]="child"
3321
+ (onClick)="$event"
3322
+ (onMouseEnter)="$event"
3323
+ (onMouseLeave)="undefined">
3324
+ </ng-container>
3325
+ </ng-container>
3326
+ </div>
3327
+ </div>
3328
+ `, standalone: false, styles: [".repeater-root{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-root.has-children .repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-root.has-children:hover .repeater-badge{opacity:1}.repeater-root.has-children .repeater-template{border:none;border-radius:0;padding:0;min-height:unset}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-template{border:1px dashed rgba(203,144,144,.4);border-radius:0 0 4px 4px;padding:4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-template-child{min-width:0}.repeater-preview-row{opacity:.55;pointer-events:none}\n"] }]
3329
+ }], ctorParameters: () => [{ type: DragEngineService }, { type: OverlayService }, { type: undefined, decorators: [{
3330
+ type: Optional
3331
+ }, {
3332
+ type: Inject,
3333
+ args: [IOX_CONTENT_SERVICE]
3334
+ }] }, { type: DataSourceRegistryService }, { type: PanelEventService }, { type: i5.ActivatedRoute }, { type: i0.ChangeDetectorRef }], propDecorators: { children: [{
3335
+ type: Input
3336
+ }], style: [{
3337
+ type: Input
3338
+ }], nodeId: [{
3339
+ type: Input
3340
+ }], dropListId: [{
3341
+ type: Input
3342
+ }], childClick: [{
3343
+ type: Output
3344
+ }], childMouseEnter: [{
3345
+ type: Output
3346
+ }], childMouseLeave: [{
3347
+ type: Output
3348
+ }], source: [{
3349
+ type: Input
3350
+ }], limit: [{
3351
+ type: Input
3352
+ }] } });
3353
+
3354
+ class PanelChildComponent {
3355
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: PanelChildComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3356
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: PanelChildComponent, isStandalone: false, selector: "panel-child", inputs: { name: "name" }, viewQueries: [{ propertyName: "template", first: true, predicate: ["tpl"], descendants: true, static: true }], ngImport: i0, template: '<ng-template #tpl><ng-content></ng-content></ng-template>', isInline: true }); }
3357
+ }
3358
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: PanelChildComponent, decorators: [{
3359
+ type: Component,
3360
+ args: [{
3361
+ selector: 'panel-child',
3362
+ template: '<ng-template #tpl><ng-content></ng-content></ng-template>',
3363
+ standalone: false,
3364
+ }]
3365
+ }], propDecorators: { name: [{
3366
+ type: Input
3367
+ }], template: [{
3368
+ type: ViewChild,
3369
+ args: ['tpl', { static: true }]
3370
+ }] } });
3371
+
3372
+ class PanelComponent {
3373
+ constructor(panelEventService) {
3374
+ this.panelEventService = panelEventService;
3375
+ this.isPanelOpen = false;
3376
+ this.activePanel = null;
3377
+ this.sub = this.panelEventService.subscribe((panelEvent) => {
3378
+ if (panelEvent.type === PanelEventTypes.PANEL_CLOSE) {
3379
+ this.isPanelOpen = false;
3380
+ this.activePanel = null;
3381
+ return;
3382
+ }
3383
+ if (panelEvent.type === PanelEventTypes.PANEL_SELECTED || panelEvent.type === PanelEventTypes.PANEL_OPEN) {
3384
+ this.isPanelOpen = true;
3385
+ this.activePanel = panelEvent.data;
3386
+ }
3387
+ });
3388
+ }
3389
+ ngAfterContentInit() { }
3390
+ ngOnDestroy() {
3391
+ this.sub?.unsubscribe();
3392
+ }
3393
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: PanelComponent, deps: [{ token: PanelEventService }], target: i0.ɵɵFactoryTarget.Component }); }
3394
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: PanelComponent, isStandalone: false, selector: "app-panel", queries: [{ propertyName: "panelChildren", predicate: PanelChildComponent }], ngImport: i0, template: "<div class=\"panel\">\n <ng-container *ngFor=\"let child of panelChildren\">\n <div class=\"panel-body\" [ngClass]=\"{ active: activePanel === child.name }\">\n <ng-container *ngTemplateOutlet=\"child.template\"></ng-container>\n </div>\n </ng-container>\n</div>", styles: [":host{display:block;width:0;flex-shrink:0;overflow:hidden;transition:width .2s ease-in-out}:host-context(.panel-open){width:220px}.panel{border-left:1px solid #ccc;background:#f7f7f7;width:220px;height:100%;position:relative}.panel .panel-body{position:absolute;inset:0;overflow-y:auto;transform:translate(100%);transition:transform .2s ease-in-out}.panel .panel-body.active{transform:translate(0)}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
3395
+ }
3396
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: PanelComponent, decorators: [{
3397
+ type: Component,
3398
+ args: [{ selector: 'app-panel', standalone: false, template: "<div class=\"panel\">\n <ng-container *ngFor=\"let child of panelChildren\">\n <div class=\"panel-body\" [ngClass]=\"{ active: activePanel === child.name }\">\n <ng-container *ngTemplateOutlet=\"child.template\"></ng-container>\n </div>\n </ng-container>\n</div>", styles: [":host{display:block;width:0;flex-shrink:0;overflow:hidden;transition:width .2s ease-in-out}:host-context(.panel-open){width:220px}.panel{border-left:1px solid #ccc;background:#f7f7f7;width:220px;height:100%;position:relative}.panel .panel-body{position:absolute;inset:0;overflow-y:auto;transform:translate(100%);transition:transform .2s ease-in-out}.panel .panel-body.active{transform:translate(0)}\n"] }]
3399
+ }], ctorParameters: () => [{ type: PanelEventService }], propDecorators: { panelChildren: [{
3400
+ type: ContentChildren,
3401
+ args: [PanelChildComponent]
3402
+ }] } });
3403
+
3404
+ var StyleCategory;
3405
+ (function (StyleCategory) {
3406
+ StyleCategory["Layout"] = "Layout";
3407
+ StyleCategory["Size"] = "Size";
3408
+ StyleCategory["Typography"] = "Typography";
3409
+ StyleCategory["Spacing"] = "Spacing";
3410
+ StyleCategory["Border"] = "Border";
3411
+ StyleCategory["Position"] = "Position";
3412
+ StyleCategory["Background"] = "Background";
3413
+ StyleCategory["Effects"] = "Effects";
3414
+ // Legacy — kept for backward compat; prefer Size
3415
+ StyleCategory["Dimensions"] = "Dimensions";
3416
+ })(StyleCategory || (StyleCategory = {}));
3417
+ const TRIGGER_OPTIONS = [
3418
+ { value: 'pageLoad', label: 'Page load', icon: 'ph-thin ph-play' },
3419
+ { value: 'viewportEnter', label: 'Enter viewport', icon: 'ph-thin ph-eye' },
3420
+ { value: 'click', label: 'Click', icon: 'ph-thin ph-cursor-click' },
3421
+ { value: 'hover', label: 'Hover', icon: 'ph-thin ph-hand-pointing' },
3422
+ { value: 'scrollProgress', label: 'Scroll progress', icon: 'ph-thin ph-arrow-fat-lines-down' },
3423
+ ];
3424
+ const ACTION_TYPE_OPTIONS = [
3425
+ { value: 'fadeIn', label: 'Fade in' },
3426
+ { value: 'fadeOut', label: 'Fade out' },
3427
+ { value: 'moveUp', label: 'Move up' },
3428
+ { value: 'moveDown', label: 'Move down' },
3429
+ { value: 'moveLeft', label: 'Move left' },
3430
+ { value: 'moveRight', label: 'Move right' },
3431
+ { value: 'scaleIn', label: 'Scale in' },
3432
+ { value: 'scaleOut', label: 'Scale out' },
3433
+ { value: 'rotate', label: 'Rotate' },
3434
+ { value: 'show', label: 'Show' },
3435
+ { value: 'hide', label: 'Hide' },
3436
+ { value: 'toggleVisibility', label: 'Toggle visibility' },
3437
+ ];
3438
+ const EASING_OPTIONS = ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'];
3439
+ var TraitInputType;
3440
+ (function (TraitInputType) {
3441
+ TraitInputType["Text"] = "text";
3442
+ TraitInputType["Number"] = "number";
3443
+ TraitInputType["Select"] = "select";
3444
+ TraitInputType["Checkbox"] = "checkbox";
3445
+ TraitInputType["Color"] = "colorPicker";
3446
+ TraitInputType["DirectionalSize"] = "directionalSize";
3447
+ TraitInputType["SelectButton"] = "selectButton";
3448
+ TraitInputType["Scrub"] = "scrub";
3449
+ TraitInputType["Icon"] = "icon";
3450
+ TraitInputType["Media"] = "media";
3451
+ TraitInputType["FontFamily"] = "fontFamily";
3452
+ })(TraitInputType || (TraitInputType = {}));
3453
+ function generateNodeId() {
3454
+ return Math.random().toString(36).substr(2, 9);
3455
+ }
3456
+ class ComponentConfig {
3457
+ constructor(type, selector, icon = 'ph-thin ph-cube', category = 'General') {
3458
+ this.id = generateNodeId();
3459
+ this.type = type;
3460
+ this.selector = selector;
3461
+ this.icon = icon;
3462
+ this.category = category;
3463
+ this.inputs = {};
3464
+ this.traits = [];
3465
+ this.styleTraits = [];
3466
+ }
3467
+ /** Override specific trait defaults after styleTraits are set.
3468
+ * Call this at the end of each subclass constructor. */
3469
+ applyStyleDefaults(defaults) {
3470
+ for (const group of this.styleTraits) {
3471
+ for (const trait of group.traits) {
3472
+ if (defaults[trait.name] !== undefined) {
3473
+ trait.default = defaults[trait.name];
3474
+ }
3475
+ }
3476
+ }
3477
+ }
3478
+ }
3479
+ class TraitConfig {
3480
+ constructor(name, label, type, options, defaultValue, inline, showWhen) {
3481
+ this.name = name;
3482
+ this.label = label;
3483
+ this.type = type;
3484
+ this.options = options;
3485
+ this.default = defaultValue;
3486
+ this.inline = inline;
3487
+ this.showWhen = showWhen;
3488
+ }
3489
+ }
3490
+ class GroupStyleConfig {
3491
+ constructor(category, traits) {
3492
+ this.category = category;
3493
+ this.traits = traits;
3494
+ }
3495
+ }
3496
+ // ─── Unit constants ───────────────────────────────────────────────────────────
3497
+ const UNITS_ALL = ['px', '%', 'rem', 'em', 'vw', 'vh'];
3498
+ const UNITS_NO_VW = ['px', '%', 'rem', 'em', 'vh'];
3499
+ const UNITS_FIXED = ['px', '%', 'rem', 'em'];
3500
+ const UNITS_DEG = ['deg'];
3501
+ // ─── Shared panel utilities ──────────────────────────────────────────────────
3502
+ function resolveTraitControllerType(type) {
3503
+ switch (type) {
3504
+ case TraitInputType.Number: return 'number';
3505
+ case TraitInputType.Select: return 'select';
3506
+ case TraitInputType.Checkbox: return 'switch';
3507
+ case TraitInputType.Color: return 'colorPicker';
3508
+ case TraitInputType.DirectionalSize: return 'directionalSize';
3509
+ case TraitInputType.SelectButton: return 'selectButton';
3510
+ case TraitInputType.Scrub: return 'scrub';
3511
+ case TraitInputType.Icon: return 'icon';
3512
+ case TraitInputType.Media: return 'media';
3513
+ case TraitInputType.FontFamily: return 'select';
3514
+ default: return 'text';
3515
+ }
3516
+ }
3517
+ function resolveTraitOptions(trait) {
3518
+ if (trait.type === TraitInputType.Color ||
3519
+ trait.type === TraitInputType.DirectionalSize ||
3520
+ trait.type === TraitInputType.SelectButton ||
3521
+ trait.type === TraitInputType.Scrub ||
3522
+ trait.type === TraitInputType.Media) {
3523
+ return trait.options;
3524
+ }
3525
+ if (trait.type === TraitInputType.FontFamily) {
3526
+ // FontFamily options are injected dynamically by the style panel via FontManagerService.
3527
+ // Return empty array here; the panel overrides this in resolveOptions().
3528
+ return [];
3529
+ }
3530
+ if (trait.type !== TraitInputType.Select) {
3531
+ return undefined;
3532
+ }
3533
+ const opts = trait.options ?? [];
3534
+ // Already formatted as {label, value}[]
3535
+ if (opts.length > 0 && typeof opts[0] === 'object' && 'label' in opts[0]) {
3536
+ return opts;
3537
+ }
3538
+ return opts.map((option) => ({ label: option, value: option }));
3539
+ }
3540
+ // ─── Style Groups ─────────────────────────────────────────────────────────────
3541
+ const NON_STATIC_POSITIONS = ['relative', 'absolute', 'fixed', 'sticky'];
3542
+ class PositionGroupStyleConfig extends GroupStyleConfig {
3543
+ constructor() {
3544
+ super(StyleCategory.Position, [
3545
+ new TraitConfig('position', 'Position', TraitInputType.Select, ['static', 'relative', 'absolute', 'fixed', 'sticky'], 'static'),
3546
+ new TraitConfig('top', 'T', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
3547
+ new TraitConfig('right', 'R', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
3548
+ new TraitConfig('bottom', 'B', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
3549
+ new TraitConfig('left', 'L', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
3550
+ new TraitConfig('zIndex', 'Z', TraitInputType.Scrub, { min: -99, max: 9999, step: 1, units: [''] }, '0', false, { trait: 'position', values: NON_STATIC_POSITIONS }),
3551
+ ]);
3552
+ }
3553
+ }
3554
+ // ─── Layout ───────────────────────────────────────────────────────────────────
3555
+ class LayoutGroupStyleConfig extends GroupStyleConfig {
3556
+ constructor() {
3557
+ super(StyleCategory.Layout, [
3558
+ new TraitConfig('display', 'Display', TraitInputType.Select, ['block', 'inline', 'inline-block', 'flex'], 'block'),
3559
+ new TraitConfig('flexDirection', 'Direction', TraitInputType.SelectButton, [
3560
+ { value: 'row', label: 'Horizontal', icon: 'ph-thin ph-arrow-right' },
3561
+ { value: 'row-reverse', label: 'Horizontal Reversed', icon: 'ph-thin ph-arrow-left' },
3562
+ { value: 'column', label: 'Vertical', icon: 'ph-thin ph-arrow-down' },
3563
+ { value: 'column-reverse', label: 'Vertical Reversed', icon: 'ph-thin ph-arrow-up' },
3564
+ ], 'row', false, { trait: 'display', values: 'flex' }),
3565
+ new TraitConfig('justifyContent', 'Justify', TraitInputType.SelectButton, [
3566
+ { value: 'flex-start', label: 'Start', icon: 'ph-thin ph-align-left' },
3567
+ { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-horizontal' },
3568
+ { value: 'flex-end', label: 'End', icon: 'ph-thin ph-align-right' },
3569
+ { value: 'space-between', label: 'Space Between', icon: 'ph-thin ph-distribute-horizontal' },
3570
+ { value: 'space-around', label: 'Space Around', icon: 'ph-thin ph-dots-three' },
3571
+ { value: 'space-evenly', label: 'Space Evenly', icon: 'ph-thin ph-dots-six' },
3572
+ ], 'flex-start', false, { trait: 'display', values: 'flex' }),
3573
+ new TraitConfig('alignItems', 'Align', TraitInputType.SelectButton, [
3574
+ { value: 'stretch', label: 'Stretch', icon: 'ph-thin ph-arrows-vertical' },
3575
+ { value: 'flex-start', label: 'Start', icon: 'ph-thin ph-align-top' },
3576
+ { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-vertical' },
3577
+ { value: 'flex-end', label: 'End', icon: 'ph-thin ph-align-bottom' },
3578
+ ], 'stretch', false, { trait: 'display', values: 'flex' }),
3579
+ new TraitConfig('flexWrap', 'Flex Wrap', TraitInputType.SelectButton, [
3580
+ { value: 'nowrap', label: 'No Wrap', icon: 'ph-thin ph-minus' },
3581
+ { value: 'wrap', label: 'Wrap', icon: 'ph-thin ph-arrow-elbow-down-right' },
3582
+ { value: 'wrap-reverse', label: 'Wrap Reversed', icon: 'ph-thin ph-arrow-elbow-up-right' },
3583
+ ], 'nowrap', false, { trait: 'display', values: 'flex' }),
3584
+ new TraitConfig('gap', 'Gap', TraitInputType.Scrub, { min: 0, max: 200, step: 1, units: UNITS_FIXED }, '0px', false, { trait: 'display', values: 'flex' }),
3585
+ new TraitConfig('direction', 'Text Direction', TraitInputType.SelectButton, [
3586
+ { value: 'ltr', label: 'LTR' },
3587
+ { value: 'rtl', label: 'RTL' },
3588
+ ], 'ltr'),
3589
+ ]);
3590
+ }
3591
+ }
3592
+ // ─── Size (replaces Dimensions) ───────────────────────────────────────────────
3593
+ class SizeGroupStyleConfig extends GroupStyleConfig {
3594
+ constructor() {
3595
+ super(StyleCategory.Size, [
3596
+ new TraitConfig('width', 'W', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
3597
+ new TraitConfig('height', 'H', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
3598
+ new TraitConfig('minWidth', 'Min W', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
3599
+ new TraitConfig('minHeight', 'Min H', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
3600
+ new TraitConfig('maxWidth', 'Max W', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
3601
+ new TraitConfig('maxHeight', 'Max H', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
3602
+ new TraitConfig('aspectRatio', 'Aspect Ratio', TraitInputType.Select, ['auto', '1/1', '4/3', '16/9', '3/2', '2/3', '9/16'], 'auto'),
3603
+ ]);
3604
+ }
3605
+ }
3606
+ /** @deprecated Use SizeGroupStyleConfig instead. */
3607
+ class DimensionsGroupStyleConfig extends SizeGroupStyleConfig {
3608
+ }
3609
+ // ─── Background ───────────────────────────────────────────────────────────────
3610
+ class BackgroundGroupStyleConfig extends GroupStyleConfig {
3611
+ constructor() {
3612
+ super(StyleCategory.Background, [
3613
+ new TraitConfig('backgroundColor', 'Background', TraitInputType.Color, { allowGradient: true, allowTransparent: true, formats: ['hex', 'rgb', 'hsl', 'transparent'] }, 'transparent'),
3614
+ ]);
3615
+ }
3616
+ }
3617
+ class SpacingGroupStyleConfig extends GroupStyleConfig {
3618
+ constructor() {
3619
+ super(StyleCategory.Spacing, [
3620
+ new TraitConfig('margin', 'Margin', TraitInputType.DirectionalSize, {
3621
+ allowNegative: true,
3622
+ allowLinkedSlider: true,
3623
+ slider: { min: 0, max: 200, step: 1 }
3624
+ }, '0px 0px 0px 0px'),
3625
+ new TraitConfig('padding', 'Padding', TraitInputType.DirectionalSize, {
3626
+ allowNegative: false,
3627
+ allowLinkedSlider: true,
3628
+ slider: { min: 0, max: 200, step: 1 }
3629
+ }, '0px 0px 0px 0px')
3630
+ ]);
3631
+ }
3632
+ }
3633
+ class BorderGroupStyleConfig extends GroupStyleConfig {
3634
+ constructor() {
3635
+ super(StyleCategory.Border, [
3636
+ new TraitConfig('borderWidth', 'Border Width', TraitInputType.DirectionalSize, {
3637
+ units: UNITS_FIXED,
3638
+ allowNegative: false,
3639
+ allowLinkedSlider: true,
3640
+ slider: { min: 0, max: 32, step: 1 }
3641
+ }, '0px'),
3642
+ new TraitConfig('borderStyle', 'Border Style', TraitInputType.SelectButton, [
3643
+ { value: 'solid', icon: 'ph-thin ph-line-segment' },
3644
+ { value: 'dashed', icon: 'ph-thin ph-dots-three' },
3645
+ { value: 'dotted', icon: 'ph-thin ph-dots-six' },
3646
+ ], 'solid'),
3647
+ new TraitConfig('borderColor', 'Border Color', TraitInputType.Color, { allowGradient: false, allowTransparent: false, formats: ['hex', 'rgb', 'hsl'] }, '#CCCCCC'),
3648
+ new TraitConfig('borderRadius', 'Border Radius', TraitInputType.DirectionalSize, {
3649
+ units: UNITS_FIXED,
3650
+ allowNegative: false,
3651
+ allowLinkedSlider: true,
3652
+ linkByDefault: true,
3653
+ slider: { min: 0, max: 120, step: 1 },
3654
+ segments: [
3655
+ { key: 'top-left', icon: 'ph-arrow-bend-up-left', ariaLabel: 'top left radius' },
3656
+ { key: 'top-right', icon: 'ph-arrow-bend-up-right', ariaLabel: 'top right radius' },
3657
+ { key: 'bottom-right', icon: 'ph-arrow-bend-down-right', ariaLabel: 'bottom right radius' },
3658
+ { key: 'bottom-left', icon: 'ph-arrow-bend-down-left', ariaLabel: 'bottom left radius' }
3659
+ ]
3660
+ }, '0px')
3661
+ ]);
3662
+ }
3663
+ }
3664
+ class TypographyGroupStyleConfig extends GroupStyleConfig {
3665
+ constructor() {
3666
+ super(StyleCategory.Typography, [
3667
+ new TraitConfig('fontFamily', 'Font Family', TraitInputType.FontFamily, [], 'Poppins, sans-serif'),
3668
+ new TraitConfig('fontSize', 'Font Size', TraitInputType.Scrub, { min: 8, max: 200, step: 1, units: UNITS_FIXED }, '16px'),
3669
+ new TraitConfig('fontWeight', 'Font Weight', TraitInputType.Select, ['100', '200', '300', '400', '500', '600', '700', '800', '900'], '400'),
3670
+ new TraitConfig('color', 'Color', TraitInputType.Color, { allowGradient: false, allowTransparent: false, formats: ['hex', 'rgb', 'hsl'] }, '#000000'),
3671
+ new TraitConfig('lineHeight', 'Line Height', TraitInputType.Select, ['1', '1.2', '1.4', '1.5', '1.75', '2', '2.5'], '1.5'),
3672
+ new TraitConfig('letterSpacing', 'Letter Spacing', TraitInputType.Scrub, { min: -5, max: 50, step: 0.5, units: UNITS_FIXED }, '0px'),
3673
+ new TraitConfig('textAlign', 'Alignment', TraitInputType.SelectButton, [
3674
+ { value: 'left', icon: 'ph-thin ph-text-align-left' },
3675
+ { value: 'center', icon: 'ph-thin ph-text-align-center' },
3676
+ { value: 'right', icon: 'ph-thin ph-text-align-right' },
3677
+ { value: 'justify', icon: 'ph-thin ph-text-align-justify' },
3678
+ ], 'left'),
3679
+ new TraitConfig('fontStyle', 'Italic', TraitInputType.SelectButton, { items: [{ value: 'italic', icon: 'ph-thin ph-text-italic', label: 'Italic' }], allowEmpty: true, noneValue: 'normal' }, 'normal', true),
3680
+ new TraitConfig('textDecoration', 'Underline', TraitInputType.SelectButton, { items: [{ value: 'underline', icon: 'ph-thin ph-text-underline', label: 'Underline' }], allowEmpty: true, noneValue: 'none' }, 'none', true),
3681
+ ]);
3682
+ }
3683
+ }
3684
+ class EffectsGroupStyleConfig extends GroupStyleConfig {
3685
+ constructor() {
3686
+ super(StyleCategory.Effects, [
3687
+ new TraitConfig('opacity', 'Opacity', TraitInputType.Scrub, { min: 0, max: 1, step: 0.01, units: [''] }, '1'),
3688
+ new TraitConfig('mixBlendMode', 'Blend Mode', TraitInputType.Select, [
3689
+ 'normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten',
3690
+ 'color-dodge', 'color-burn', 'hard-light', 'soft-light',
3691
+ 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity',
3692
+ ], 'normal'),
3693
+ new TraitConfig('cursor', 'Cursor', TraitInputType.Select, [
3694
+ 'auto', 'default', 'pointer', 'text', 'move', 'not-allowed',
3695
+ 'grab', 'grabbing', 'crosshair', 'zoom-in', 'zoom-out', 'none',
3696
+ ], 'auto'),
3697
+ ]);
3698
+ }
3699
+ }
3700
+ function flattenSchemaFields(properties, prefix = '') {
3701
+ const result = [];
3702
+ for (const [key, field] of Object.entries(properties)) {
3703
+ const fullPath = prefix ? `${prefix}.${key}` : key;
3704
+ if (field.type === 'object' && field.properties) {
3705
+ result.push(...flattenSchemaFields(field.properties, fullPath));
3706
+ }
3707
+ else {
3708
+ result.push({ label: fullPath, value: fullPath });
3709
+ }
3710
+ }
3711
+ return result;
3712
+ }
3713
+
3714
+ class InteractionsPanelComponent {
3715
+ constructor(overlayService) {
3716
+ this.overlayService = overlayService;
3717
+ this.node = null;
3718
+ this.triggerOptions = TRIGGER_OPTIONS;
3719
+ this.actionTypeOptions = ACTION_TYPE_OPTIONS;
3720
+ this.easingOptions = EASING_OPTIONS;
3721
+ this.interactions = [];
3722
+ /** State for the "add interaction" form */
3723
+ this.isAdding = false;
3724
+ this.newTrigger = 'pageLoad';
3725
+ this.newActionType = 'fadeIn';
3726
+ this.newDuration = 600;
3727
+ this.newDelay = 0;
3728
+ this.newEasing = 'ease-out';
3729
+ this.newOnce = true;
3730
+ /** Expanded interaction ID for editing */
3731
+ this.expandedId = null;
3732
+ }
3733
+ ngOnInit() {
3734
+ this.selectSub = this.overlayService.select$.subscribe(entry => {
3735
+ this.node = entry?.node ?? null;
3736
+ this.interactions = this.node?.interactions ?? [];
3737
+ this.isAdding = false;
3738
+ this.expandedId = null;
3739
+ });
3740
+ }
3741
+ ngOnDestroy() {
3742
+ this.selectSub?.unsubscribe();
3743
+ }
3744
+ getTriggerLabel(trigger) {
3745
+ return this.triggerOptions.find(t => t.value === trigger)?.label ?? trigger;
3746
+ }
3747
+ getTriggerIcon(trigger) {
3748
+ return this.triggerOptions.find(t => t.value === trigger)?.icon ?? 'ph-thin ph-lightning';
3749
+ }
3750
+ getActionLabel(type) {
3751
+ return this.actionTypeOptions.find(a => a.value === type)?.label ?? type;
3752
+ }
3753
+ getActionsSummary(interaction) {
3754
+ return interaction.actions.map(a => this.getActionLabel(a.type)).join(', ');
3755
+ }
3756
+ toggleExpand(id) {
3757
+ this.expandedId = this.expandedId === id ? null : id;
3758
+ }
3759
+ // ── Add interaction ──────────────────────────────────────
3760
+ startAdd() {
3761
+ this.isAdding = true;
3762
+ this.newTrigger = 'pageLoad';
3763
+ this.newActionType = 'fadeIn';
3764
+ this.newDuration = 600;
3765
+ this.newDelay = 0;
3766
+ this.newEasing = 'ease-out';
3767
+ this.newOnce = true;
3768
+ }
3769
+ cancelAdd() {
3770
+ this.isAdding = false;
3771
+ }
3772
+ confirmAdd() {
3773
+ if (!this.node)
3774
+ return;
3775
+ const action = {
3776
+ type: this.newActionType,
3777
+ target: 'self',
3778
+ duration: this.newDuration,
3779
+ delay: this.newDelay,
3780
+ easing: this.newEasing,
3781
+ once: this.newOnce,
3782
+ };
3783
+ const interaction = {
3784
+ id: generateNodeId(),
3785
+ trigger: this.newTrigger,
3786
+ actions: [action],
3787
+ };
3788
+ if (!this.node.interactions) {
3789
+ this.node.interactions = [];
3790
+ }
3791
+ this.node.interactions.push(interaction);
3792
+ this.interactions = this.node.interactions;
3793
+ this.isAdding = false;
3794
+ }
3795
+ // ── Remove interaction ───────────────────────────────────
3796
+ removeInteraction(id) {
3797
+ if (!this.node?.interactions)
3798
+ return;
3799
+ this.node.interactions = this.node.interactions.filter(i => i.id !== id);
3800
+ this.interactions = this.node.interactions;
3801
+ if (this.expandedId === id) {
3802
+ this.expandedId = null;
3803
+ }
3804
+ }
3805
+ // ── Add action to existing interaction ───────────────────
3806
+ addAction(interaction) {
3807
+ interaction.actions.push({
3808
+ type: 'fadeIn',
3809
+ target: 'self',
3810
+ duration: 600,
3811
+ delay: 0,
3812
+ easing: 'ease-out',
3813
+ once: true,
3814
+ });
3815
+ }
3816
+ removeAction(interaction, index) {
3817
+ interaction.actions.splice(index, 1);
3818
+ // Remove the interaction if it has no actions left
3819
+ if (interaction.actions.length === 0) {
3820
+ this.removeInteraction(interaction.id);
3821
+ }
3822
+ }
3823
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InteractionsPanelComponent, deps: [{ token: OverlayService }], target: i0.ɵɵFactoryTarget.Component }); }
3824
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: InteractionsPanelComponent, isStandalone: false, selector: "app-interactions-panel", inputs: { node: "node" }, ngImport: i0, template: "<div class=\"interactions-panel\">\n <!-- No element selected -->\n <div *ngIf=\"!node\" class=\"section-empty\">\n Select an element to manage interactions\n </div>\n\n <ng-container *ngIf=\"node\">\n <!-- Interaction cards -->\n <div *ngFor=\"let ix of interactions\" class=\"ix-card\" [class.is-expanded]=\"expandedId === ix.id\">\n <!-- Summary row -->\n <div class=\"ix-card-header\" (click)=\"toggleExpand(ix.id)\">\n <i [class]=\"getTriggerIcon(ix.trigger)\" class=\"ix-trigger-icon\"></i>\n <div class=\"ix-card-summary\">\n <span class=\"ix-trigger-label\">{{ getTriggerLabel(ix.trigger) }}</span>\n <span class=\"ix-arrow\">\u2192</span>\n <span class=\"ix-action-label\">{{ getActionsSummary(ix) }}</span>\n </div>\n <button class=\"ix-remove-btn\" title=\"Remove\" (click)=\"removeInteraction(ix.id); $event.stopPropagation()\">\n <i class=\"ph-thin ph-x\"></i>\n </button>\n </div>\n\n <!-- Expanded detail -->\n <div *ngIf=\"expandedId === ix.id\" class=\"ix-card-body\">\n <div class=\"ix-field\">\n <label>Trigger</label>\n <select [(ngModel)]=\"ix.trigger\">\n <option *ngFor=\"let t of triggerOptions\" [value]=\"t.value\">{{ t.label }}</option>\n </select>\n </div>\n\n <div *ngFor=\"let action of ix.actions; let ai = index\" class=\"ix-action-row\">\n <div class=\"ix-action-header\">\n <span class=\"ix-action-index\">Action {{ ai + 1 }}</span>\n <button *ngIf=\"ix.actions.length > 1\"\n class=\"ix-remove-btn small\"\n title=\"Remove action\"\n (click)=\"removeAction(ix, ai)\">\n <i class=\"ph-thin ph-x\"></i>\n </button>\n </div>\n\n <div class=\"ix-field\">\n <label>Type</label>\n <select [(ngModel)]=\"action.type\">\n <option *ngFor=\"let a of actionTypeOptions\" [value]=\"a.value\">{{ a.label }}</option>\n </select>\n </div>\n\n <div class=\"ix-field-row\">\n <div class=\"ix-field\">\n <label>Duration (ms)</label>\n <input type=\"number\" [(ngModel)]=\"action.duration\" min=\"0\" step=\"50\">\n </div>\n <div class=\"ix-field\">\n <label>Delay (ms)</label>\n <input type=\"number\" [(ngModel)]=\"action.delay\" min=\"0\" step=\"50\">\n </div>\n </div>\n\n <div class=\"ix-field\">\n <label>Easing</label>\n <select [(ngModel)]=\"action.easing\">\n <option *ngFor=\"let e of easingOptions\" [value]=\"e\">{{ e }}</option>\n </select>\n </div>\n\n <div class=\"ix-field ix-checkbox-field\">\n <label>\n <input type=\"checkbox\" [(ngModel)]=\"action.once\">\n Play once\n </label>\n </div>\n </div>\n\n <button class=\"ix-add-action-btn\" (click)=\"addAction(ix)\">\n <i class=\"ph-thin ph-plus\"></i> Add action\n </button>\n </div>\n </div>\n\n <!-- Add interaction form -->\n <div *ngIf=\"isAdding\" class=\"ix-add-form\">\n <div class=\"ix-field\">\n <label>Trigger</label>\n <select [(ngModel)]=\"newTrigger\">\n <option *ngFor=\"let t of triggerOptions\" [value]=\"t.value\">{{ t.label }}</option>\n </select>\n </div>\n <div class=\"ix-field\">\n <label>Action</label>\n <select [(ngModel)]=\"newActionType\">\n <option *ngFor=\"let a of actionTypeOptions\" [value]=\"a.value\">{{ a.label }}</option>\n </select>\n </div>\n <div class=\"ix-field-row\">\n <div class=\"ix-field\">\n <label>Duration (ms)</label>\n <input type=\"number\" [(ngModel)]=\"newDuration\" min=\"0\" step=\"50\">\n </div>\n <div class=\"ix-field\">\n <label>Delay (ms)</label>\n <input type=\"number\" [(ngModel)]=\"newDelay\" min=\"0\" step=\"50\">\n </div>\n </div>\n <div class=\"ix-field\">\n <label>Easing</label>\n <select [(ngModel)]=\"newEasing\">\n <option *ngFor=\"let e of easingOptions\" [value]=\"e\">{{ e }}</option>\n </select>\n </div>\n <div class=\"ix-field ix-checkbox-field\">\n <label>\n <input type=\"checkbox\" [(ngModel)]=\"newOnce\">\n Play once\n </label>\n </div>\n <div class=\"ix-form-actions\">\n <button class=\"ix-btn ix-btn--secondary\" (click)=\"cancelAdd()\">Cancel</button>\n <button class=\"ix-btn ix-btn--primary\" (click)=\"confirmAdd()\">Add</button>\n </div>\n </div>\n\n <!-- Add button -->\n <button *ngIf=\"!isAdding\" class=\"ix-add-btn\" (click)=\"startAdd()\">\n <i class=\"ph-thin ph-plus\"></i> Add interaction\n </button>\n </ng-container>\n</div>\n", styles: [".interactions-panel{padding:.5rem}.section-empty{color:var(--p-text-muted-color);font-size:12px;text-align:center;padding:1rem}.ix-card{border:1px solid var(--p-surface-200);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .15s}.ix-card.is-expanded{border-color:#cb9090}.ix-card-header{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;font-size:12px;transition:background .12s}.ix-card-header:hover{background:var(--p-surface-50)}.ix-trigger-icon{font-size:14px;color:#cb9090;flex-shrink:0}.ix-card-summary{flex:1;min-width:0;display:flex;align-items:center;gap:4px;overflow:hidden}.ix-trigger-label{font-weight:500;white-space:nowrap}.ix-arrow{color:var(--p-text-muted-color);flex-shrink:0}.ix-action-label{color:var(--p-text-muted-color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ix-remove-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;border-radius:4px;flex-shrink:0;font-size:12px}.ix-remove-btn:hover{background:#dc26261a;color:#dc2626}.ix-remove-btn.small{width:16px;height:16px;font-size:10px}.ix-card-body{padding:0 10px 10px;border-top:1px solid var(--p-surface-100)}.ix-action-row{padding:8px 0;border-bottom:1px dashed var(--p-surface-100)}.ix-action-row:last-of-type{border-bottom:none}.ix-action-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.ix-action-index{font-size:11px;font-weight:600;color:var(--p-text-muted-color);text-transform:uppercase;letter-spacing:.03em}.ix-add-action-btn{display:flex;align-items:center;gap:4px;margin-top:8px;padding:4px 8px;border:1px dashed var(--p-surface-300);background:transparent;color:var(--p-text-muted-color);cursor:pointer;border-radius:6px;font-size:11px;transition:border-color .15s,color .15s}.ix-add-action-btn:hover{border-color:#cb9090;color:#cb9090}.ix-field{margin-bottom:8px}.ix-field label{display:block;font-size:11px;color:var(--p-text-muted-color);margin-bottom:3px;font-weight:500}.ix-field select,.ix-field input[type=number]{width:100%;padding:5px 8px;border:1px solid var(--p-surface-200);border-radius:6px;font-size:12px;background:var(--p-surface-0);color:var(--p-text-color);outline:none;transition:border-color .15s}.ix-field select:focus,.ix-field input[type=number]:focus{border-color:#cb9090}.ix-field-row{display:flex;gap:8px}.ix-field-row .ix-field{flex:1}.ix-checkbox-field label{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;color:var(--p-text-color)}.ix-checkbox-field input[type=checkbox]{accent-color:#cb9090}.ix-add-form{border:1px solid var(--p-surface-200);border-radius:8px;padding:10px;margin-bottom:6px}.ix-form-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:4px}.ix-btn{padding:5px 12px;border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:background .12s,color .12s}.ix-btn--primary{background:#cb9090;color:#fff}.ix-btn--primary:hover{background:#be7474}.ix-btn--secondary{background:transparent;color:var(--p-text-muted-color)}.ix-btn--secondary:hover{background:var(--p-surface-100);color:var(--p-text-color)}.ix-add-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px;border:1px dashed var(--p-surface-300);background:transparent;color:var(--p-text-muted-color);cursor:pointer;border-radius:8px;font-size:12px;transition:border-color .15s,color .15s}.ix-add-btn:hover{border-color:#cb9090;color:#cb9090}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i3.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i3.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
3825
+ }
3826
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InteractionsPanelComponent, decorators: [{
3827
+ type: Component,
3828
+ args: [{ selector: 'app-interactions-panel', standalone: false, template: "<div class=\"interactions-panel\">\n <!-- No element selected -->\n <div *ngIf=\"!node\" class=\"section-empty\">\n Select an element to manage interactions\n </div>\n\n <ng-container *ngIf=\"node\">\n <!-- Interaction cards -->\n <div *ngFor=\"let ix of interactions\" class=\"ix-card\" [class.is-expanded]=\"expandedId === ix.id\">\n <!-- Summary row -->\n <div class=\"ix-card-header\" (click)=\"toggleExpand(ix.id)\">\n <i [class]=\"getTriggerIcon(ix.trigger)\" class=\"ix-trigger-icon\"></i>\n <div class=\"ix-card-summary\">\n <span class=\"ix-trigger-label\">{{ getTriggerLabel(ix.trigger) }}</span>\n <span class=\"ix-arrow\">\u2192</span>\n <span class=\"ix-action-label\">{{ getActionsSummary(ix) }}</span>\n </div>\n <button class=\"ix-remove-btn\" title=\"Remove\" (click)=\"removeInteraction(ix.id); $event.stopPropagation()\">\n <i class=\"ph-thin ph-x\"></i>\n </button>\n </div>\n\n <!-- Expanded detail -->\n <div *ngIf=\"expandedId === ix.id\" class=\"ix-card-body\">\n <div class=\"ix-field\">\n <label>Trigger</label>\n <select [(ngModel)]=\"ix.trigger\">\n <option *ngFor=\"let t of triggerOptions\" [value]=\"t.value\">{{ t.label }}</option>\n </select>\n </div>\n\n <div *ngFor=\"let action of ix.actions; let ai = index\" class=\"ix-action-row\">\n <div class=\"ix-action-header\">\n <span class=\"ix-action-index\">Action {{ ai + 1 }}</span>\n <button *ngIf=\"ix.actions.length > 1\"\n class=\"ix-remove-btn small\"\n title=\"Remove action\"\n (click)=\"removeAction(ix, ai)\">\n <i class=\"ph-thin ph-x\"></i>\n </button>\n </div>\n\n <div class=\"ix-field\">\n <label>Type</label>\n <select [(ngModel)]=\"action.type\">\n <option *ngFor=\"let a of actionTypeOptions\" [value]=\"a.value\">{{ a.label }}</option>\n </select>\n </div>\n\n <div class=\"ix-field-row\">\n <div class=\"ix-field\">\n <label>Duration (ms)</label>\n <input type=\"number\" [(ngModel)]=\"action.duration\" min=\"0\" step=\"50\">\n </div>\n <div class=\"ix-field\">\n <label>Delay (ms)</label>\n <input type=\"number\" [(ngModel)]=\"action.delay\" min=\"0\" step=\"50\">\n </div>\n </div>\n\n <div class=\"ix-field\">\n <label>Easing</label>\n <select [(ngModel)]=\"action.easing\">\n <option *ngFor=\"let e of easingOptions\" [value]=\"e\">{{ e }}</option>\n </select>\n </div>\n\n <div class=\"ix-field ix-checkbox-field\">\n <label>\n <input type=\"checkbox\" [(ngModel)]=\"action.once\">\n Play once\n </label>\n </div>\n </div>\n\n <button class=\"ix-add-action-btn\" (click)=\"addAction(ix)\">\n <i class=\"ph-thin ph-plus\"></i> Add action\n </button>\n </div>\n </div>\n\n <!-- Add interaction form -->\n <div *ngIf=\"isAdding\" class=\"ix-add-form\">\n <div class=\"ix-field\">\n <label>Trigger</label>\n <select [(ngModel)]=\"newTrigger\">\n <option *ngFor=\"let t of triggerOptions\" [value]=\"t.value\">{{ t.label }}</option>\n </select>\n </div>\n <div class=\"ix-field\">\n <label>Action</label>\n <select [(ngModel)]=\"newActionType\">\n <option *ngFor=\"let a of actionTypeOptions\" [value]=\"a.value\">{{ a.label }}</option>\n </select>\n </div>\n <div class=\"ix-field-row\">\n <div class=\"ix-field\">\n <label>Duration (ms)</label>\n <input type=\"number\" [(ngModel)]=\"newDuration\" min=\"0\" step=\"50\">\n </div>\n <div class=\"ix-field\">\n <label>Delay (ms)</label>\n <input type=\"number\" [(ngModel)]=\"newDelay\" min=\"0\" step=\"50\">\n </div>\n </div>\n <div class=\"ix-field\">\n <label>Easing</label>\n <select [(ngModel)]=\"newEasing\">\n <option *ngFor=\"let e of easingOptions\" [value]=\"e\">{{ e }}</option>\n </select>\n </div>\n <div class=\"ix-field ix-checkbox-field\">\n <label>\n <input type=\"checkbox\" [(ngModel)]=\"newOnce\">\n Play once\n </label>\n </div>\n <div class=\"ix-form-actions\">\n <button class=\"ix-btn ix-btn--secondary\" (click)=\"cancelAdd()\">Cancel</button>\n <button class=\"ix-btn ix-btn--primary\" (click)=\"confirmAdd()\">Add</button>\n </div>\n </div>\n\n <!-- Add button -->\n <button *ngIf=\"!isAdding\" class=\"ix-add-btn\" (click)=\"startAdd()\">\n <i class=\"ph-thin ph-plus\"></i> Add interaction\n </button>\n </ng-container>\n</div>\n", styles: [".interactions-panel{padding:.5rem}.section-empty{color:var(--p-text-muted-color);font-size:12px;text-align:center;padding:1rem}.ix-card{border:1px solid var(--p-surface-200);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .15s}.ix-card.is-expanded{border-color:#cb9090}.ix-card-header{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;font-size:12px;transition:background .12s}.ix-card-header:hover{background:var(--p-surface-50)}.ix-trigger-icon{font-size:14px;color:#cb9090;flex-shrink:0}.ix-card-summary{flex:1;min-width:0;display:flex;align-items:center;gap:4px;overflow:hidden}.ix-trigger-label{font-weight:500;white-space:nowrap}.ix-arrow{color:var(--p-text-muted-color);flex-shrink:0}.ix-action-label{color:var(--p-text-muted-color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ix-remove-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;border-radius:4px;flex-shrink:0;font-size:12px}.ix-remove-btn:hover{background:#dc26261a;color:#dc2626}.ix-remove-btn.small{width:16px;height:16px;font-size:10px}.ix-card-body{padding:0 10px 10px;border-top:1px solid var(--p-surface-100)}.ix-action-row{padding:8px 0;border-bottom:1px dashed var(--p-surface-100)}.ix-action-row:last-of-type{border-bottom:none}.ix-action-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.ix-action-index{font-size:11px;font-weight:600;color:var(--p-text-muted-color);text-transform:uppercase;letter-spacing:.03em}.ix-add-action-btn{display:flex;align-items:center;gap:4px;margin-top:8px;padding:4px 8px;border:1px dashed var(--p-surface-300);background:transparent;color:var(--p-text-muted-color);cursor:pointer;border-radius:6px;font-size:11px;transition:border-color .15s,color .15s}.ix-add-action-btn:hover{border-color:#cb9090;color:#cb9090}.ix-field{margin-bottom:8px}.ix-field label{display:block;font-size:11px;color:var(--p-text-muted-color);margin-bottom:3px;font-weight:500}.ix-field select,.ix-field input[type=number]{width:100%;padding:5px 8px;border:1px solid var(--p-surface-200);border-radius:6px;font-size:12px;background:var(--p-surface-0);color:var(--p-text-color);outline:none;transition:border-color .15s}.ix-field select:focus,.ix-field input[type=number]:focus{border-color:#cb9090}.ix-field-row{display:flex;gap:8px}.ix-field-row .ix-field{flex:1}.ix-checkbox-field label{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;color:var(--p-text-color)}.ix-checkbox-field input[type=checkbox]{accent-color:#cb9090}.ix-add-form{border:1px solid var(--p-surface-200);border-radius:8px;padding:10px;margin-bottom:6px}.ix-form-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:4px}.ix-btn{padding:5px 12px;border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:background .12s,color .12s}.ix-btn--primary{background:#cb9090;color:#fff}.ix-btn--primary:hover{background:#be7474}.ix-btn--secondary{background:transparent;color:var(--p-text-muted-color)}.ix-btn--secondary:hover{background:var(--p-surface-100);color:var(--p-text-color)}.ix-add-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px;border:1px dashed var(--p-surface-300);background:transparent;color:var(--p-text-muted-color);cursor:pointer;border-radius:8px;font-size:12px;transition:border-color .15s,color .15s}.ix-add-btn:hover{border-color:#cb9090;color:#cb9090}\n"] }]
3829
+ }], ctorParameters: () => [{ type: OverlayService }], propDecorators: { node: [{
3830
+ type: Input
3831
+ }] } });
3832
+
3833
+ // StylePanelComponent and PagePanelComponent use <dynamic-input> which must be
3834
+ // provided by the consuming app (InputsModule). Declare them there, not here.
3835
+ const DECLARATIONS = [
3836
+ BuilderComponent,
3837
+ RenderDirective,
3838
+ IoxDraggableDirective,
3839
+ IoxDropzoneDirective,
3840
+ OverlayComponent,
3841
+ ToolbarComponent,
3842
+ SectionComponent,
3843
+ BuilderContainerComponent,
3844
+ BuilderLinkedContainerComponent,
3845
+ BuilderRepeaterComponent,
3846
+ PanelComponent,
3847
+ PanelChildComponent,
3848
+ LayerTreeComponent,
3849
+ InteractionsPanelComponent,
3850
+ ];
3851
+ class IoxBuilderModule {
3852
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxBuilderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
3853
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.11", ngImport: i0, type: IoxBuilderModule, declarations: [BuilderComponent,
3854
+ RenderDirective,
3855
+ IoxDraggableDirective,
3856
+ IoxDropzoneDirective,
3857
+ OverlayComponent,
3858
+ ToolbarComponent,
3859
+ SectionComponent,
3860
+ BuilderContainerComponent,
3861
+ BuilderLinkedContainerComponent,
3862
+ BuilderRepeaterComponent,
3863
+ PanelComponent,
3864
+ PanelChildComponent,
3865
+ LayerTreeComponent,
3866
+ InteractionsPanelComponent], imports: [CommonModule,
3867
+ FormsModule,
3868
+ DragDropModule,
3869
+ AccordionModule,
3870
+ ButtonModule,
3871
+ PopoverModule,
3872
+ InputTextModule,
3873
+ SelectModule,
3874
+ TooltipModule,
3875
+ DialogModule,
3876
+ IoxPageModule], exports: [
3877
+ // Angular/PrimeNG modules re-exported so consumers can use them
3878
+ CommonModule,
3879
+ FormsModule,
3880
+ DragDropModule,
3881
+ AccordionModule,
3882
+ ButtonModule,
3883
+ PopoverModule,
3884
+ InputTextModule,
3885
+ SelectModule,
3886
+ TooltipModule,
3887
+ DialogModule,
3888
+ IoxPageModule,
3889
+ // Builder declarations
3890
+ BuilderComponent,
3891
+ RenderDirective,
3892
+ IoxDraggableDirective,
3893
+ IoxDropzoneDirective,
3894
+ OverlayComponent,
3895
+ ToolbarComponent,
3896
+ SectionComponent,
3897
+ BuilderContainerComponent,
3898
+ BuilderLinkedContainerComponent,
3899
+ BuilderRepeaterComponent,
3900
+ PanelComponent,
3901
+ PanelChildComponent,
3902
+ LayerTreeComponent,
3903
+ InteractionsPanelComponent] }); }
3904
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxBuilderModule, providers: [
3905
+ DragEngineService,
3906
+ OverlayService,
3907
+ ComponentRegistryService,
3908
+ DataSourceRegistryService,
3909
+ InteractionEngineService,
3910
+ ViewportService,
3911
+ StyleRegistryService,
3912
+ PanelEventService,
3913
+ ], imports: [CommonModule,
3914
+ FormsModule,
3915
+ DragDropModule,
3916
+ AccordionModule,
3917
+ ButtonModule,
3918
+ PopoverModule,
3919
+ InputTextModule,
3920
+ SelectModule,
3921
+ TooltipModule,
3922
+ DialogModule,
3923
+ IoxPageModule,
3924
+ // Angular/PrimeNG modules re-exported so consumers can use them
3925
+ CommonModule,
3926
+ FormsModule,
3927
+ DragDropModule,
3928
+ AccordionModule,
3929
+ ButtonModule,
3930
+ PopoverModule,
3931
+ InputTextModule,
3932
+ SelectModule,
3933
+ TooltipModule,
3934
+ DialogModule,
3935
+ IoxPageModule] }); }
3936
+ }
3937
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxBuilderModule, decorators: [{
3938
+ type: NgModule,
3939
+ args: [{
3940
+ declarations: DECLARATIONS,
3941
+ imports: [
3942
+ CommonModule,
3943
+ FormsModule,
3944
+ DragDropModule,
3945
+ AccordionModule,
3946
+ ButtonModule,
3947
+ PopoverModule,
3948
+ InputTextModule,
3949
+ SelectModule,
3950
+ TooltipModule,
3951
+ DialogModule,
3952
+ IoxPageModule,
3953
+ ],
3954
+ providers: [
3955
+ DragEngineService,
3956
+ OverlayService,
3957
+ ComponentRegistryService,
3958
+ DataSourceRegistryService,
3959
+ InteractionEngineService,
3960
+ ViewportService,
3961
+ StyleRegistryService,
3962
+ PanelEventService,
3963
+ ],
3964
+ exports: [
3965
+ // Angular/PrimeNG modules re-exported so consumers can use them
3966
+ CommonModule,
3967
+ FormsModule,
3968
+ DragDropModule,
3969
+ AccordionModule,
3970
+ ButtonModule,
3971
+ PopoverModule,
3972
+ InputTextModule,
3973
+ SelectModule,
3974
+ TooltipModule,
3975
+ DialogModule,
3976
+ IoxPageModule,
3977
+ // Builder declarations
3978
+ BuilderComponent,
3979
+ RenderDirective,
3980
+ IoxDraggableDirective,
3981
+ IoxDropzoneDirective,
3982
+ OverlayComponent,
3983
+ ToolbarComponent,
3984
+ SectionComponent,
3985
+ BuilderContainerComponent,
3986
+ BuilderLinkedContainerComponent,
3987
+ BuilderRepeaterComponent,
3988
+ PanelComponent,
3989
+ PanelChildComponent,
3990
+ LayerTreeComponent,
3991
+ InteractionsPanelComponent,
3992
+ ],
3993
+ schemas: [NO_ERRORS_SCHEMA],
3994
+ }]
3995
+ }] });
3996
+
3997
+ class BuilderButtonComponentConfig extends ComponentConfig {
3998
+ constructor() {
3999
+ super('Button', 'app-builder-button', 'ph-thin ph-cursor-click', 'Basic');
4000
+ this.inputs = {
4001
+ label: 'text',
4002
+ link: 'text',
4003
+ variant: 'select'
4004
+ };
4005
+ this.traits = [
4006
+ new TraitConfig('label', 'Label', TraitInputType.Text, undefined, 'Click me'),
4007
+ new TraitConfig('link', 'Link', TraitInputType.Text, undefined, '#'),
4008
+ new TraitConfig('variant', 'Variant', TraitInputType.Select, ['primary', 'outlined', 'text'], 'primary'),
4009
+ ];
4010
+ this.styleTraits = [
4011
+ new PositionGroupStyleConfig(),
4012
+ new LayoutGroupStyleConfig(),
4013
+ new SizeGroupStyleConfig(),
4014
+ new TypographyGroupStyleConfig(),
4015
+ new SpacingGroupStyleConfig(),
4016
+ new BorderGroupStyleConfig(),
4017
+ new BackgroundGroupStyleConfig(),
4018
+ new EffectsGroupStyleConfig(),
4019
+ ];
4020
+ }
4021
+ }
4022
+
4023
+ class CardComponentConfig extends ComponentConfig {
4024
+ constructor() {
4025
+ super('Card', 'app-card', 'ph-thin ph-cards', 'Content');
4026
+ this.inputs = {
4027
+ title: 'text',
4028
+ image: 'image',
4029
+ description: 'text'
4030
+ };
4031
+ this.traits = [
4032
+ new TraitConfig('title', 'Title', TraitInputType.Text, undefined, 'Default Title'),
4033
+ new TraitConfig('image', 'Image URL', TraitInputType.Media, { mode: 'compact' }, 'https://via.placeholder.com/150'),
4034
+ new TraitConfig('description', 'Description', TraitInputType.Text, undefined, 'This is a default card description.')
4035
+ ];
4036
+ this.styleTraits = [
4037
+ new PositionGroupStyleConfig(),
4038
+ new LayoutGroupStyleConfig(),
4039
+ new SizeGroupStyleConfig(),
4040
+ new SpacingGroupStyleConfig(),
4041
+ new BorderGroupStyleConfig(),
4042
+ new BackgroundGroupStyleConfig(),
4043
+ new EffectsGroupStyleConfig(),
4044
+ ];
4045
+ this.applyStyleDefaults({
4046
+ width: '200px',
4047
+ height: 'auto',
4048
+ padding: '16px 16px 16px 16px',
4049
+ borderWidth: '1px 1px 1px 1px',
4050
+ borderStyle: 'solid',
4051
+ borderColor: '#cccccc',
4052
+ borderRadius: '8px',
4053
+ backgroundColor: '#ffffff',
4054
+ });
4055
+ }
4056
+ }
4057
+
4058
+ class BuilderContainerComponentConfig extends ComponentConfig {
4059
+ constructor() {
4060
+ super('Container', 'app-builder-container', 'ph-thin ph-square', 'Layout');
4061
+ this.children = [];
4062
+ this.traits = [];
4063
+ this.styleTraits = [
4064
+ new LayoutGroupStyleConfig(),
4065
+ new SizeGroupStyleConfig(),
4066
+ new SpacingGroupStyleConfig(),
4067
+ new BorderGroupStyleConfig(),
4068
+ new BackgroundGroupStyleConfig(),
4069
+ new EffectsGroupStyleConfig(),
4070
+ ];
4071
+ this.applyStyleDefaults({
4072
+ width: '100%',
4073
+ height: 'auto',
4074
+ minHeight: '200px',
4075
+ display: 'block',
4076
+ backgroundColor: 'transparent',
4077
+ });
4078
+ }
4079
+ }
4080
+
4081
+ class BuilderDividerComponentConfig extends ComponentConfig {
4082
+ constructor() {
4083
+ super('Divider', 'app-builder-divider', 'ph-thin ph-minus', 'Layout');
4084
+ this.traits = [];
4085
+ this.styleTraits = [
4086
+ new LayoutGroupStyleConfig(),
4087
+ new SizeGroupStyleConfig(),
4088
+ new BackgroundGroupStyleConfig(),
4089
+ new SpacingGroupStyleConfig(),
4090
+ new BorderGroupStyleConfig(),
4091
+ new EffectsGroupStyleConfig(),
4092
+ ];
4093
+ this.applyStyleDefaults({
4094
+ width: '100%',
4095
+ height: '1px',
4096
+ backgroundColor: '#cccccc',
4097
+ margin: '8px 0px 8px 0px',
4098
+ });
4099
+ }
4100
+ }
4101
+
4102
+ class BuilderHeadingComponentConfig extends ComponentConfig {
4103
+ constructor() {
4104
+ super('Heading', 'app-builder-heading', 'ph-thin ph-text-h', 'Basic');
4105
+ this.traits = [
4106
+ new TraitConfig('content', 'Content', TraitInputType.Text, undefined, 'Heading'),
4107
+ new TraitConfig('level', 'Level', TraitInputType.Select, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], 'h2'),
4108
+ ];
4109
+ this.styleTraits = [
4110
+ new PositionGroupStyleConfig(),
4111
+ new LayoutGroupStyleConfig(),
4112
+ new SizeGroupStyleConfig(),
4113
+ new TypographyGroupStyleConfig(),
4114
+ new SpacingGroupStyleConfig(),
4115
+ new BorderGroupStyleConfig(),
4116
+ new BackgroundGroupStyleConfig(),
4117
+ new EffectsGroupStyleConfig(),
4118
+ ];
4119
+ this.applyStyleDefaults({
4120
+ fontSize: '32px',
4121
+ fontWeight: '700',
4122
+ width: 'auto',
4123
+ });
4124
+ }
4125
+ }
4126
+
4127
+ class BuilderIconComponentConfig extends ComponentConfig {
4128
+ constructor() {
4129
+ super('Icon', 'app-builder-icon', 'ph-thin ph-star', 'Basic');
4130
+ this.traits = [
4131
+ new TraitConfig('iconClass', 'Icon', TraitInputType.Icon, undefined, 'ph-thin ph-star'),
4132
+ ];
4133
+ this.styleTraits = [
4134
+ new PositionGroupStyleConfig(),
4135
+ new LayoutGroupStyleConfig(),
4136
+ new SizeGroupStyleConfig(),
4137
+ new GroupStyleConfig(StyleCategory.Typography, [
4138
+ new TraitConfig('fontSize', 'Size', TraitInputType.Scrub, { min: 8, max: 200, step: 1, units: UNITS_FIXED }, '24px'),
4139
+ new TraitConfig('color', 'Color', TraitInputType.Color, { allowGradient: false, allowTransparent: false, formats: ['hex', 'rgb', 'hsl'] }, '#374151'),
4140
+ ]),
4141
+ new SpacingGroupStyleConfig(),
4142
+ new BorderGroupStyleConfig(),
4143
+ new BackgroundGroupStyleConfig(),
4144
+ new EffectsGroupStyleConfig(),
4145
+ ];
4146
+ }
4147
+ }
4148
+
4149
+ class BuilderImageComponentConfig extends ComponentConfig {
4150
+ constructor() {
4151
+ super('Image', 'app-builder-image', 'ph-thin ph-image', 'Basic');
4152
+ this.inputs = {
4153
+ src: 'text',
4154
+ alt: 'text',
4155
+ objectFit: 'select'
4156
+ };
4157
+ this.traits = [
4158
+ new TraitConfig('src', 'Image URL', TraitInputType.Media, { mode: 'compact' }, 'https://placehold.co/600x400?text=Image'),
4159
+ new TraitConfig('alt', 'Alt Text', TraitInputType.Text, undefined, 'Image'),
4160
+ new TraitConfig('objectFit', 'Object Fit', TraitInputType.Select, ['cover', 'contain', 'fill', 'none'], 'cover'),
4161
+ ];
4162
+ this.styleTraits = [
4163
+ new PositionGroupStyleConfig(),
4164
+ new LayoutGroupStyleConfig(),
4165
+ new SizeGroupStyleConfig(),
4166
+ new SpacingGroupStyleConfig(),
4167
+ new BorderGroupStyleConfig(),
4168
+ new BackgroundGroupStyleConfig(),
4169
+ ];
4170
+ this.applyStyleDefaults({
4171
+ width: '300px',
4172
+ aspectRatio: '1/1',
4173
+ });
4174
+ }
4175
+ }
4176
+
4177
+ class BuilderLinkComponentConfig extends ComponentConfig {
4178
+ constructor() {
4179
+ super('Link', 'app-builder-link', 'ph-thin ph-link', 'Basic');
4180
+ this.inputs = {
4181
+ label: 'text',
4182
+ linkType: 'select',
4183
+ url: 'text',
4184
+ pageRoute: 'text',
4185
+ target: 'select',
4186
+ };
4187
+ this.traits = [
4188
+ new TraitConfig('label', 'Label', TraitInputType.Text, undefined, 'Click here'),
4189
+ new TraitConfig('linkType', 'Link Type', TraitInputType.Select, [{ label: 'External URL', value: 'external' }, { label: 'Website Page', value: 'page' }], 'external'),
4190
+ new TraitConfig('url', 'URL', TraitInputType.Text, undefined, 'https://', false, { trait: 'linkType', values: 'external' }),
4191
+ new TraitConfig('pageRoute', 'Page Route', TraitInputType.Text, undefined, '', false, { trait: 'linkType', values: 'page' }),
4192
+ new TraitConfig('target', 'Target', TraitInputType.Select, [
4193
+ { label: 'Same tab', value: '_self' },
4194
+ { label: 'New tab', value: '_blank' },
4195
+ { label: 'Parent frame', value: '_parent' },
4196
+ { label: 'Full window', value: '_top' },
4197
+ ], '_self'),
4198
+ ];
4199
+ this.styleTraits = [
4200
+ new PositionGroupStyleConfig(),
4201
+ new SpacingGroupStyleConfig(),
4202
+ new TypographyGroupStyleConfig(),
4203
+ new EffectsGroupStyleConfig(),
4204
+ ];
4205
+ }
4206
+ }
4207
+
4208
+ class BuilderLinkedContainerConfig extends ComponentConfig {
4209
+ constructor() {
4210
+ super('LinkedContainer', 'app-builder-linked-container', 'ph-thin ph-link-simple', 'Layout');
4211
+ this.children = [];
4212
+ this.inputs = {
4213
+ linkType: 'select',
4214
+ url: 'text',
4215
+ pageRoute: 'text',
4216
+ target: 'select',
4217
+ };
4218
+ this.traits = [
4219
+ new TraitConfig('linkType', 'Link Type', TraitInputType.Select, [{ label: 'External URL', value: 'external' }, { label: 'Website Page', value: 'page' }], 'external'),
4220
+ new TraitConfig('url', 'URL', TraitInputType.Text, undefined, 'https://', false, { trait: 'linkType', values: 'external' }),
4221
+ new TraitConfig('pageRoute', 'Page Route', TraitInputType.Text, undefined, '', false, { trait: 'linkType', values: 'page' }),
4222
+ new TraitConfig('target', 'Target', TraitInputType.Select, [
4223
+ { label: 'Same tab', value: '_self' },
4224
+ { label: 'New tab', value: '_blank' },
4225
+ { label: 'Parent frame', value: '_parent' },
4226
+ { label: 'Full window', value: '_top' },
4227
+ ], '_self'),
4228
+ ];
4229
+ this.styleTraits = [
4230
+ new PositionGroupStyleConfig(),
4231
+ new LayoutGroupStyleConfig(),
4232
+ new SizeGroupStyleConfig(),
4233
+ new SpacingGroupStyleConfig(),
4234
+ new BorderGroupStyleConfig(),
4235
+ new BackgroundGroupStyleConfig(),
4236
+ new EffectsGroupStyleConfig(),
4237
+ ];
4238
+ this.applyStyleDefaults({
4239
+ width: 'auto',
4240
+ height: 'auto',
4241
+ minHeight: '80px',
4242
+ display: 'block',
4243
+ });
4244
+ }
4245
+ }
4246
+
4247
+ class RepeaterComponentConfig extends ComponentConfig {
4248
+ constructor() {
4249
+ super('Repeater', 'app-builder-repeater', 'ph-thin ph-repeat', 'Data');
4250
+ this.children = [];
4251
+ this.traits = [
4252
+ {
4253
+ name: 'source',
4254
+ label: 'Source',
4255
+ type: TraitInputType.Select,
4256
+ options: [], // populated at runtime from DataSourceRegistryService aliases
4257
+ default: '',
4258
+ },
4259
+ {
4260
+ name: 'limit',
4261
+ label: 'Limit',
4262
+ type: TraitInputType.Number,
4263
+ default: 10,
4264
+ },
4265
+ ];
4266
+ this.styleTraits = [
4267
+ new LayoutGroupStyleConfig(),
4268
+ new SizeGroupStyleConfig(),
4269
+ new SpacingGroupStyleConfig(),
4270
+ new BorderGroupStyleConfig(),
4271
+ new BackgroundGroupStyleConfig(),
4272
+ new EffectsGroupStyleConfig(),
4273
+ ];
4274
+ this.applyStyleDefaults({
4275
+ width: '100%',
4276
+ height: 'auto',
4277
+ });
4278
+ }
4279
+ }
4280
+
4281
+ class SectionComponentConfig extends ComponentConfig {
4282
+ constructor() {
4283
+ super('Section', 'app-section', 'ph-thin ph-rows', 'Layout');
4284
+ this.children = []; // marks this config as a layout component — RenderDirective checks `node.children !== undefined`
4285
+ this.traits = [];
4286
+ this.styleTraits = [
4287
+ new LayoutGroupStyleConfig(),
4288
+ new SizeGroupStyleConfig(),
4289
+ new SpacingGroupStyleConfig(),
4290
+ new BorderGroupStyleConfig(),
4291
+ new BackgroundGroupStyleConfig(),
4292
+ new EffectsGroupStyleConfig(),
4293
+ ];
4294
+ this.applyStyleDefaults({
4295
+ width: 'auto',
4296
+ height: 'auto',
4297
+ minHeight: '200px',
4298
+ display: 'block',
4299
+ backgroundColor: 'transparent',
4300
+ });
4301
+ }
4302
+ }
4303
+
4304
+ class BuilderSpacerComponentConfig extends ComponentConfig {
4305
+ constructor() {
4306
+ super('Spacer', 'app-builder-spacer', 'ph-thin ph-arrows-vertical', 'Layout');
4307
+ this.traits = [];
4308
+ this.styleTraits = [
4309
+ new LayoutGroupStyleConfig(),
4310
+ new SizeGroupStyleConfig(),
4311
+ new EffectsGroupStyleConfig(),
4312
+ ];
4313
+ this.applyStyleDefaults({
4314
+ width: '100%',
4315
+ height: '40px',
4316
+ });
4317
+ }
4318
+ }
4319
+
4320
+ class TextBlockComponentConfig extends ComponentConfig {
4321
+ constructor() {
4322
+ super('Text', 'app-text-block', 'ph-thin ph-text-aa', 'Basic');
4323
+ this.inputs = {
4324
+ content: 'text',
4325
+ tag: 'select',
4326
+ };
4327
+ this.traits = [
4328
+ new TraitConfig('content', 'Content', TraitInputType.Text, undefined, 'Enter text here'),
4329
+ new TraitConfig('tag', 'Tag', TraitInputType.Select, ['p', 'span'], 'p'),
4330
+ ];
4331
+ this.styleTraits = [
4332
+ new PositionGroupStyleConfig(),
4333
+ new LayoutGroupStyleConfig(),
4334
+ new SizeGroupStyleConfig(),
4335
+ new TypographyGroupStyleConfig(),
4336
+ new SpacingGroupStyleConfig(),
4337
+ new BorderGroupStyleConfig(),
4338
+ new BackgroundGroupStyleConfig(),
4339
+ new EffectsGroupStyleConfig(),
4340
+ ];
4341
+ this.applyStyleDefaults({
4342
+ width: '200px',
4343
+ height: 'auto',
4344
+ });
4345
+ }
4346
+ }
4347
+
4348
+ /*
4349
+ * Public API Surface of @vectoriox/iox-builder
4350
+ */
4351
+
4352
+ /**
4353
+ * Generated bundle index. Do not edit.
4354
+ */
4355
+
4356
+ export { ACTION_TYPE_OPTIONS, BuilderButtonComponentConfig, BuilderComponent, BuilderContainerComponent, BuilderContainerComponentConfig, BuilderDividerComponentConfig, BuilderHeadingComponentConfig, BuilderIconComponentConfig, BuilderImageComponentConfig, BuilderLinkComponentConfig, BuilderLinkedContainerComponent, BuilderLinkedContainerConfig, BuilderMode, BuilderRepeaterComponent, BuilderSpacerComponentConfig, CardComponentConfig, ComponentConfig, ComponentRegistryService, DEVICE_OPTIONS, DataSourceRegistryService, DeviceMode, DragEngineService, EASING_OPTIONS$1 as EASING_OPTIONS, GroupStyleConfig, IOX_CONTENT_SERVICE, IOX_FONT_MANAGER, InteractionEngineService, InteractionsPanelComponent, IoxBuilderModule, IoxDraggableDirective, IoxDropzoneDirective, LayerTreeComponent, NodeAction, OverlayComponent, OverlayService, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, SectionComponent, SectionComponentConfig, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, ViewportService, ZOOM_OPTIONS, defaultPageSettings, generateNodeId, resolveTraitControllerType, resolveTraitOptions };
4357
+ //# sourceMappingURL=vectoriox-iox-builder.mjs.map