@vectoriox/iox-builder 11.11.0 → 11.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ import { TooltipModule } from 'primeng/tooltip';
17
17
  import * as i20 from 'primeng/dialog';
18
18
  import { DialogModule } from 'primeng/dialog';
19
19
  import * as i1$1 from '@vectoriox/iox-ui';
20
- import { nodeCssId, needsGroupName, resolveBoundValue, VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, TEXT_DIRECTION_KEY, TEXT_ALIGN_KEY, TextAlign, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, LinkType, MAX_LINES_KEY, A11yTrait, BREAKPOINTS, Breakpoint, withSchemeInk, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, targetedStateSelector, stateSelectorSuffix, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, isLastEmittedStateKey, wrapInBreakpointQuery, BreakpointQueryKind, NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, applyElementStyle, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, ContextStore, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, ComponentInstanceRegistryService, entranceStartsHidden, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, effectiveActionType, INTERACTION_HIDDEN_ATTRIBUTE, motionAllowed, finalKeyframe, animateAction, entranceDelay, repeatedRowIndex, keyframeStyleEntries, entrancePreStateStyle, buildInteractionKeyframes, cascadeChain, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, nodeTraitValue, syncNodeA11yAttributes, nodeA11yAttributes, safeSetInput, colorSchemeOf, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, switchElementStyle, releaseElementStyle, countOverrides, setOverride, breakpointDef, breakpointForWidth, FormRenderState, needsAccessibleName, resolveInputs, setInputOverride, barReserveClass, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, IS_PREVIEW, buttonBlockHref, bootSlider, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
20
+ import { nodeCssId, needsGroupName, resolveBoundValue, STRUCTURAL_STATES as STRUCTURAL_STATES$1, VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, TEXT_DIRECTION_KEY, TEXT_ALIGN_KEY, TextAlign, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, LinkType, MAX_LINES_KEY, A11yTrait, BREAKPOINTS, Breakpoint, withSchemeInk, stripSuppressedProps, DeclarationContext, planPartRule, gateHoverRule, planResponsiveRules, resolveStyleProps, planStateRule, splitOuterProps, OUTER_STYLE_PROPS, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, isLastEmittedStateKey, wrapInBreakpointQuery, BreakpointQueryKind, NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, applyElementStyle, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, ContextStore, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, ComponentInstanceRegistryService, entranceStartsHidden, isActionEnabled, splitPressFeedback, isHoverPointer, isContextAction, contextActionEffect, resolveContextValue, effectiveActionType, INTERACTION_HIDDEN_ATTRIBUTE, motionAllowed, settleInteraction, settleModeFor, playInteraction, resolveInteractionEasing, entranceDelay, repeatedRowIndex, entrancePreStateStyle, buildInteractionKeyframes, cascadeChain, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, nodeTraitValue, syncNodeA11yAttributes, nodeA11yAttributes, safeSetInput, colorSchemeOf, withStateTransitions, composeVirtualTraits, stateNamesIn, partClassName, switchElementStyle, releaseElementStyle, countOverrides, setOverride, breakpointDef, breakpointForWidth, FormRenderState, needsAccessibleName, resolveInputs, setInputOverride, barReserveClass, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, isScrolled, SCROLLED_ATTRIBUTE, syncSubmitButtons, IS_PREVIEW, buttonBlockHref, bootSlider, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
21
21
  export { ComponentInstanceRegistryService, VIRTUAL_TRAIT_KEYS, composeVirtualTraits } from '@vectoriox/iox-ui';
22
22
  import { StructureEdit, isAccordionPart, isDisclosurePart, isSliderPart, isTabsPart, isSegmentedPart, structureRefusal, StructureRefusal, SegmentedRole, SEGMENTED_MAX_OPTIONS, AccordionRole, SEGMENTED_MIN_OPTIONS, isExclusiveItemPart, TabsRole, ITEM_OPEN_STATE, readStyle, isSegmentedRoot, segmentedDefaultOn, segmentedOptions, isTabsRoot, isTabsFromContent, tabsTriggers, isAccordionRoot, isAccordionFromContent, accordionItems, initialAccordionOpen, accordionBehaviour, exclusiveItemLabel, exclusiveRootOf, tabTriggerOf, tabLabel, removeTabPair, tabPanelOf, tabsListOf, tabsPanelsOf, nextTabKey, CompositeBehaviourTrait, ACCORDION_DEFAULT_ONS, TabsBehaviourTrait, TABS_ACTIVATE_ONS, TABS_ORIENTATIONS, onCompositeItemState, isCompositeItemOn, runCompositeItemAction, shouldScrollToItem, rowIndexOf as rowIndexOf$1, slideIndexForRow, DisclosureRole, isExclusiveTrigger, compositeScopeId, compositeRowKeys, isExclusiveRoot, ExclusiveRuntime, exclusiveDefaultOf, exclusiveLabelOf, exclusiveBehaviourOf, exclusiveKindOf, compositeKindOfRole, AccordionRuntime, isToggleKind, closestAccordionItem, isAnyCompositeRoot, SceneRole, isExclusiveList, buildCardRecipe, buildBadge, buildChip, buildAccordionRecipe, buildDisclosureRecipe, buildTabsRecipe, buildSegmentedRecipe, buildLightboxRecipe, NO_ELEMENT_STYLE, CompositeKind, SliderRole, accordionItemLabel, segmentedOptionLabel, isExclusivePanels, isLockedPart, accordionRootOf, accordionItemOf, addAccordionItem, addTabPair, segmentedAddRefusal, addSegmentedOption, isCompositeItem, accordionHeadingLevel, isCompositePanel, isCompositeTrigger, splitSliderChildren, hasSliderParts } from '@vectoriox/iox-ui/composite-core';
23
23
  import { formContentInHost, isInteractiveHost, findFormContentInHosts } from '@vectoriox/iox-ui/forms-core';
@@ -1346,11 +1346,13 @@ const ACTION_TYPE_OPTIONS = [
1346
1346
  ];
1347
1347
  /** Dialog actions, for the engines' dispatch branch (mirror of SWIPER's set). */
1348
1348
  const DIALOG_ACTION_TYPES = new Set(['openDialog', 'closeDialog', 'toggleDialog']);
1349
+ /** The keyword easings — the panel lists the look's easing tokens (iox-ui `EASING_TOKENS`) and a custom curve beside them. */
1349
1350
  const EASING_OPTIONS = ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'];
1350
1351
  /** Interaction pseudo-classes — compiled to .iox-node-{id}:state */
1351
1352
  const INTERACTION_STATES = ['hover', 'active', 'focus'];
1352
- /** Structural pseudo-classes — compiled to .iox-outer-{id}:state */
1353
- const STRUCTURAL_STATES = ['first-child', 'last-child', 'nth-child(odd)', 'nth-child(even)'];
1353
+ /** Structural pseudo-classes — matched on the outer element by the shared `planStateRule` (@vectoriox/iox-ui), the
1354
+ * engine's planner too (iox-client-engine#14). */
1355
+ const STRUCTURAL_STATES = [...STRUCTURAL_STATES$1];
1354
1356
  /**
1355
1357
  * Relational pseudo-classes — the styled element is a descendant, the pseudo-class
1356
1358
  * lives on an ANCESTOR. Compiled to `.iox-node-{parentId}:{base} .iox-node-{id} { … }`,
@@ -1397,7 +1399,7 @@ const VALIDATION_STATE_CLASS = VALIDATION_STATE_CLASS$1;
1397
1399
  * (`stateSelectorSuffix` in @vectoriox/iox-ui), never a pseudo-class. Offered per type by
1398
1400
  * `componentStatesFor`.
1399
1401
  */
1400
- const COMPONENT_STATES = ['current', 'on'];
1402
+ const COMPONENT_STATES = ['current', 'on', 'disabled', 'busy', 'selected', 'scrolled'];
1401
1403
  const SUPPORTED_STATES = [...INTERACTION_STATES, ...STRUCTURAL_STATES, ...RELATIONAL_STATES, ...TARGETED_STATES, ...VALIDATION_STATES, ...COMPONENT_STATES];
1402
1404
  /**
1403
1405
  * Build a full StyleTraitGroup[] schema populated with values from a flat
@@ -1827,6 +1829,19 @@ class EffectsGroupStyleConfig extends GroupStyleConfig {
1827
1829
  new TraitConfig('rotate', 'Rotate', TraitInputType.Scrub, { min: -360, max: 360, step: 1, units: UNITS_DEG }, '0deg', true),
1828
1830
  new TraitConfig('skewX', 'Skew X', TraitInputType.Scrub, { min: -180, max: 180, step: 1, units: UNITS_DEG }, '0deg', true),
1829
1831
  new TraitConfig('skewY', 'Skew Y', TraitInputType.Scrub, { min: -180, max: 180, step: 1, units: UNITS_DEG }, '0deg', true),
1832
+ // G3: where the element scales and turns FROM — a popover grows from its trigger (Emil 47). Physical
1833
+ // keywords: transform-origin has no logical ones. The default is the CSS initial (never emitted).
1834
+ new TraitConfig('transformOrigin', 'Origin', TraitInputType.Select, [
1835
+ { label: 'Center', value: 'center' },
1836
+ { label: 'Top', value: 'top' },
1837
+ { label: 'Bottom', value: 'bottom' },
1838
+ { label: 'Left', value: 'left' },
1839
+ { label: 'Right', value: 'right' },
1840
+ { label: 'Top left', value: 'top left' },
1841
+ { label: 'Top right', value: 'top right' },
1842
+ { label: 'Bottom left', value: 'bottom left' },
1843
+ { label: 'Bottom right', value: 'bottom right' },
1844
+ ], 'center'),
1830
1845
  // ── Filter (virtual — composed to CSS filter: …) ──────
1831
1846
  new TraitConfig('_sep_filter', 'Filter', TraitInputType.SectionLabel),
1832
1847
  new TraitConfig('filterBlur', 'Blur', TraitInputType.Scrub, { min: 0, max: 50, step: 1, units: ['px'] }, '0px', true),
@@ -1859,8 +1874,10 @@ class EffectsGroupStyleConfig extends GroupStyleConfig {
1859
1874
  { label: 'Hexagon', value: 'polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)' },
1860
1875
  { label: 'Chevron', value: 'polygon(0 0, 75% 0, 100% 50%, 75% 100%, 0 100%, 25% 50%)' },
1861
1876
  ], 'none'),
1862
- // ── Transition (virtual — composed to CSS transition: all …) ──
1877
+ // ── Transition (virtual — composed to one CSS transition entry per property, never `all`: G2) ──
1863
1878
  new TraitConfig('_sep_transition', 'Transition', TraitInputType.SectionLabel),
1879
+ // Empty: the properties the element's states change (the renderers fill them in); a list wins.
1880
+ new TraitConfig('transitionProperty', 'Properties', TraitInputType.Text, undefined, ''),
1864
1881
  new TraitConfig('transitionDuration', 'Duration', TraitInputType.Scrub, { min: 0, max: 3000, step: 50, units: ['ms'] }, '0ms', true),
1865
1882
  new TraitConfig('transitionDelay', 'Delay', TraitInputType.Scrub, { min: 0, max: 2000, step: 50, units: ['ms'] }, '0ms', true),
1866
1883
  new TraitConfig('transitionTimingFunction', 'Easing', TraitInputType.Select, [
@@ -2145,26 +2162,6 @@ class StyleRegistryService {
2145
2162
  this.styleEl = null;
2146
2163
  this._flushPending = false;
2147
2164
  }
2148
- /**
2149
- * Properties that must live on the OUTER wrapper (.iox-outer-{id}) because they
2150
- * affect how the element participates in its PARENT layout (flex/block flow).
2151
- * Everything else goes to the INNER element (.iox-node-{id}).
2152
- *
2153
- * Exception — out-of-flow positioning (position:fixed/absolute):
2154
- * When position is fixed or absolute, width is redirected to the inner element
2155
- * (the positioned one) and a default width:100% is added if none is set, so the
2156
- * element stays visible. top/right/bottom/left/zIndex are always inner because
2157
- * they are co-ordinates of the positioned element, not the flow wrapper.
2158
- * position itself goes to inner with `!important` to beat component-scoped CSS.
2159
- */
2160
- static { this.OUTER_PROPS = new Set([
2161
- 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
2162
- 'alignSelf', 'flexGrow', 'flexShrink', 'flexBasis', 'order',
2163
- // Grid-child placement — like the flex-child props above, these position the node
2164
- // within the PARENT's grid, so they must land on the outer host, not the inner div.
2165
- 'gridColumn', 'gridRow', 'justifySelf',
2166
- 'width',
2167
- ]); }
2168
2165
  /**
2169
2166
  * Properties that need !important on the inner element to beat Angular's scoped-CSS
2170
2167
  * specificity boost. CANVAS-ONLY (H2 audit, 2026-10-11): the shared compiler never forces
@@ -2221,8 +2218,7 @@ class StyleRegistryService {
2221
2218
  // breakpoint is scoped identically without re-resolving it (see upsertResponsive).
2222
2219
  if (relativeToId || triggerId)
2223
2220
  this.stateScopes.set(key, { relativeToId, triggerId });
2224
- const selector = this.stateSelector(nodeId, state, relativeToId, triggerId);
2225
- const css = selector ? this.compile(selector, styles) : '';
2221
+ const css = this.stateRuleCss(nodeId, state, styles, relativeToId, triggerId, DeclarationContext.Base);
2226
2222
  if (css)
2227
2223
  this.rules.set(key, css);
2228
2224
  else
@@ -2256,9 +2252,10 @@ class StyleRegistryService {
2256
2252
  if (!nodeId || !partClassName)
2257
2253
  return;
2258
2254
  const key = `${nodeId}:part:${partClassName}${state ? ':' + state : ''}`;
2259
- const validationClass = state ? VALIDATION_STATE_CLASS[state] : undefined;
2260
- const selector = buildPartSelector(nodeId, partClassName, validationClass);
2261
- const css = Object.keys(styles).length ? this.compile(selector, styles) : '';
2255
+ // The shared planner the engine uses (iox-client-engine#14): a part's hover / focus is the PART's own pseudo-class
2256
+ // — before, any non-validation state compiled to the bare part selector and applied at all times.
2257
+ const selector = planPartRule(nodeId, partClassName, state);
2258
+ const css = selector && Object.keys(styles).length ? gateHoverRule(state, this.compileSelector(selector, styles)) : '';
2262
2259
  if (css)
2263
2260
  this.rules.set(key, css);
2264
2261
  else
@@ -2304,10 +2301,9 @@ class StyleRegistryService {
2304
2301
  this.setResponsiveRule(nodeId, bp, `${nodeId}::marker`, marker);
2305
2302
  for (const [state, map] of Object.entries(layer.stateStyles)) {
2306
2303
  const scope = this.stateScopes.get(`${nodeId}:${state}`);
2307
- const selector = this.stateSelector(nodeId, state, scope?.relativeToId, scope?.triggerId);
2308
2304
  // A relational/targeted state whose scope isn't known yet is skipped; it is re-emitted
2309
2305
  // when the base state rule arrives with its scope (see upsert).
2310
- const css = selector ? this.compile(selector, this.ownProps(nodeId, map), DeclarationContext.Override) : '';
2306
+ const css = this.stateRuleCss(nodeId, state, this.ownProps(nodeId, map), scope?.relativeToId, scope?.triggerId, DeclarationContext.Override);
2311
2307
  if (css)
2312
2308
  this.setResponsiveRule(nodeId, bp, `${nodeId}:${state}`, css);
2313
2309
  }
@@ -2333,37 +2329,21 @@ class StyleRegistryService {
2333
2329
  return pos === 'fixed' || pos === 'absolute';
2334
2330
  }
2335
2331
  /**
2336
- * The selector BODY (no leading dot) for a state rule, or null when a relational / targeted
2337
- * state has no scope yet (emitting it unscoped would be wrong — a page-global `:has()`).
2332
+ * A state rule's CSS through the SHARED planner the engine emits with (`planStateRule`, iox-client-engine#14), or ''
2333
+ * when a relational / targeted state has no scope yet (emitting it unscoped would be wrong — a page-global `:has()`).
2334
+ * `relativeToId` is the parent for a relational state (parent-hover) and the nearest common ancestor of the node and
2335
+ * its trigger for a targeted one (trigger-hover, trigger-on). A structural state is matched on the outer element and
2336
+ * reaches the node's own; its outer props stay outer.
2338
2337
  */
2339
- stateSelector(nodeId, state, relativeToId, triggerId) {
2340
- // Targeted states (trigger-hover): the styled node restyles when an ARBITRARY chosen
2341
- // element is hovered. The selector is `:has()`-scoped to `relativeToId` (the nearest
2342
- // common ancestor of styled + trigger) so it stays repetition-safe with one shared rule.
2343
- // `trigger-on` (CC2 "Item open"): the same scoping, keyed on the item's `[data-iox-on]` — the ONE
2344
- // shared `targetedStateSelector` the engine uses too.
2345
- if (TARGETED_STATES.includes(state)) {
2346
- return relativeToId && triggerId ? targetedStateSelector(state, relativeToId, triggerId, nodeId) : null;
2347
- }
2348
- // Relational states (parent-hover): the pseudo-class lives on an ANCESTOR
2349
- // and cascades to this element. Requires the ancestor's css id.
2350
- if (RELATIONAL_STATES.includes(state)) {
2351
- const base = RELATIONAL_STATE_BASE[state];
2352
- return relativeToId && base ? `iox-node-${relativeToId}:${base} .iox-node-${nodeId}` : null;
2353
- }
2354
- // Validation states (wrong/accepted, board 7) are not real CSS pseudo-classes — `:wrong`
2355
- // doesn't exist. Compound a CLASS instead, matching the host class IoxFieldComponent
2356
- // already sets from forceInvalid/showValid. Reuses buildPartSelector with no part, since
2357
- // "whole node, when wrong" is exactly that function's no-part case.
2358
- const validationClass = VALIDATION_STATE_CLASS[state];
2359
- if (validationClass)
2360
- return buildPartSelector(nodeId, undefined, validationClass);
2361
- // Structural pseudo-classes (first-child, last-child, nth-child) target the
2362
- // outer wrapper element so they participate correctly in grid/flex layout.
2363
- const prefix = STRUCTURAL_STATES.includes(state) ? 'iox-outer' : 'iox-node';
2364
- // `focus` compiles to :focus-visible (DS2) and a component state to its attribute (`current` →
2365
- // [aria-current="page"], CC1) — shared with the engine via stateSelectorSuffix.
2366
- return `${prefix}-${nodeId}${stateSelectorSuffix(state)}`;
2338
+ stateRuleCss(nodeId, state, styles, relativeToId, triggerId, context) {
2339
+ const targeted = TARGETED_STATES.includes(state);
2340
+ const plan = planStateRule(nodeId, state, targeted ? { scopeId: relativeToId, triggerId } : { parentId: relativeToId });
2341
+ if (!plan)
2342
+ return '';
2343
+ const { inner, outer } = plan.outer ? splitOuterProps(styles) : { inner: styles, outer: {} };
2344
+ // A hover state's rule only where there is a hover (G1) — the engine gates the same rules the same way.
2345
+ return gateHoverRule(state, [this.compileSelector(plan.inner, inner, context), plan.outer ? this.compileSelector(plan.outer, outer, context) : '']
2346
+ .filter(Boolean).join('\n'));
2367
2347
  }
2368
2348
  /**
2369
2349
  * The inner + outer rule (joined) and the `::marker` rule for one composed style map.
@@ -2380,7 +2360,7 @@ class StyleRegistryService {
2380
2360
  for (const [k, v] of Object.entries(styles)) {
2381
2361
  if (k === '__markerStyles')
2382
2362
  continue;
2383
- const wantsOuter = StyleRegistryService.OUTER_PROPS.has(k);
2363
+ const wantsOuter = OUTER_STYLE_PROPS.has(k);
2384
2364
  // width goes to inner for fixed/absolute (the positioned element owns its own width)
2385
2365
  if (wantsOuter && !(isOutOfFlow && k === 'width')) {
2386
2366
  outer[k] = v;
@@ -2526,14 +2506,16 @@ class StyleRegistryService {
2526
2506
  }
2527
2507
  }
2528
2508
  compile(className, styles, context = DeclarationContext.Base) {
2529
- // Declaration compilation (kebab-case, viewport-unit rewrite, !important) lives in
2530
- // the shared style-compiler so the SSR engine produces byte-identical output.
2531
- // The inner/outer partition + selector emission stay here (builder-only concern).
2509
+ return this.compileSelector(`.${className}`, styles, context);
2510
+ }
2511
+ /** One rule for a FULL selector. Declaration compilation (kebab-case, viewport-unit rewrite, !important) lives in
2512
+ * the shared style-compiler so the SSR engine produces byte-identical output. */
2513
+ compileSelector(selector, styles, context = DeclarationContext.Base) {
2532
2514
  const decls = compileDeclarations(styles, StyleRegistryService.IMPORTANT_PROPS, context);
2533
2515
  if (!decls.length)
2534
2516
  return '';
2535
2517
  const body = decls.map(d => ` ${d};`).join('\n');
2536
- return `.${className} {\n${body}\n}`;
2518
+ return `${selector} {\n${body}\n}`;
2537
2519
  }
2538
2520
  flush() {
2539
2521
  if (!this.styleEl || this._flushPending)
@@ -4085,6 +4067,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
4085
4067
  // Re-exported from the shared iox-ui package so iox-builder consumers
4086
4068
  // don't need to import from two different packages.
4087
4069
 
4070
+ /** What the canvas never writes inline (a transform inside the scaled page breaks Lenis). */
4071
+ const CANVAS_SKIPPED_PROPS = new Set(['transform']);
4088
4072
  /**
4089
4073
  * Wires ComponentNode interactions to live DOM elements via the
4090
4074
  * OverlayService node-element registry.
@@ -4444,6 +4428,9 @@ class InteractionEngineService {
4444
4428
  requestAnimationFrame(poll);
4445
4429
  }
4446
4430
  attachClick(node, ix, element) {
4431
+ // G7: press FEEDBACK (an animation of the pressed element that hides and shows nothing) lands on pointer-down,
4432
+ // at once; the rest commits on the click. A toggle keeps its whole list on the click (the shared split, iox-ui).
4433
+ const { onPress, onClick } = ix.reverseActions?.length ? { onPress: [], onClick: ix.actions } : splitPressFeedback(ix.actions);
4447
4434
  const handler = () => {
4448
4435
  if (ix.reverseActions?.length) {
4449
4436
  // Toggle: alternate between primary and reverse actions on each click.
@@ -4452,18 +4439,29 @@ class InteractionEngineService {
4452
4439
  this.toggleStates.set(ix.id, !isToggled);
4453
4440
  }
4454
4441
  else {
4455
- this.runActions(node, ix.actions, element);
4442
+ this.runActions(node, onClick, element);
4456
4443
  }
4457
4444
  };
4445
+ const press = (e) => { if (e.button === 0 || e.pointerType !== 'mouse')
4446
+ this.runActions(node, onPress, element); };
4458
4447
  // CAPTURE, like the composite runtime: on the canvas every part's own listener stops the click to select
4459
4448
  // that part, so a click on a button's LABEL never bubbled to the button's click interaction — the live
4460
4449
  // site runs it (CC2.1: "Open an item" on a ButtonBlock did nothing on the canvas).
4461
4450
  element.addEventListener('click', handler, true);
4462
- return () => element.removeEventListener('click', handler, true);
4451
+ if (onPress.length)
4452
+ element.addEventListener('pointerdown', press, true);
4453
+ return () => {
4454
+ element.removeEventListener('click', handler, true);
4455
+ element.removeEventListener('pointerdown', press, true);
4456
+ };
4463
4457
  }
4464
4458
  attachHover(node, ix, element) {
4465
- const enterHandler = () => this.runActions(node, ix.actions, element);
4466
- const leaveHandler = () => {
4459
+ // G7: a hover trigger answers a mouse or a pen — a finger's emulated hover stuck until the next tap elsewhere.
4460
+ const enterHandler = (e) => { if (isHoverPointer(e))
4461
+ this.runActions(node, ix.actions, element); };
4462
+ const leaveHandler = (e) => {
4463
+ if (!isHoverPointer(e))
4464
+ return;
4467
4465
  // Reverse animations on leave for hover triggers
4468
4466
  for (const action of ix.actions) {
4469
4467
  if (!isActionEnabled(action))
@@ -4480,11 +4478,11 @@ class InteractionEngineService {
4480
4478
  this.reverseAction(target, action);
4481
4479
  }
4482
4480
  };
4483
- element.addEventListener('mouseenter', enterHandler);
4484
- element.addEventListener('mouseleave', leaveHandler);
4481
+ element.addEventListener('pointerenter', enterHandler);
4482
+ element.addEventListener('pointerleave', leaveHandler);
4485
4483
  return () => {
4486
- element.removeEventListener('mouseenter', enterHandler);
4487
- element.removeEventListener('mouseleave', leaveHandler);
4484
+ element.removeEventListener('pointerenter', enterHandler);
4485
+ element.removeEventListener('pointerleave', leaveHandler);
4488
4486
  };
4489
4487
  }
4490
4488
  // ── Actions ──────────────────────────────────────────────
@@ -4706,22 +4704,28 @@ class InteractionEngineService {
4706
4704
  element.removeAttribute(INTERACTION_HIDDEN_ATTRIBUTE);
4707
4705
  else if (this.EXIT_TYPES.has(type))
4708
4706
  element.setAttribute(INTERACTION_HIDDEN_ATTRIBUTE, '');
4709
- // Reduced motion (CC1 R1): no animation runs — the element takes its final frame at once.
4707
+ // Reduced motion (CC1 R1): no animation runs — the element is left as the run would leave it (the shared settle
4708
+ // rule: an entrance releases to the stylesheet, an exit holds its hidden frame).
4710
4709
  if (!motionAllowed()) {
4711
- this.applyFrame(element, finalKeyframe(keyframes));
4710
+ settleInteraction(element, keyframes, settleModeFor(type), this.settleOptions());
4712
4711
  this.settleAfter(element, type);
4713
4712
  return;
4714
4713
  }
4715
- // 'both' = backwards (holds first keyframe during delay) + forwards (holds last keyframe after end). A look
4714
+ // The ONE call the live site makes too: fill both (holds the first frame through a delay), started from the live
4715
+ // value, settled by the shared rule — an element that has entered keeps its hover and press (iox-ui#41). A look
4716
4716
  // entrance on a Repeater's template staggers its rows: each copy adds its row's share (shared `entranceDelay`).
4717
- const anim = animateAction(element, { type, params: action.params }, keyframes, {
4717
+ const anim = playInteraction(element, { type, params: action.params }, keyframes, {
4718
4718
  duration: action.duration,
4719
4719
  delay: entranceDelay(action, repeatedRowIndex(element)),
4720
- easing: action.easing,
4721
- fill: 'both',
4722
- });
4720
+ // A keyword, a curve, or the look's easing token as this element sees it (G6) — the live site's resolver.
4721
+ easing: resolveInteractionEasing(action.easing, element),
4722
+ }, this.settleOptions());
4723
4723
  anim.finished.then(() => this.settleAfter(element, type)).catch(() => { });
4724
4724
  }
4725
+ /** On the canvas a run never writes a transform inline (one inside the scaled page breaks Lenis). */
4726
+ settleOptions() {
4727
+ return this.isBuilderMode() ? { skip: CANVAS_SKIPPED_PROPS } : {};
4728
+ }
4725
4729
  /** What an element is left as once an action is done: a revealed one interactive, a hidden one inert. */
4726
4730
  settleAfter(element, type) {
4727
4731
  if (this.ENTRANCE_TYPES.has(type)) {
@@ -4738,15 +4742,6 @@ class InteractionEngineService {
4738
4742
  element.style.setProperty('pointer-events', 'none');
4739
4743
  }
4740
4744
  }
4741
- /** Write a frame inline. On the canvas never a transform: one inside the scaled page breaks Lenis. */
4742
- applyFrame(element, frame) {
4743
- const canvas = this.isBuilderMode();
4744
- for (const [prop, value] of keyframeStyleEntries(frame)) {
4745
- if (canvas && prop === 'transform')
4746
- continue;
4747
- element.style.setProperty(prop, value);
4748
- }
4749
- }
4750
4745
  /**
4751
4746
  * Freeze an element at the animation's starting frame immediately, before any
4752
4747
  * trigger fires. Called for ALL entrance-type actions regardless of trigger so
@@ -4787,20 +4782,22 @@ class InteractionEngineService {
4787
4782
  element.style.removeProperty('pointer-events');
4788
4783
  element.removeAttribute(INTERACTION_HIDDEN_ATTRIBUTE);
4789
4784
  }
4785
+ /** A hover-out: the action played back from where the element IS (never from its end frame), settled by the shared
4786
+ * rule — played back to hidden, it holds hidden; to shown, it releases. */
4790
4787
  reverseAction(element, action) {
4791
- const keyframes = this.keyframesFor(effectiveActionType(action.type, element.hasAttribute(INTERACTION_HIDDEN_ATTRIBUTE)), action);
4788
+ const type = effectiveActionType(action.type, element.hasAttribute(INTERACTION_HIDDEN_ATTRIBUTE));
4789
+ const keyframes = this.keyframesFor(type, action);
4792
4790
  if (!keyframes.length)
4793
4791
  return;
4792
+ const frames = [...keyframes].reverse();
4794
4793
  if (!motionAllowed()) {
4795
- this.applyFrame(element, keyframes[0]);
4794
+ settleInteraction(element, frames, settleModeFor(type, true), this.settleOptions());
4796
4795
  return;
4797
4796
  }
4798
- // Reverse: play the keyframes backwards
4799
- animateAction(element, action, [...keyframes].reverse(), {
4797
+ playInteraction(element, { type, params: action.params }, frames, {
4800
4798
  duration: action.duration,
4801
- easing: action.easing,
4802
- fill: 'forwards',
4803
- }, { reverse: true });
4799
+ easing: resolveInteractionEasing(action.easing, element),
4800
+ }, { ...this.settleOptions(), reverse: true });
4804
4801
  }
4805
4802
  /** The action's keyframes from the ONE shared table both runtimes animate from (composite T2). */
4806
4803
  keyframesFor(type, action) {
@@ -5713,7 +5710,9 @@ class RenderDirective {
5713
5710
  // The default hover transition (shared, marker-aware — the engine emits the same one):
5714
5711
  // without it a hovering node's base rule, re-upserted here on every render, dropped the
5715
5712
  // transition page-ui had added on load, and the canvas snapped where the site eased.
5716
- const composedBase = composeVirtualTraits(withDefaultHoverTransition(initialStyle, node.stateStyles, node.elementStyle));
5713
+ // The transition names what the node's states change (G2) — the engine composes the same, from the same call.
5714
+ const timedBase = withStateTransitions(initialStyle, node.stateStyles, node.elementStyle, node.responsive);
5715
+ const composedBase = composeVirtualTraits(timedBase);
5717
5716
  this.styleRegistry.upsert(cssId, composedBase);
5718
5717
  // Restore per-state overrides saved from a previous session.
5719
5718
  // Every state set at base OR at any breakpoint — so a breakpoint-only state (a Mobile-only
@@ -5760,7 +5759,7 @@ class RenderDirective {
5760
5759
  }
5761
5760
  }
5762
5761
  // Breakpoint layers — AFTER the state loop, so every scope resolved above is recorded.
5763
- this.styleRegistry.upsertResponsive(cssId, initialStyle, node.stateStyles, node.responsive);
5762
+ this.styleRegistry.upsertResponsive(cssId, timedBase, node.stateStyles, node.responsive);
5764
5763
  // Part styles (board 7 — "the failure design"). Simpler than the state loop above:
5765
5764
  // a part+state combo always compounds onto THIS node (no relational/targeted scope
5766
5765
  // to resolve), so every entry just needs its base plus each of its own state overrides.
@@ -6962,8 +6961,8 @@ function nodeStyleMap(node) {
6962
6961
  return base;
6963
6962
  }
6964
6963
  /**
6965
- * Re-register a node's compiled rules from its CURRENT style values — the base rule (with the default
6966
- * hover transition), its self-scoped state rules and its breakpoint layers — exactly as the
6964
+ * Re-register a node's compiled rules from its CURRENT style values — the base rule (its transition naming what its
6965
+ * states change, G2), its self-scoped state rules and its breakpoint layers — exactly as the
6967
6966
  * RenderDirective registers them at render. For edits that rewrite a node's styles from outside the Style
6968
6967
  * panel (a Card's layout preset, a reset to defaults).
6969
6968
  */
@@ -6973,13 +6972,14 @@ function upsertNodeStyles(styleRegistry, node) {
6973
6972
  return;
6974
6973
  const base = nodeStyleMap(node);
6975
6974
  styleRegistry.setNodeType(cssId, node.type, colorSchemeOf(node));
6976
- styleRegistry.upsert(cssId, composeVirtualTraits(withDefaultHoverTransition(base, node.stateStyles, node.elementStyle)));
6975
+ const timedBase = withStateTransitions(base, node.stateStyles, node.elementStyle, node.responsive);
6976
+ styleRegistry.upsert(cssId, composeVirtualTraits(timedBase));
6977
6977
  for (const [state, map] of Object.entries(node.stateStyles ?? {})) {
6978
6978
  if (DOM_SCOPED_STATES.has(state) || !map || !Object.keys(map).length)
6979
6979
  continue;
6980
6980
  styleRegistry.upsert(cssId, composeVirtualTraits(map, base), state);
6981
6981
  }
6982
- styleRegistry.upsertResponsive(cssId, base, node.stateStyles, node.responsive);
6982
+ styleRegistry.upsertResponsive(cssId, timedBase, node.stateStyles, node.responsive);
6983
6983
  }
6984
6984
  /** Push every content trait of a node to its rendered component (a Card title's link switched on). */
6985
6985
  function syncNodeInputs(ref, node) {
@@ -10007,6 +10007,15 @@ class BuilderComponent {
10007
10007
  this.globalOverlayRef?.nativeElement?.style.setProperty('--preview-vw', vwVal);
10008
10008
  this.canvasFrameHeight = Math.round(frameH);
10009
10009
  }
10010
+ /** Keep the Scrolled attribute (iox-ui SCROLLED_ATTRIBUTE) on the canvas page and the header overlay — written only
10011
+ * when it changes, so the overlay's mutation observers never wake for a scroll tick. */
10012
+ syncScrolled(offset) {
10013
+ const scrolled = isScrolled(offset);
10014
+ for (const el of [this.previewCanvas?.nativeElement, this.globalOverlayRef?.nativeElement]) {
10015
+ if (el && el.hasAttribute(SCROLLED_ATTRIBUTE) !== scrolled)
10016
+ el.toggleAttribute(SCROLLED_ATTRIBUTE, scrolled);
10017
+ }
10018
+ }
10010
10019
  initLenis() {
10011
10020
  this.destroyLenis();
10012
10021
  if (!this.previewScrollRef?.nativeElement || !this.lenisContentRef?.nativeElement)
@@ -10021,6 +10030,8 @@ class BuilderComponent {
10021
10030
  this.lenis.on('scroll', (instance) => {
10022
10031
  // A Scene's stage is held against this very scroll: a frame later, it would paint carried off its pin.
10023
10032
  this.sceneCanvas?.pinNow();
10033
+ // The Scrolled state (G4) on the canvas: the page and its header globals know the canvas has moved.
10034
+ this.syncScrolled(instance.scroll);
10024
10035
  const clientH = this.previewScrollRef.nativeElement.clientHeight;
10025
10036
  this.onPageScroll({
10026
10037
  scrollTop: instance.scroll,
@@ -11147,6 +11158,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
11147
11158
  type: Output
11148
11159
  }] } });
11149
11160
 
11161
+ /** The formClass a Form's canvas twin wears (form.config). */
11162
+ const FORM_CLASS = 'iox-form';
11150
11163
  const ACCORDION_TRACK_WRAPPER = 'track';
11151
11164
  /**
11152
11165
  * Directive-host Container (Phase 2 of the Moveable migration = task 9c). The real tag (div/section/
@@ -11157,6 +11170,14 @@ const ACCORDION_TRACK_WRAPPER = 'track';
11157
11170
  * See moveable-migration.md.
11158
11171
  */
11159
11172
  class BuilderContainerComponent {
11173
+ /** Which state the canvas is showing. Only meaningful on a form host: its submit buttons follow it (busy, disabled
11174
+ * while Sending — the ONE `syncSubmitButtons` both live paths call, iox-ui#42); "While sending" is the shared CSS's. */
11175
+ set formState(state) {
11176
+ this._formState = state;
11177
+ if (this.formClass === FORM_CLASS)
11178
+ syncSubmitButtons(this.elRef?.nativeElement, state);
11179
+ }
11180
+ get formState() { return this._formState; }
11160
11181
  /** The wrapper a child renders in: an Accordion item's header → its heading level; an item's panel (either
11161
11182
  * kind) → the track. A disclosure's toggle has no heading — it is a button in the flow. */
11162
11183
  wrapperOf(child) {
@@ -11186,8 +11207,7 @@ class BuilderContainerComponent {
11186
11207
  * stylesheet treat them exactly as it treats the engine's own twins.
11187
11208
  */
11188
11209
  this.formClass = '';
11189
- /** Which state the canvas is showing. Only meaningful on a form host. */
11190
- this.formState = 'idle';
11210
+ this._formState = 'idle';
11191
11211
  this.childClick = new EventEmitter();
11192
11212
  this.childMouseEnter = new EventEmitter();
11193
11213
  this.childMouseLeave = new EventEmitter();