@vectoriox/iox-builder 4.38.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@ import { SelectModule } from 'primeng/select';
16
16
  import { TooltipModule } from 'primeng/tooltip';
17
17
  import { DialogModule } from 'primeng/dialog';
18
18
  import * as i1 from '@vectoriox/iox-ui';
19
- import { VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, Breakpoint, BREAKPOINTS, countOverrides, setOverride, defaultElementStyleFor, applyElementStyle, breakpointDef, breakpointForWidth, FormRenderState, safeSetInput, ContextStore, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, buildTargetedStateSelector, statePseudoClass, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, composeVirtualTraits, stateNamesIn, partClassName, resolveInputs, setInputOverride, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, resolveBoundValue, IS_PREVIEW, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, IoxPageModule, PALETTE_PRESETS, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
19
+ import { VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, Breakpoint, BREAKPOINTS, countOverrides, setOverride, defaultElementStyleFor, applyElementStyle, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, buildTargetedStateSelector, statePseudoClass, compileTokenCssBlocks, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, breakpointDef, breakpointForWidth, FormRenderState, safeSetInput, ContextStore, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, syncElementStyleMarker, resolveInputs, setInputOverride, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, resolveBoundValue, IS_PREVIEW, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, IoxPageModule, PALETTE_PRESETS, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
20
20
  export { ComponentInstanceRegistryService, VIRTUAL_TRAIT_KEYS, composeVirtualTraits } from '@vectoriox/iox-ui';
21
21
  import Lenis from 'lenis';
22
22
  import { Subject, BehaviorSubject, of, forkJoin } from 'rxjs';
@@ -1559,9 +1559,7 @@ class ComponentRegistryService {
1559
1559
  const config = this.createBaseConfig(type);
1560
1560
  if (!config)
1561
1561
  return null;
1562
- const id = defaultElementStyleFor(type, traitValues(config));
1563
- if (id)
1564
- applyElementStyle(config, id);
1562
+ wearDefaultStyles(config);
1565
1563
  return config;
1566
1564
  }
1567
1565
  /**
@@ -1594,6 +1592,16 @@ class ComponentRegistryService {
1594
1592
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ComponentRegistryService, decorators: [{
1595
1593
  type: Injectable
1596
1594
  }] });
1595
+ /** A new node AND the default children its config builds (a palette Form's fields, submit and
1596
+ * notices) are all new elements: each wears its type's default style. */
1597
+ function wearDefaultStyles(node) {
1598
+ const id = defaultElementStyleFor(node.type, traitValues(node));
1599
+ if (id)
1600
+ applyElementStyle(node, id);
1601
+ for (const child of node.children ?? [])
1602
+ if (child?.type)
1603
+ wearDefaultStyles(child);
1604
+ }
1597
1605
  function traitValues(config) {
1598
1606
  const out = {};
1599
1607
  for (const trait of config.traits ?? [])
@@ -1797,76 +1805,461 @@ function resolveDragTarget(el, selectedElement, isDragHandle) {
1797
1805
  }
1798
1806
 
1799
1807
  /**
1800
- * SymbolRegistryService — in-memory store for org-level symbols.
1808
+ * CSS container name of the builder canvas frame (and of the global-elements overlay). Breakpoint
1809
+ * rules are emitted as `@container iox-canvas (max-width: …)`: the frame's layout width IS the
1810
+ * simulated viewport and is unaffected by the zoom `transform: scale`, whereas a `@media` query
1811
+ * would read the editor window. Set on the frame in builder.component.scss.
1812
+ */
1813
+ const BUILDER_CANVAS_CONTAINER = 'iox-canvas';
1814
+ /**
1815
+ * The class that carries the ORG's design tokens on the canvas. Tokens used to be declared on
1816
+ * `:root`, where they collided with the admin's own `--iox-*` chrome tokens (`accent`, `surface-2`,
1817
+ * `scrim`, `focus-ring`… — DS2 seeds all 161 Crisp rows, so the clash became universal and the
1818
+ * admin turned the studio's accent colour). Every surface that renders site content through this
1819
+ * registry — the canvas frame, the global-elements overlay, preview, specimens — wears this class.
1820
+ */
1821
+ const BUILDER_TOKEN_SCOPE_CLASS = 'iox-token-scope';
1822
+ /**
1823
+ * StyleRegistryService — manages a single <style> tag for the builder canvas.
1801
1824
  *
1802
- * Loaded once per page by PageUiComponent alongside the page layout.
1803
- * Scoped to PageUiComponent.providers[] via IoxBuilderModule — one registry per builder instance.
1825
+ * Each builder component gets two CSS classes:
1826
+ * .iox-node-{id} — applied to the inner element; handles layout/visual props
1827
+ * (display, flex, background, border, padding, etc.)
1828
+ * .iox-outer-{id} — applied to the host element by RenderDirective; handles
1829
+ * properties that participate in the PARENT flow (margin,
1830
+ * align-self, flex-grow/shrink/basis, order).
1831
+ *
1832
+ * This split is necessary because margin on an inner wrapper only adds internal
1833
+ * space — it does not push sibling elements. Applying margin to the host element
1834
+ * (which IS the flex/block child in the parent container) makes it work correctly.
1835
+ *
1836
+ * State styles (hover/active/focus) are stored under key `${nodeId}:${state}`
1837
+ * and compiled to `.iox-node-{id}:hover { … }` selectors.
1838
+ *
1839
+ * Design tokens are compiled by the SHARED token compiler (`compileTokenCssBlocks`, design-core)
1840
+ * scoped to `.iox-token-scope`: its base half (variables, scheme classes, reduced-motion and
1841
+ * reduced-transparency blocks) is prepended to all other rules and its dark half appended LAST —
1842
+ * the same two halves, in the same places, as the SSR engine.
1843
+ *
1844
+ * Scoped to PageUiComponent.providers[] — one stylesheet per builder instance.
1804
1845
  */
1805
- class SymbolRegistryService {
1846
+ class StyleRegistryService {
1806
1847
  constructor() {
1807
- this.symbols = new Map();
1808
- /** symbolIds that have a live reusable block in the DB — used to guard isDetachedSymbol. */
1809
- this.blockSymbolIds = new Set();
1848
+ this.changes$ = new Subject();
1849
+ this.rules = new Map();
1850
+ /** Breakpoint rules, kept apart so they flush AFTER every base rule — equal specificity, so
1851
+ * source order decides (plan trap 1). Keyed per breakpoint like `rules` (`id`, `id:state`). */
1852
+ this.responsiveRules = new Map();
1853
+ this.responsiveInputs = new Map();
1854
+ /** Which breakpoint rule keys each node owns — so re-emitting one node's rules is O(its rules),
1855
+ * not a scan of every breakpoint rule on the page (this runs on every node render). */
1856
+ this.responsiveKeys = new Map();
1857
+ this.stateScopes = new Map();
1858
+ /** Node type per css id — the compiler strips the props a type's renderer owns (a Dialog's
1859
+ * positioning, an Icon's line-height) from EVERY rule of that node, whoever upserts it. */
1860
+ this.nodeTypes = new Map();
1861
+ this.tokenCss = { base: '', dark: '' };
1862
+ this.styleEl = null;
1863
+ this._flushPending = false;
1810
1864
  }
1811
- setSymbols(symbols) {
1812
- this.symbols.clear();
1813
- for (const s of symbols) {
1814
- this.symbols.set(s.id, s);
1815
- }
1865
+ /**
1866
+ * Properties that must live on the OUTER wrapper (.iox-outer-{id}) because they
1867
+ * affect how the element participates in its PARENT layout (flex/block flow).
1868
+ * Everything else goes to the INNER element (.iox-node-{id}).
1869
+ *
1870
+ * Exception — out-of-flow positioning (position:fixed/absolute):
1871
+ * When position is fixed or absolute, width is redirected to the inner element
1872
+ * (the positioned one) and a default width:100% is added if none is set, so the
1873
+ * element stays visible. top/right/bottom/left/zIndex are always inner because
1874
+ * they are co-ordinates of the positioned element, not the flow wrapper.
1875
+ * position itself goes to inner with `!important` to beat component-scoped CSS.
1876
+ */
1877
+ static { this.OUTER_PROPS = new Set([
1878
+ 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
1879
+ 'alignSelf', 'flexGrow', 'flexShrink', 'flexBasis', 'order',
1880
+ // Grid-child placement — like the flex-child props above, these position the node
1881
+ // within the PARENT's grid, so they must land on the outer host, not the inner div.
1882
+ 'gridColumn', 'gridRow', 'justifySelf',
1883
+ 'width',
1884
+ ]); }
1885
+ /**
1886
+ * Properties that need !important on the inner element to beat Angular's scoped-CSS
1887
+ * specificity boost. CANVAS-ONLY (H2 audit, 2026-10-11): the shared compiler never forces
1888
+ * importance and the SSR engine never emits it — this is the builder's own fight with its
1889
+ * scoped component styles. The A4 dialog centring clash this once caused is resolved
1890
+ * structurally (the grid stage PLACES a static surface), so the set stays at exactly one
1891
+ * documented member; adding to it needs the same scrutiny.
1892
+ */
1893
+ static { this.IMPORTANT_PROPS = new Set(['position']); }
1894
+ init() {
1895
+ this.styleEl = document.createElement('style');
1896
+ this.styleEl.id = 'iox-runtime-styles';
1897
+ document.head.appendChild(this.styleEl);
1816
1898
  }
1817
- getSymbol(id) {
1818
- return this.symbols.get(id);
1899
+ /**
1900
+ * Write or update the base styles for a node, or a pseudo-class state override.
1901
+ *
1902
+ * @param nodeId The node's CSS id (used in `.iox-node-{nodeId}`)
1903
+ * @param styles Flat CSS property map (camelCase keys)
1904
+ * @param state Optional pseudo-class state ('hover' | 'active' | 'focus' | …).
1905
+ * When provided, compiles to `.iox-node-{id}:{state} { … }`.
1906
+ * State rules are NOT partitioned into inner/outer.
1907
+ * @param relativeToId For RELATIONAL states (e.g. 'parent-hover') this is the ancestor
1908
+ * css id; compiles to `.iox-node-{relativeToId}:{base} .iox-node-{nodeId} { … }`.
1909
+ * For TARGETED states (e.g. 'trigger-hover') this is the SCOPE css id
1910
+ * (nearest common ancestor of the styled node + trigger).
1911
+ * @param triggerId For TARGETED states only: the css id of the trigger node whose
1912
+ * pseudo-class drives the state. Compiles a `:has()`-scoped selector.
1913
+ */
1914
+ /** Record a node's type (RenderDirective, before its first upsert) and recompile nothing —
1915
+ * every later rule for the node is stripped of what the type's renderer owns. */
1916
+ setNodeType(nodeId, type) {
1917
+ if (nodeId && type)
1918
+ this.nodeTypes.set(nodeId, type);
1819
1919
  }
1820
- getAll() {
1821
- return Array.from(this.symbols.values());
1920
+ ownProps(nodeId, styles) {
1921
+ const type = this.nodeTypes.get(nodeId);
1922
+ return type ? stripSuppressedProps(type, styles) : styles;
1822
1923
  }
1823
- upsert(symbol) {
1824
- this.symbols.set(symbol.id, symbol);
1924
+ upsert(nodeId, styles, state, relativeToId, triggerId) {
1925
+ if (!nodeId)
1926
+ return;
1927
+ styles = this.ownProps(nodeId, styles);
1928
+ if (state) {
1929
+ const key = `${nodeId}:${state}`;
1930
+ // Remember the scope a relational / targeted rule was given, so the same state at a
1931
+ // breakpoint is scoped identically without re-resolving it (see upsertResponsive).
1932
+ if (relativeToId || triggerId)
1933
+ this.stateScopes.set(key, { relativeToId, triggerId });
1934
+ const selector = this.stateSelector(nodeId, state, relativeToId, triggerId);
1935
+ const css = selector ? this.compile(selector, styles) : '';
1936
+ if (css)
1937
+ this.rules.set(key, css);
1938
+ else
1939
+ this.rules.delete(key);
1940
+ if (this.responsiveInputs.has(nodeId))
1941
+ this.emitResponsive(nodeId);
1942
+ this.flush();
1943
+ return;
1944
+ }
1945
+ const { main, marker } = this.nodeRuleCss(nodeId, styles, this.isOutOfFlow(styles), DeclarationContext.Base);
1946
+ if (marker)
1947
+ this.rules.set(`${nodeId}::marker`, marker);
1948
+ else
1949
+ this.rules.delete(`${nodeId}::marker`);
1950
+ if (main)
1951
+ this.rules.set(nodeId, main);
1952
+ else
1953
+ this.rules.delete(nodeId);
1954
+ this.flush();
1825
1955
  }
1826
- remove(id) {
1827
- this.symbols.delete(id);
1956
+ /**
1957
+ * Write or clear the styles for one PART of a compound component (board 7 — "the failure
1958
+ * design"), optionally further scoped to a validation state ('wrong'/'accepted'). Compiles
1959
+ * through the exact same `compile()` every other rule uses — `buildPartSelector` (shared,
1960
+ * `@vectoriox/iox-ui`) just builds a different selector string. Unlike interaction/relational/
1961
+ * targeted states, a validation state compounds a CLASS onto the node (`.iox-invalid`), not a
1962
+ * `:pseudo-class`, since `IoxFieldComponent` sets that class itself from `forceInvalid`/
1963
+ * `showValid` — there is no real "wrong" pseudo-class to hook.
1964
+ */
1965
+ upsertPart(nodeId, partClassName, styles, state) {
1966
+ if (!nodeId || !partClassName)
1967
+ return;
1968
+ const key = `${nodeId}:part:${partClassName}${state ? ':' + state : ''}`;
1969
+ const validationClass = state ? VALIDATION_STATE_CLASS[state] : undefined;
1970
+ const selector = buildPartSelector(nodeId, partClassName, validationClass);
1971
+ const css = Object.keys(styles).length ? this.compile(selector, styles) : '';
1972
+ if (css)
1973
+ this.rules.set(key, css);
1974
+ else
1975
+ this.rules.delete(key);
1976
+ this.flush();
1828
1977
  }
1829
- /** Set the symbolIds that have a live reusable block. Called on page load. */
1830
- setBlockSymbolIds(ids) {
1831
- this.blockSymbolIds = new Set(ids);
1978
+ /**
1979
+ * Emit (or clear) a node's breakpoint rules. `planResponsiveRules` — shared with the SSR engine —
1980
+ * decides what each breakpoint emits; this only turns it into selectors, through the SAME
1981
+ * inner/outer split and state selectors as the base rules, compiled in the Override context
1982
+ * (a CSS initial value undoes the wider rule there, so it must not be dropped).
1983
+ *
1984
+ * @param styleProps The node's full BASE style map (raw — virtual traits not yet composed).
1985
+ */
1986
+ upsertResponsive(nodeId, styleProps, stateStyles, responsive) {
1987
+ if (!nodeId)
1988
+ return;
1989
+ const hasOverrides = !!responsive && Object.keys(responsive).length > 0;
1990
+ // The common case — a node with no overrides that never had any — costs nothing.
1991
+ if (!hasOverrides && !this.responsiveInputs.has(nodeId))
1992
+ return;
1993
+ if (hasOverrides)
1994
+ this.responsiveInputs.set(nodeId, { styleProps, stateStyles, responsive: responsive });
1995
+ else
1996
+ this.responsiveInputs.delete(nodeId);
1997
+ this.emitResponsive(nodeId);
1998
+ this.flush();
1832
1999
  }
1833
- /** Returns true only when the symbol has a live backing reusable block. */
1834
- isBlockSymbol(id) {
1835
- return this.blockSymbolIds.has(id);
2000
+ emitResponsive(nodeId) {
2001
+ this.clearResponsive(nodeId);
2002
+ const input = this.responsiveInputs.get(nodeId);
2003
+ if (!input)
2004
+ return;
2005
+ for (const layer of planResponsiveRules(input.styleProps, input.stateStyles, input.responsive)) {
2006
+ const bp = layer.breakpoint;
2007
+ const layerProps = this.ownProps(nodeId, layer.styleProps);
2008
+ // Where `width` lands depends on the position in force AT this breakpoint.
2009
+ const outOfFlow = this.isOutOfFlow(resolveStyleProps(input.styleProps, input.responsive, bp));
2010
+ const { main, marker } = this.nodeRuleCss(nodeId, layerProps, outOfFlow, DeclarationContext.Override);
2011
+ if (main)
2012
+ this.setResponsiveRule(nodeId, bp, nodeId, main);
2013
+ if (marker)
2014
+ this.setResponsiveRule(nodeId, bp, `${nodeId}::marker`, marker);
2015
+ for (const [state, map] of Object.entries(layer.stateStyles)) {
2016
+ const scope = this.stateScopes.get(`${nodeId}:${state}`);
2017
+ const selector = this.stateSelector(nodeId, state, scope?.relativeToId, scope?.triggerId);
2018
+ // A relational/targeted state whose scope isn't known yet is skipped; it is re-emitted
2019
+ // when the base state rule arrives with its scope (see upsert).
2020
+ const css = selector ? this.compile(selector, this.ownProps(nodeId, map), DeclarationContext.Override) : '';
2021
+ if (css)
2022
+ this.setResponsiveRule(nodeId, bp, `${nodeId}:${state}`, css);
2023
+ }
2024
+ }
1836
2025
  }
1837
- /** Remove a symbolId from the block-backed set (called when a block is deleted). */
1838
- removeBlockSymbolId(id) {
1839
- this.blockSymbolIds.delete(id);
2026
+ setResponsiveRule(nodeId, bp, key, css) {
2027
+ let bucket = this.responsiveRules.get(bp);
2028
+ if (!bucket)
2029
+ this.responsiveRules.set(bp, (bucket = new Map()));
2030
+ bucket.set(key, css);
2031
+ let owned = this.responsiveKeys.get(nodeId);
2032
+ if (!owned)
2033
+ this.responsiveKeys.set(nodeId, (owned = []));
2034
+ owned.push([bp, key]);
1840
2035
  }
1841
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SymbolRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1842
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SymbolRegistryService }); }
1843
- }
1844
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SymbolRegistryService, decorators: [{
1845
- type: Injectable
1846
- }] });
1847
-
1848
- /** Marks the overlay's dedicated drag-handle chrome (see overlay.component.html) — it carries no
1849
- * node of its own, so a drag started on it always redirects to whatever is currently selected. */
1850
- const DRAG_HANDLE_ATTR = 'data-iox-drag-handle';
1851
- // Ease-out-quint — fast start, precise stop, no overshoot. Used for FLIP sibling
1852
- // reflow and the insertion indicator open/close. Deliberately NOT a spring/back
1853
- // curve (e.g. cubic-bezier(0.34,1.56,0.64,1)): multiple siblings animating with
1854
- // overshoot at once reads as jitter, not polish — reserve overshoot for single-
1855
- // element motion (ghost drop-settle, a later phase).
1856
- const MOTION_EASE = 'cubic-bezier(0.22, 1, 0.36, 1)';
1857
- // Per-frame exponential smoothing factor for the drag ghost's cursor-follow.
1858
- // Was: ghost position set 1:1 to the pointer every dragmove (rigid/teleporty).
1859
- // Lower = more lag/weight, higher = snappier. 0.35 catches up to the pointer
1860
- // within ~5-6 frames (~80-100ms) — reads as "responsive but weighted".
1861
- const GHOST_FOLLOW_SMOOTHING = 0.35;
1862
- function deepCloneNode(node) {
1863
- const clone = JSON.parse(JSON.stringify(node));
1864
- // Fresh ids + remap intra-subtree references (stateTriggers, interaction targets) so a cloned
1865
- // structure stays self-consistent — e.g. trigger-hover keeps pointing at the clone's own trigger.
1866
- regenerateNodeIds(clone);
1867
- return clone;
1868
- }
1869
- class DragEngineService {
2036
+ clearResponsive(nodeId) {
2037
+ for (const [bp, key] of this.responsiveKeys.get(nodeId) ?? [])
2038
+ this.responsiveRules.get(bp)?.delete(key);
2039
+ this.responsiveKeys.delete(nodeId);
2040
+ }
2041
+ isOutOfFlow(styles) {
2042
+ const pos = String(styles['position'] ?? '');
2043
+ return pos === 'fixed' || pos === 'absolute';
2044
+ }
2045
+ /**
2046
+ * The selector BODY (no leading dot) for a state rule, or null when a relational / targeted
2047
+ * state has no scope yet (emitting it unscoped would be wrong — a page-global `:has()`).
2048
+ */
2049
+ stateSelector(nodeId, state, relativeToId, triggerId) {
2050
+ // Targeted states (trigger-hover): the styled node restyles when an ARBITRARY chosen
2051
+ // element is hovered. The selector is `:has()`-scoped to `relativeToId` (the nearest
2052
+ // common ancestor of styled + trigger) so it stays repetition-safe with one shared rule.
2053
+ if (TARGETED_STATES.includes(state)) {
2054
+ const base = TARGETED_STATE_BASE[state];
2055
+ return relativeToId && triggerId && base
2056
+ ? buildTargetedStateSelector(relativeToId, triggerId, nodeId, base) : null;
2057
+ }
2058
+ // Relational states (parent-hover): the pseudo-class lives on an ANCESTOR
2059
+ // and cascades to this element. Requires the ancestor's css id.
2060
+ if (RELATIONAL_STATES.includes(state)) {
2061
+ const base = RELATIONAL_STATE_BASE[state];
2062
+ return relativeToId && base ? `iox-node-${relativeToId}:${base} .iox-node-${nodeId}` : null;
2063
+ }
2064
+ // Validation states (wrong/accepted, board 7) are not real CSS pseudo-classes — `:wrong`
2065
+ // doesn't exist. Compound a CLASS instead, matching the host class IoxFieldComponent
2066
+ // already sets from forceInvalid/showValid. Reuses buildPartSelector with no part, since
2067
+ // "whole node, when wrong" is exactly that function's no-part case.
2068
+ const validationClass = VALIDATION_STATE_CLASS[state];
2069
+ if (validationClass)
2070
+ return buildPartSelector(nodeId, undefined, validationClass);
2071
+ // Structural pseudo-classes (first-child, last-child, nth-child) target the
2072
+ // outer wrapper element so they participate correctly in grid/flex layout.
2073
+ const prefix = STRUCTURAL_STATES.includes(state) ? 'iox-outer' : 'iox-node';
2074
+ // `focus` compiles to :focus-visible (DS2) — shared with the engine via statePseudoClass.
2075
+ return `${prefix}-${nodeId}:${statePseudoClass(state)}`;
2076
+ }
2077
+ /**
2078
+ * The inner + outer rule (joined) and the `::marker` rule for one composed style map.
2079
+ * `isOutOfFlow` is passed in rather than read from `styles` because a breakpoint layer is
2080
+ * sparse — the position in force may come from a wider layer.
2081
+ */
2082
+ nodeRuleCss(nodeId, styles, isOutOfFlow, context) {
2083
+ // ::marker pseudo-element — emitted as a separate rule, not part of inner/outer split.
2084
+ const markerStyles = styles['__markerStyles'];
2085
+ const marker = markerStyles && Object.keys(markerStyles).length
2086
+ ? this.compile(`iox-node-${nodeId}::marker`, markerStyles, context) : '';
2087
+ const inner = {};
2088
+ const outer = {};
2089
+ for (const [k, v] of Object.entries(styles)) {
2090
+ if (k === '__markerStyles')
2091
+ continue;
2092
+ const wantsOuter = StyleRegistryService.OUTER_PROPS.has(k);
2093
+ // width goes to inner for fixed/absolute (the positioned element owns its own width)
2094
+ if (wantsOuter && !(isOutOfFlow && k === 'width')) {
2095
+ outer[k] = v;
2096
+ }
2097
+ else {
2098
+ inner[k] = v;
2099
+ }
2100
+ }
2101
+ // Ensure fixed/absolute elements stay visible when no explicit width is set. A base-rule
2102
+ // render default only — a sparse breakpoint layer must not invent a width.
2103
+ if (context === DeclarationContext.Base && isOutOfFlow && inner['width'] === undefined) {
2104
+ inner['width'] = '100%';
2105
+ }
2106
+ const innerCss = this.compile(`iox-node-${nodeId}`, inner, context);
2107
+ const outerCss = this.compile(`iox-outer-${nodeId}`, outer, context);
2108
+ return { main: [innerCss, outerCss].filter(Boolean).join('\n'), marker };
2109
+ }
2110
+ /**
2111
+ * The org's design tokens on the canvas — through the SHARED compiler (`compileTokenCssBlocks`,
2112
+ * design-core), scoped to `.iox-token-scope` (BUILDER_TOKEN_SCOPE_CLASS — never `:root`, which
2113
+ * the admin's chrome tokens own). So the canvas gets exactly what the live site gets: the
2114
+ * variables, the scheme classes, the reduced-motion / reduced-transparency blocks and the dark
2115
+ * block, LAST. It used to be a hand-written copy that emitted variables and dark only.
2116
+ * Pass the full row set every time (an empty list clears).
2117
+ */
2118
+ upsertTokens(tokens) {
2119
+ this.tokenCss = compileTokenCssBlocks(tokens, { scope: `.${BUILDER_TOKEN_SCOPE_CLASS}` });
2120
+ this.flush();
2121
+ }
2122
+ remove(nodeId) {
2123
+ if (!nodeId)
2124
+ return;
2125
+ // Remove base rule and all state rules for this node
2126
+ this.rules.delete(nodeId);
2127
+ for (const key of this.rules.keys()) {
2128
+ if (key.startsWith(`${nodeId}:`)) {
2129
+ this.rules.delete(key);
2130
+ }
2131
+ }
2132
+ for (const key of this.stateScopes.keys()) {
2133
+ if (key.startsWith(`${nodeId}:`))
2134
+ this.stateScopes.delete(key);
2135
+ }
2136
+ this.responsiveInputs.delete(nodeId);
2137
+ this.clearResponsive(nodeId);
2138
+ this.nodeTypes.delete(nodeId);
2139
+ this.flush();
2140
+ }
2141
+ destroy() {
2142
+ this.rules.clear();
2143
+ this.responsiveRules.clear();
2144
+ this.responsiveInputs.clear();
2145
+ this.responsiveKeys.clear();
2146
+ this.stateScopes.clear();
2147
+ this.nodeTypes.clear();
2148
+ this.tokenCss = { base: '', dark: '' };
2149
+ if (this.styleEl) {
2150
+ this.styleEl.remove();
2151
+ this.styleEl = null;
2152
+ }
2153
+ }
2154
+ compile(className, styles, context = DeclarationContext.Base) {
2155
+ // Declaration compilation (kebab-case, viewport-unit rewrite, !important) lives in
2156
+ // the shared style-compiler so the SSR engine produces byte-identical output.
2157
+ // The inner/outer partition + selector emission stay here (builder-only concern).
2158
+ const decls = compileDeclarations(styles, StyleRegistryService.IMPORTANT_PROPS, context);
2159
+ if (!decls.length)
2160
+ return '';
2161
+ const body = decls.map(d => ` ${d};`).join('\n');
2162
+ return `.${className} {\n${body}\n}`;
2163
+ }
2164
+ flush() {
2165
+ if (!this.styleEl || this._flushPending)
2166
+ return;
2167
+ this._flushPending = true;
2168
+ requestAnimationFrame(() => {
2169
+ this._flushPending = false;
2170
+ if (!this.styleEl)
2171
+ return;
2172
+ // The token base first, so variables are available to all subsequent rules.
2173
+ const rest = Array.from(this.rules.values());
2174
+ // Breakpoint rules LAST, widest → narrowest, each as a canvas container query.
2175
+ const breakpoints = BREAKPOINTS
2176
+ .map(({ id }) => this.responsiveRules.get(id))
2177
+ .map((bucket, i) => bucket?.size
2178
+ ? wrapInBreakpointQuery(BREAKPOINTS[i].id, Array.from(bucket.values()).join('\n'), BreakpointQueryKind.Container, BUILDER_CANVAS_CONTAINER)
2179
+ : '');
2180
+ // …and the token dark half LAST (the appended-last contract the engine also keeps).
2181
+ this.styleEl.textContent = [this.tokenCss.base, ...rest, ...breakpoints, this.tokenCss.dark].filter(Boolean).join('\n');
2182
+ this.changes$.next();
2183
+ });
2184
+ }
2185
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: StyleRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2186
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: StyleRegistryService }); }
2187
+ }
2188
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: StyleRegistryService, decorators: [{
2189
+ type: Injectable
2190
+ }] });
2191
+
2192
+ /**
2193
+ * SymbolRegistryService — in-memory store for org-level symbols.
2194
+ *
2195
+ * Loaded once per page by PageUiComponent alongside the page layout.
2196
+ * Scoped to PageUiComponent.providers[] via IoxBuilderModule — one registry per builder instance.
2197
+ */
2198
+ class SymbolRegistryService {
2199
+ constructor() {
2200
+ this.symbols = new Map();
2201
+ /** symbolIds that have a live reusable block in the DB — used to guard isDetachedSymbol. */
2202
+ this.blockSymbolIds = new Set();
2203
+ }
2204
+ setSymbols(symbols) {
2205
+ this.symbols.clear();
2206
+ for (const s of symbols) {
2207
+ this.symbols.set(s.id, s);
2208
+ }
2209
+ }
2210
+ getSymbol(id) {
2211
+ return this.symbols.get(id);
2212
+ }
2213
+ getAll() {
2214
+ return Array.from(this.symbols.values());
2215
+ }
2216
+ upsert(symbol) {
2217
+ this.symbols.set(symbol.id, symbol);
2218
+ }
2219
+ remove(id) {
2220
+ this.symbols.delete(id);
2221
+ }
2222
+ /** Set the symbolIds that have a live reusable block. Called on page load. */
2223
+ setBlockSymbolIds(ids) {
2224
+ this.blockSymbolIds = new Set(ids);
2225
+ }
2226
+ /** Returns true only when the symbol has a live backing reusable block. */
2227
+ isBlockSymbol(id) {
2228
+ return this.blockSymbolIds.has(id);
2229
+ }
2230
+ /** Remove a symbolId from the block-backed set (called when a block is deleted). */
2231
+ removeBlockSymbolId(id) {
2232
+ this.blockSymbolIds.delete(id);
2233
+ }
2234
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SymbolRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2235
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SymbolRegistryService }); }
2236
+ }
2237
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SymbolRegistryService, decorators: [{
2238
+ type: Injectable
2239
+ }] });
2240
+
2241
+ /** Marks the overlay's dedicated drag-handle chrome (see overlay.component.html) — it carries no
2242
+ * node of its own, so a drag started on it always redirects to whatever is currently selected. */
2243
+ const DRAG_HANDLE_ATTR = 'data-iox-drag-handle';
2244
+ // Ease-out-quint — fast start, precise stop, no overshoot. Used for FLIP sibling
2245
+ // reflow and the insertion indicator open/close. Deliberately NOT a spring/back
2246
+ // curve (e.g. cubic-bezier(0.34,1.56,0.64,1)): multiple siblings animating with
2247
+ // overshoot at once reads as jitter, not polish — reserve overshoot for single-
2248
+ // element motion (ghost drop-settle, a later phase).
2249
+ const MOTION_EASE = 'cubic-bezier(0.22, 1, 0.36, 1)';
2250
+ // Per-frame exponential smoothing factor for the drag ghost's cursor-follow.
2251
+ // Was: ghost position set 1:1 to the pointer every dragmove (rigid/teleporty).
2252
+ // Lower = more lag/weight, higher = snappier. 0.35 catches up to the pointer
2253
+ // within ~5-6 frames (~80-100ms) — reads as "responsive but weighted".
2254
+ const GHOST_FOLLOW_SMOOTHING = 0.35;
2255
+ function deepCloneNode(node) {
2256
+ const clone = JSON.parse(JSON.stringify(node));
2257
+ // Fresh ids + remap intra-subtree references (stateTriggers, interaction targets) so a cloned
2258
+ // structure stays self-consistent — e.g. trigger-hover keeps pointing at the clone's own trigger.
2259
+ regenerateNodeIds(clone);
2260
+ return clone;
2261
+ }
2262
+ class DragEngineService {
1870
2263
  constructor(registry, symbolRegistry, overlayService) {
1871
2264
  this.registry = registry;
1872
2265
  this.symbolRegistry = symbolRegistry;
@@ -2231,6 +2624,10 @@ class DragEngineService {
2231
2624
  'margin:0',
2232
2625
  ].join(';');
2233
2626
  clone.classList.add('iox-drag-moving');
2627
+ // The org's tokens are declared on the token scope (never :root), and <body> is outside
2628
+ // it — so a ghost without the scope class resolved every var(--iox-…) to nothing and an
2629
+ // element-styled button lost its fill and padding mid-drag (DS2 verification).
2630
+ clone.classList.add(BUILDER_TOKEN_SCOPE_CLASS);
2234
2631
  document.body.appendChild(clone);
2235
2632
  this._overlay = clone;
2236
2633
  this._startGhostFollow(rect.left, rect.top);
@@ -3483,588 +3880,204 @@ class InteractionEngineService {
3483
3880
  const { keyframes } = this.buildAnimation(action);
3484
3881
  if (!keyframes.length)
3485
3882
  return;
3486
- // 'both' = backwards (holds first keyframe during delay) + forwards (holds last keyframe after end).
3487
- const anim = element.animate(keyframes, {
3488
- duration: action.duration,
3489
- delay: action.delay,
3490
- easing: action.easing,
3491
- fill: 'both',
3492
- });
3493
- if (this.ENTRANCE_TYPES.has(action.type)) {
3494
- anim.finished.then(() => {
3495
- element.style.removeProperty('pointer-events');
3496
- element.removeAttribute('data-iox-hidden');
3497
- this.preStatedElements.delete(element);
3498
- // Builder: clear inline pre-state so cancelling animations later
3499
- // doesn't leave the element invisible after a play preview.
3500
- if (this.isBuilderMode())
3501
- this.clearInlineAnimationStyles(element);
3502
- }).catch(() => { });
3503
- }
3504
- else if (this.EXIT_TYPES.has(action.type)) {
3505
- // Once the exit animation finishes the element is invisible —
3506
- // disable pointer events so it doesn't silently block clicks underneath it.
3507
- anim.finished.then(() => {
3508
- element.style.setProperty('pointer-events', 'none');
3509
- }).catch(() => { });
3510
- }
3511
- }
3512
- /**
3513
- * Freeze an element at the animation's starting frame immediately, before any
3514
- * trigger fires. Called for ALL entrance-type actions regardless of trigger so
3515
- * that a click-triggered fadeIn on a menu overlay also starts it hidden.
3516
- *
3517
- * In builder mode only opacity:0 is applied — transforms on canvas elements
3518
- * inside iox-page-component's scale() transform break Lenis scroll. The transform
3519
- * jump at animation t=0 is invisible because the element is already opacity:0.
3520
- * In production the full keyframe is applied + pointer-events:none.
3521
- */
3522
- applyPreState(element, action) {
3523
- const { keyframes } = this.buildAnimation(action);
3524
- if (!keyframes.length)
3525
- return;
3526
- const first = keyframes[0];
3527
- if (this.isBuilderMode()) {
3528
- // No transforms — inline transforms on children inside iox-page-component's
3529
- // scale() break Lenis scroll. pointer-events:none lets wheel events and
3530
- // hover fall through to visible elements underneath.
3531
- element.style.setProperty('opacity', '0');
3532
- element.style.setProperty('pointer-events', 'none');
3533
- }
3534
- else {
3535
- for (const [prop, val] of Object.entries(first)) {
3536
- element.style[prop] = String(val);
3537
- }
3538
- element.style.setProperty('pointer-events', 'none');
3539
- }
3540
- element.setAttribute('data-iox-hidden', '');
3541
- this.preStatedElements.add(element);
3542
- }
3543
- clearInlineAnimationStyles(element) {
3544
- element.style.removeProperty('opacity');
3545
- element.style.removeProperty('transform');
3546
- element.style.removeProperty('visibility');
3547
- element.style.removeProperty('pointer-events');
3548
- element.removeAttribute('data-iox-hidden');
3549
- }
3550
- reverseAction(element, action) {
3551
- const { keyframes } = this.buildAnimation(action);
3552
- if (!keyframes.length)
3553
- return;
3554
- // Reverse: play the keyframes backwards
3555
- element.animate([...keyframes].reverse(), {
3556
- duration: action.duration,
3557
- easing: action.easing,
3558
- fill: 'forwards',
3559
- });
3560
- }
3561
- buildAnimation(action) {
3562
- const dist = action.params?.['distance'] ?? 20;
3563
- let keyframes = [];
3564
- switch (action.type) {
3565
- case 'fadeIn':
3566
- keyframes = [{ opacity: 0 }, { opacity: 1 }];
3567
- break;
3568
- case 'fadeOut':
3569
- keyframes = [{ opacity: 1 }, { opacity: 0 }];
3570
- break;
3571
- case 'moveUp':
3572
- keyframes = [{ transform: `translateY(${dist}px)`, opacity: 0 }, { transform: 'translateY(0)', opacity: 1 }];
3573
- break;
3574
- case 'moveDown':
3575
- keyframes = [{ transform: `translateY(-${dist}px)`, opacity: 0 }, { transform: 'translateY(0)', opacity: 1 }];
3576
- break;
3577
- case 'moveLeft':
3578
- keyframes = [{ transform: `translateX(${dist}px)`, opacity: 0 }, { transform: 'translateX(0)', opacity: 1 }];
3579
- break;
3580
- case 'moveRight':
3581
- keyframes = [{ transform: `translateX(-${dist}px)`, opacity: 0 }, { transform: 'translateX(0)', opacity: 1 }];
3582
- break;
3583
- case 'scaleIn':
3584
- keyframes = [{ transform: 'scale(0.8)', opacity: 0 }, { transform: 'scale(1)', opacity: 1 }];
3585
- break;
3586
- case 'scaleOut':
3587
- keyframes = [{ transform: 'scale(1)', opacity: 1 }, { transform: 'scale(0.8)', opacity: 0 }];
3588
- break;
3589
- case 'rotate':
3590
- const deg = action.params?.['degrees'] ?? 360;
3591
- keyframes = [{ transform: `rotate(0deg)` }, { transform: `rotate(${deg}deg)` }];
3592
- break;
3593
- case 'show':
3594
- keyframes = [{ opacity: 0, visibility: 'hidden' }, { opacity: 1, visibility: 'visible' }];
3595
- break;
3596
- case 'hide':
3597
- keyframes = [{ opacity: 1, visibility: 'visible' }, { opacity: 0, visibility: 'hidden' }];
3598
- break;
3599
- case 'toggleVisibility':
3600
- // Toggle is stateful — handled separately via element style
3601
- keyframes = [{ opacity: 0 }, { opacity: 1 }];
3602
- break;
3603
- }
3604
- return {
3605
- keyframes,
3606
- options: {
3607
- duration: action.duration,
3608
- delay: action.delay,
3609
- easing: action.easing,
3610
- fill: 'forwards',
3611
- },
3612
- };
3613
- }
3614
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService, deps: [{ token: OverlayService }], target: i0.ɵɵFactoryTarget.Injectable }); }
3615
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService }); }
3616
- }
3617
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService, decorators: [{
3618
- type: Injectable
3619
- }], ctorParameters: () => [{ type: OverlayService }] });
3620
-
3621
- /**
3622
- * InteractionPresetRegistryService — in-memory store for org-level interaction presets.
3623
- *
3624
- * Loaded once per page by PageUiComponent alongside the page layout.
3625
- * Scoped to PageUiComponent.providers[] via IoxBuilderModule — one registry per builder instance.
3626
- */
3627
- class InteractionPresetRegistryService {
3628
- constructor() {
3629
- this.presets = new Map();
3630
- }
3631
- setPresets(presets) {
3632
- this.presets.clear();
3633
- for (const p of presets) {
3634
- this.presets.set(p.id, p);
3635
- }
3636
- }
3637
- getPreset(id) {
3638
- return this.presets.get(id);
3639
- }
3640
- getAll() {
3641
- return Array.from(this.presets.values());
3642
- }
3643
- upsert(preset) {
3644
- this.presets.set(preset.id, preset);
3645
- }
3646
- remove(id) {
3647
- this.presets.delete(id);
3648
- }
3649
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionPresetRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3650
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionPresetRegistryService }); }
3651
- }
3652
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionPresetRegistryService, decorators: [{
3653
- type: Injectable
3654
- }] });
3655
-
3656
- /**
3657
- * BuilderClipboardService — in-memory typed clipboard for the page builder.
3658
- *
3659
- * Stores element copies, style preset references, and interaction preset references.
3660
- * Lives in memory only — no navigator.clipboard usage.
3661
- * Scoped to PageUiComponent.providers[] — one clipboard per builder instance.
3662
- */
3663
- class BuilderClipboardService {
3664
- constructor() {
3665
- this._clip = {};
3666
- }
3667
- copyNode(node) {
3668
- this._clip = { node: JSON.parse(JSON.stringify(node)) };
3669
- }
3670
- copyPreset(presetId) {
3671
- this._clip = { ...this._clip, presetId };
3672
- }
3673
- copyInteractionPreset(interactionPresetId) {
3674
- this._clip = { ...this._clip, interactionPresetId };
3675
- }
3676
- getNode() { return this._clip.node; }
3677
- getPresetId() { return this._clip.presetId; }
3678
- getInteractionPresetId() { return this._clip.interactionPresetId; }
3679
- clear() { this._clip = {}; }
3680
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderClipboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3681
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderClipboardService }); }
3682
- }
3683
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderClipboardService, decorators: [{
3684
- type: Injectable
3685
- }] });
3686
-
3687
- /**
3688
- * CSS container name of the builder canvas frame (and of the global-elements overlay). Breakpoint
3689
- * rules are emitted as `@container iox-canvas (max-width: …)`: the frame's layout width IS the
3690
- * simulated viewport and is unaffected by the zoom `transform: scale`, whereas a `@media` query
3691
- * would read the editor window. Set on the frame in builder.component.scss.
3692
- */
3693
- const BUILDER_CANVAS_CONTAINER = 'iox-canvas';
3694
- /**
3695
- * The class that carries the ORG's design tokens on the canvas. Tokens used to be declared on
3696
- * `:root`, where they collided with the admin's own `--iox-*` chrome tokens (`accent`, `surface-2`,
3697
- * `scrim`, `focus-ring`… — DS2 seeds all 161 Crisp rows, so the clash became universal and the
3698
- * admin turned the studio's accent colour). Every surface that renders site content through this
3699
- * registry — the canvas frame, the global-elements overlay, preview, specimens — wears this class.
3700
- */
3701
- const BUILDER_TOKEN_SCOPE_CLASS = 'iox-token-scope';
3702
- /**
3703
- * StyleRegistryService — manages a single <style> tag for the builder canvas.
3704
- *
3705
- * Each builder component gets two CSS classes:
3706
- * .iox-node-{id} — applied to the inner element; handles layout/visual props
3707
- * (display, flex, background, border, padding, etc.)
3708
- * .iox-outer-{id} — applied to the host element by RenderDirective; handles
3709
- * properties that participate in the PARENT flow (margin,
3710
- * align-self, flex-grow/shrink/basis, order).
3711
- *
3712
- * This split is necessary because margin on an inner wrapper only adds internal
3713
- * space — it does not push sibling elements. Applying margin to the host element
3714
- * (which IS the flex/block child in the parent container) makes it work correctly.
3715
- *
3716
- * State styles (hover/active/focus) are stored under key `${nodeId}:${state}`
3717
- * and compiled to `.iox-node-{id}:hover { … }` selectors.
3718
- *
3719
- * Design token variables are stored under the reserved key `__tokens__` and
3720
- * compiled to a `.iox-token-scope { … }` block prepended to all other rules.
3721
- *
3722
- * Scoped to PageUiComponent.providers[] — one stylesheet per builder instance.
3723
- */
3724
- class StyleRegistryService {
3725
- constructor() {
3726
- this.changes$ = new Subject();
3727
- this.rules = new Map();
3728
- /** Breakpoint rules, kept apart so they flush AFTER every base rule — equal specificity, so
3729
- * source order decides (plan trap 1). Keyed per breakpoint like `rules` (`id`, `id:state`). */
3730
- this.responsiveRules = new Map();
3731
- this.responsiveInputs = new Map();
3732
- /** Which breakpoint rule keys each node owns — so re-emitting one node's rules is O(its rules),
3733
- * not a scan of every breakpoint rule on the page (this runs on every node render). */
3734
- this.responsiveKeys = new Map();
3735
- this.stateScopes = new Map();
3736
- this.styleEl = null;
3737
- this._flushPending = false;
3738
- }
3739
- /**
3740
- * Properties that must live on the OUTER wrapper (.iox-outer-{id}) because they
3741
- * affect how the element participates in its PARENT layout (flex/block flow).
3742
- * Everything else goes to the INNER element (.iox-node-{id}).
3743
- *
3744
- * Exception — out-of-flow positioning (position:fixed/absolute):
3745
- * When position is fixed or absolute, width is redirected to the inner element
3746
- * (the positioned one) and a default width:100% is added if none is set, so the
3747
- * element stays visible. top/right/bottom/left/zIndex are always inner because
3748
- * they are co-ordinates of the positioned element, not the flow wrapper.
3749
- * position itself goes to inner with `!important` to beat component-scoped CSS.
3750
- */
3751
- static { this.OUTER_PROPS = new Set([
3752
- 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
3753
- 'alignSelf', 'flexGrow', 'flexShrink', 'flexBasis', 'order',
3754
- // Grid-child placement — like the flex-child props above, these position the node
3755
- // within the PARENT's grid, so they must land on the outer host, not the inner div.
3756
- 'gridColumn', 'gridRow', 'justifySelf',
3757
- 'width',
3758
- ]); }
3759
- /**
3760
- * Properties that need !important on the inner element to beat Angular's scoped-CSS
3761
- * specificity boost. CANVAS-ONLY (H2 audit, 2026-10-11): the shared compiler never forces
3762
- * importance and the SSR engine never emits it — this is the builder's own fight with its
3763
- * scoped component styles. The A4 dialog centring clash this once caused is resolved
3764
- * structurally (the grid stage PLACES a static surface), so the set stays at exactly one
3765
- * documented member; adding to it needs the same scrutiny.
3766
- */
3767
- static { this.IMPORTANT_PROPS = new Set(['position']); }
3768
- init() {
3769
- this.styleEl = document.createElement('style');
3770
- this.styleEl.id = 'iox-runtime-styles';
3771
- document.head.appendChild(this.styleEl);
3772
- }
3773
- /**
3774
- * Write or update the base styles for a node, or a pseudo-class state override.
3775
- *
3776
- * @param nodeId The node's CSS id (used in `.iox-node-{nodeId}`)
3777
- * @param styles Flat CSS property map (camelCase keys)
3778
- * @param state Optional pseudo-class state ('hover' | 'active' | 'focus' | …).
3779
- * When provided, compiles to `.iox-node-{id}:{state} { … }`.
3780
- * State rules are NOT partitioned into inner/outer.
3781
- * @param relativeToId For RELATIONAL states (e.g. 'parent-hover') this is the ancestor
3782
- * css id; compiles to `.iox-node-{relativeToId}:{base} .iox-node-{nodeId} { … }`.
3783
- * For TARGETED states (e.g. 'trigger-hover') this is the SCOPE css id
3784
- * (nearest common ancestor of the styled node + trigger).
3785
- * @param triggerId For TARGETED states only: the css id of the trigger node whose
3786
- * pseudo-class drives the state. Compiles a `:has()`-scoped selector.
3787
- */
3788
- upsert(nodeId, styles, state, relativeToId, triggerId) {
3789
- if (!nodeId)
3790
- return;
3791
- if (state) {
3792
- const key = `${nodeId}:${state}`;
3793
- // Remember the scope a relational / targeted rule was given, so the same state at a
3794
- // breakpoint is scoped identically without re-resolving it (see upsertResponsive).
3795
- if (relativeToId || triggerId)
3796
- this.stateScopes.set(key, { relativeToId, triggerId });
3797
- const selector = this.stateSelector(nodeId, state, relativeToId, triggerId);
3798
- const css = selector ? this.compile(selector, styles) : '';
3799
- if (css)
3800
- this.rules.set(key, css);
3801
- else
3802
- this.rules.delete(key);
3803
- if (this.responsiveInputs.has(nodeId))
3804
- this.emitResponsive(nodeId);
3805
- this.flush();
3806
- return;
3807
- }
3808
- const { main, marker } = this.nodeRuleCss(nodeId, styles, this.isOutOfFlow(styles), DeclarationContext.Base);
3809
- if (marker)
3810
- this.rules.set(`${nodeId}::marker`, marker);
3811
- else
3812
- this.rules.delete(`${nodeId}::marker`);
3813
- if (main)
3814
- this.rules.set(nodeId, main);
3815
- else
3816
- this.rules.delete(nodeId);
3817
- this.flush();
3818
- }
3819
- /**
3820
- * Write or clear the styles for one PART of a compound component (board 7 — "the failure
3821
- * design"), optionally further scoped to a validation state ('wrong'/'accepted'). Compiles
3822
- * through the exact same `compile()` every other rule uses — `buildPartSelector` (shared,
3823
- * `@vectoriox/iox-ui`) just builds a different selector string. Unlike interaction/relational/
3824
- * targeted states, a validation state compounds a CLASS onto the node (`.iox-invalid`), not a
3825
- * `:pseudo-class`, since `IoxFieldComponent` sets that class itself from `forceInvalid`/
3826
- * `showValid` — there is no real "wrong" pseudo-class to hook.
3827
- */
3828
- upsertPart(nodeId, partClassName, styles, state) {
3829
- if (!nodeId || !partClassName)
3830
- return;
3831
- const key = `${nodeId}:part:${partClassName}${state ? ':' + state : ''}`;
3832
- const validationClass = state ? VALIDATION_STATE_CLASS[state] : undefined;
3833
- const selector = buildPartSelector(nodeId, partClassName, validationClass);
3834
- const css = Object.keys(styles).length ? this.compile(selector, styles) : '';
3835
- if (css)
3836
- this.rules.set(key, css);
3837
- else
3838
- this.rules.delete(key);
3839
- this.flush();
3840
- }
3841
- /**
3842
- * Emit (or clear) a node's breakpoint rules. `planResponsiveRules` — shared with the SSR engine —
3843
- * decides what each breakpoint emits; this only turns it into selectors, through the SAME
3844
- * inner/outer split and state selectors as the base rules, compiled in the Override context
3845
- * (a CSS initial value undoes the wider rule there, so it must not be dropped).
3846
- *
3847
- * @param styleProps The node's full BASE style map (raw — virtual traits not yet composed).
3848
- */
3849
- upsertResponsive(nodeId, styleProps, stateStyles, responsive) {
3850
- if (!nodeId)
3851
- return;
3852
- const hasOverrides = !!responsive && Object.keys(responsive).length > 0;
3853
- // The common case — a node with no overrides that never had any — costs nothing.
3854
- if (!hasOverrides && !this.responsiveInputs.has(nodeId))
3855
- return;
3856
- if (hasOverrides)
3857
- this.responsiveInputs.set(nodeId, { styleProps, stateStyles, responsive: responsive });
3858
- else
3859
- this.responsiveInputs.delete(nodeId);
3860
- this.emitResponsive(nodeId);
3861
- this.flush();
3862
- }
3863
- emitResponsive(nodeId) {
3864
- this.clearResponsive(nodeId);
3865
- const input = this.responsiveInputs.get(nodeId);
3866
- if (!input)
3867
- return;
3868
- for (const layer of planResponsiveRules(input.styleProps, input.stateStyles, input.responsive)) {
3869
- const bp = layer.breakpoint;
3870
- // Where `width` lands depends on the position in force AT this breakpoint.
3871
- const outOfFlow = this.isOutOfFlow(resolveStyleProps(input.styleProps, input.responsive, bp));
3872
- const { main, marker } = this.nodeRuleCss(nodeId, layer.styleProps, outOfFlow, DeclarationContext.Override);
3873
- if (main)
3874
- this.setResponsiveRule(nodeId, bp, nodeId, main);
3875
- if (marker)
3876
- this.setResponsiveRule(nodeId, bp, `${nodeId}::marker`, marker);
3877
- for (const [state, map] of Object.entries(layer.stateStyles)) {
3878
- const scope = this.stateScopes.get(`${nodeId}:${state}`);
3879
- const selector = this.stateSelector(nodeId, state, scope?.relativeToId, scope?.triggerId);
3880
- // A relational/targeted state whose scope isn't known yet is skipped; it is re-emitted
3881
- // when the base state rule arrives with its scope (see upsert).
3882
- const css = selector ? this.compile(selector, map, DeclarationContext.Override) : '';
3883
- if (css)
3884
- this.setResponsiveRule(nodeId, bp, `${nodeId}:${state}`, css);
3885
- }
3886
- }
3887
- }
3888
- setResponsiveRule(nodeId, bp, key, css) {
3889
- let bucket = this.responsiveRules.get(bp);
3890
- if (!bucket)
3891
- this.responsiveRules.set(bp, (bucket = new Map()));
3892
- bucket.set(key, css);
3893
- let owned = this.responsiveKeys.get(nodeId);
3894
- if (!owned)
3895
- this.responsiveKeys.set(nodeId, (owned = []));
3896
- owned.push([bp, key]);
3897
- }
3898
- clearResponsive(nodeId) {
3899
- for (const [bp, key] of this.responsiveKeys.get(nodeId) ?? [])
3900
- this.responsiveRules.get(bp)?.delete(key);
3901
- this.responsiveKeys.delete(nodeId);
3902
- }
3903
- isOutOfFlow(styles) {
3904
- const pos = String(styles['position'] ?? '');
3905
- return pos === 'fixed' || pos === 'absolute';
3906
- }
3907
- /**
3908
- * The selector BODY (no leading dot) for a state rule, or null when a relational / targeted
3909
- * state has no scope yet (emitting it unscoped would be wrong — a page-global `:has()`).
3910
- */
3911
- stateSelector(nodeId, state, relativeToId, triggerId) {
3912
- // Targeted states (trigger-hover): the styled node restyles when an ARBITRARY chosen
3913
- // element is hovered. The selector is `:has()`-scoped to `relativeToId` (the nearest
3914
- // common ancestor of styled + trigger) so it stays repetition-safe with one shared rule.
3915
- if (TARGETED_STATES.includes(state)) {
3916
- const base = TARGETED_STATE_BASE[state];
3917
- return relativeToId && triggerId && base
3918
- ? buildTargetedStateSelector(relativeToId, triggerId, nodeId, base) : null;
3919
- }
3920
- // Relational states (parent-hover): the pseudo-class lives on an ANCESTOR
3921
- // and cascades to this element. Requires the ancestor's css id.
3922
- if (RELATIONAL_STATES.includes(state)) {
3923
- const base = RELATIONAL_STATE_BASE[state];
3924
- return relativeToId && base ? `iox-node-${relativeToId}:${base} .iox-node-${nodeId}` : null;
3925
- }
3926
- // Validation states (wrong/accepted, board 7) are not real CSS pseudo-classes — `:wrong`
3927
- // doesn't exist. Compound a CLASS instead, matching the host class IoxFieldComponent
3928
- // already sets from forceInvalid/showValid. Reuses buildPartSelector with no part, since
3929
- // "whole node, when wrong" is exactly that function's no-part case.
3930
- const validationClass = VALIDATION_STATE_CLASS[state];
3931
- if (validationClass)
3932
- return buildPartSelector(nodeId, undefined, validationClass);
3933
- // Structural pseudo-classes (first-child, last-child, nth-child) target the
3934
- // outer wrapper element so they participate correctly in grid/flex layout.
3935
- const prefix = STRUCTURAL_STATES.includes(state) ? 'iox-outer' : 'iox-node';
3936
- // `focus` compiles to :focus-visible (DS2) — shared with the engine via statePseudoClass.
3937
- return `${prefix}-${nodeId}:${statePseudoClass(state)}`;
3938
- }
3939
- /**
3940
- * The inner + outer rule (joined) and the `::marker` rule for one composed style map.
3941
- * `isOutOfFlow` is passed in rather than read from `styles` because a breakpoint layer is
3942
- * sparse — the position in force may come from a wider layer.
3943
- */
3944
- nodeRuleCss(nodeId, styles, isOutOfFlow, context) {
3945
- // ::marker pseudo-element — emitted as a separate rule, not part of inner/outer split.
3946
- const markerStyles = styles['__markerStyles'];
3947
- const marker = markerStyles && Object.keys(markerStyles).length
3948
- ? this.compile(`iox-node-${nodeId}::marker`, markerStyles, context) : '';
3949
- const inner = {};
3950
- const outer = {};
3951
- for (const [k, v] of Object.entries(styles)) {
3952
- if (k === '__markerStyles')
3953
- continue;
3954
- const wantsOuter = StyleRegistryService.OUTER_PROPS.has(k);
3955
- // width goes to inner for fixed/absolute (the positioned element owns its own width)
3956
- if (wantsOuter && !(isOutOfFlow && k === 'width')) {
3957
- outer[k] = v;
3958
- }
3959
- else {
3960
- inner[k] = v;
3961
- }
3883
+ // 'both' = backwards (holds first keyframe during delay) + forwards (holds last keyframe after end).
3884
+ const anim = element.animate(keyframes, {
3885
+ duration: action.duration,
3886
+ delay: action.delay,
3887
+ easing: action.easing,
3888
+ fill: 'both',
3889
+ });
3890
+ if (this.ENTRANCE_TYPES.has(action.type)) {
3891
+ anim.finished.then(() => {
3892
+ element.style.removeProperty('pointer-events');
3893
+ element.removeAttribute('data-iox-hidden');
3894
+ this.preStatedElements.delete(element);
3895
+ // Builder: clear inline pre-state so cancelling animations later
3896
+ // doesn't leave the element invisible after a play preview.
3897
+ if (this.isBuilderMode())
3898
+ this.clearInlineAnimationStyles(element);
3899
+ }).catch(() => { });
3962
3900
  }
3963
- // Ensure fixed/absolute elements stay visible when no explicit width is set. A base-rule
3964
- // render default only — a sparse breakpoint layer must not invent a width.
3965
- if (context === DeclarationContext.Base && isOutOfFlow && inner['width'] === undefined) {
3966
- inner['width'] = '100%';
3901
+ else if (this.EXIT_TYPES.has(action.type)) {
3902
+ // Once the exit animation finishes the element is invisible —
3903
+ // disable pointer events so it doesn't silently block clicks underneath it.
3904
+ anim.finished.then(() => {
3905
+ element.style.setProperty('pointer-events', 'none');
3906
+ }).catch(() => { });
3967
3907
  }
3968
- const innerCss = this.compile(`iox-node-${nodeId}`, inner, context);
3969
- const outerCss = this.compile(`iox-outer-${nodeId}`, outer, context);
3970
- return { main: [innerCss, outerCss].filter(Boolean).join('\n'), marker };
3971
3908
  }
3972
3909
  /**
3973
- * Write the org-level design token variables as a `.iox-token-scope { … }` block (see
3974
- * BUILDER_TOKEN_SCOPE_CLASS — never `:root`, which the admin's chrome tokens own).
3975
- * Token names should be CSS custom property names without the `--` prefix
3976
- * (e.g., `{ 'iox-primary-color': '#cb9090' }`).
3977
- * Pass an empty object to clear all tokens.
3910
+ * Freeze an element at the animation's starting frame immediately, before any
3911
+ * trigger fires. Called for ALL entrance-type actions regardless of trigger so
3912
+ * that a click-triggered fadeIn on a menu overlay also starts it hidden.
3913
+ *
3914
+ * In builder mode only opacity:0 is applied — transforms on canvas elements
3915
+ * inside iox-page-component's scale() transform break Lenis scroll. The transform
3916
+ * jump at animation t=0 is invisible because the element is already opacity:0.
3917
+ * In production the full keyframe is applied + pointer-events:none.
3978
3918
  */
3979
- upsertTokens(tokens, darkTokens) {
3980
- const entries = Object.entries(tokens)
3981
- .filter(([, v]) => v !== undefined && v !== null && v !== '')
3982
- .map(([k, v]) => ` --${k}: ${v};`);
3983
- if (entries.length) {
3984
- this.rules.set('__tokens__', `.${BUILDER_TOKEN_SCOPE_CLASS} {\n${entries.join('\n')}\n}`);
3985
- }
3986
- else {
3987
- this.rules.delete('__tokens__');
3988
- }
3989
- // Dark-scheme overrides (flavors F0): variables only, appended as the token layer's
3990
- // LAST rule — one ordering convention for every @media block (responsive A3 lesson).
3991
- const darkEntries = Object.entries(darkTokens ?? {})
3992
- .filter(([, v]) => v !== undefined && v !== null && v !== '')
3993
- .map(([k, v]) => ` --${k}: ${v};`);
3994
- if (darkEntries.length) {
3995
- this.rules.set('__tokens_dark__', `@media (prefers-color-scheme: dark) {\n .${BUILDER_TOKEN_SCOPE_CLASS} {\n${darkEntries.join('\n')}\n }\n}`);
3919
+ applyPreState(element, action) {
3920
+ const { keyframes } = this.buildAnimation(action);
3921
+ if (!keyframes.length)
3922
+ return;
3923
+ const first = keyframes[0];
3924
+ if (this.isBuilderMode()) {
3925
+ // No transforms — inline transforms on children inside iox-page-component's
3926
+ // scale() break Lenis scroll. pointer-events:none lets wheel events and
3927
+ // hover fall through to visible elements underneath.
3928
+ element.style.setProperty('opacity', '0');
3929
+ element.style.setProperty('pointer-events', 'none');
3996
3930
  }
3997
3931
  else {
3998
- this.rules.delete('__tokens_dark__');
3932
+ for (const [prop, val] of Object.entries(first)) {
3933
+ element.style[prop] = String(val);
3934
+ }
3935
+ element.style.setProperty('pointer-events', 'none');
3999
3936
  }
4000
- this.flush();
3937
+ element.setAttribute('data-iox-hidden', '');
3938
+ this.preStatedElements.add(element);
4001
3939
  }
4002
- remove(nodeId) {
4003
- if (!nodeId)
3940
+ clearInlineAnimationStyles(element) {
3941
+ element.style.removeProperty('opacity');
3942
+ element.style.removeProperty('transform');
3943
+ element.style.removeProperty('visibility');
3944
+ element.style.removeProperty('pointer-events');
3945
+ element.removeAttribute('data-iox-hidden');
3946
+ }
3947
+ reverseAction(element, action) {
3948
+ const { keyframes } = this.buildAnimation(action);
3949
+ if (!keyframes.length)
4004
3950
  return;
4005
- // Remove base rule and all state rules for this node
4006
- this.rules.delete(nodeId);
4007
- for (const key of this.rules.keys()) {
4008
- if (key.startsWith(`${nodeId}:`)) {
4009
- this.rules.delete(key);
4010
- }
4011
- }
4012
- for (const key of this.stateScopes.keys()) {
4013
- if (key.startsWith(`${nodeId}:`))
4014
- this.stateScopes.delete(key);
3951
+ // Reverse: play the keyframes backwards
3952
+ element.animate([...keyframes].reverse(), {
3953
+ duration: action.duration,
3954
+ easing: action.easing,
3955
+ fill: 'forwards',
3956
+ });
3957
+ }
3958
+ buildAnimation(action) {
3959
+ const dist = action.params?.['distance'] ?? 20;
3960
+ let keyframes = [];
3961
+ switch (action.type) {
3962
+ case 'fadeIn':
3963
+ keyframes = [{ opacity: 0 }, { opacity: 1 }];
3964
+ break;
3965
+ case 'fadeOut':
3966
+ keyframes = [{ opacity: 1 }, { opacity: 0 }];
3967
+ break;
3968
+ case 'moveUp':
3969
+ keyframes = [{ transform: `translateY(${dist}px)`, opacity: 0 }, { transform: 'translateY(0)', opacity: 1 }];
3970
+ break;
3971
+ case 'moveDown':
3972
+ keyframes = [{ transform: `translateY(-${dist}px)`, opacity: 0 }, { transform: 'translateY(0)', opacity: 1 }];
3973
+ break;
3974
+ case 'moveLeft':
3975
+ keyframes = [{ transform: `translateX(${dist}px)`, opacity: 0 }, { transform: 'translateX(0)', opacity: 1 }];
3976
+ break;
3977
+ case 'moveRight':
3978
+ keyframes = [{ transform: `translateX(-${dist}px)`, opacity: 0 }, { transform: 'translateX(0)', opacity: 1 }];
3979
+ break;
3980
+ case 'scaleIn':
3981
+ keyframes = [{ transform: 'scale(0.8)', opacity: 0 }, { transform: 'scale(1)', opacity: 1 }];
3982
+ break;
3983
+ case 'scaleOut':
3984
+ keyframes = [{ transform: 'scale(1)', opacity: 1 }, { transform: 'scale(0.8)', opacity: 0 }];
3985
+ break;
3986
+ case 'rotate':
3987
+ const deg = action.params?.['degrees'] ?? 360;
3988
+ keyframes = [{ transform: `rotate(0deg)` }, { transform: `rotate(${deg}deg)` }];
3989
+ break;
3990
+ case 'show':
3991
+ keyframes = [{ opacity: 0, visibility: 'hidden' }, { opacity: 1, visibility: 'visible' }];
3992
+ break;
3993
+ case 'hide':
3994
+ keyframes = [{ opacity: 1, visibility: 'visible' }, { opacity: 0, visibility: 'hidden' }];
3995
+ break;
3996
+ case 'toggleVisibility':
3997
+ // Toggle is stateful — handled separately via element style
3998
+ keyframes = [{ opacity: 0 }, { opacity: 1 }];
3999
+ break;
4015
4000
  }
4016
- this.responsiveInputs.delete(nodeId);
4017
- this.clearResponsive(nodeId);
4018
- this.flush();
4001
+ return {
4002
+ keyframes,
4003
+ options: {
4004
+ duration: action.duration,
4005
+ delay: action.delay,
4006
+ easing: action.easing,
4007
+ fill: 'forwards',
4008
+ },
4009
+ };
4019
4010
  }
4020
- destroy() {
4021
- this.rules.clear();
4022
- this.responsiveRules.clear();
4023
- this.responsiveInputs.clear();
4024
- this.responsiveKeys.clear();
4025
- this.stateScopes.clear();
4026
- if (this.styleEl) {
4027
- this.styleEl.remove();
4028
- this.styleEl = null;
4011
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService, deps: [{ token: OverlayService }], target: i0.ɵɵFactoryTarget.Injectable }); }
4012
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService }); }
4013
+ }
4014
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService, decorators: [{
4015
+ type: Injectable
4016
+ }], ctorParameters: () => [{ type: OverlayService }] });
4017
+
4018
+ /**
4019
+ * InteractionPresetRegistryService — in-memory store for org-level interaction presets.
4020
+ *
4021
+ * Loaded once per page by PageUiComponent alongside the page layout.
4022
+ * Scoped to PageUiComponent.providers[] via IoxBuilderModule — one registry per builder instance.
4023
+ */
4024
+ class InteractionPresetRegistryService {
4025
+ constructor() {
4026
+ this.presets = new Map();
4027
+ }
4028
+ setPresets(presets) {
4029
+ this.presets.clear();
4030
+ for (const p of presets) {
4031
+ this.presets.set(p.id, p);
4029
4032
  }
4030
4033
  }
4031
- compile(className, styles, context = DeclarationContext.Base) {
4032
- // Declaration compilation (kebab-case, viewport-unit rewrite, !important) lives in
4033
- // the shared style-compiler so the SSR engine produces byte-identical output.
4034
- // The inner/outer partition + selector emission stay here (builder-only concern).
4035
- const decls = compileDeclarations(styles, StyleRegistryService.IMPORTANT_PROPS, context);
4036
- if (!decls.length)
4037
- return '';
4038
- const body = decls.map(d => ` ${d};`).join('\n');
4039
- return `.${className} {\n${body}\n}`;
4034
+ getPreset(id) {
4035
+ return this.presets.get(id);
4040
4036
  }
4041
- flush() {
4042
- if (!this.styleEl || this._flushPending)
4043
- return;
4044
- this._flushPending = true;
4045
- requestAnimationFrame(() => {
4046
- this._flushPending = false;
4047
- if (!this.styleEl)
4048
- return;
4049
- // __tokens__ always first so variables are available to all subsequent rules
4050
- const tokenBlock = this.rules.get('__tokens__');
4051
- const rest = Array.from(this.rules.entries())
4052
- .filter(([k]) => k !== '__tokens__')
4053
- .map(([, v]) => v);
4054
- // Breakpoint rules LAST, widest → narrowest, each as a canvas container query.
4055
- const breakpoints = BREAKPOINTS
4056
- .map(({ id }) => this.responsiveRules.get(id))
4057
- .map((bucket, i) => bucket?.size
4058
- ? wrapInBreakpointQuery(BREAKPOINTS[i].id, Array.from(bucket.values()).join('\n'), BreakpointQueryKind.Container, BUILDER_CANVAS_CONTAINER)
4059
- : '');
4060
- this.styleEl.textContent = [tokenBlock, ...rest, ...breakpoints].filter(Boolean).join('\n');
4061
- this.changes$.next();
4062
- });
4037
+ getAll() {
4038
+ return Array.from(this.presets.values());
4063
4039
  }
4064
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: StyleRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4065
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: StyleRegistryService }); }
4040
+ upsert(preset) {
4041
+ this.presets.set(preset.id, preset);
4042
+ }
4043
+ remove(id) {
4044
+ this.presets.delete(id);
4045
+ }
4046
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionPresetRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4047
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionPresetRegistryService }); }
4066
4048
  }
