@vectoriox/iox-builder 11.10.3 → 11.11.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.
@@ -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, 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, isContextAction, contextActionEffect, resolveContextValue, effectiveActionType, INTERACTION_HIDDEN_ATTRIBUTE, motionAllowed, settleInteraction, settleModeFor, playInteraction, entranceDelay, repeatedRowIndex, 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, 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';
@@ -32,7 +32,7 @@ import Lenis from 'lenis';
32
32
  import Moveable from 'moveable';
33
33
  import Selecto from 'selecto';
34
34
  import * as i18 from 'primeng/api';
35
- import { planVideo, VideoSource, VIDEO_PROVIDER_LABEL, VideoTrait, VideoMode } from '@vectoriox/iox-ui/media-core';
35
+ import { planVideo, VideoKind, VideoSource, VIDEO_PROVIDER_LABEL, VideoTrait, VideoMode } from '@vectoriox/iox-ui/media-core';
36
36
  import { ConsentCategory } from '@vectoriox/iox-ui/consent-core';
37
37
 
38
38
  /**
@@ -105,6 +105,7 @@ const BUILDER_CHROME_EN = Object.freeze({
105
105
  'builder.chrome.kindSlider': 'Slider',
106
106
  'builder.chrome.kindVideo': 'Video',
107
107
  'builder.chrome.videoPosterOnly': 'poster only',
108
+ 'builder.chrome.videoFrames': 'played by the scroll',
108
109
  'builder.chrome.videoFile': 'Uploaded file',
109
110
  'builder.chrome.refuseLockedSliderPartMove': 'This control belongs to its slider — it can’t be moved out.',
110
111
  'builder.chrome.refuseLockedSliderPause': 'The pause button stays while Autoplay is on — moving content must be pausable. Turn Autoplay off in the slider’s Bindings to remove it.',
@@ -1348,8 +1349,9 @@ const DIALOG_ACTION_TYPES = new Set(['openDialog', 'closeDialog', 'toggleDialog'
1348
1349
  const EASING_OPTIONS = ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'];
1349
1350
  /** Interaction pseudo-classes — compiled to .iox-node-{id}:state */
1350
1351
  const INTERACTION_STATES = ['hover', 'active', 'focus'];
