@vectoriox/iox-builder 1.7.4 → 1.7.5

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.
@@ -1838,305 +1838,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
1838
1838
  type: Injectable
1839
1839
  }] });
1840
1840
 
1841
- /**
1842
- * MoveableCanvasService — Phase 1 of the Moveable + Selecto migration
1843
- * (see iox-ai-guidance/architecture/builder/moveable-migration.md).
1844
- *
1845
- * Drives DRAG for the SELECTED element via Moveable instead of interact.js, while REUSING the
1846
- * proven reorder machinery (`DragEngineService.handleDrop`). It is a pure INPUT device: during a
1847
- * drag it applies a TRANSIENT transform, and on drop it RESETS the transform to nothing and instead
1848
- * reorders the layout tree (constraint C1 — a persisted transform on an in-flow canvas element
1849
- * breaks Lenis scroll). Everything here runs ONLY when `moveableMode` is on; interact.js is
1850
- * untouched, so the flag-off path is byte-identical to today.
1851
- *
1852
- * Scale: Moveable reads `getBoundingClientRect()` (already post-scale) and works in viewport
1853
- * client coordinates, so drop hit-testing via `elementsFromPoint` / rects is scale-correct without
1854
- * extra math. The control box is rendered into the same overlay container the selection overlay uses.
1855
- */
1856
- class MoveableCanvasService {
1857
- constructor(dragEngine) {
1858
- this.dragEngine = dragEngine;
1859
- this.moveable = null;
1860
- this.container = null;
1861
- this.scaleGetter = () => 1;
1862
- this.target = null;
1863
- this.node = null;
1864
- this.sourceId = null;
1865
- /** Called after a successful reorder so the host can emit LAYOUT_CHANGED / refresh selection. */
1866
- this.onReorder = null;
1867
- }
1868
- /** Wire the Moveable control box into `container`; `scaleGetter` returns the live canvas zoom. */
1869
- init(container, scaleGetter) {
1870
- this.container = container;
1871
- this.scaleGetter = scaleGetter;
1872
- }
1873
- /** Point Moveable at the selected element (skips inline-edit + preview-clone targets). */
1874
- select(element, node) {
1875
- this.target = null;
1876
- this.node = null;
1877
- this.sourceId = null;
1878
- if (!element || !node || !this.container) {
1879
- this.destroyMoveable();
1880
- return;
1881
- }
1882
- // Preview clones (repeater/slider rows) and hidden nodes are not editable.
1883
- if (element.closest('[data-iox-hidden]') || node.__previewClone) {
1884
- this.destroyMoveable();
1885
- return;
1886
- }
1887
- this.target = element;
1888
- this.node = node;
1889
- this.sourceId = this.resolveDropzoneId(element);
1890
- this.ensureMoveable();
1891
- this.moveable.target = element;
1892
- // Drag is initiated by grabbing the ELEMENT itself (like interact.js) — not a Moveable
1893
- // overlay area — so a plain click still passes through to selection/child-selection and only
1894
- // a movement past the threshold starts a drag. Keeps Moveable visually silent (no hit-area box).
1895
- this.moveable.dragTarget = element;
1896
- this.moveable.updateRect();
1897
- }
1898
- /** Recompute the control-box position (call on scroll/zoom/layout changes). */
1899
- refresh() { this.moveable?.updateRect(); }
1900
- destroy() { this.destroyMoveable(); this.container = null; }
1901
- // ── internals ────────────────────────────────────────────────────────────
1902
- ensureMoveable() {
1903
- if (this.moveable)
1904
- return;
1905
- this.moveable = new Moveable(this.container, {
1906
- target: undefined,
1907
- draggable: true,
1908
- resizable: false, // Phase 3
1909
- rotatable: false, // Phase 3 (out-of-flow only)
1910
- origin: false,
1911
- zoom: 1,
1912
- // INVISIBLE interaction layer: the builder's OverlayService owns ALL selection visuals
1913
- // (pink outline, hover margin/padding rings, toolbar, badges). Moveable must draw nothing
1914
- // here — it only provides the draggable hit-area. `iox-moveable-silent` + the SCSS in
1915
- // builder.component.scss hide any residual control chrome and drop it below the overlay.
1916
- hideDefaultLines: true,
1917
- renderDirections: [],
1918
- className: 'iox-moveable-silent',
1919
- });
1920
- this.moveable.on('dragStart', (e) => {
1921
- // Never start a drag from inside inline text editing.
1922
- if (e.inputEvent?.target?.closest?.('[contenteditable="true"]')) {
1923
- e.stop?.();
1924
- }
1925
- });
1926
- // Transient transform only — visual feedback during the drag.
1927
- this.moveable.on('drag', (e) => { e.target.style.transform = e.transform; });
1928
- this.moveable.on('dragEnd', (e) => this.commitDrag(e));
1929
- }
1930
- destroyMoveable() {
1931
- if (this.moveable) {
1932
- this.moveable.destroy();
1933
- this.moveable = null;
1934
- }
1935
- }
1936
- commitDrag(e) {
1937
- const el = e.target;
1938
- // C1: never persist the transform — reset it; the result is a tree reorder, not a position.
1939
- el.style.transform = '';
1940
- if (!this.node)
1941
- return;
1942
- const pointer = this.lastPointer(e);
1943
- if (!pointer) {
1944
- this.onReorder?.();
1945
- return;
1946
- }
1947
- const targetId = this.dropzoneUnderPoint(pointer.x, pointer.y, el);
1948
- if (!targetId) {
1949
- this.onReorder?.();
1950
- return;
1951
- }
1952
- const children = this.dragEngine.getData(targetId);
1953
- const cdr = this.dragEngine.getCdr(targetId);
1954
- if (!children || !cdr) {
1955
- this.onReorder?.();
1956
- return;
1957
- }
1958
- const insertIndex = this.calcInsertIndex(targetId, pointer.y);
1959
- const srcChildren = this.sourceId ? this.dragEngine.getData(this.sourceId) : undefined;
1960
- const sourceIndex = srcChildren ? srcChildren.indexOf(this.node) : -1;
1961
- const payload = { type: 'internal', data: this.node, sourceId: this.sourceId ?? null, sourceIndex };
1962
- this.dragEngine.handleDrop(children, { payload, targetId, insertIndex }, targetId, cdr, () => {
1963
- this.onReorder?.();
1964
- // Re-point the control box at the moved element's (possibly new) host.
1965
- setTimeout(() => this.moveable?.updateRect(), 0);
1966
- });
1967
- }
1968
- /** Final pointer position from the Moveable end event (client/viewport px). */
1969
- lastPointer(e) {
1970
- const ie = e.inputEvent;
1971
- if (ie && typeof ie.clientX === 'number')
1972
- return { x: ie.clientX, y: ie.clientY };
1973
- if (Array.isArray(e.clientX))
1974
- return { x: e.clientX[0], y: e.clientY[0] };
1975
- if (typeof e.clientX === 'number')
1976
- return { x: e.clientX, y: e.clientY };
1977
- return null;
1978
- }
1979
- /** Walk up from an element to the nearest registered dropzone id (el.id is stamped by DragEngine). */
1980
- resolveDropzoneId(el) {
1981
- let e = el;
1982
- while (e) {
1983
- if (e.id && this.dragEngine.getData(e.id) !== undefined)
1984
- return e.id;
1985
- e = e.parentElement;
1986
- }
1987
- return null;
1988
- }
1989
- /** Topmost registered dropzone under the pointer that is NOT inside the dragged element's subtree. */
1990
- dropzoneUnderPoint(x, y, draggedEl) {
1991
- const stack = document.elementsFromPoint(x, y);
1992
- for (const el of stack) {
1993
- const id = this.resolveDropzoneId(el);
1994
- if (!id)
1995
- continue;
1996
- const zoneEl = document.getElementById(id);
1997
- if (zoneEl && draggedEl.contains(zoneEl))
1998
- continue; // don't drop into own subtree
1999
- return id;
2000
- }
2001
- return null;
2002
- }
2003
- /** Insertion index within a dropzone by comparing pointer Y to each child's vertical midpoint. */
2004
- calcInsertIndex(targetId, y) {
2005
- const zoneEl = document.getElementById(targetId);
2006
- if (!zoneEl)
2007
- return 0;
2008
- const childWraps = Array.from(zoneEl.children).filter((c) => c.matches('.container-child, .section-child, [data-iox-draggable]'));
2009
- let idx = 0;
2010
- for (const w of childWraps) {
2011
- const r = w.getBoundingClientRect();
2012
- if (y > r.top + r.height / 2)
2013
- idx++;
2014
- }
2015
- return idx;
2016
- }
2017
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: MoveableCanvasService, deps: [{ token: DragEngineService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2018
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: MoveableCanvasService, providedIn: 'root' }); }
2019
- }
2020
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: MoveableCanvasService, decorators: [{
2021
- type: Injectable,
2022
- args: [{ providedIn: 'root' }]
2023
- }], ctorParameters: () => [{ type: DragEngineService }] });
2024
-
2025
- // Virtual trait → composed CSS property helpers.
2026
- //
2027
- // Several style traits are "virtual" — they don't map 1:1 to a CSS property but
2028
- // are components of a multi-function CSS value. composeVirtualTraits() strips
2029
- // these virtual keys from the style map and replaces them with the correctly
2030
- // composed CSS properties (filter, backdrop-filter, transform, transition).
2031
- //
2032
- // The same function is used by the render directive (initial mount + state
2033
- // restore) and by the style panel's onUpdate / setStateOverride paths, so
2034
- // the CSS output is always consistent regardless of how styles are applied.
2035
- // ─── Filter ──────────────────────────────────────────────────────────────────
2036
- const FILTER_FUNS = [
2037
- ['filterBlur', 'blur', '0px'],
2038
- ['filterBrightness', 'brightness', '1'],
2039
- ['filterContrast', 'contrast', '1'],
2040
- ['filterGrayscale', 'grayscale', '0'],
2041
- ['filterSaturate', 'saturate', '1'],
2042
- ['filterHueRotate', 'hue-rotate', '0deg'],
2043
- ['filterSepia', 'sepia', '0'],
2044
- ['filterInvert', 'invert', '0'],
2045
- ];
2046
- // ─── Backdrop-filter ─────────────────────────────────────────────────────────
2047
- const BACKDROP_FUNS = [
2048
- ['backdropBlur', 'blur', '0px'],
2049
- ['backdropBrightness', 'brightness', '1'],
2050
- ['backdropContrast', 'contrast', '1'],
2051
- ['backdropSaturate', 'saturate', '1'],
2052
- ];
2053
- // ─── Transform ───────────────────────────────────────────────────────────────
2054
- const TRANSFORM_FUNS = [
2055
- ['translateX', 'translateX', '0px'],
2056
- ['translateY', 'translateY', '0px'],
2057
- ['scaleX', 'scaleX', '1'],
2058
- ['scaleY', 'scaleY', '1'],
2059
- ['rotate', 'rotate', '0deg'],
2060
- ['skewX', 'skewX', '0deg'],
2061
- ['skewY', 'skewY', '0deg'],
2062
- ];
2063
- // ─── ::marker pseudo-element ─────────────────────────────────────────────────
2064
- const MARKER_FUNS = [
2065
- ['markerColor', 'color'],
2066
- ['markerFontSize', 'fontSize'],
2067
- ];
2068
- // ─── All virtual trait names ──────────────────────────────────────────────────
2069
- const VIRTUAL_TRAIT_KEYS = new Set([
2070
- ...FILTER_FUNS.map(([t]) => t),
2071
- ...BACKDROP_FUNS.map(([t]) => t),
2072
- ...TRANSFORM_FUNS.map(([t]) => t),
2073
- 'transitionDuration', 'transitionTimingFunction', 'transitionDelay',
2074
- ...MARKER_FUNS.map(([t]) => t),
2075
- ]);
2076
- // ─── Helpers ─────────────────────────────────────────────────────────────────
2077
- function buildFns(funs, src) {
2078
- return funs
2079
- .filter(([t, , d]) => src[t] != null && src[t] !== '' && src[t] !== d)
2080
- .map(([t, fn]) => `${fn}(${src[t]})`)
2081
- .join(' ');
2082
- }
2083
- // ─── Public API ───────────────────────────────────────────────────────────────
2084
- /**
2085
- * Strip virtual trait keys from `raw` and emit their composed CSS equivalents.
2086
- *
2087
- * When composing a partial state-override map (hover, active…), pass the full
2088
- * base style map as `base` so non-overridden components (e.g. filterBrightness
2089
- * when only filterBlur is overridden) are preserved in the composed output.
2090
- * When operating on a complete style map, omit `base` — it defaults to `raw`.
2091
- */
2092
- function composeVirtualTraits(raw, base = raw) {
2093
- // Copy all non-virtual properties as-is
2094
- const result = {};
2095
- for (const [k, v] of Object.entries(raw)) {
2096
- if (!VIRTUAL_TRAIT_KEYS.has(k))
2097
- result[k] = v;
2098
- }
2099
- // Merge base values for any virtual key absent from the partial override map
2100
- const merged = (funs) => {
2101
- const m = {};
2102
- for (const [t] of funs)
2103
- m[t] = raw[t] ?? base[t];
2104
- return m;
2105
- };
2106
- // filter
2107
- const filterStr = buildFns(FILTER_FUNS, merged(FILTER_FUNS));
2108
- if (filterStr)
2109
- result['filter'] = filterStr;
2110
- // backdrop-filter
2111
- const backdropStr = buildFns(BACKDROP_FUNS, merged(BACKDROP_FUNS));
2112
- if (backdropStr)
2113
- result['backdropFilter'] = backdropStr;
2114
- // transform
2115
- const transformStr = buildFns(TRANSFORM_FUNS, merged(TRANSFORM_FUNS));
2116
- if (transformStr)
2117
- result['transform'] = transformStr;
2118
- // transition — only emit when duration is non-zero
2119
- const dur = raw['transitionDuration'] ?? base['transitionDuration'];
2120
- const ease = raw['transitionTimingFunction'] ?? base['transitionTimingFunction'];
2121
- const del = raw['transitionDelay'] ?? base['transitionDelay'];
2122
- if (dur && ease && dur !== '0ms' && dur !== '0s') {
2123
- const delPart = del && del !== '0ms' && del !== '0s' ? ` ${del}` : '';
2124
- result['transition'] = `all ${dur} ${ease}${delPart}`;
2125
- }
2126
- // ::marker pseudo-element — collected into __markerStyles so StyleRegistryService
2127
- // can emit a separate `.iox-node-{id}::marker { … }` rule.
2128
- const markerStyles = {};
2129
- for (const [trait, cssProp] of MARKER_FUNS) {
2130
- const val = raw[trait] ?? base[trait];
2131
- if (val != null && val !== '')
2132
- markerStyles[cssProp] = val;
2133
- }
2134
- if (Object.keys(markerStyles).length) {
2135
- result['__markerStyles'] = markerStyles;
2136
- }
2137
- return result;
2138
- }
2139
-
2140
1841
  var StyleCategory;
