@vectoriox/iox-builder 7.0.2 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, inject, NgZone, EventEmitter, createComponent, Output, Input, Inject, Directive, ChangeDetectionStrategy, Component, ViewChild, effect, Optional, ContentChildren, NO_ERRORS_SCHEMA, NgModule } from '@angular/core';
2
+ import { Injectable, InjectionToken, Optional, Inject, inject, NgZone, EventEmitter, createComponent, Output, Input, Directive, ChangeDetectionStrategy, Component, ViewChild, effect, ContentChildren, NO_ERRORS_SCHEMA, NgModule } from '@angular/core';
3
3
  import * as i2 from '@angular/common';
4
4
  import { DOCUMENT, CommonModule } from '@angular/common';
5
5
  import * as i16 from '@angular/forms';
6
6
  import { FormsModule } from '@angular/forms';
7
- import * as i4 from '@angular/cdk/drag-drop';
7
+ import * as i5 from '@angular/cdk/drag-drop';
8
8
  import { DragDropModule } from '@angular/cdk/drag-drop';
9
9
  import { AccordionModule } from 'primeng/accordion';
10
10
  import * as i17 from 'primeng/button';
@@ -16,7 +16,7 @@ import { SelectModule } from 'primeng/select';
16
16
  import { TooltipModule } from 'primeng/tooltip';
17
17
  import { DialogModule } from 'primeng/dialog';
18
18
  import * as i1 from '@vectoriox/iox-ui';