1351
- /** Structural pseudo-classes — compiled to .iox-outer-{id}:state */
1352
- const STRUCTURAL_STATES = ['first-child', 'last-child', 'nth-child(odd)', 'nth-child(even)'];
1352
+ /** Structural pseudo-classes — matched on the outer element by the shared `planStateRule` (@vectoriox/iox-ui), the
1353
+ * engine's planner too (iox-client-engine#14). */
1354
+ const STRUCTURAL_STATES = [...STRUCTURAL_STATES$1];
1353
1355
  /**
1354
1356
  * Relational pseudo-classes — the styled element is a descendant, the pseudo-class
1355
1357
  * lives on an ANCESTOR. Compiled to `.iox-node-{parentId}:{base} .iox-node-{id} { … }`,
@@ -2144,26 +2146,6 @@ class StyleRegistryService {
2144
2146
  this.styleEl = null;
2145
2147
  this._flushPending = false;
2146
2148
  }
2147
- /**
2148
- * Properties that must live on the OUTER wrapper (.iox-outer-{id}) because they
2149
- * affect how the element participates in its PARENT layout (flex/block flow).
2150
- * Everything else goes to the INNER element (.iox-node-{id}).
2151
- *
2152
- * Exception — out-of-flow positioning (position:fixed/absolute):
2153
- * When position is fixed or absolute, width is redirected to the inner element
2154
- * (the positioned one) and a default width:100% is added if none is set, so the
2155
- * element stays visible. top/right/bottom/left/zIndex are always inner because
2156
- * they are co-ordinates of the positioned element, not the flow wrapper.
2157
- * position itself goes to inner with `!important` to beat component-scoped CSS.
2158
- */
2159
- static { this.OUTER_PROPS = new Set([
2160
- 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
2161
- 'alignSelf', 'flexGrow', 'flexShrink', 'flexBasis', 'order',
2162
- // Grid-child placement — like the flex-child props above, these position the node
2163
- // within the PARENT's grid, so they must land on the outer host, not the inner div.
2164
- 'gridColumn', 'gridRow', 'justifySelf',
2165
- 'width',
2166
- ]); }
2167
2149
  /**
2168
2150
  * Properties that need !important on the inner element to beat Angular's scoped-CSS
2169
2151
  * specificity boost. CANVAS-ONLY (H2 audit, 2026-10-11): the shared compiler never forces
@@ -2220,8 +2202,7 @@ class StyleRegistryService {
2220
2202
  // breakpoint is scoped identically without re-resolving it (see upsertResponsive).
2221
2203
  if (relativeToId || triggerId)
2222
2204
  this.stateScopes.set(key, { relativeToId, triggerId });
2223
- const selector = this.stateSelector(nodeId, state, relativeToId, triggerId);
2224
- const css = selector ? this.compile(selector, styles) : '';
2205
+ const css = this.stateRuleCss(nodeId, state, styles, relativeToId, triggerId, DeclarationContext.Base);
2225
2206
  if (css)
2226
2207
  this.rules.set(key, css);
2227
2208
  else
@@ -2255,9 +2236,10 @@ class StyleRegistryService {
2255
2236
  if (!nodeId || !partClassName)
2256
2237
  return;
2257
2238
  const key = `${nodeId}:part:${partClassName}${state ? ':' + state : ''}`;
2258
- const validationClass = state ? VALIDATION_STATE_CLASS[state] : undefined;
2259
- const selector = buildPartSelector(nodeId, partClassName, validationClass);
2260
- const css = Object.keys(styles).length ? this.compile(selector, styles) : '';
2239
+ // The shared planner the engine uses (iox-client-engine#14): a part's hover / focus is the PART's own pseudo-class
2240
+ // — before, any non-validation state compiled to the bare part selector and applied at all times.
2241
+ const selector = planPartRule(nodeId, partClassName, state);
2242
+ const css = selector && Object.keys(styles).length ? this.compileSelector(selector, styles) : '';
2261
2243
  if (css)
2262
2244
  this.rules.set(key, css);
2263
2245
  else
@@ -2303,10 +2285,9 @@ class StyleRegistryService {
2303
2285
  this.setResponsiveRule(nodeId, bp, `${nodeId}::marker`, marker);
2304
2286
  for (const [state, map] of Object.entries(layer.stateStyles)) {
2305
2287
  const scope = this.stateScopes.get(`${nodeId}:${state}`);
2306
- const selector = this.stateSelector(nodeId, state, scope?.relativeToId, scope?.triggerId);
2307
2288
  // A relational/targeted state whose scope isn't known yet is skipped; it is re-emitted
2308
2289
  // when the base state rule arrives with its scope (see upsert).
2309
- const css = selector ? this.compile(selector, this.ownProps(nodeId, map), DeclarationContext.Override) : '';
2290
+ const css = this.stateRuleCss(nodeId, state, this.ownProps(nodeId, map), scope?.relativeToId, scope?.triggerId, DeclarationContext.Override);
2310
2291
  if (css)
2311
2292
  this.setResponsiveRule(nodeId, bp, `${nodeId}:${state}`, css);
2312
2293
  }
@@ -2332,37 +2313,20 @@ class StyleRegistryService {
2332
2313
  return pos === 'fixed' || pos === 'absolute';
2333
2314
  }
2334
2315
  /**
2335
- * The selector BODY (no leading dot) for a state rule, or null when a relational / targeted
2336
- * state has no scope yet (emitting it unscoped would be wrong — a page-global `:has()`).
2316
+ * A state rule's CSS through the SHARED planner the engine emits with (`planStateRule`, iox-client-engine#14), or ''
2317
+ * when a relational / targeted state has no scope yet (emitting it unscoped would be wrong — a page-global `:has()`).
2318
+ * `relativeToId` is the parent for a relational state (parent-hover) and the nearest common ancestor of the node and
2319
+ * its trigger for a targeted one (trigger-hover, trigger-on). A structural state is matched on the outer element and
2320
+ * reaches the node's own; its outer props stay outer.
2337
2321
  */