2141
1842
  (function (StyleCategory) {
2142
1843
  StyleCategory["Layout"] = "Layout";
@@ -2773,78 +2474,421 @@ class StyleRegistryService {
2773
2474
  else {
2774
2475
  this.rules.delete(nodeId);
2775
2476
  }
2776
- this.flush();
2477
+ this.flush();
2478
+ }
2479
+ /**
2480
+ * Write the org-level design token variables as a `:root { … }` block.
2481
+ * Token names should be CSS custom property names without the `--` prefix
2482
+ * (e.g., `{ 'iox-primary-color': '#cb9090' }`).
2483
+ * Pass an empty object to clear all tokens.
2484
+ */
2485
+ upsertTokens(tokens) {
2486
+ const entries = Object.entries(tokens)
2487
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
2488
+ .map(([k, v]) => ` --${k}: ${v};`);
2489
+ if (entries.length) {
2490
+ this.rules.set('__tokens__', `:root {\n${entries.join('\n')}\n}`);
2491
+ }
2492
+ else {
2493
+ this.rules.delete('__tokens__');
2494
+ }
2495
+ this.flush();
2496
+ }
2497
+ remove(nodeId) {
2498
+ if (!nodeId)
2499
+ return;
2500
+ // Remove base rule and all state rules for this node
2501
+ this.rules.delete(nodeId);
2502
+ for (const key of this.rules.keys()) {
2503
+ if (key.startsWith(`${nodeId}:`)) {
2504
+ this.rules.delete(key);
2505
+ }
2506
+ }
2507
+ this.flush();
2508
+ }
2509
+ destroy() {
2510
+ this.rules.clear();
2511
+ if (this.styleEl) {
2512
+ this.styleEl.remove();
2513
+ this.styleEl = null;
2514
+ }
2515
+ }
2516
+ compile(className, styles) {
2517
+ // Declaration compilation (kebab-case, viewport-unit rewrite, !important) lives in
2518
+ // the shared style-compiler so the SSR engine produces byte-identical output.
2519
+ // The inner/outer partition + selector emission stay here (builder-only concern).
2520
+ const decls = compileDeclarations(styles, StyleRegistryService.IMPORTANT_PROPS);
2521
+ if (!decls.length)
2522
+ return '';
2523
+ const body = decls.map(d => ` ${d};`).join('\n');
2524
+ return `.${className} {\n${body}\n}`;
2525
+ }
2526
+ flush() {
2527
+ if (!this.styleEl || this._flushPending)
2528
+ return;
2529
+ this._flushPending = true;
2530
+ requestAnimationFrame(() => {
2531
+ this._flushPending = false;
2532
+ if (!this.styleEl)
2533
+ return;
2534
+ // __tokens__ always first so variables are available to all subsequent rules
2535
+ const tokenBlock = this.rules.get('__tokens__');
2536
+ const rest = Array.from(this.rules.entries())
2537
+ .filter(([k]) => k !== '__tokens__')
2538
+ .map(([, v]) => v);
2539
+ this.styleEl.textContent = [tokenBlock, ...rest].filter(Boolean).join('\n');
2540
+ this.changes$.next();
2541
+ });
2542
+ }
2543
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2544
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService }); }
2545
+ }
2546
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, decorators: [{
2547
+ type: Injectable
2548
+ }] });
2549
+
2550
+ /**
2551
+ * MoveableCanvasService — Phase 1 of the Moveable + Selecto migration
2552
+ * (see iox-ai-guidance/architecture/builder/moveable-migration.md).
2553
+ *
2554
+ * Drives DRAG for the SELECTED element via Moveable instead of interact.js, while REUSING the
2555
+ * proven reorder machinery (`DragEngineService.handleDrop`). It is a pure INPUT device: during a
2556
+ * drag it applies a TRANSIENT transform, and on drop it RESETS the transform to nothing and instead
2557
+ * reorders the layout tree (constraint C1 — a persisted transform on an in-flow canvas element
2558
+ * breaks Lenis scroll). Everything here runs ONLY when `moveableMode` is on; interact.js is
2559
+ * untouched, so the flag-off path is byte-identical to today.
2560
+ *
2561
+ * Scale: Moveable reads `getBoundingClientRect()` (already post-scale) and works in viewport
2562
+ * client coordinates, so drop hit-testing via `elementsFromPoint` / rects is scale-correct without
2563
+ * extra math. The control box is rendered into the same overlay container the selection overlay uses.
2564
+ */
2565
+ class MoveableCanvasService {
2566
+ constructor(dragEngine, styleRegistry) {
2567
+ this.dragEngine = dragEngine;
2568
+ this.styleRegistry = styleRegistry;
2569
+ this.moveable = null;
2570
+ this.container = null;
2571
+ this.scaleGetter = () => 1;
2572
+ this.target = null;
2573
+ this.node = null;
2574
+ this.sourceId = null;
2575
+ /** Called after a successful reorder so the host can emit LAYOUT_CHANGED / refresh selection. */
2576
+ this.onReorder = null;
2577
+ /** Called after a geometry commit (resize) so the host can emit LAYOUT_CHANGED (undo/save). */
2578
+ this.onLayoutChanged = null;
2579
+ }
2580
+ /** Wire the Moveable control box into `container`; `scaleGetter` returns the live canvas zoom. */
2581
+ init(container, scaleGetter) {
2582
+ this.container = container;
2583
+ this.scaleGetter = scaleGetter;
2584
+ }
2585
+ /** Point Moveable at the selected element (skips inline-edit + preview-clone targets). */
2586
+ select(element, node) {
2587
+ this.target = null;
2588
+ this.node = null;
2589
+ this.sourceId = null;
2590
+ if (!element || !node || !this.container) {
2591
+ this.destroyMoveable();
2592
+ return;
2593
+ }
2594
+ // Preview clones (repeater/slider rows) and hidden nodes are not editable.
2595
+ if (element.closest('[data-iox-hidden]') || node.__previewClone) {
2596
+ this.destroyMoveable();
2597
+ return;
2598
+ }
2599
+ this.target = element;
2600
+ this.node = node;
2601
+ this.sourceId = this.resolveDropzoneId(element);
2602
+ this.ensureMoveable();
2603
+ this.moveable.target = element;
2604
+ // Drag is initiated by grabbing the ELEMENT itself (like interact.js) — not a Moveable
2605
+ // overlay area — so a plain click still passes through to selection/child-selection and only
2606
+ // a movement past the threshold starts a drag. Keeps Moveable visually silent (no hit-area box).
2607
+ this.moveable.dragTarget = element;
2608
+ this.moveable.updateRect();
2609
+ }
2610
+ /** Recompute the control-box position (call on scroll/zoom/layout changes). */
2611
+ refresh() { this.moveable?.updateRect(); }
2612
+ destroy() { this.destroyMoveable(); this.container = null; }
2613
+ // ── internals ────────────────────────────────────────────────────────────
2614
+ ensureMoveable() {
2615
+ if (this.moveable)
2616
+ return;
2617
+ this.moveable = new Moveable(this.container, {
2618
+ target: undefined,
2619
+ draggable: true,
2620
+ resizable: true, // Phase 3 — corner handles → styleProps width/height (C2)
2621
+ rotatable: false, // (out-of-flow only — C1)
2622
+ origin: false,
2623
+ zoom: 1,
2624
+ // The OverlayService owns ALL selection visuals (pink outline, hover margin/padding rings,
2625
+ // toolbar, badges) — Moveable draws NO box/lines. It shows ONLY the 4 corner RESIZE handles
2626
+ // (renderDirections) and provides the drag hit-area. `iox-moveable-silent` + SCSS in
2627
+ // builder.component.scss hide the lines and style the handles to match, below the overlay.
2628
+ hideDefaultLines: true,
2629
+ renderDirections: ['nw', 'ne', 'sw', 'se'],
2630
+ className: 'iox-moveable-silent',
2631
+ });
2632
+ this.moveable.on('dragStart', (e) => {
2633
+ // Never start a drag from inside inline text editing.
2634
+ if (e.inputEvent?.target?.closest?.('[contenteditable="true"]')) {
2635
+ e.stop?.();
2636
+ }
2637
+ });
2638
+ // Transient transform only — visual feedback during the drag (C1: reset on drop).
2639
+ this.moveable.on('drag', (e) => { e.target.style.transform = e.transform; });
2640
+ this.moveable.on('dragEnd', (e) => this.commitDrag(e));
2641
+ // Resize: live inline width/height for feedback, then commit to styleProps on end (C2 — never
2642
+ // a transform:scale, so no Lenis-scroll break; the panel + save + SSR all see the real size).
2643
+ this.moveable.on('resize', (e) => {
2644
+ e.target.style.width = `${Math.round(e.width)}px`;
2645
+ e.target.style.height = `${Math.round(e.height)}px`;
2646
+ e.target.style.transform = e.drag.transform; // keeps the box anchored while resizing
2647
+ });
2648
+ this.moveable.on('resizeEnd', (e) => this.commitResize(e));
2649
+ }
2650
+ destroyMoveable() {
2651
+ if (this.moveable) {
2652
+ this.moveable.destroy();
2653
+ this.moveable = null;
2654
+ }
2777
2655
  }
2778
- /**
2779
- * Write the org-level design token variables as a `:root { … }` block.
2780
- * Token names should be CSS custom property names without the `--` prefix
2781
- * (e.g., `{ 'iox-primary-color': '#cb9090' }`).
2782
- * Pass an empty object to clear all tokens.
2783
- */
2784
- upsertTokens(tokens) {
2785
- const entries = Object.entries(tokens)
2786
- .filter(([, v]) => v !== undefined && v !== null && v !== '')
2787
- .map(([k, v]) => ` --${k}: ${v};`);
2788
- if (entries.length) {
2789
- this.rules.set('__tokens__', `:root {\n${entries.join('\n')}\n}`);
2656
+ commitDrag(e) {
2657
+ const el = e.target;
2658
+ // C1: never persist the transform — reset it; the result is a tree reorder, not a position.
2659
+ el.style.transform = '';
2660
+ if (!this.node)
2661
+ return;
2662
+ const pointer = this.lastPointer(e);
2663
+ if (!pointer) {
2664
+ this.onReorder?.();
2665
+ return;
2790
2666
  }
2791
- else {
2792
- this.rules.delete('__tokens__');
2667
+ const targetId = this.dropzoneUnderPoint(pointer.x, pointer.y, el);
2668
+ if (!targetId) {
2669
+ this.onReorder?.();
2670
+ return;
2793
2671
  }
2794
- this.flush();
2672
+ const children = this.dragEngine.getData(targetId);
2673
+ const cdr = this.dragEngine.getCdr(targetId);
2674
+ if (!children || !cdr) {
2675
+ this.onReorder?.();
2676
+ return;
2677
+ }
2678
+ const insertIndex = this.calcInsertIndex(targetId, pointer.y);
2679
+ const srcChildren = this.sourceId ? this.dragEngine.getData(this.sourceId) : undefined;
2680
+ const sourceIndex = srcChildren ? srcChildren.indexOf(this.node) : -1;
2681
+ const payload = { type: 'internal', data: this.node, sourceId: this.sourceId ?? null, sourceIndex };
2682
+ this.dragEngine.handleDrop(children, { payload, targetId, insertIndex }, targetId, cdr, () => {
2683
+ this.onReorder?.();
2684
+ // Re-point the control box at the moved element's (possibly new) host.
2685
+ setTimeout(() => this.moveable?.updateRect(), 0);
2686
+ });
2795
2687
  }
2796
- remove(nodeId) {
2797
- if (!nodeId)
2688
+ commitResize(e) {
2689
+ const el = e.target;
2690
+ el.style.transform = ''; // C1: never persist a transform on an in-flow canvas element
2691
+ if (!this.node) {
2798
2692
  return;
2799
- // Remove base rule and all state rules for this node
2800
- this.rules.delete(nodeId);
2801
- for (const key of this.rules.keys()) {
2802
- if (key.startsWith(`${nodeId}:`)) {
2803
- this.rules.delete(key);
2693
+ }
2694
+ const last = e.lastEvent ?? e;
2695
+ const w = Math.round(last.width ?? el.getBoundingClientRect().width);
2696
+ const h = Math.round(last.height ?? el.getBoundingClientRect().height);
2697
+ // C2: geometry goes to styleProps (width/height) — never a transform:scale.
2698
+ this.applyStyle(this.node, { width: `${w}px`, height: `${h}px` });
2699
+ // Clear the transient inline size so the styleProps `.iox-node` rule drives it (single source).
2700
+ el.style.width = '';
2701
+ el.style.height = '';
2702
+ this.onLayoutChanged?.();
2703
+ setTimeout(() => this.moveable?.updateRect(), 0);
2704
+ }
2705
+ /** Write geometry changes into the node's styleTraits (the serialized source of truth) and the
2706
+ * central stylesheet, so the style panel, save (self-describing), and SSR engine all agree. */
2707
+ applyStyle(node, changes) {
2708
+ const styles = {};
2709
+ for (const group of (node.styleTraits ?? [])) {
2710
+ for (const trait of (group.traits ?? [])) {
2711
+ if (trait.name in changes)
2712
+ trait.default = changes[trait.name];
2713
+ if (trait.name !== undefined && trait.default !== undefined)
2714
+ styles[trait.name] = trait.default;
2804
2715
  }
2805
2716
  }
2806
- this.flush();
2717
+ const cssId = node.styleId ?? node.id;
2718
+ if (cssId)
2719
+ this.styleRegistry.upsert(cssId, styles);
2807
2720
  }
2808
- destroy() {
2809
- this.rules.clear();
2810
- if (this.styleEl) {
2811
- this.styleEl.remove();
2812
- this.styleEl = null;
2721
+ /** Final pointer position from the Moveable end event (client/viewport px). */
2722
+ lastPointer(e) {
2723
+ const ie = e.inputEvent;
2724
+ if (ie && typeof ie.clientX === 'number')
2725
+ return { x: ie.clientX, y: ie.clientY };
2726
+ if (Array.isArray(e.clientX))
2727
+ return { x: e.clientX[0], y: e.clientY[0] };
2728
+ if (typeof e.clientX === 'number')
2729
+ return { x: e.clientX, y: e.clientY };
2730
+ return null;
2731
+ }
2732
+ /** Walk up from an element to the nearest registered dropzone id (el.id is stamped by DragEngine). */
2733
+ resolveDropzoneId(el) {
2734
+ let e = el;
2735
+ while (e) {
2736
+ if (e.id && this.dragEngine.getData(e.id) !== undefined)
2737
+ return e.id;
2738
+ e = e.parentElement;
2813
2739
  }
2740
+ return null;
2814
2741
  }
2815
- compile(className, styles) {
2816
- // Declaration compilation (kebab-case, viewport-unit rewrite, !important) lives in
2817
- // the shared style-compiler so the SSR engine produces byte-identical output.
2818
- // The inner/outer partition + selector emission stay here (builder-only concern).
2819
- const decls = compileDeclarations(styles, StyleRegistryService.IMPORTANT_PROPS);
2820
- if (!decls.length)
2821
- return '';
2822
- const body = decls.map(d => ` ${d};`).join('\n');
2823
- return `.${className} {\n${body}\n}`;
2742
+ /** Topmost registered dropzone under the pointer that is NOT inside the dragged element's subtree. */
2743
+ dropzoneUnderPoint(x, y, draggedEl) {
2744
+ const stack = document.elementsFromPoint(x, y);
2745
+ for (const el of stack) {
2746
+ const id = this.resolveDropzoneId(el);
2747
+ if (!id)
2748
+ continue;
2749
+ const zoneEl = document.getElementById(id);
2750
+ if (zoneEl && draggedEl.contains(zoneEl))
2751
+ continue; // don't drop into own subtree
2752
+ return id;
2753
+ }
2754
+ return null;
2824
2755
  }
2825
- flush() {
2826
- if (!this.styleEl || this._flushPending)
2827
- return;
2828
- this._flushPending = true;
2829
- requestAnimationFrame(() => {
2830
- this._flushPending = false;
2831
- if (!this.styleEl)
2832
- return;
2833
- // __tokens__ always first so variables are available to all subsequent rules
2834
- const tokenBlock = this.rules.get('__tokens__');
2835
- const rest = Array.from(this.rules.entries())
2836
- .filter(([k]) => k !== '__tokens__')
2837
- .map(([, v]) => v);
2838
- this.styleEl.textContent = [tokenBlock, ...rest].filter(Boolean).join('\n');
2839
- this.changes$.next();
2840
- });
2756
+ /** Insertion index within a dropzone by comparing pointer Y to each child's vertical midpoint. */
2757
+ calcInsertIndex(targetId, y) {
2758
+ const zoneEl = document.getElementById(targetId);
2759
+ if (!zoneEl)
2760
+ return 0;
2761
+ const childWraps = Array.from(zoneEl.children).filter((c) => c.matches('.container-child, .section-child, [data-iox-draggable]'));
2762
+ let idx = 0;
2763
+ for (const w of childWraps) {
2764
+ const r = w.getBoundingClientRect();
2765
+ if (y > r.top + r.height / 2)
2766
+ idx++;
2767
+ }
2768
+ return idx;
2841
2769
  }
2842
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2843
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService }); }
2770
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: MoveableCanvasService, deps: [{ token: DragEngineService }, { token: StyleRegistryService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2771
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: MoveableCanvasService, providedIn: 'root' }); }
2772
+ }
2773
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: MoveableCanvasService, decorators: [{
2774
+ type: Injectable,
2775
+ args: [{ providedIn: 'root' }]
2776
+ }], ctorParameters: () => [{ type: DragEngineService }, { type: StyleRegistryService }] });
2777
+
2778
+ // Virtual trait → composed CSS property helpers.
2779
+ //
2780
+ // Several style traits are "virtual" — they don't map 1:1 to a CSS property but
2781
+ // are components of a multi-function CSS value. composeVirtualTraits() strips
2782
+ // these virtual keys from the style map and replaces them with the correctly
2783
+ // composed CSS properties (filter, backdrop-filter, transform, transition).
2784
+ //
2785
+ // The same function is used by the render directive (initial mount + state
2786
+ // restore) and by the style panel's onUpdate / setStateOverride paths, so
2787
+ // the CSS output is always consistent regardless of how styles are applied.
2788
+ // ─── Filter ──────────────────────────────────────────────────────────────────
2789
+ const FILTER_FUNS = [
2790
+ ['filterBlur', 'blur', '0px'],
2791
+ ['filterBrightness', 'brightness', '1'],
2792
+ ['filterContrast', 'contrast', '1'],
2793
+ ['filterGrayscale', 'grayscale', '0'],
2794
+ ['filterSaturate', 'saturate', '1'],
2795
+ ['filterHueRotate', 'hue-rotate', '0deg'],
2796
+ ['filterSepia', 'sepia', '0'],
2797
+ ['filterInvert', 'invert', '0'],
2798
+ ];
2799
+ // ─── Backdrop-filter ─────────────────────────────────────────────────────────
2800
+ const BACKDROP_FUNS = [
2801
+ ['backdropBlur', 'blur', '0px'],
2802
+ ['backdropBrightness', 'brightness', '1'],
2803
+ ['backdropContrast', 'contrast', '1'],
2804
+ ['backdropSaturate', 'saturate', '1'],
2805
+ ];
2806
+ // ─── Transform ───────────────────────────────────────────────────────────────
2807
+ const TRANSFORM_FUNS = [
2808
+ ['translateX', 'translateX', '0px'],
2809
+ ['translateY', 'translateY', '0px'],
2810
+ ['scaleX', 'scaleX', '1'],
2811
+ ['scaleY', 'scaleY', '1'],
2812
+ ['rotate', 'rotate', '0deg'],
2813
+ ['skewX', 'skewX', '0deg'],
2814
+ ['skewY', 'skewY', '0deg'],
2815
+ ];
2816
+ // ─── ::marker pseudo-element ─────────────────────────────────────────────────
2817
+ const MARKER_FUNS = [
2818
+ ['markerColor', 'color'],
2819
+ ['markerFontSize', 'fontSize'],
2820
+ ];
2821
+ // ─── All virtual trait names ──────────────────────────────────────────────────
2822
+ const VIRTUAL_TRAIT_KEYS = new Set([
2823
+ ...FILTER_FUNS.map(([t]) => t),
2824
+ ...BACKDROP_FUNS.map(([t]) => t),
2825
+ ...TRANSFORM_FUNS.map(([t]) => t),
2826
+ 'transitionDuration', 'transitionTimingFunction', 'transitionDelay',
2827
+ ...MARKER_FUNS.map(([t]) => t),
2828
+ ]);
2829
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
2830
+ function buildFns(funs, src) {
2831
+ return funs
2832
+ .filter(([t, , d]) => src[t] != null && src[t] !== '' && src[t] !== d)
2833
+ .map(([t, fn]) => `${fn}(${src[t]})`)
2834
+ .join(' ');
2835
+ }
2836
+ // ─── Public API ───────────────────────────────────────────────────────────────
2837
+ /**
2838
+ * Strip virtual trait keys from `raw` and emit their composed CSS equivalents.
2839
+ *
2840
+ * When composing a partial state-override map (hover, active…), pass the full
2841
+ * base style map as `base` so non-overridden components (e.g. filterBrightness
2842
+ * when only filterBlur is overridden) are preserved in the composed output.
2843
+ * When operating on a complete style map, omit `base` — it defaults to `raw`.
2844
+ */
2845
+ function composeVirtualTraits(raw, base = raw) {
2846
+ // Copy all non-virtual properties as-is
2847
+ const result = {};
2848
+ for (const [k, v] of Object.entries(raw)) {
2849
+ if (!VIRTUAL_TRAIT_KEYS.has(k))
2850
+ result[k] = v;
2851
+ }
2852
+ // Merge base values for any virtual key absent from the partial override map
2853
+ const merged = (funs) => {
2854
+ const m = {};
2855
+ for (const [t] of funs)
2856
+ m[t] = raw[t] ?? base[t];
2857
+ return m;
2858
+ };
2859
+ // filter
2860
+ const filterStr = buildFns(FILTER_FUNS, merged(FILTER_FUNS));
2861
+ if (filterStr)
2862
+ result['filter'] = filterStr;
2863
+ // backdrop-filter
2864
+ const backdropStr = buildFns(BACKDROP_FUNS, merged(BACKDROP_FUNS));
2865
+ if (backdropStr)
2866
+ result['backdropFilter'] = backdropStr;
2867
+ // transform
2868
+ const transformStr = buildFns(TRANSFORM_FUNS, merged(TRANSFORM_FUNS));
2869
+ if (transformStr)
2870
+ result['transform'] = transformStr;
2871
+ // transition — only emit when duration is non-zero
2872
+ const dur = raw['transitionDuration'] ?? base['transitionDuration'];
2873
+ const ease = raw['transitionTimingFunction'] ?? base['transitionTimingFunction'];
2874
+ const del = raw['transitionDelay'] ?? base['transitionDelay'];
2875
+ if (dur && ease && dur !== '0ms' && dur !== '0s') {
2876
+ const delPart = del && del !== '0ms' && del !== '0s' ? ` ${del}` : '';
2877
+ result['transition'] = `all ${dur} ${ease}${delPart}`;
2878
+ }
2879
+ // ::marker pseudo-element — collected into __markerStyles so StyleRegistryService
2880
+ // can emit a separate `.iox-node-{id}::marker { … }` rule.
2881
+ const markerStyles = {};
2882
+ for (const [trait, cssProp] of MARKER_FUNS) {
2883
+ const val = raw[trait] ?? base[trait];
2884
+ if (val != null && val !== '')
2885
+ markerStyles[cssProp] = val;
2886
+ }
2887
+ if (Object.keys(markerStyles).length) {
2888
+ result['__markerStyles'] = markerStyles;
2889
+ }
2890
+ return result;
2844
2891
  }
2845
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, decorators: [{
2846
- type: Injectable
2847
- }] });
2848
2892
 
2849
2893
  // Module-level counter — unique drop-list ID per Section instance across the whole app session.
2850
2894
  let dropListCounter = 0;
@@ -4254,6 +4298,8 @@ class BuilderComponent {
4254
4298
  // iox-ai-guidance/architecture/builder/moveable-migration.md.
4255
4299
  if (this.moveableMode && this.previewCanvas?.nativeElement) {
4256
4300
  this.moveableCanvas.init(this.previewCanvas.nativeElement, () => this.viewportService.getScale());
4301
+ // Resize commits geometry to styleProps → emit LAYOUT_CHANGED so undo/save pick it up.
4302
+ this.moveableCanvas.onLayoutChanged = () => this.panelEventService.emit(PanelEventTypes.LAYOUT_CHANGED, this.layout);
4257
4303
  this.sub?.add(this.overlayService.select$.subscribe(entry => {
4258
4304
  this.moveableCanvas.select(entry?.element ?? null, entry?.node ?? null);
4259
4305
  }));
@@ -4845,11 +4891,11 @@ class BuilderComponent {
4845
4891
  }
4846
4892
  }
4847
4893
  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: InteractionPresetRegistryService }, { token: BuilderClipboardService }, { token: SymbolRegistryService }, { token: i0.ChangeDetectorRef }, { token: i0.ApplicationRef }, { token: MoveableCanvasService }], target: i0.ɵɵFactoryTarget.Component }); }