4067
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: StyleRegistryService, decorators: [{
4049
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionPresetRegistryService, decorators: [{
4050
+ type: Injectable
4051
+ }] });
4052
+
4053
+ /**
4054
+ * BuilderClipboardService — in-memory typed clipboard for the page builder.
4055
+ *
4056
+ * Stores element copies, style preset references, and interaction preset references.
4057
+ * Lives in memory only — no navigator.clipboard usage.
4058
+ * Scoped to PageUiComponent.providers[] — one clipboard per builder instance.
4059
+ */
4060
+ class BuilderClipboardService {
4061
+ constructor() {
4062
+ this._clip = {};
4063
+ }
4064
+ copyNode(node) {
4065
+ this._clip = { node: JSON.parse(JSON.stringify(node)) };
4066
+ }
4067
+ copyPreset(presetId) {
4068
+ this._clip = { ...this._clip, presetId };
4069
+ }
4070
+ copyInteractionPreset(interactionPresetId) {
4071
+ this._clip = { ...this._clip, interactionPresetId };
4072
+ }
4073
+ getNode() { return this._clip.node; }
4074
+ getPresetId() { return this._clip.presetId; }
4075
+ getInteractionPresetId() { return this._clip.interactionPresetId; }
4076
+ clear() { this._clip = {}; }
4077
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderClipboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4078
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderClipboardService }); }
4079
+ }
4080
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderClipboardService, decorators: [{
4068
4081
  type: Injectable
4069
4082
  }] });