2338
- stateSelector(nodeId, state, relativeToId, triggerId) {
2339
- // Targeted states (trigger-hover): the styled node restyles when an ARBITRARY chosen
2340
- // element is hovered. The selector is `:has()`-scoped to `relativeToId` (the nearest
2341
- // common ancestor of styled + trigger) so it stays repetition-safe with one shared rule.
2342
- // `trigger-on` (CC2 "Item open"): the same scoping, keyed on the item's `[data-iox-on]` — the ONE
2343
- // shared `targetedStateSelector` the engine uses too.
2344
- if (TARGETED_STATES.includes(state)) {
2345
- return relativeToId && triggerId ? targetedStateSelector(state, relativeToId, triggerId, nodeId) : null;
2346
- }
2347
- // Relational states (parent-hover): the pseudo-class lives on an ANCESTOR
2348
- // and cascades to this element. Requires the ancestor's css id.
2349
- if (RELATIONAL_STATES.includes(state)) {
2350
- const base = RELATIONAL_STATE_BASE[state];
2351
- return relativeToId && base ? `iox-node-${relativeToId}:${base} .iox-node-${nodeId}` : null;
2352
- }
2353
- // Validation states (wrong/accepted, board 7) are not real CSS pseudo-classes — `:wrong`
2354
- // doesn't exist. Compound a CLASS instead, matching the host class IoxFieldComponent
2355
- // already sets from forceInvalid/showValid. Reuses buildPartSelector with no part, since
2356
- // "whole node, when wrong" is exactly that function's no-part case.
2357
- const validationClass = VALIDATION_STATE_CLASS[state];
2358
- if (validationClass)
2359
- return buildPartSelector(nodeId, undefined, validationClass);
2360
- // Structural pseudo-classes (first-child, last-child, nth-child) target the
2361
- // outer wrapper element so they participate correctly in grid/flex layout.
2362
- const prefix = STRUCTURAL_STATES.includes(state) ? 'iox-outer' : 'iox-node';
2363
- // `focus` compiles to :focus-visible (DS2) and a component state to its attribute (`current` →
2364
- // [aria-current="page"], CC1) — shared with the engine via stateSelectorSuffix.
2365
- return `${prefix}-${nodeId}${stateSelectorSuffix(state)}`;
2322
+ stateRuleCss(nodeId, state, styles, relativeToId, triggerId, context) {
2323
+ const targeted = TARGETED_STATES.includes(state);
2324
+ const plan = planStateRule(nodeId, state, targeted ? { scopeId: relativeToId, triggerId } : { parentId: relativeToId });
2325
+ if (!plan)
2326
+ return '';
2327
+ const { inner, outer } = plan.outer ? splitOuterProps(styles) : { inner: styles, outer: {} };
2328
+ return [this.compileSelector(plan.inner, inner, context), plan.outer ? this.compileSelector(plan.outer, outer, context) : '']
2329
+ .filter(Boolean).join('\n');
2366
2330
  }
2367
2331
  /**
2368
2332
  * The inner + outer rule (joined) and the `::marker` rule for one composed style map.
@@ -2379,7 +2343,7 @@ class StyleRegistryService {
2379
2343
  for (const [k, v] of Object.entries(styles)) {
2380
2344
  if (k === '__markerStyles')
2381
2345
  continue;
2382
- const wantsOuter = StyleRegistryService.OUTER_PROPS.has(k);
2346
+ const wantsOuter = OUTER_STYLE_PROPS.has(k);
2383
2347
  // width goes to inner for fixed/absolute (the positioned element owns its own width)
2384
2348
  if (wantsOuter && !(isOutOfFlow && k === 'width')) {
2385
2349
  outer[k] = v;
@@ -2525,14 +2489,16 @@ class StyleRegistryService {
2525
2489
  }
2526
2490
  }
2527
2491
  compile(className, styles, context = DeclarationContext.Base) {
2528
- // Declaration compilation (kebab-case, viewport-unit rewrite, !important) lives in
2529
- // the shared style-compiler so the SSR engine produces byte-identical output.
2530
- // The inner/outer partition + selector emission stay here (builder-only concern).
2492
+ return this.compileSelector(`.${className}`, styles, context);
2493
+ }
2494
+ /** One rule for a FULL selector. Declaration compilation (kebab-case, viewport-unit rewrite, !important) lives in
2495
+ * the shared style-compiler so the SSR engine produces byte-identical output. */
2496
+ compileSelector(selector, styles, context = DeclarationContext.Base) {
2531
2497
  const decls = compileDeclarations(styles, StyleRegistryService.IMPORTANT_PROPS, context);
2532
2498
  if (!decls.length)
2533
2499
  return '';
2534
2500
  const body = decls.map(d => ` ${d};`).join('\n');
2535
- return `.${className} {\n${body}\n}`;
2501
+ return `${selector} {\n${body}\n}`;
2536
2502
  }
2537
2503
  flush() {
2538
2504
  if (!this.styleEl || this._flushPending)
@@ -4084,6 +4050,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
4084
4050
  // Re-exported from the shared iox-ui package so iox-builder consumers
4085
4051
  // don't need to import from two different packages.
4086
4052
 
4053
+ /** What the canvas never writes inline (a transform inside the scaled page breaks Lenis). */
4054
+ const CANVAS_SKIPPED_PROPS = new Set(['transform']);
4087
4055
  /**
4088
4056
  * Wires ComponentNode interactions to live DOM elements via the
4089
4057
  * OverlayService node-element registry.
@@ -4705,22 +4673,27 @@ class InteractionEngineService {
4705
4673
  element.removeAttribute(INTERACTION_HIDDEN_ATTRIBUTE);
4706
4674
  else if (this.EXIT_TYPES.has(type))
4707
4675
  element.setAttribute(INTERACTION_HIDDEN_ATTRIBUTE, '');
4708
- // Reduced motion (CC1 R1): no animation runs — the element takes its final frame at once.
4676
+ // Reduced motion (CC1 R1): no animation runs — the element is left as the run would leave it (the shared settle
4677
+ // rule: an entrance releases to the stylesheet, an exit holds its hidden frame).
4709
4678
  if (!motionAllowed()) {
4710
- this.applyFrame(element, finalKeyframe(keyframes));
4679
+ settleInteraction(element, keyframes, settleModeFor(type), this.settleOptions());
4711
4680
  this.settleAfter(element, type);
4712
4681
  return;
4713
4682
  }
4714
- // 'both' = backwards (holds first keyframe during delay) + forwards (holds last keyframe after end). A look
4683
+ // The ONE call the live site makes too: fill both (holds the first frame through a delay), started from the live
4684
+ // value, settled by the shared rule — an element that has entered keeps its hover and press (iox-ui#41). A look
4715
4685
  // entrance on a Repeater's template staggers its rows: each copy adds its row's share (shared `entranceDelay`).
4716
- const anim = animateAction(element, { type, params: action.params }, keyframes, {
4686
+ const anim = playInteraction(element, { type, params: action.params }, keyframes, {
4717
4687
  duration: action.duration,
4718
4688
  delay: entranceDelay(action, repeatedRowIndex(element)),
4719
4689
  easing: action.easing,
4720
- fill: 'both',
4721
- });
4690
+ }, this.settleOptions());
4722
4691
  anim.finished.then(() => this.settleAfter(element, type)).catch(() => { });
4723
4692
  }
4693
+ /** On the canvas a run never writes a transform inline (one inside the scaled page breaks Lenis). */
4694
+ settleOptions() {
4695
+ return this.isBuilderMode() ? { skip: CANVAS_SKIPPED_PROPS } : {};
4696
+ }
4724
4697
  /** What an element is left as once an action is done: a revealed one interactive, a hidden one inert. */
4725
4698
  settleAfter(element, type) {
4726
4699
  if (this.ENTRANCE_TYPES.has(type)) {
@@ -4737,15 +4710,6 @@ class InteractionEngineService {
4737
4710
  element.style.setProperty('pointer-events', 'none');
4738
4711
  }
4739
4712
  }
4740
- /** Write a frame inline. On the canvas never a transform: one inside the scaled page breaks Lenis. */
4741
- applyFrame(element, frame) {
4742
- const canvas = this.isBuilderMode();
4743
- for (const [prop, value] of keyframeStyleEntries(frame)) {
4744
- if (canvas && prop === 'transform')
4745
- continue;
4746
- element.style.setProperty(prop, value);
4747
- }
4748
- }
4749
4713
  /**
4750
4714
  * Freeze an element at the animation's starting frame immediately, before any
4751
4715
  * trigger fires. Called for ALL entrance-type actions regardless of trigger so
@@ -4786,20 +4750,22 @@ class InteractionEngineService {
4786
4750
  element.style.removeProperty('pointer-events');
4787
4751
  element.removeAttribute(INTERACTION_HIDDEN_ATTRIBUTE);
4788
4752
  }
4753
+ /** A hover-out: the action played back from where the element IS (never from its end frame), settled by the shared
4754
+ * rule — played back to hidden, it holds hidden; to shown, it releases. */
4789
4755
  reverseAction(element, action) {
4790
- const keyframes = this.keyframesFor(effectiveActionType(action.type, element.hasAttribute(INTERACTION_HIDDEN_ATTRIBUTE)), action);
4756
+ const type = effectiveActionType(action.type, element.hasAttribute(INTERACTION_HIDDEN_ATTRIBUTE));
4757
+ const keyframes = this.keyframesFor(type, action);
4791
4758
  if (!keyframes.length)
4792
4759
  return;
4760
+ const frames = [...keyframes].reverse();
4793
4761
  if (!motionAllowed()) {
4794
- this.applyFrame(element, keyframes[0]);
4762
+ settleInteraction(element, frames, settleModeFor(type, true), this.settleOptions());
4795
4763
  return;
4796
4764
  }
4797
- // Reverse: play the keyframes backwards
4798
- animateAction(element, action, [...keyframes].reverse(), {
4765
+ playInteraction(element, { type, params: action.params }, frames, {
4799
4766
  duration: action.duration,
4800
4767
  easing: action.easing,
4801
- fill: 'forwards',
4802
- }, { reverse: true });
4768
+ }, { ...this.settleOptions(), reverse: true });
4803
4769
  }
4804
4770
  /** The action's keyframes from the ONE shared table both runtimes animate from (composite T2). */
4805
4771
  keyframesFor(type, action) {
@@ -5360,6 +5326,18 @@ class SceneCanvasService {
5360
5326
  if (el)
5361
5327
  this.driver?.reread(el);
5362
5328
  }
5329
+ /**
5330
+ * A value a layer's own component draws changed (a Video's pictures): its instance takes it now. Its Scene reads the new
5331
+ * attribute when it lands, with no re-render of the page.
5332
+ */
5333
+ setLayerInput(node, name, value) {
5334
+ const ref = this.overlay?.getNodeRef(node) ?? (node.id ? this.overlay?.findRefByNodeId(node.id)?.ref : undefined);
5335
+ const component = ref?.componentRef;
5336
+ if (!component?.instance)
5337
+ return;
5338
+ component.instance[name] = value;
5339
+ component.changeDetectorRef.detectChanges();
5340
+ }
5363
5341
  ngOnDestroy() {
5364
5342
  this.driver?.destroy();
5365
5343
  this.driver = null;
@@ -7854,6 +7832,7 @@ const VIDEO_TYPE = 'Video';
7854
7832
  const VIDEO_KIND_KEY = 'builder.chrome.kindVideo';
7855
7833
  const VIDEO_POSTER_ONLY_KEY = 'builder.chrome.videoPosterOnly';
7856
7834
  const VIDEO_FILE_KEY = 'builder.chrome.videoFile';
7835
+ const VIDEO_FRAMES_KEY = 'builder.chrome.videoFrames';
7857
7836
  /** The parts that offer "Add item" on the selection toolbar (board 3: the "Add slide" pattern). */
7858
7837
  const ADD_ITEM_ROLES = new Set([
7859
7838
  AccordionRole.Accordion, AccordionRole.Item, AccordionRole.Trigger, AccordionRole.Panel,
@@ -8113,6 +8092,9 @@ class OverlayComponent {
8113
8092
  }
8114
8093
  videoBadge(node) {
8115
8094
  const plan = planVideo(node, null);
8095
+ // Unlike every other Video, the canvas really plays this one.
8096
+ if (plan.kind === VideoKind.Frames)
8097
+ return `${this.builderLabels(VIDEO_KIND_KEY, BUILDER_CHROME_EN[VIDEO_KIND_KEY])} · ${this.builderLabels(VIDEO_FRAMES_KEY, BUILDER_CHROME_EN[VIDEO_FRAMES_KEY])}`;
8116
8098
  const where = plan.source === VideoSource.File
8117
8099
  ? this.builderLabels(VIDEO_FILE_KEY, BUILDER_CHROME_EN[VIDEO_FILE_KEY])
8118
8100
  : VIDEO_PROVIDER_LABEL[plan.source];
@@ -11130,6 +11112,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
11130
11112
  type: Output
11131
11113
  }] } });
11132
11114
 
11115
+ /** The formClass a Form's canvas twin wears (form.config). */
11116
+ const FORM_CLASS = 'iox-form';
11133
11117
  const ACCORDION_TRACK_WRAPPER = 'track';
11134
11118
  /**
11135
11119
  * Directive-host Container (Phase 2 of the Moveable migration = task 9c). The real tag (div/section/
@@ -11140,6 +11124,14 @@ const ACCORDION_TRACK_WRAPPER = 'track';
11140
11124
  * See moveable-migration.md.
11141
11125
  */
11142
11126
  class BuilderContainerComponent {
11127
+ /** Which state the canvas is showing. Only meaningful on a form host: its submit buttons follow it (busy, disabled
11128
+ * while Sending — the ONE `syncSubmitButtons` both live paths call, iox-ui#42); "While sending" is the shared CSS's. */
11129
+ set formState(state) {
11130
+ this._formState = state;
11131
+ if (this.formClass === FORM_CLASS)
11132
+ syncSubmitButtons(this.elRef?.nativeElement, state);
11133
+ }
11134
+ get formState() { return this._formState; }
11143
11135
  /** The wrapper a child renders in: an Accordion item's header → its heading level; an item's panel (either
11144
11136
  * kind) → the track. A disclosure's toggle has no heading — it is a button in the flow. */
11145
11137
  wrapperOf(child) {
@@ -11169,8 +11161,7 @@ class BuilderContainerComponent {
11169
11161
  * stylesheet treat them exactly as it treats the engine's own twins.
11170
11162
  */
11171
11163
  this.formClass = '';
11172
- /** Which state the canvas is showing. Only meaningful on a form host. */
11173
- this.formState = 'idle';
11164
+ this._formState = 'idle';
11174
11165
  this.childClick = new EventEmitter();
11175
11166
  this.childMouseEnter = new EventEmitter();
11176
11167
  this.childMouseLeave = new EventEmitter();
@@ -14074,6 +14065,8 @@ class VideoConfig extends ComponentConfig {
14074
14065
  ], VideoSource.YouTube),
14075
14066
  new TraitConfig(VideoTrait.Src, 'Video address', TraitInputType.Media, { mode: 'compact' }, ''),
14076
14067
  new TraitConfig(VideoTrait.Poster, 'Poster', TraitInputType.Media, { mode: 'compact' }, ''),
14068
+ // Pictures the scroll plays (SC4): the Scene group writes it, so the Bindings panel lists no control for it.
14069
+ new TraitConfig(VideoTrait.Frames, 'Frames', TraitInputType.Text, undefined, null),
14077
14070
  // ── Behaviour ─────────────────────────────────────────────────
14078
14071
  new TraitConfig('__behaviourLabel', 'Behaviour', TraitInputType.SectionLabel),
14079
14072
  new TraitConfig(VideoTrait.Mode, 'Mode', TraitInputType.Select, [