4848
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.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", globalElements: "globalElements", canUndo: "canUndo", canRedo: "canRedo", moveableMode: "moveableMode" }, outputs: { save: "save", undo: "undo", redo: "redo", back: "back", preview: "preview", publishToggle: "publishToggle", pageSettingsChange: "pageSettingsChange", applyAnimationToAll: "applyAnimationToAll", saveBlock: "saveBlock", globalElementsChange: "globalElementsChange" }, 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 }, { propertyName: "globalOverlayRef", first: true, predicate: ["globalOverlay"], 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'\" title=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeDevice]=\"activeDevice\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (deviceChange)=\"handleToolbarDeviceChange($event)\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n </app-toolbar>\n\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\">\n <i class=\"ph-thin ph-file\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></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 <div *ngIf=\"activeSidebar === 'tokens'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\"\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\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 <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height matches viewportHeight so position:fixed + inset:0 fills the\n simulated viewport correctly (CSS calc(100%) would give the wrong\n containing-block height and break full-screen fixed overlays). -->\n <div *ngIf=\"globalElements.length\"\n #globalOverlay\n class=\"global-elements-overlay\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n <ng-container *ngFor=\"let item of globalElementsBefore\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n <ng-container *ngFor=\"let item of globalElementsAfter\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n </div>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\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.height.px]=\"canvasFrameHeight || viewportHeight\"\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 [ngClass]=\"['preview-node', 'iox-outer-' + (item.styleId ?? item.id)]\"\n [class.is-selected]=\"selectedItem === 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 <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\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 }\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:var(--p-surface-0);padding:.375rem .75rem;border-bottom:1px solid var(--p-surface-200);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:.5rem;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 ease-out;margin-inline-end:.25rem}.builder .builder-header .header-back:hover{background:var(--p-surface-50);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:.25rem}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:6px;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--p-surface-50);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:#cb9090;color:#fff}.builder .panel-switcher{justify-self:end}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden}.builder .sidebar{width:260px;min-width:260px;background:var(--p-surface-0);border-inline-end: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;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;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:visible!important}.builder #canvas-preview{background:#fff}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.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}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:5!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-direction{display:none!important}\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$1.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: i4$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i14.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: i15.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", "activeScreenWidth", "canUndo", "canRedo", "isSaving", "pageStatus"], outputs: ["modeChange", "deviceChange", "zoomChange", "screenWidthChange", "undo", "redo", "save", "preview", "publishToggle"] }, { kind: "component", type: LayerTreeComponent, selector: "app-layer-tree", inputs: ["layout", "globalElements"], outputs: ["nodeSelect", "nodeAction", "nodeMove"] }] }); }
4894
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.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", globalElements: "globalElements", canUndo: "canUndo", canRedo: "canRedo", moveableMode: "moveableMode" }, outputs: { save: "save", undo: "undo", redo: "redo", back: "back", preview: "preview", publishToggle: "publishToggle", pageSettingsChange: "pageSettingsChange", applyAnimationToAll: "applyAnimationToAll", saveBlock: "saveBlock", globalElementsChange: "globalElementsChange" }, 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 }, { propertyName: "globalOverlayRef", first: true, predicate: ["globalOverlay"], 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'\" title=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeDevice]=\"activeDevice\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (deviceChange)=\"handleToolbarDeviceChange($event)\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n </app-toolbar>\n\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\">\n <i class=\"ph-thin ph-file\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></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 <div *ngIf=\"activeSidebar === 'tokens'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\"\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\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 <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height matches viewportHeight so position:fixed + inset:0 fills the\n simulated viewport correctly (CSS calc(100%) would give the wrong\n containing-block height and break full-screen fixed overlays). -->\n <div *ngIf=\"globalElements.length\"\n #globalOverlay\n class=\"global-elements-overlay\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n <ng-container *ngFor=\"let item of globalElementsBefore\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n <ng-container *ngFor=\"let item of globalElementsAfter\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n </div>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\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.height.px]=\"canvasFrameHeight || viewportHeight\"\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 [ngClass]=\"['preview-node', 'iox-outer-' + (item.styleId ?? item.id)]\"\n [class.is-selected]=\"selectedItem === 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 <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\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 }\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:var(--p-surface-0);padding:.375rem .75rem;border-bottom:1px solid var(--p-surface-200);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:.5rem;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 ease-out;margin-inline-end:.25rem}.builder .builder-header .header-back:hover{background:var(--p-surface-50);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:.25rem}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:6px;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--p-surface-50);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:#cb9090;color:#fff}.builder .panel-switcher{justify-self:end}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden}.builder .sidebar{width:260px;min-width:260px;background:var(--p-surface-0);border-inline-end: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;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;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:visible!important}.builder #canvas-preview{background:#fff}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.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}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-left:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\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$1.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: i4$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i14.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: i15.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", "activeScreenWidth", "canUndo", "canRedo", "isSaving", "pageStatus"], outputs: ["modeChange", "deviceChange", "zoomChange", "screenWidthChange", "undo", "redo", "save", "preview", "publishToggle"] }, { kind: "component", type: LayerTreeComponent, selector: "app-layer-tree", inputs: ["layout", "globalElements"], outputs: ["nodeSelect", "nodeAction", "nodeMove"] }] }); }
4849
4895
  }