4070
4083
 
@@ -4496,6 +4509,11 @@ const DIRECTIVE_HOST_TYPES = new Set([
4496
4509
  'Image', 'Icon', 'Divider', 'Spacer', 'Button', 'Link', 'Heading', 'Text', 'ButtonBlock',
4497
4510
  // Phase 2 (9c): layout components converted to directive-hosts (host IS the real tag).
4498
4511
  'Section', 'Container', 'List', 'ListItem',
4512
+ // Forms: Field/SubmitButton are the engine's own directive-host twins, and Form/FieldGroup/
4513
+ // FormSuccess/FormError render through the Container twin (a directive-host). The forced
4514
+ // inline `display:block` out-ranked the node rule (a SubmitButton wearing a button style) —
4515
+ // the engine never had it (DS2.1).
4516
+ 'Form', 'Field', 'FieldGroup', 'SubmitButton', 'FormSuccess', 'FormError',
4499
4517
  ]);
4500
4518
  const CONTAINER_TAGS = ['div', 'section', 'article', 'header', 'footer', 'main', 'nav', 'ul', 'ol', 'li'];
4501
4519
  /**
@@ -4700,9 +4718,14 @@ class RenderDirective {
4700
4718
  }
4701
4719
  // Use styleId when present (clone nodes share the template node's CSS rule).
4702
4720
  const cssId = node.styleId ?? node.id;
4703
- // Register CSS rules in the central stylesheet (inner + outer split).
4721
+ // Register CSS rules in the central stylesheet (inner + outer split). The type first: the
4722
+ // registry strips the props a type's renderer owns (a Dialog's positioning) on every rule.
4704
4723
  if (cssId) {
4705
- const composedBase = composeVirtualTraits(initialStyle);
4724
+ this.styleRegistry.setNodeType(cssId, node.type);
4725
+ // The default hover transition (shared, marker-aware — the engine emits the same one):
4726
+ // without it a hovering node's base rule, re-upserted here on every render, dropped the
4727
+ // transition page-ui had added on load, and the canvas snapped where the site eased.
4728
+ const composedBase = composeVirtualTraits(withDefaultHoverTransition(initialStyle, node.stateStyles, node.elementStyle));
4706
4729
  this.styleRegistry.upsert(cssId, composedBase);
4707
4730
  // Restore per-state overrides saved from a previous session.
4708
4731
  // Every state set at base OR at any breakpoint — so a breakpoint-only state (a Mobile-only
@@ -4801,6 +4824,10 @@ class RenderDirective {
4801
4824
  // For layout components the host element is <app-section>; for leaf components
4802
4825
  // it is e.g. <app-card>. Either way this is the correct inspectable element.
4803
4826
  const nativeEl = componentRef.location?.nativeElement ?? null;
4827
+ // The element-style marker (design-system §3b-bis) — on the HOST, as both engine paths do:
4828
+ // component CSS that carries the system look is scoped to it, so a node saved before DS2
4829
+ // keeps its exact pre-DS2 look. Clones carry it too (they render the same look).
4830
+ syncElementStyleMarker(nativeEl, node.elementStyle);
4804
4831
  // Custom element host defaults to display:inline. Set it to block so it participates
4805
4832
  // properly in layout and CDK can capture its bounding rect. SKIP for directive-hosts: the
4806
4833
  // host is a real element with its own display, and an inline style here out-ranks both
@@ -7826,7 +7853,6 @@ class BuilderLinkedContainerComponent {
7826
7853
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: BuilderLinkedContainerComponent, isStandalone: false, selector: "app-builder-linked-container", inputs: { children: "children", style: "style", nodeId: "nodeId", dropListId: "dropListId", linkType: "linkType", url: "url", pageRoute: "pageRoute", target: "target" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
7827
7854
  <a [href]="resolvedHref"
7828
7855
  [target]="target"
7829
- [ngStyle]="style"
7830
7856
  [ngClass]="['iox-node-' + nodeId, listOrientation === 'horizontal' ? 'is-horizontal' : '']"
7831
7857
  class="linked-container-root"
7832
7858
  ioxDropzone
@@ -7849,14 +7875,13 @@ class BuilderLinkedContainerComponent {
7849
7875
  (onMouseLeave)="childMouseLeave.emit()">
7850
7876
  </ng-container>
7851
7877
  </a>
7852
- `, isInline: true, styles: [".linked-container-root{display:block;position:relative;box-sizing:border-box;text-decoration:none;color:inherit;cursor:pointer}.linked-container-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}.linked-container-root:hover .linked-container-placeholder,.linked-container-root.iox-drag-over .linked-container-placeholder{opacity:1}.linked-container-placeholder i{font-size:18px}:host ::ng-deep .linked-container-root>[class*=iox-outer-]{min-width:0}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
7878
+ `, isInline: true, styles: [":where(.linked-container-root){display:block;position:relative;box-sizing:border-box;text-decoration:none;color:inherit;cursor:pointer}.linked-container-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}.linked-container-root:hover .linked-container-placeholder,.linked-container-root.iox-drag-over .linked-container-placeholder{opacity:1}.linked-container-placeholder i{font-size:18px}:host ::ng-deep .linked-container-root>[class*=iox-outer-]{min-width:0}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
7853
7879
  }
7854
7880
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderLinkedContainerComponent, decorators: [{
7855
7881
  type: Component,
7856
7882
  args: [{ selector: 'app-builder-linked-container', template: `
7857
7883
  <a [href]="resolvedHref"
7858
7884
  [target]="target"
7859
- [ngStyle]="style"
7860
7885
  [ngClass]="['iox-node-' + nodeId, listOrientation === 'horizontal' ? 'is-horizontal' : '']"
7861
7886
  class="linked-container-root"
7862
7887
  ioxDropzone
@@ -7879,7 +7904,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
7879
7904
  (onMouseLeave)="childMouseLeave.emit()">
7880
7905
  </ng-container>
7881
7906
  </a>
7882
- `, standalone: false, styles: [".linked-container-root{display:block;position:relative;box-sizing:border-box;text-decoration:none;color:inherit;cursor:pointer}.linked-container-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}.linked-container-root:hover .linked-container-placeholder,.linked-container-root.iox-drag-over .linked-container-placeholder{opacity:1}.linked-container-placeholder i{font-size:18px}:host ::ng-deep .linked-container-root>[class*=iox-outer-]{min-width:0}\n"] }]
7907
+ `, standalone: false, styles: [":where(.linked-container-root){display:block;position:relative;box-sizing:border-box;text-decoration:none;color:inherit;cursor:pointer}.linked-container-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}.linked-container-root:hover .linked-container-placeholder,.linked-container-root.iox-drag-over .linked-container-placeholder{opacity:1}.linked-container-placeholder i{font-size:18px}:host ::ng-deep .linked-container-root>[class*=iox-outer-]{min-width:0}\n"] }]
7883
7908
  }], ctorParameters: () => [{ type: DragEngineService }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: undefined, decorators: [{
7884
7909
  type: Optional
7885
7910
  }, {
@@ -8548,7 +8573,6 @@ class BuilderDialogComponent {
8548
8573
  this.overlayService = overlayService;
8549
8574
  this.cdr = cdr;
8550
8575
  this.children = [];
8551
- this.style = {};
8552
8576
  this.nodeId = '';
8553
8577
  this.dropListId = '';
8554
8578
  this.closeOnBackdrop = true;
@@ -8619,11 +8643,10 @@ class BuilderDialogComponent {
8619
8643
  this.dragEngine.handleDrop(this.children, event, this.dropListId, this.cdr);
8620
8644
  }
8621
8645
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderDialogComponent, deps: [{ token: DragEngineService }, { token: i1.ComponentInstanceRegistryService }, { token: OverlayService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
8622
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: BuilderDialogComponent, isStandalone: false, selector: "app-builder-dialog", inputs: { children: "children", style: "style", nodeId: "nodeId", dropListId: "dropListId", closeOnBackdrop: "closeOnBackdrop", showCloseButton: "showCloseButton", placement: "placement" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
8646
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: BuilderDialogComponent, isStandalone: false, selector: "app-builder-dialog", inputs: { children: "children", nodeId: "nodeId", dropListId: "dropListId", closeOnBackdrop: "closeOnBackdrop", showCloseButton: "showCloseButton", placement: "placement" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
8623
8647
  <div *ngIf="designOpen" class="dialog-stage" [attr.data-placement]="placementValue"
8624
8648
  (click)="onStageClick($event)">
8625
- <div [ngStyle]="style"
8626
- [ngClass]="'iox-node-' + nodeId"
8649
+ <div [ngClass]="['iox-node-' + nodeId, 'iox-outer-' + nodeId]"
8627
8650
  class="dialog-surface"
8628
8651
  ioxDropzone
8629
8652
  [ioxDropzoneId]="dropListId"
@@ -8652,15 +8675,14 @@ class BuilderDialogComponent {
8652
8675
  </ng-container>
8653
8676
  </div>
8654
8677
  </div>
8655
- `, isInline: true, styles: [".dialog-stage{position:fixed;inset:0;z-index:50;display:grid;place-items:center;padding:24px;box-sizing:border-box;background:var(--iox-scrim, rgba(15, 23, 42, .45))}.dialog-stage[data-placement=top]{align-items:start}.dialog-stage[data-placement=bottom]{align-items:end}.dialog-stage[data-placement=start]{justify-items:start}.dialog-stage[data-placement=end]{justify-items:end}.dialog-surface{box-sizing:border-box;max-width:100%;max-height:100%;overflow:auto;contain:layout}.dialog-close{position:absolute;inset-inline-end:10px;top:10px;width:26px;height:26px;border:none;border-radius:var(--iox-radius-pill, 999px);background:var(--iox-dialog-close-bg, rgba(15, 23, 42, .06));color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.dialog-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:90px;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px}.dialog-placeholder i{font-size:18px}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
8678
+ `, isInline: true, styles: [".dialog-stage{position:fixed;inset:0;z-index:50;display:grid;place-items:center;padding:24px;box-sizing:border-box;background:#0f172a73}:host(.iox-es) .dialog-stage{background:var(--iox-scrim)}.dialog-stage[data-placement=top]{align-items:start}.dialog-stage[data-placement=bottom]{align-items:end}.dialog-stage[data-placement=start]{justify-items:start}.dialog-stage[data-placement=end]{justify-items:end}:where(.dialog-surface){box-sizing:border-box;max-width:100%;max-height:100%;overflow:auto;contain:layout}.dialog-close{position:absolute;inset-inline-end:10px;top:10px;width:26px;height:26px;border:none;border-radius:999px;background:#0f172a0f;color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}:host(.iox-es) .dialog-close{border-radius:var(--iox-radius-pill);background:var(--iox-surface-2);color:var(--iox-text)}.dialog-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:90px;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px}.dialog-placeholder i{font-size:18px}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
8656
8679
  }
8657
8680
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderDialogComponent, decorators: [{
8658
8681
  type: Component,
8659
8682
  args: [{ selector: 'app-builder-dialog', template: `
8660
8683
  <div *ngIf="designOpen" class="dialog-stage" [attr.data-placement]="placementValue"
8661
8684
  (click)="onStageClick($event)">
8662
- <div [ngStyle]="style"
8663
- [ngClass]="'iox-node-' + nodeId"
8685
+ <div [ngClass]="['iox-node-' + nodeId, 'iox-outer-' + nodeId]"
8664
8686
  class="dialog-surface"
8665
8687
  ioxDropzone
8666
8688
  [ioxDropzoneId]="dropListId"
@@ -8689,11 +8711,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
8689
8711
  </ng-container>
8690
8712
  </div>
8691
8713
  </div>
8692
- `, standalone: false, styles: [".dialog-stage{position:fixed;inset:0;z-index:50;display:grid;place-items:center;padding:24px;box-sizing:border-box;background:var(--iox-scrim, rgba(15, 23, 42, .45))}.dialog-stage[data-placement=top]{align-items:start}.dialog-stage[data-placement=bottom]{align-items:end}.dialog-stage[data-placement=start]{justify-items:start}.dialog-stage[data-placement=end]{justify-items:end}.dialog-surface{box-sizing:border-box;max-width:100%;max-height:100%;overflow:auto;contain:layout}.dialog-close{position:absolute;inset-inline-end:10px;top:10px;width:26px;height:26px;border:none;border-radius:var(--iox-radius-pill, 999px);background:var(--iox-dialog-close-bg, rgba(15, 23, 42, .06));color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.dialog-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:90px;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px}.dialog-placeholder i{font-size:18px}\n"] }]
8714
+ `, standalone: false, styles: [".dialog-stage{position:fixed;inset:0;z-index:50;display:grid;place-items:center;padding:24px;box-sizing:border-box;background:#0f172a73}:host(.iox-es) .dialog-stage{background:var(--iox-scrim)}.dialog-stage[data-placement=top]{align-items:start}.dialog-stage[data-placement=bottom]{align-items:end}.dialog-stage[data-placement=start]{justify-items:start}.dialog-stage[data-placement=end]{justify-items:end}:where(.dialog-surface){box-sizing:border-box;max-width:100%;max-height:100%;overflow:auto;contain:layout}.dialog-close{position:absolute;inset-inline-end:10px;top:10px;width:26px;height:26px;border:none;border-radius:999px;background:#0f172a0f;color:inherit;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}:host(.iox-es) .dialog-close{border-radius:var(--iox-radius-pill);background:var(--iox-surface-2);color:var(--iox-text)}.dialog-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:90px;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px}.dialog-placeholder i{font-size:18px}\n"] }]
8693
8715
  }], ctorParameters: () => [{ type: DragEngineService }, { type: i1.ComponentInstanceRegistryService }, { type: OverlayService }, { type: i0.ChangeDetectorRef }], propDecorators: { children: [{
8694
8716
  type: Input
8695
- }], style: [{
8696
- type: Input
8697
8717
  }], nodeId: [{
8698
8718
  type: Input
8699
8719
  }], dropListId: [{
@@ -9512,24 +9532,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
9512
9532
  }] } });
9513
9533
 
9514
9534
  class SliderSlideComponent {
9515
- set style(v) {
9516
- if (v === this._style)
9517
- return;
9518
- this._style = v;
9519
- // Strip width constraints; rebuild cached object so ngStyle only updates DOM
9520
- // when the input actually changed — prevents spurious MutationObserver triggers.
9521
- const { width, minWidth, maxWidth, ...rest } = v || {};
9522
- this._slideEditStyle = rest;
9523
- }
9524
- get style() { return this._style; }
9525
- get slideEditStyle() { return this._slideEditStyle; }
9526
9535
  constructor(dragEngine, cdr, isPreview) {
9527
9536
  this.dragEngine = dragEngine;
9528
9537
  this.cdr = cdr;
9529
9538
  this.isPreview = isPreview;
9530
9539
  this.children = [];
9531
- this._style = {};
9532
- this._slideEditStyle = {};
9533
9540
  this.nodeId = '';
9534
9541
  this.dropListId = '';
9535
9542
  this.childClick = new EventEmitter();
@@ -9543,18 +9550,15 @@ class SliderSlideComponent {
9543
9550
  this.dragEngine.handleDrop(this.children, event, this.dropListId, this.cdr);
9544
9551
  }
9545
9552
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SliderSlideComponent, deps: [{ token: DragEngineService }, { token: i0.ChangeDetectorRef }, { token: IS_PREVIEW, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
9546
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: SliderSlideComponent, isStandalone: false, selector: "app-slider-slide", inputs: { children: "children", style: "style", nodeId: "nodeId", dropListId: "dropListId" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
9553
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: SliderSlideComponent, isStandalone: false, selector: "app-slider-slide", inputs: { children: "children", nodeId: "nodeId", dropListId: "dropListId" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
9547
9554
  <!-- The .swiper-slide wrapper (with the iox-outer-* class) is provided by SliderContainer —
9548
9555
  Swiper needs it as a direct child of .swiper-wrapper — and the SLIDE'S size lands there
9549
- via StyleRegistryService. This is only the styled INNER block, so width/min-width/
9550
- max-width MUST be stripped (slideEditStyle): the slide is already sized on the outer
9551
- wrapper, and re-applying width here would size the content to 71% OF the 71% slide (the
9552
- engine's iox-slider-slide is a single host, so it never had this trap). Preview and edit
9553
- behave identically — the earlier preview branch used the un-stripped style input and
9554
- shrank the content inside its own slide. -->
9556
+ via StyleRegistryService (width is an outer prop). This is only the styled INNER block:
9557
+ its look (aspect-ratio, background, radius, the media style's hover scale) comes from
9558
+ the node's .iox-node-{id} class rule ONLY, like every other component — an inline
9559
+ base map beat the rule's :hover (DS2 verification). -->
9555
9560
  <div class="slide-edit-block"
9556
9561
  [ngClass]="['iox-node-' + nodeId]"
9557
- [ngStyle]="slideEditStyle"
9558
9562
  ioxDropzone
9559
9563
  [ioxDropzoneId]="dropListId"
9560
9564
  [ioxDropzoneData]="children"
@@ -9578,22 +9582,19 @@ class SliderSlideComponent {
9578
9582
  </ng-container>
9579
9583
  </div>
9580
9584
  </div>
9581
- `, isInline: true, styles: [".slide-edit-block{position:relative;box-sizing:border-box;display:flex;align-items:center;justify-content:center;overflow:hidden;background-color:var(--p-surface-300)}.slide-edit-block{width:100%}.slide-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}:host:hover .slide-placeholder,:host.iox-drag-over .slide-placeholder{opacity:1}.slide-placeholder i{font-size:22px}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId", "ioxDragDisabled"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
9585
+ `, isInline: true, styles: [":where(.slide-edit-block){position:relative;box-sizing:border-box;display:flex;align-items:center;justify-content:center;overflow:hidden;width:100%;background-color:var(--p-surface-300)}.slide-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}:host:hover .slide-placeholder,:host.iox-drag-over .slide-placeholder{opacity:1}.slide-placeholder i{font-size:22px}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId", "ioxDragDisabled"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
9582
9586
  }
9583
9587
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SliderSlideComponent, decorators: [{
9584
9588
  type: Component,
9585
9589
  args: [{ selector: 'app-slider-slide', template: `
9586
9590
  <!-- The .swiper-slide wrapper (with the iox-outer-* class) is provided by SliderContainer —
9587
9591
  Swiper needs it as a direct child of .swiper-wrapper — and the SLIDE'S size lands there
9588
- via StyleRegistryService. This is only the styled INNER block, so width/min-width/
9589
- max-width MUST be stripped (slideEditStyle): the slide is already sized on the outer
9590
- wrapper, and re-applying width here would size the content to 71% OF the 71% slide (the
9591
- engine's iox-slider-slide is a single host, so it never had this trap). Preview and edit
9592
- behave identically — the earlier preview branch used the un-stripped style input and
9593
- shrank the content inside its own slide. -->
9592
+ via StyleRegistryService (width is an outer prop). This is only the styled INNER block:
9593
+ its look (aspect-ratio, background, radius, the media style's hover scale) comes from
9594
+ the node's .iox-node-{id} class rule ONLY, like every other component — an inline
9595
+ base map beat the rule's :hover (DS2 verification). -->
9594
9596
  <div class="slide-edit-block"
9595
9597
  [ngClass]="['iox-node-' + nodeId]"
9596
- [ngStyle]="slideEditStyle"
9597
9598
  ioxDropzone
9598
9599
  [ioxDropzoneId]="dropListId"
9599
9600
  [ioxDropzoneData]="children"
@@ -9617,7 +9618,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
9617
9618
  </ng-container>
9618
9619
  </div>
9619
9620
  </div>
9620
- `, standalone: false, styles: [".slide-edit-block{position:relative;box-sizing:border-box;display:flex;align-items:center;justify-content:center;overflow:hidden;background-color:var(--p-surface-300)}.slide-edit-block{width:100%}.slide-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}:host:hover .slide-placeholder,:host.iox-drag-over .slide-placeholder{opacity:1}.slide-placeholder i{font-size:22px}\n"] }]
9621
+ `, standalone: false, styles: [":where(.slide-edit-block){position:relative;box-sizing:border-box;display:flex;align-items:center;justify-content:center;overflow:hidden;width:100%;background-color:var(--p-surface-300)}.slide-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}:host:hover .slide-placeholder,:host.iox-drag-over .slide-placeholder{opacity:1}.slide-placeholder i{font-size:22px}\n"] }]
9621
9622
  }], ctorParameters: () => [{ type: DragEngineService }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
9622
9623
  type: Optional
9623
9624
  }, {
@@ -9625,8 +9626,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
9625
9626
  args: [IS_PREVIEW]
9626
9627
  }] }], propDecorators: { children: [{
9627
9628
  type: Input
9628
- }], style: [{
9629
- type: Input
9630
9629
  }], nodeId: [{
9631
9630
  type: Input
9632
9631
  }], dropListId: [{