19
- import { VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, Breakpoint, BREAKPOINTS, countOverrides, setOverride, defaultElementStyleFor, applyElementStyle, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, buildTargetedStateSelector, statePseudoClass, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, breakpointDef, breakpointForWidth, FormRenderState, safeSetInput, ContextStore, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, resolveInputs, setInputOverride, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, resolveBoundValue, IS_PREVIEW, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
19
+ import { VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, Breakpoint, BREAKPOINTS, countOverrides, setOverride, NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, applyElementStyle, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, buildTargetedStateSelector, statePseudoClass, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, breakpointDef, breakpointForWidth, FormRenderState, safeSetInput, ContextStore, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, resolveInputs, setInputOverride, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, resolveBoundValue, IS_PREVIEW, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
20
20
  export { ComponentInstanceRegistryService, VIRTUAL_TRAIT_KEYS, composeVirtualTraits } from '@vectoriox/iox-ui';
21
21
  import Lenis from 'lenis';
22
22
  import { Subject, BehaviorSubject, of, forkJoin } from 'rxjs';
@@ -784,6 +784,24 @@ function reissueIdentityTraits(node) {
784
784
  trait.default = `${prefix}-${freshId()}`;
785
785
  }
786
786
  }
787
+ /**
788
+ * A NEW node is born with its identity: an empty identity trait (a palette Form's `formId`) is issued,
789
+ * one already set (a preset's `contact-…`) is kept. A Form inserted from the palette used to keep
790
+ * `formId: ''` — its submissions posted to `forms//submit` and nothing was ever stored.
791
+ */
792
+ function issueIdentityTraits(root) {
793
+ for (const trait of (root.traits ?? [])) {
794
+ if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
795
+ continue;
796
+ if (trait.default === undefined || trait.default === '')
797
+ trait.default = `${IDENTITY_PREFIX[trait.name] ?? trait.name}-${freshId()}`;
798
+ }
799
+ for (const child of root.children ?? [])
800
+ if (child)
801
+ issueIdentityTraits(child);
802
+ }
803
+ /** The readable prefix a freshly issued identity carries (`form-a1b2c3`). */
804
+ const IDENTITY_PREFIX = { formId: 'form' };
787
805
  function regenerateNodeIds(root, opts = {}) {
788
806
  const idMap = new Map();
789
807
  const assign = (node) => {
@@ -1601,6 +1619,8 @@ const BUILDER_CHROME_EN = Object.freeze({
1601
1619
  'builder.chrome.save': 'Save',
1602
1620
  'builder.chrome.preview': 'Preview',
1603
1621
  'builder.chrome.addImage': 'Add image',
1622
+ 'builder.chrome.refuseNestedForm': 'A form can’t go inside another form. Place it outside the form.',
1623
+ 'builder.chrome.refuseStateBlock': 'Success and error messages belong to their form. They can’t be placed on their own.',
1604
1624
  });
1605
1625
  const CANVAS_EMPTY_IMAGE_LABEL_KEY = 'builder.chrome.addImage';
1606
1626
  const CANVAS_EMPTY_IMAGE_LABEL_EN = BUILDER_CHROME_EN[CANVAS_EMPTY_IMAGE_LABEL_KEY];
@@ -1651,6 +1671,7 @@ class ComponentRegistryService {
1651
1671
  if (!config)
1652
1672
  return null;
1653
1673
  wearDefaultStyles(config);
1674
+ issueIdentityTraits(config);
1654
1675
  return config;
1655
1676
  }
1656
1677
  /**
@@ -1664,16 +1685,33 @@ class ComponentRegistryService {
1664
1685
  }
1665
1686
  /** The unstyled base style map of a NEW node of the type — `switchElementStyle`'s baseline. */
1666
1687
  baselineStyleProps(type) {
1688
+ const out = this.adoptionBaseline(type);
1689
+ for (const [name, value] of Object.entries(NEW_NODE_STYLE_DEFAULTS))
1690
+ if (name in out)
1691
+ out[name] = value;
1692
+ return out;
1693
+ }
1694
+ /** What an untouched SAVED node of the type holds — its constructor defaults, nothing on top.
1695
+ * Adopt (DS7) rewrites an owned trait only while it still holds this. */
1696
+ adoptionBaseline(type) {
1667
1697
  const out = {};
1668
1698
  for (const group of this.createBaseConfig(type)?.styleTraits ?? []) {
1669
1699
  for (const trait of group.traits ?? [])
1670
1700
  out[trait.name] = trait.default;
1671
1701
  }
1672
- for (const [name, value] of Object.entries(NEW_NODE_STYLE_DEFAULTS))
1673
- if (name in out)
1674
- out[name] = value;
1675
1702
  return out;
1676
1703
  }
1704
+ /** The context `adoptElementStyles` (design-core) reads baselines from. */
1705
+ adoptionContext() {
1706
+ const cache = new Map();
1707
+ return {
1708
+ baseline: type => {
1709
+ if (!cache.has(type))
1710
+ cache.set(type, this.adoptionBaseline(type));
1711
+ return cache.get(type);
1712
+ },
1713
+ };
1714
+ }
1677
1715
  register(type, component, config) {
1678
1716
  this.components[type] = { type, component, config };
1679
1717
  }
@@ -1686,13 +1724,6 @@ class ComponentRegistryService {
1686
1724
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ComponentRegistryService, decorators: [{
1687
1725
  type: Injectable
1688
1726
  }] });
1689
- /**
1690
- * What a new node holds before its element style is written. `color: inherit` because a new node
1691
- * belongs to the section it is dropped into: running text inherits (contract `body`/`lead`), so a
1692
- * literal on any ancestor — a Form, a slide, a field group — would override the section's text.
1693
- * A style that owns a colour (headings, buttons, notices) writes its own over this.
1694
- */
1695
- const NEW_NODE_STYLE_DEFAULTS = { color: 'inherit' };
1696
1727
  /** A new node AND the default children its config builds (a palette Form's fields, submit and
1697
1728
  * notices) are all new elements: each wears its type's default style. */
1698
1729
  function wearDefaultStyles(node) {
@@ -1911,6 +1942,93 @@ function resolveDragTarget(el, selectedElement, isDragHandle) {
1911
1942
  return (isDragHandle || isDescendantOfSelected) ? selectedElement : el;
1912
1943
  }
1913
1944
 
1945
+ /**
1946
+ * What may NOT be placed where — the ONE rule every insert path asks (drag, click-to-insert, quick-add,
1947
+ * the Form presets, paste, duplicate). Everything not refused here is accepted: any container-capable
1948
+ * node takes any child, a Form included.
1949
+ */
1950
+ var InsertRefusal;
1951
+ (function (InsertRefusal) {
1952
+ /** A form inside a form — fields belong to their NEAREST form (forms plan D1), and HTML forbids it. */
1953
+ InsertRefusal["NestedForm"] = "nestedForm";
1954
+ /** A Success / Error block is part of its form's state machine; it is never placed on its own. */
1955
+ InsertRefusal["StateBlockAlone"] = "stateBlockAlone";
1956
+ })(InsertRefusal || (InsertRefusal = {}));
1957
+ const FORM_STATE_BLOCK_TYPES = new Set(['FormSuccess', 'FormError']);
1958
+ /** The builder-chrome label that says each refusal on screen (English in `BUILDER_CHROME_EN`). */
1959
+ const INSERT_REFUSAL_LABEL_KEYS = Object.freeze({
1960
+ [InsertRefusal.NestedForm]: 'builder.chrome.refuseNestedForm',
1961
+ [InsertRefusal.StateBlockAlone]: 'builder.chrome.refuseStateBlock',
1962
+ });
1963
+ /** The refusal in the host's language (the library's English when no host resolver is provided). */
1964
+ function insertRefusalLabel(reason, labels = defaultBuilderLabels) {
1965
+ const key = INSERT_REFUSAL_LABEL_KEYS[reason];
1966
+ return labels(key, BUILDER_CHROME_EN[key]);
1967
+ }
1968
+ function containsType(node, type) {
1969
+ if (node.type === type)
1970
+ return true;
1971
+ return (node.children ?? []).some(child => containsType(child, type));
1972
+ }
1973
+ /**
1974
+ * Why `node` may not be inserted under `ancestors` (outermost first, the new parent last; empty = the page),
1975
+ * or null when it may. `node` is the whole subtree being placed — a Container holding a Form is refused
1976
+ * inside a Form just as a bare Form is. Reordering a node inside its CURRENT parent is not an insert and
1977
+ * never asks.
1978
+ */
1979
+ function insertRefusal(node, ancestors) {
1980
+ if (!node)
1981
+ return null;
1982
+ if (node.type && FORM_STATE_BLOCK_TYPES.has(node.type))
1983
+ return InsertRefusal.StateBlockAlone;
1984
+ if (ancestors.some(a => a?.type === FORM_TYPE) && containsType(node, FORM_TYPE))
1985
+ return InsertRefusal.NestedForm;
1986
+ return null;
1987
+ }
1988
+
1989
+ /**
1990
+ * ROOT ESCAPE — near the top or bottom edge of a ROOT-level container the drop goes to the PAGE, between
1991
+ * root containers, instead of inside the container. This file is the decision; DragEngineService only
1992
+ * measures and applies it.
1993
+ *
1994
+ * ⚠️ The bug this replaced (2026-10-19, "a Form cannot be inserted into a Section / Container / Linked
1995
+ * Container"): escape switched on when the pointer was in the edge band and the PREVIOUS target was the
1996
+ * same container — which is also true on the second sample after ENTERING through the band — and it only
1997
+ * switched off once the pointer LEFT the container. So approaching a root container from above or below
1998
+ * locked every drop inside it to the page, even at its centre; an empty (short) container was almost
1999
+ * always entered that way. Now escape needs a move OUTWARD — from the container's inner region to its
2000
+ * edge — and it ends as soon as the pointer is back in the inner region.
2001
+ */
2002
+ /** The edge band on a tall container, in screen px. */
2003
+ const ROOT_ESCAPE_PX = 14;
2004
+ var RootZone;
2005
+ (function (RootZone) {
2006
+ RootZone["Outside"] = "outside";
2007
+ RootZone["Edge"] = "edge";
2008
+ RootZone["Inner"] = "inner";
2009
+ })(RootZone || (RootZone = {}));
2010
+ /** The band shrinks on a short container (a quarter of its height), so its middle stays a drop target. */
2011
+ function rootEscapeBand(height) {
2012
+ return Math.min(ROOT_ESCAPE_PX, Math.max(0, height) / 4);
2013
+ }
2014
+ /** Where the pointer's y sits relative to a root container's vertical extent. */
2015
+ function rootZone(py, top, bottom) {
2016
+ if (py < top || py > bottom)
2017
+ return RootZone.Outside;
2018
+ const band = rootEscapeBand(bottom - top);
2019
+ return py <= top + band || py >= bottom - band ? RootZone.Edge : RootZone.Inner;
2020
+ }
2021
+ /**
2022
+ * Whether this sample escapes `rootId` to the page: the pointer is on its edge band AND it either was
2023
+ * already escaping it or has just moved out to the edge from its inner region. Entering from outside
2024
+ * through the band never escapes.
2025
+ */
2026
+ function escapesRoot(rootId, zone, memory) {
2027
+ if (zone !== RootZone.Edge)
2028
+ return false;
2029
+ return memory.escapingFor === rootId || memory.innerOf === rootId;
2030
+ }
2031
+
1914
2032
  /**
1915
2033
  * CSS container name of the builder canvas frame (and of the global-elements overlay). Breakpoint
1916
2034
  * rules are emitted as `@container iox-canvas (max-width: …)`: the frame's layout width IS the
@@ -2452,6 +2570,11 @@ const MOTION_EASE = 'cubic-bezier(0.22, 1, 0.36, 1)';
2452
2570
  // Lower = more lag/weight, higher = snappier. 0.35 catches up to the pointer
2453
2571
  // within ~5-6 frames (~80-100ms) — reads as "responsive but weighted".
2454
2572
  const GHOST_FOLLOW_SMOOTHING = 0.35;
2573
+ const TARGET_CHIP_BG = '#6366f1';
2574
+ // The admin chrome's danger fill (toolbar.component.scss's unpublish hover reads the same token).
2575
+ const REFUSED_CHIP_BG = 'var(--iox-danger-text, #8E211A)';
2576
+ /** On the drop-target overlay while the target refuses the dragged payload (styled by the host). */
2577
+ const DROP_REFUSED_CLASS = 'iox-drop-target-overlay--refused';
2455
2578
  function deepCloneNode(node) {
2456
2579
  const clone = JSON.parse(JSON.stringify(node));
2457
2580
  // Fresh ids + remap intra-subtree references (stateTriggers, interaction targets) so a cloned
@@ -2460,10 +2583,12 @@ function deepCloneNode(node) {
2460
2583
  return clone;
2461
2584
  }
2462
2585
  class DragEngineService {
2463
- constructor(registry, symbolRegistry, overlayService) {
2586
+ constructor(registry, symbolRegistry, overlayService, labels) {
2464
2587
  this.registry = registry;
2465
2588
  this.symbolRegistry = symbolRegistry;
2466
2589
  this.overlayService = overlayService;
2590
+ /** Every insert the builder refused, for the host to say on screen (a toast). */
2591
+ this.insertRefused$ = new Subject();
2467
2592
  // ── Drop-zone registry ────────────────────────────────────
2468
2593
  this.ids = [];
2469
2594
  this.elMap = new Map();
@@ -2514,9 +2639,13 @@ class DragEngineService {
2514
2639
  this._scale = 1;
2515
2640
  // ── Dropzone rect cache — snapshotted at drag start and on scroll ─────────
2516
2641
  this._dropzoneRectCache = new Map();
2517
- // ── Root-escape ───────────────────────────────────────────
2518
- this.ROOT_ESCAPE_PX = 14;
2642
+ // ── Root-escape (decision: utils/root-escape.util.ts) ─────
2519
2643
  this._rootEscapeActiveFor = null;
2644
+ /** The root container whose inner region held the previous sample. */
2645
+ this._innerRootId = null;
2646
+ // ── Drop refusal (utils/insert-rules.util.ts) ─────────────
2647
+ /** Why the current target would refuse the dragged payload; null = it accepts. */
2648
+ this._targetRefusal = null;
2520
2649
  // ─────────────────────────────────────────────────────────
2521
2650
  // Draggable registration
2522
2651
  // ─────────────────────────────────────────────────────────
@@ -2534,10 +2663,47 @@ class DragEngineService {
2534
2663
  this._overlay.style.top = this._ghostY + 'px';
2535
2664
  this._ghostRaf = requestAnimationFrame(this._ghostFollowStep);
2536
2665
  };
2666
+ this.labels = labels ?? defaultBuilderLabels;
2667
+ }
2668
+ /**
2669
+ * The ONE acceptance check for every insert path: null when `data` (a panel payload or a node) may go
2670
+ * under `ancestors`; otherwise the refusal, already announced on `insertRefused$`.
2671
+ */
2672
+ refuseInsert(data, ancestors) {
2673
+ const reason = insertRefusal(this.payloadNode(data), ancestors);
2674
+ if (reason)
2675
+ this.insertRefused$.next({ reason, message: insertRefusalLabel(reason, this.labels) });
2676
+ return reason;
2677
+ }
2678
+ /** The node a payload WOULD insert, without building it: a type string, a block/symbol, or a node. */
2679
+ payloadNode(data) {
2680
+ if (typeof data === 'string')
2681
+ return { type: data };
2682
+ if (!data || typeof data !== 'object')
2683
+ return null;
2684
+ const blk = data;
2685
+ const symbolNode = blk.symbolId ? this.symbolRegistry.getSymbol(blk.symbolId)?.node : null;
2686
+ return (symbolNode ?? blk.node ?? blk);
2687
+ }
2688
+ /** The nodes a dropzone sits in, outermost first, its own node last — read from the DOM, which is
2689
+ * where a dropzone id lives. Empty for the page itself. */
2690
+ dropzoneAncestors(id) {
2691
+ const out = [];
2692
+ for (let el = this._getEl(id); el && el.id !== 'canvas-preview'; el = el.parentElement) {
2693
+ const match = this.overlayService.findNodeByElement(el);
2694
+ if (match)
2695
+ out.unshift(match.node);
2696
+ }
2697
+ return out;
2537
2698
  }
2538
2699
  // ── Shared drop handler — used by all layout components ───────────────────
2539
2700
  handleDrop(children, event, dropListId, cdr, afterDrop) {
2540
2701
  const { payload, insertIndex } = event;
2702
+ const changesParent = payload.type === 'external' || payload.sourceId !== dropListId;
2703
+ if (changesParent && this.refuseInsert(payload.data, this.dropzoneAncestors(dropListId))) {
2704
+ afterDrop?.();
2705
+ return;
2706
+ }
2541
2707
  if (payload.type === 'external') {
2542
2708
  const node = this.materialize(payload.data);
2543
2709
  if (node) {
@@ -2611,7 +2777,9 @@ class DragEngineService {
2611
2777
  * `dropComplete$` exactly like a drop, so the builder selects the new node and records one undo
2612
2778
  * step through the same path. Returns the inserted node, or null if the payload was unusable.
2613
2779
  */
2614
- insertPayload(children, index, data) {
2780
+ insertPayload(children, index, data, ancestors = []) {
2781
+ if (this.refuseInsert(data, ancestors))
2782
+ return null;
2615
2783
  const node = this.materialize(data);
2616
2784
  if (!node)
2617
2785
  return null;
@@ -2657,7 +2825,7 @@ class DragEngineService {
2657
2825
  registerDropzone(id, el, data, cdr, dropCb, postDrop) {
2658
2826
  if (!id)
2659
2827
  return;
2660
- // Stamp id on element so DOM-walk helpers (_isRootContainer) find it.
2828
+ // Stamp id on element so DOM-walk helpers (_rootContainerOf, dropzoneAncestors) find it.
2661
2829
  el.id = id;
2662
2830
  if (!this.ids.includes(id)) {
2663
2831
  this.ids = [...this.ids, id];
@@ -2763,6 +2931,8 @@ class DragEngineService {
2763
2931
  this._sourceEl = el;
2764
2932
  this._isDragging = true;
2765
2933
  this._rootEscapeActiveFor = null;
2934
+ this._innerRootId = null;
2935
+ this._targetRefusal = null;
2766
2936
  // Suppress native text selection for the duration of the drag — without this,
2767
2937
  // dragging the pointer across any text (canvas or sidebar) selects/highlights it.
2768
2938
  document.body.classList.add('iox-dragging');
@@ -2877,6 +3047,8 @@ class DragEngineService {
2877
3047
  this._removeTargetOverlay();
2878
3048
  document.querySelectorAll('.iox-drag-over').forEach(el => el.classList.remove('iox-drag-over'));
2879
3049
  this._rootEscapeActiveFor = null;
3050
+ this._innerRootId = null;
3051
+ this._targetRefusal = null;
2880
3052
  this._isDragging = false;
2881
3053
  this._payload = null;
2882
3054
  this._sourceEl = null;
@@ -2964,55 +3136,14 @@ class DragEngineService {
2964
3136
  }
2965
3137
  }
2966
3138
  }
2967
- // Root-escape with hysteresis.
2968
- // Purpose: when the cursor is near the top/bottom edge of a root-level
2969
- // section, treat the target as canvas-preview so the user can insert
2970
- // between root sections.
2971
- //
2972
- // IMPORTANT: only activate if the cursor was ALREADY inside this section
2973
- // (i.e. deepTarget is the section itself or a nested child of it).
2974
- // Without this guard, crossing from Section A into the top 14px of
2975
- // Section C immediately locks the target to canvas-preview and prevents
2976
- // the user from ever reaching Section C's nested containers.
2977
- if (this._rootEscapeActiveFor) {
2978
- const escEl = this._getEl(this._rootEscapeActiveFor);
2979
- if (escEl) {
2980
- const r = this._dropzoneRectCache.get(this._rootEscapeActiveFor) ?? escEl.getBoundingClientRect();
2981
- const inside = px >= r.left && px <= r.right && py >= r.top && py <= r.bottom;
2982
- if (inside) {
2983
- bestId = 'canvas-preview';
2984
- }
2985
- else {
2986
- this._rootEscapeActiveFor = null;
2987
- }
2988
- }
2989
- else {
2990
- this._rootEscapeActiveFor = null;
2991
- }
2992
- }
2993
- if (!this._rootEscapeActiveFor &&
2994
- bestId && bestId !== 'canvas-preview' && this._isRootContainer(bestId)) {
2995
- const el = this._getEl(bestId);
2996
- if (el) {
2997
- const rect = this._dropzoneRectCache.get(bestId) ?? el.getBoundingClientRect();
2998
- const nearEdge = py <= rect.top + this.ROOT_ESCAPE_PX ||
2999
- py >= rect.bottom - this.ROOT_ESCAPE_PX;
3000
- // Only escape if we were already targeting this section or
3001
- // something nested inside it — not when crossing into it fresh.
3002
- const wasInsideThisSection = this._deepTargetId === bestId ||
3003
- this._isDescendantDropzone(this._deepTargetId, bestId);
3004
- if (nearEdge && wasInsideThisSection) {
3005
- this._rootEscapeActiveFor = bestId;
3006
- bestId = 'canvas-preview';
3007
- }
3008
- }
3009
- }
3139
+ bestId = this._applyRootEscape(bestId, py);
3010
3140
  if (bestId !== this._deepTargetId) {
3011
3141
  if (this._deepTargetId) {
3012
3142
  const old = this._getEl(this._deepTargetId);
3013
3143
  old?.classList.remove('iox-drag-over');
3014
3144
  }
3015
3145
  this._deepTargetId = bestId;
3146
+ this._targetRefusal = this._refusalAt(bestId);
3016
3147
  if (bestId) {
3017
3148
  this._getEl(bestId)?.classList.add('iox-drag-over');
3018
3149
  }
@@ -3024,6 +3155,50 @@ class DragEngineService {
3024
3155
  this._indicatorContainerId = null;
3025
3156
  }
3026
3157
  }
3158
+ /** Near a ROOT container's edge, after moving there from its inner region, the drop goes to the page. */
3159
+ _applyRootEscape(bestId, py) {
3160
+ const rootId = bestId && bestId !== 'canvas-preview' ? this._rootContainerOf(bestId) : null;
3161
+ const rootEl = rootId ? this._getEl(rootId) : null;
3162
+ const rect = rootId && rootEl ? (this._dropzoneRectCache.get(rootId) ?? rootEl.getBoundingClientRect()) : null;
3163
+ const zone = rect ? rootZone(py, rect.top, rect.bottom) : RootZone.Outside;
3164
+ const escape = !!rootId && rootId === bestId &&
3165
+ escapesRoot(rootId, zone, { escapingFor: this._rootEscapeActiveFor, innerOf: this._innerRootId });
3166
+ this._rootEscapeActiveFor = escape ? rootId : null;
3167
+ this._innerRootId = zone === RootZone.Inner ? rootId : null;
3168
+ return escape ? 'canvas-preview' : bestId;
3169
+ }
3170
+ /** The ROOT-level dropzone (a direct child zone of the page) that holds dropzone `id`, or null when it
3171
+ * is not on the page canvas (the global-elements layer). */
3172
+ _rootContainerOf(id) {
3173
+ let root = null;
3174
+ for (let el = this._getEl(id); el; el = el.parentElement) {
3175
+ if (el.id === 'canvas-preview')
3176
+ return root;
3177
+ if (el.id && this.elMap.has(el.id))
3178
+ root = el.id;
3179
+ }
3180
+ return null;
3181
+ }
3182
+ /** Why the dragged payload may not land in `targetId` (null = it may). Reordering inside the source
3183
+ * container is not an insert. */
3184
+ _refusalAt(targetId) {
3185
+ if (!targetId || !this._payload)
3186
+ return null;
3187
+ if (this._payload.type === 'internal' && this._payload.sourceId === targetId)
3188
+ return null;
3189
+ const ancestors = targetId === 'canvas-preview' ? [] : this.dropzoneAncestors(targetId);
3190
+ return insertRefusal(this.payloadNode(this._payload.data), ancestors);
3191
+ }
3192
+ /** The node a dropzone element belongs to — the element itself or the nearest registered ancestor
3193
+ * (a LinkedContainer, a Dialog, a Repeater or a slide registers an INNER element as its zone). */
3194
+ _ownerNode(el) {
3195
+ for (let cur = el; cur && cur.id !== 'canvas-preview'; cur = cur.parentElement) {
3196
+ const match = this.overlayService.findNodeByElement(cur);
3197
+ if (match)
3198
+ return match.node;
3199
+ }
3200
+ return undefined;
3201
+ }
3027
3202
  // ─────────────────────────────────────────────────────────
3028
3203
  // Target overlay — fixed to <body>, sized from getBoundingClientRect
3029
3204
  // so it is always above all canvas content regardless of stacking
@@ -3057,7 +3232,7 @@ class DragEngineService {
3057
3232
  this._targetChip.style.cssText = [
3058
3233
  'position:absolute', 'left:0', 'top:-18px', 'max-width:100%', 'overflow:hidden',
3059
3234
  'white-space:nowrap', 'text-overflow:ellipsis', 'padding:1px 6px',
3060
- 'border-radius:4px 4px 0 0', 'background:#6366f1', 'color:#fff',
3235
+ 'border-radius:4px 4px 0 0', `background:${TARGET_CHIP_BG}`, 'color:#fff',
3061
3236
  'font:500 10px/1.5 system-ui,sans-serif', 'pointer-events:none',
3062
3237
  ].join(';');
3063
3238
  this._targetOverlay.appendChild(this._targetChip);
@@ -3085,40 +3260,19 @@ class DragEngineService {
3085
3260
  _updateTargetChip(targetId, el) {
3086
3261
  if (!this._targetChip)
3087
3262
  return;
3088
- const name = this.overlayService.findNodeByElement(el)?.node;
3263
+ const refused = this._targetRefusal;
3264
+ this._targetOverlay?.classList.toggle(DROP_REFUSED_CLASS, !!refused);
3265
+ this._targetChip.style.background = refused ? REFUSED_CHIP_BG : TARGET_CHIP_BG;
3266
+ if (refused) {
3267
+ this._targetChip.textContent = insertRefusalLabel(refused, this.labels);
3268
+ return;
3269
+ }
3270
+ const name = this._ownerNode(el);
3089
3271
  const childCount = this.getData(targetId)?.length ?? 0;
3090
3272
  const index = this._indicatorContainerId === targetId && this._indicatorIndex >= 0
3091
3273
  ? this._indicatorIndex : childCount;
3092
3274
  this._targetChip.textContent = dropTargetLabel(name?.label || name?.type, index, childCount);
3093
3275
  }
3094
- _isRootContainer(id) {
3095
- const el = this._getEl(id);
3096
- if (!el)
3097
- return false;
3098
- let parent = el.parentElement;
3099
- while (parent) {
3100
- if (parent.id === 'canvas-preview')
3101
- return true;
3102
- if (parent.id && this.ids.includes(parent.id))
3103
- return false;
3104
- parent = parent.parentElement;
3105
- }
3106
- return false;
3107
- }
3108
- /**
3109
- * Returns true if the dropzone with `childId` is a DOM descendant of the
3110
- * dropzone with `parentId`. Used to decide whether the cursor was already
3111
- * "inside" a root section before triggering root-escape.
3112
- */
3113
- _isDescendantDropzone(childId, parentId) {
3114
- if (!childId)
3115
- return false;
3116
- const childEl = this._getEl(childId);
3117
- const parentEl = this._getEl(parentId);
3118
- if (!childEl || !parentEl)
3119
- return false;
3120
- return parentEl.contains(childEl);
3121
- }
3122
3276
  // ─────────────────────────────────────────────────────────
3123
3277
  // Insertion indicator (animates open so siblings shift smoothly)
3124
3278
  // ─────────────────────────────────────────────────────────
@@ -3271,12 +3425,17 @@ class DragEngineService {
3271
3425
  }
3272
3426
  return children.length;
3273
3427
  }
3274
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: DragEngineService, deps: [{ token: ComponentRegistryService }, { token: SymbolRegistryService }, { token: OverlayService }], target: i0.ɵɵFactoryTarget.Injectable }); }
3428
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: DragEngineService, deps: [{ token: ComponentRegistryService }, { token: SymbolRegistryService }, { token: OverlayService }, { token: IOX_BUILDER_LABELS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
3275
3429
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: DragEngineService }); }
3276
3430
  }
3277
3431
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: DragEngineService, decorators: [{
3278
3432
  type: Injectable
3279
- }], ctorParameters: () => [{ type: ComponentRegistryService }, { type: SymbolRegistryService }, { type: OverlayService }] });
3433
+ }], ctorParameters: () => [{ type: ComponentRegistryService }, { type: SymbolRegistryService }, { type: OverlayService }, { type: undefined, decorators: [{
3434
+ type: Optional
3435
+ }, {
3436
+ type: Inject,
3437
+ args: [IOX_BUILDER_LABELS]
3438
+ }] }] });
3280
3439
 
3281
3440
  /**
3282
3441
  * Page-scoped registry that holds the current page's data sources.
@@ -5982,9 +6141,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
5982
6141
  }] } });
5983
6142
 
5984
6143
  class LayerTreeComponent {
5985
- constructor(registry, overlayService) {
6144
+ constructor(registry, overlayService, dragEngine) {
5986
6145
  this.registry = registry;
5987
6146
  this.overlayService = overlayService;
6147
+ this.dragEngine = dragEngine;
5988
6148
  this.layout = [];
5989
6149
  this.globalElements = [];
5990
6150
  this.nodeSelect = new EventEmitter();
@@ -6092,6 +6252,13 @@ class LayerTreeComponent {
6092
6252
  const targetTreeNode = this.flatTree[event.currentIndex];
6093
6253
  if (!targetTreeNode)
6094
6254
  return;
6255
+ // Landing beside the target means landing in the target's parent — the same acceptance rule
6256
+ // every canvas insert asks (a form never goes inside a form).
6257
+ if (targetTreeNode.parentArray !== draggedTreeNode.parentArray) {
6258
+ const ancestors = (nodeAncestorPathInEither(this.layout, this.globalElements, targetTreeNode.node) ?? []).slice(0, -1);
6259
+ if (this.dragEngine.refuseInsert(draggedTreeNode.node, ancestors))
6260
+ return;
6261
+ }
6095
6262
  // Remove from source parent
6096
6263
  const srcArray = draggedTreeNode.parentArray;
6097
6264
  const srcIdx = draggedTreeNode.indexInParent;
@@ -6155,13 +6322,13 @@ class LayerTreeComponent {
6155
6322
  }
6156
6323
  }
6157
6324
  }
6158
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: LayerTreeComponent, deps: [{ token: ComponentRegistryService }, { token: OverlayService }], target: i0.ɵɵFactoryTarget.Component }); }
6159
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: LayerTreeComponent, isStandalone: false, selector: "app-layer-tree", inputs: { layout: "layout", globalElements: "globalElements" }, outputs: { nodeSelect: "nodeSelect", nodeAction: "nodeAction", nodeMove: "nodeMove" }, ngImport: i0, template: "<div class=\"layer-tree\">\n <div class=\"tree-header\">\n <button class=\"section-tab\" [class.section-tab--active]=\"activeSection === 'page'\" (click)=\"activeSection = 'page'\">\n <i class=\"ph-thin ph-stack\"></i> Page\n </button>\n <button class=\"section-tab\" [class.section-tab--active]=\"activeSection === 'global'\" (click)=\"activeSection = 'global'\">\n <i class=\"ph-thin ph-globe\"></i> Global\n <span *ngIf=\"flatGlobalTree.length\" class=\"section-tab-badge\">{{ flatGlobalTree.length }}</span>\n </button>\n </div>\n\n <!-- \u2500\u2500 Page elements tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div *ngIf=\"activeSection === 'page'\"\n 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 <i class=\"ph-thin ph-dots-six-vertical drag-handle\" cdkDragHandle></i>\n\n <button *ngIf=\"tn.hasChildren\" class=\"expand-btn\" (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 <i [class]=\"tn.icon\" class=\"node-icon\"></i>\n <ng-container *ngIf=\"editingNodeId === tn.node.id; else staticLabel\">\n <input class=\"node-rename-input\"\n [value]=\"editingValue\"\n (input)=\"editingValue = $any($event.target).value\"\n (keydown.enter)=\"saveRename(tn)\"\n (keydown.escape)=\"cancelRename()\"\n (blur)=\"saveRename(tn)\"\n (click)=\"$event.stopPropagation()\"\n autofocus>\n </ng-container>\n <ng-template #staticLabel>\n <span class=\"node-label\" [class.node-label--named]=\"tn.node.label\">{{ tn.label }}</span>\n </ng-template>\n\n <span class=\"node-actions\">\n <button class=\"action-btn\" title=\"Rename\" (click)=\"startRename(tn, $event)\">\n <i class=\"ph-thin ph-pencil-simple\"></i>\n </button>\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\n <!-- \u2500\u2500 Global elements tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div *ngIf=\"activeSection === 'global'\" class=\"tree-body\">\n <div *ngIf=\"!flatGlobalTree.length\" class=\"tree-empty\">\n <i class=\"ph-thin ph-globe\"></i>\n <span>No global elements</span>\n </div>\n\n <div *ngFor=\"let tn of flatGlobalTree\"\n class=\"tree-row tree-row--global\"\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 <button *ngIf=\"tn.hasChildren\" class=\"expand-btn\" (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 <i [class]=\"tn.icon\" class=\"node-icon\"></i>\n <span class=\"node-label\">{{ tn.label }}</span>\n <span class=\"node-position-badge\">{{ tn.node.globalPosition }}</span>\n\n <span class=\"node-actions\">\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{display:flex;gap:2px;padding:6px 8px;border-bottom:1px solid var(--p-surface-200);flex-shrink:0}.layer-tree .section-tab{display:inline-flex;align-items:center;gap:4px;flex:1;justify-content:center;padding:4px 8px;font-size:11px;font-weight:500;border:1px solid var(--p-surface-200);border-radius:4px;background:transparent;color:var(--p-text-muted-color);cursor:pointer;transition:background .12s,color .12s,border-color .12s;white-space:nowrap}.layer-tree .section-tab i{font-size:11px}.layer-tree .section-tab:hover{background:var(--p-surface-100);color:var(--p-text-color)}.layer-tree .section-tab--active{background:#cb909026;border-color:#cb9090;color:#cb9090}.layer-tree .section-tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 4px;font-size:10px;font-weight:600;background:#cb9090;color:#fff;border-radius:8px}.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-label.node-label--named{color:var(--p-primary-color, #cb9090);font-weight:500}.layer-tree .node-rename-input{flex:1;min-width:0;border:1px solid var(--p-primary-color, #cb9090);border-radius:4px;background:var(--p-surface-0, #fff);color:var(--p-text-color);font-family:inherit;font-size:.72rem;padding:1px 4px;outline:none}.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 .tree-section-header{display:flex;align-items:center;gap:6px;padding:6px 12px 4px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--p-text-muted-color);border-top:1px solid var(--p-surface-200);margin-top:4px}.layer-tree .tree-section-header i{font-size:12px}.layer-tree .tree-body--global{flex:0 0 auto}.layer-tree .tree-row--global{border-inline-start-color:transparent}.layer-tree .tree-row--global.is-selected{background:#638fcb1a;border-inline-start-color:#638fcb}.layer-tree .node-position-badge{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#638fcb;background:#638fcb1f;padding:1px 5px;border-radius:3px;flex-shrink:0}.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"] }] }); }
6325
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: LayerTreeComponent, deps: [{ token: ComponentRegistryService }, { token: OverlayService }, { token: DragEngineService }], target: i0.ɵɵFactoryTarget.Component }); }
6326
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: LayerTreeComponent, isStandalone: false, selector: "app-layer-tree", inputs: { layout: "layout", globalElements: "globalElements" }, outputs: { nodeSelect: "nodeSelect", nodeAction: "nodeAction", nodeMove: "nodeMove" }, ngImport: i0, template: "<div class=\"layer-tree\">\n <div class=\"tree-header\">\n <button class=\"section-tab\" [class.section-tab--active]=\"activeSection === 'page'\" (click)=\"activeSection = 'page'\">\n <i class=\"ph-thin ph-stack\"></i> Page\n </button>\n <button class=\"section-tab\" [class.section-tab--active]=\"activeSection === 'global'\" (click)=\"activeSection = 'global'\">\n <i class=\"ph-thin ph-globe\"></i> Global\n <span *ngIf=\"flatGlobalTree.length\" class=\"section-tab-badge\">{{ flatGlobalTree.length }}</span>\n </button>\n </div>\n\n <!-- \u2500\u2500 Page elements tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div *ngIf=\"activeSection === 'page'\"\n 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 <i class=\"ph-thin ph-dots-six-vertical drag-handle\" cdkDragHandle></i>\n\n <button *ngIf=\"tn.hasChildren\" class=\"expand-btn\" (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 <i [class]=\"tn.icon\" class=\"node-icon\"></i>\n <ng-container *ngIf=\"editingNodeId === tn.node.id; else staticLabel\">\n <input class=\"node-rename-input\"\n [value]=\"editingValue\"\n (input)=\"editingValue = $any($event.target).value\"\n (keydown.enter)=\"saveRename(tn)\"\n (keydown.escape)=\"cancelRename()\"\n (blur)=\"saveRename(tn)\"\n (click)=\"$event.stopPropagation()\"\n autofocus>\n </ng-container>\n <ng-template #staticLabel>\n <span class=\"node-label\" [class.node-label--named]=\"tn.node.label\">{{ tn.label }}</span>\n </ng-template>\n\n <span class=\"node-actions\">\n <button class=\"action-btn\" title=\"Rename\" (click)=\"startRename(tn, $event)\">\n <i class=\"ph-thin ph-pencil-simple\"></i>\n </button>\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\n <!-- \u2500\u2500 Global elements tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div *ngIf=\"activeSection === 'global'\" class=\"tree-body\">\n <div *ngIf=\"!flatGlobalTree.length\" class=\"tree-empty\">\n <i class=\"ph-thin ph-globe\"></i>\n <span>No global elements</span>\n </div>\n\n <div *ngFor=\"let tn of flatGlobalTree\"\n class=\"tree-row tree-row--global\"\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 <button *ngIf=\"tn.hasChildren\" class=\"expand-btn\" (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 <i [class]=\"tn.icon\" class=\"node-icon\"></i>\n <span class=\"node-label\">{{ tn.label }}</span>\n <span class=\"node-position-badge\">{{ tn.node.globalPosition }}</span>\n\n <span class=\"node-actions\">\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{display:flex;gap:2px;padding:6px 8px;border-bottom:1px solid var(--p-surface-200);flex-shrink:0}.layer-tree .section-tab{display:inline-flex;align-items:center;gap:4px;flex:1;justify-content:center;padding:4px 8px;font-size:11px;font-weight:500;border:1px solid var(--p-surface-200);border-radius:4px;background:transparent;color:var(--p-text-muted-color);cursor:pointer;transition:background .12s,color .12s,border-color .12s;white-space:nowrap}.layer-tree .section-tab i{font-size:11px}.layer-tree .section-tab:hover{background:var(--p-surface-100);color:var(--p-text-color)}.layer-tree .section-tab--active{background:#cb909026;border-color:#cb9090;color:#cb9090}.layer-tree .section-tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 4px;font-size:10px;font-weight:600;background:#cb9090;color:#fff;border-radius:8px}.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-label.node-label--named{color:var(--p-primary-color, #cb9090);font-weight:500}.layer-tree .node-rename-input{flex:1;min-width:0;border:1px solid var(--p-primary-color, #cb9090);border-radius:4px;background:var(--p-surface-0, #fff);color:var(--p-text-color);font-family:inherit;font-size:.72rem;padding:1px 4px;outline:none}.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 .tree-section-header{display:flex;align-items:center;gap:6px;padding:6px 12px 4px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--p-text-muted-color);border-top:1px solid var(--p-surface-200);margin-top:4px}.layer-tree .tree-section-header i{font-size:12px}.layer-tree .tree-body--global{flex:0 0 auto}.layer-tree .tree-row--global{border-inline-start-color:transparent}.layer-tree .tree-row--global.is-selected{background:#638fcb1a;border-inline-start-color:#638fcb}.layer-tree .node-position-badge{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#638fcb;background:#638fcb1f;padding:1px 5px;border-radius:3px;flex-shrink:0}.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: i5.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: i5.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: i5.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }] }); }
6160
6327
  }
6161
6328
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: LayerTreeComponent, decorators: [{
6162
6329
  type: Component,
6163
6330
  args: [{ selector: 'app-layer-tree', standalone: false, template: "<div class=\"layer-tree\">\n <div class=\"tree-header\">\n <button class=\"section-tab\" [class.section-tab--active]=\"activeSection === 'page'\" (click)=\"activeSection = 'page'\">\n <i class=\"ph-thin ph-stack\"></i> Page\n </button>\n <button class=\"section-tab\" [class.section-tab--active]=\"activeSection === 'global'\" (click)=\"activeSection = 'global'\">\n <i class=\"ph-thin ph-globe\"></i> Global\n <span *ngIf=\"flatGlobalTree.length\" class=\"section-tab-badge\">{{ flatGlobalTree.length }}</span>\n </button>\n </div>\n\n <!-- \u2500\u2500 Page elements tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div *ngIf=\"activeSection === 'page'\"\n 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 <i class=\"ph-thin ph-dots-six-vertical drag-handle\" cdkDragHandle></i>\n\n <button *ngIf=\"tn.hasChildren\" class=\"expand-btn\" (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 <i [class]=\"tn.icon\" class=\"node-icon\"></i>\n <ng-container *ngIf=\"editingNodeId === tn.node.id; else staticLabel\">\n <input class=\"node-rename-input\"\n [value]=\"editingValue\"\n (input)=\"editingValue = $any($event.target).value\"\n (keydown.enter)=\"saveRename(tn)\"\n (keydown.escape)=\"cancelRename()\"\n (blur)=\"saveRename(tn)\"\n (click)=\"$event.stopPropagation()\"\n autofocus>\n </ng-container>\n <ng-template #staticLabel>\n <span class=\"node-label\" [class.node-label--named]=\"tn.node.label\">{{ tn.label }}</span>\n </ng-template>\n\n <span class=\"node-actions\">\n <button class=\"action-btn\" title=\"Rename\" (click)=\"startRename(tn, $event)\">\n <i class=\"ph-thin ph-pencil-simple\"></i>\n </button>\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\n <!-- \u2500\u2500 Global elements tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div *ngIf=\"activeSection === 'global'\" class=\"tree-body\">\n <div *ngIf=\"!flatGlobalTree.length\" class=\"tree-empty\">\n <i class=\"ph-thin ph-globe\"></i>\n <span>No global elements</span>\n </div>\n\n <div *ngFor=\"let tn of flatGlobalTree\"\n class=\"tree-row tree-row--global\"\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 <button *ngIf=\"tn.hasChildren\" class=\"expand-btn\" (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 <i [class]=\"tn.icon\" class=\"node-icon\"></i>\n <span class=\"node-label\">{{ tn.label }}</span>\n <span class=\"node-position-badge\">{{ tn.node.globalPosition }}</span>\n\n <span class=\"node-actions\">\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{display:flex;gap:2px;padding:6px 8px;border-bottom:1px solid var(--p-surface-200);flex-shrink:0}.layer-tree .section-tab{display:inline-flex;align-items:center;gap:4px;flex:1;justify-content:center;padding:4px 8px;font-size:11px;font-weight:500;border:1px solid var(--p-surface-200);border-radius:4px;background:transparent;color:var(--p-text-muted-color);cursor:pointer;transition:background .12s,color .12s,border-color .12s;white-space:nowrap}.layer-tree .section-tab i{font-size:11px}.layer-tree .section-tab:hover{background:var(--p-surface-100);color:var(--p-text-color)}.layer-tree .section-tab--active{background:#cb909026;border-color:#cb9090;color:#cb9090}.layer-tree .section-tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 4px;font-size:10px;font-weight:600;background:#cb9090;color:#fff;border-radius:8px}.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-label.node-label--named{color:var(--p-primary-color, #cb9090);font-weight:500}.layer-tree .node-rename-input{flex:1;min-width:0;border:1px solid var(--p-primary-color, #cb9090);border-radius:4px;background:var(--p-surface-0, #fff);color:var(--p-text-color);font-family:inherit;font-size:.72rem;padding:1px 4px;outline:none}.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 .tree-section-header{display:flex;align-items:center;gap:6px;padding:6px 12px 4px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--p-text-muted-color);border-top:1px solid var(--p-surface-200);margin-top:4px}.layer-tree .tree-section-header i{font-size:12px}.layer-tree .tree-body--global{flex:0 0 auto}.layer-tree .tree-row--global{border-inline-start-color:transparent}.layer-tree .tree-row--global.is-selected{background:#638fcb1a;border-inline-start-color:#638fcb}.layer-tree .node-position-badge{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#638fcb;background:#638fcb1f;padding:1px 5px;border-radius:3px;flex-shrink:0}.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"] }]
6164
- }], ctorParameters: () => [{ type: ComponentRegistryService }, { type: OverlayService }], propDecorators: { layout: [{
6331
+ }], ctorParameters: () => [{ type: ComponentRegistryService }, { type: OverlayService }, { type: DragEngineService }], propDecorators: { layout: [{
6165
6332
  type: Input
6166
6333
  }], globalElements: [{
6167
6334
  type: Input
@@ -7002,6 +7169,9 @@ class BuilderComponent {
7002
7169
  }
7003
7170
  onDrop(event) {
7004
7171
  const { payload, insertIndex } = event;
7172
+ const changesParent = payload.type === 'external' || payload.sourceId !== 'canvas-preview';
7173
+ if (changesParent && this.dragEngine.refuseInsert(payload.data, []))
7174
+ return;
7005
7175
  if (payload.type === 'external') {
7006
7176
  if (typeof payload.data === 'string') {
7007
7177
  const config = this.registry.createConfig(payload.data);
@@ -7292,7 +7462,7 @@ class BuilderComponent {
7292
7462
  }
7293
7463
  case ToolbarAction.Duplicate: {
7294
7464
  const result = this.findNodeInTree(node, this.layout) ?? this.findNodeInTree(node, this.globalElements);
7295
- if (result) {
7465
+ if (result && !this.dragEngine.refuseInsert(node, this.siblingAncestors(node))) {
7296
7466
  const clone = this.deepCloneWithNewIds(node);
7297
7467
  result.parent.splice(result.index + 1, 0, clone);
7298
7468
  this.cdr.detectChanges();
@@ -7452,6 +7622,8 @@ class BuilderComponent {
7452
7622
  const pastedNode = this.clipboard.getNode();
7453
7623
  if (!pastedNode)
7454
7624
  break;
7625
+ if (this.dragEngine.refuseInsert(pastedNode, this.siblingAncestors(node)))
7626
+ break;
7455
7627
  const clone = this.deepCloneWithNewIds(pastedNode);
7456
7628
  const result = this.findNodeInTree(node, this.layout) ?? this.findNodeInTree(node, this.globalElements);
7457
7629
  if (result) {
@@ -7534,6 +7706,10 @@ class BuilderComponent {
7534
7706
  this.cdr.markForCheck();
7535
7707
  }, 1200);
7536
7708
  }
7709
+ /** The nodes a sibling of `node` sits under — `node`'s ancestors, outermost first (empty at a root). */
7710
+ siblingAncestors(node) {
7711
+ return (nodeAncestorPathInEither(this.layout, this.globalElements, node) ?? [node]).slice(0, -1);
7712
+ }
7537
7713
  findNodeInTree(target, children) {
7538
7714
  for (let i = 0; i < children.length; i++) {
7539
7715
  if (children[i] === target) {
@@ -7627,7 +7803,8 @@ class BuilderComponent {
7627
7803
  if (inGlobals && !target.parent) {
7628
7804
  target = resolveInsertTarget(this.layout, null, n => this.dragEngine.isDropContainer(n.children));
7629
7805
  }
7630
- if (!this.dragEngine.insertPayload(target.children, target.index, data))
7806
+ const ancestors = target.parent ? (nodeAncestorPathInEither(this.layout, this.globalElements, target.parent) ?? [target.parent]) : [];
7807
+ if (!this.dragEngine.insertPayload(target.children, target.index, data, ancestors))
7631
7808
  return;
7632
7809
  this.cdr.detectChanges();
7633
7810
  }
@@ -9896,10 +10073,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
9896
10073
  }] } });
9897
10074
 
9898
10075
  const MAX_HISTORY = 50;
10076
+ const clone = (value) => (value === undefined ? value : JSON.parse(JSON.stringify(value)));
9899
10077
  /**
9900
10078
  * BuilderHistoryService — snapshot-based undo/redo for the page builder.
9901
10079
  *
9902
- * Snapshots are JSON deep-clones of the full layout array.
10080
+ * Snapshots are JSON deep-clones of the full layout array; a step that also changed state outside
10081
+ * the layout records that change, so one undo reverts both.
9903
10082
  * Session-only — not persisted to the backend.
9904
10083
  * Scoped to PageUiComponent.providers[] — one history stack per builder instance.
9905
10084
  */
@@ -9910,8 +10089,8 @@ class BuilderHistoryService {
9910
10089
  }
9911
10090
  get canUndo() { return this._past.length > 1; }
9912
10091
  get canRedo() { return this._future.length > 0; }
9913
- push(layout) {
9914
- this._past.push({ layout: JSON.parse(JSON.stringify(layout)), timestamp: Date.now() });
10092
+ push(layout, change) {
10093
+ this._past.push({ layout: clone(layout), timestamp: Date.now(), ...(change ? { change: clone(change) } : {}) });
9915
10094
  if (this._past.length > MAX_HISTORY) {
9916
10095
  this._past.shift();
9917
10096
  }
@@ -9922,19 +10101,22 @@ class BuilderHistoryService {
9922
10101
  return null;
9923
10102
  const current = this._past.pop();
9924
10103
  this._future.unshift(current);
9925
- return JSON.parse(JSON.stringify(this._past[this._past.length - 1].layout));
10104
+ return this.step(this._past[this._past.length - 1].layout, current.change?.before, !!current.change);
9926
10105
  }
9927
10106
  redo() {
9928
10107
  if (!this.canRedo)
9929
10108
  return null;
9930
10109
  const next = this._future.shift();
9931
10110
  this._past.push(next);
9932
- return JSON.parse(JSON.stringify(next.layout));
10111
+ return this.step(next.layout, next.change?.after, !!next.change);
9933
10112
  }
9934
10113
  clear() {
9935
10114
  this._past = [];
9936
10115
  this._future = [];
9937
10116
  }
10117
+ step(layout, restore, changed) {
10118
+ return changed ? { layout: clone(layout), restore: clone(restore) } : { layout: clone(layout) };
10119
+ }
9938
10120
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderHistoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
9939
10121
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderHistoryService }); }
9940
10122
  }
@@ -11661,5 +11843,5 @@ class BuilderSpacerComponentConfig extends ComponentConfig {
11661
11843
  * Generated bundle index. Do not edit.
11662
11844
  */
11663
11845
 
11664
- export { ACTION_TYPE_OPTIONS, AuditCode, AuditPanelComponent, AuditSeverity, BODY_CRUMB_LABEL, BREAKPOINT_PRESENTATION, BUILDER_CANVAS_CONTAINER, BUILDER_CHROME_EN, BUILDER_FONT_LINK_ATTR, BUILDER_TOKEN_SCOPE_CLASS, BackgroundGroupStyleConfig, BorderGroupStyleConfig, BreadcrumbBarComponent, BuilderButtonBlockComponent, BuilderButtonComponentConfig, BuilderClipboardService, BuilderComponent, BuilderContainerComponent, BuilderContainerComponentConfig, BuilderDataSourceService, BuilderDialogComponent, BuilderDividerComponentConfig, BuilderHeadingComponentConfig, BuilderHistoryService, BuilderIconComponentConfig, BuilderImageComponentConfig, BuilderLinkComponentConfig, BuilderLinkedContainerComponent, BuilderLinkedContainerConfig, BuilderListComponentConfig, BuilderListItemComponentConfig, BuilderMode, BuilderRepeaterComponent, BuilderSpacerComponentConfig, ButtonBlockComponentConfig, CANVAS_EMPTY_IMAGE_LABEL_EN, CANVAS_EMPTY_IMAGE_LABEL_KEY, CANVAS_ROOT_RESET_CSS, CardComponentConfig, ChildLayoutGroupStyleConfig, ComponentConfig, ComponentRegistryService, DEFAULT_ESSENTIAL_TRAITS, DRAG_HANDLE_ATTR, DataSourceRegistryService, DataSourceType, DialogComponentConfig, DimensionsGroupStyleConfig, DragEngineService, EASING_OPTIONS$1 as EASING_OPTIONS, EDITOR_CHROME_ATTRIBUTE, ESSENTIAL_TRAITS_BY_TYPE, EffectsGroupStyleConfig, FIELD_KIND_PRESETS, FIELD_PARTS, FORM_PRESETS, FORM_TYPE, FieldComponentConfig, FieldGroupComponentConfig, FormComponentConfig, FormErrorComponentConfig, FormStateService, FormSuccessComponentConfig, GalleryConfig, GroupStyleConfig, INTERACTION_STATES, INVERSE_ACTION, IOX_BUILDER_CANVAS, IOX_BUILDER_LABELS, IOX_DATA_SOURCE_ENDPOINTS, InteractionEngineService, InteractionPresetRegistryService, IoxBuilderModule, IoxDraggableDirective, IoxDropzoneDirective, LAYOUT_PRESETS, LayerTreeComponent, LayoutGroupStyleConfig, ListGroupStyleConfig, NodeAction, OverlayComponent, OverlayService, PALETTE_PRESET_DEFS, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, PositionGroupStyleConfig, PresetRegistryService, RELATIONAL_STATES, RELATIONAL_STATE_BASE, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, SCREEN_WIDTH_OPTIONS, STRUCTURAL_STATES, SUPPORTED_STATES, SectionComponent, SectionComponentConfig, SizeGroupStyleConfig, SliderContainerComponent, SliderContainerConfig, SliderSlideComponent, SliderSlideConfig, SpacingGroupStyleConfig, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, SubmitButtonComponentConfig, SymbolRegistryService, TARGETED_STATES, TARGETED_STATE_BASE, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, TypographyGroupStyleConfig, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, VALIDATION_STATES, VALIDATION_STATE_CLASS, ViewportService, ZOOM_OPTIONS, arrayItemSchemaFields, arraySchemaFields, auditNodes, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, canFluidise, canvasEmptyImageLabel, collapseColumnsProps, columnCountOf, defaultBuilderLabels, defaultPageSettings, dropTargetLabel, essentialTraitsFor, findNodeLocation, findParentOf, flattenSchemaFields, fluidiseProps, generateNodeId, isHiddenAt, isSpecimen, markSpecimen, matchesControlSearch, nearestFormNode, nodeAncestorPath, nodeAncestorPathInEither, reorderProps, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, setStyleTrait, setTrait, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, textNode, visibilityProps, wrapNode };
11846
+ export { ACTION_TYPE_OPTIONS, AuditCode, AuditPanelComponent, AuditSeverity, BODY_CRUMB_LABEL, BREAKPOINT_PRESENTATION, BUILDER_CANVAS_CONTAINER, BUILDER_CHROME_EN, BUILDER_FONT_LINK_ATTR, BUILDER_TOKEN_SCOPE_CLASS, BackgroundGroupStyleConfig, BorderGroupStyleConfig, BreadcrumbBarComponent, BuilderButtonBlockComponent, BuilderButtonComponentConfig, BuilderClipboardService, BuilderComponent, BuilderContainerComponent, BuilderContainerComponentConfig, BuilderDataSourceService, BuilderDialogComponent, BuilderDividerComponentConfig, BuilderHeadingComponentConfig, BuilderHistoryService, BuilderIconComponentConfig, BuilderImageComponentConfig, BuilderLinkComponentConfig, BuilderLinkedContainerComponent, BuilderLinkedContainerConfig, BuilderListComponentConfig, BuilderListItemComponentConfig, BuilderMode, BuilderRepeaterComponent, BuilderSpacerComponentConfig, ButtonBlockComponentConfig, CANVAS_EMPTY_IMAGE_LABEL_EN, CANVAS_EMPTY_IMAGE_LABEL_KEY, CANVAS_ROOT_RESET_CSS, CardComponentConfig, ChildLayoutGroupStyleConfig, ComponentConfig, ComponentRegistryService, DEFAULT_ESSENTIAL_TRAITS, DRAG_HANDLE_ATTR, DROP_REFUSED_CLASS, DataSourceRegistryService, DataSourceType, DialogComponentConfig, DimensionsGroupStyleConfig, DragEngineService, EASING_OPTIONS$1 as EASING_OPTIONS, EDITOR_CHROME_ATTRIBUTE, ESSENTIAL_TRAITS_BY_TYPE, EffectsGroupStyleConfig, FIELD_KIND_PRESETS, FIELD_PARTS, FORM_PRESETS, FORM_STATE_BLOCK_TYPES, FORM_TYPE, FieldComponentConfig, FieldGroupComponentConfig, FormComponentConfig, FormErrorComponentConfig, FormStateService, FormSuccessComponentConfig, GalleryConfig, GroupStyleConfig, INSERT_REFUSAL_LABEL_KEYS, INTERACTION_STATES, INVERSE_ACTION, IOX_BUILDER_CANVAS, IOX_BUILDER_LABELS, IOX_DATA_SOURCE_ENDPOINTS, InsertRefusal, InteractionEngineService, InteractionPresetRegistryService, IoxBuilderModule, IoxDraggableDirective, IoxDropzoneDirective, LAYOUT_PRESETS, LayerTreeComponent, LayoutGroupStyleConfig, ListGroupStyleConfig, NodeAction, OverlayComponent, OverlayService, PALETTE_PRESET_DEFS, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, PositionGroupStyleConfig, PresetRegistryService, RELATIONAL_STATES, RELATIONAL_STATE_BASE, ROOT_ESCAPE_PX, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, RootZone, SCREEN_WIDTH_OPTIONS, STRUCTURAL_STATES, SUPPORTED_STATES, SectionComponent, SectionComponentConfig, SizeGroupStyleConfig, SliderContainerComponent, SliderContainerConfig, SliderSlideComponent, SliderSlideConfig, SpacingGroupStyleConfig, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, SubmitButtonComponentConfig, SymbolRegistryService, TARGETED_STATES, TARGETED_STATE_BASE, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, TypographyGroupStyleConfig, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, VALIDATION_STATES, VALIDATION_STATE_CLASS, ViewportService, ZOOM_OPTIONS, arrayItemSchemaFields, arraySchemaFields, auditNodes, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, canFluidise, canvasEmptyImageLabel, collapseColumnsProps, columnCountOf, defaultBuilderLabels, defaultPageSettings, dropTargetLabel, escapesRoot, essentialTraitsFor, findNodeLocation, findParentOf, flattenSchemaFields, fluidiseProps, generateNodeId, insertRefusal, insertRefusalLabel, isHiddenAt, isSpecimen, markSpecimen, matchesControlSearch, nearestFormNode, nodeAncestorPath, nodeAncestorPathInEither, reorderProps, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, rootEscapeBand, rootZone, setStyleTrait, setTrait, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, textNode, visibilityProps, wrapNode };
11665
11847
  //# sourceMappingURL=vectoriox-iox-builder.mjs.map