4850
4896
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderComponent, decorators: [{
4851
4897
  type: Component,
4852
- 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'\" title=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeDevice]=\"activeDevice\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (deviceChange)=\"handleToolbarDeviceChange($event)\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n </app-toolbar>\n\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\">\n <i class=\"ph-thin ph-file\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></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 <div *ngIf=\"activeSidebar === 'tokens'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\"\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\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 <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height matches viewportHeight so position:fixed + inset:0 fills the\n simulated viewport correctly (CSS calc(100%) would give the wrong\n containing-block height and break full-screen fixed overlays). -->\n <div *ngIf=\"globalElements.length\"\n #globalOverlay\n class=\"global-elements-overlay\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n <ng-container *ngFor=\"let item of globalElementsBefore\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n <ng-container *ngFor=\"let item of globalElementsAfter\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n </div>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\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.height.px]=\"canvasFrameHeight || viewportHeight\"\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 [ngClass]=\"['preview-node', 'iox-outer-' + (item.styleId ?? item.id)]\"\n [class.is-selected]=\"selectedItem === 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 <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\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 }\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:var(--p-surface-0);padding:.375rem .75rem;border-bottom:1px solid var(--p-surface-200);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:.5rem;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 ease-out;margin-inline-end:.25rem}.builder .builder-header .header-back:hover{background:var(--p-surface-50);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:.25rem}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:6px;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--p-surface-50);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:#cb9090;color:#fff}.builder .panel-switcher{justify-self:end}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden}.builder .sidebar{width:260px;min-width:260px;background:var(--p-surface-0);border-inline-end: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;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;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:visible!important}.builder #canvas-preview{background:#fff}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.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}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:5!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-direction{display:none!important}\n"] }]
4898
+ 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'\" title=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeDevice]=\"activeDevice\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (deviceChange)=\"handleToolbarDeviceChange($event)\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n </app-toolbar>\n\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\">\n <i class=\"ph-thin ph-file\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></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 <div *ngIf=\"activeSidebar === 'tokens'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\"\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\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 <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height matches viewportHeight so position:fixed + inset:0 fills the\n simulated viewport correctly (CSS calc(100%) would give the wrong\n containing-block height and break full-screen fixed overlays). -->\n <div *ngIf=\"globalElements.length\"\n #globalOverlay\n class=\"global-elements-overlay\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n <ng-container *ngFor=\"let item of globalElementsBefore\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n <ng-container *ngFor=\"let item of globalElementsAfter\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n </div>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\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.height.px]=\"canvasFrameHeight || viewportHeight\"\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 [ngClass]=\"['preview-node', 'iox-outer-' + (item.styleId ?? item.id)]\"\n [class.is-selected]=\"selectedItem === 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 <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\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 }\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:var(--p-surface-0);padding:.375rem .75rem;border-bottom:1px solid var(--p-surface-200);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:.5rem;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 ease-out;margin-inline-end:.25rem}.builder .builder-header .header-back:hover{background:var(--p-surface-50);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:.25rem}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:6px;border:none;background:transparent;color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--p-surface-50);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:#cb9090;color:#fff}.builder .panel-switcher{justify-self:end}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden}.builder .sidebar{width:260px;min-width:260px;background:var(--p-surface-0);border-inline-end: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;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;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:visible!important}.builder #canvas-preview{background:#fff}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.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}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-left:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\n"] }]
4853
4899
  }], ctorParameters: () => [{ type: ComponentRegistryService }, { type: OverlayService }, { type: PanelEventService }, { type: DragEngineService }, { type: DataSourceRegistryService }, { type: ViewportService }, { type: InteractionEngineService }, { type: InteractionPresetRegistryService }, { type: BuilderClipboardService }, { type: SymbolRegistryService }, { type: i0.ChangeDetectorRef }, { type: i0.ApplicationRef }, { type: MoveableCanvasService }], propDecorators: { layout: [{
4854
4900
  type: Input
4855
4901
  }], dataSources: [{