@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
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
.
|
|
2787
|
-
|
|
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
|
-
|
|
2792
|
-
|
|
2667
|
+
const targetId = this.dropzoneUnderPoint(pointer.x, pointer.y, el);
|
|
2668
|
+
if (!targetId) {
|
|
2669
|
+
this.onReorder?.();
|
|
2670
|
+
return;
|
|
2793
2671
|
}
|
|
2794
|
-
this.
|
|
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
|
-
|
|
2797
|
-
|
|
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
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
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
|
-
|
|
2717
|
+
const cssId = node.styleId ?? node.id;
|
|
2718
|
+
if (cssId)
|
|
2719
|
+
this.styleRegistry.upsert(cssId, styles);
|
|
2807
2720
|
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
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
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
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
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
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:
|
|
2843
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type:
|
|
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: [{
|