@vectoriox/iox-builder 11.15.0 → 11.16.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,9 +17,9 @@ import { TooltipModule } from 'primeng/tooltip';
17
17
  import * as i20 from 'primeng/dialog';
18
18
  import { DialogModule } from 'primeng/dialog';
19
19
  import * as i2$1 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, showToast, 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, attachTooltips, showToastDesign, TOOLTIP_ATTRIBUTE, hidingHeadersIn, HeaderRuntime, isScrolled, SCROLLED_ATTRIBUTE, syncSubmitButtons, IS_PREVIEW, buttonBlockHref, layerFrame, rectInLayer, placeFloating, FloatingAlign, popoverSide, 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, PROGRESS_ATTRIBUTE, interactionMarkerAttributes, cascadeChain, attachScrollProgress, ContextStore, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, ComponentInstanceRegistryService, syncInteractionMarkers, entranceStartsHidden, isActionEnabled, restoreCountUp, SCROLL_PROGRESS_TRIGGER, armInteractionCounts, splitPressFeedback, isHoverPointer, isContextAction, contextActionEffect, resolveContextValue, effectiveActionType, INTERACTION_HIDDEN_ATTRIBUTE, COUNT_UP_ACTION, motionAllowed, settleInteraction, settleModeFor, playInteraction, resolveInteractionEasing, entranceDelay, repeatedRowIndex, entrancePreStateStyle, buildInteractionKeyframes, showToast, 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, attachTooltips, showToastDesign, TOOLTIP_ATTRIBUTE, hidingHeadersIn, HeaderRuntime, isScrolled, SCROLLED_ATTRIBUTE, syncSubmitButtons, IS_PREVIEW, buttonBlockHref, layerFrame, rectInLayer, placeFloating, FloatingAlign, popoverSide, 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
- 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, SLIDE_CAPTION_TRAIT, SLIDER_SCROLL_PLAYS_TRAIT, SLIDER_PLACEMENT_TRAIT, SliderPlacement, SliderFlag } from '@vectoriox/iox-ui/composite-core';
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, MarqueeTrait, MarqueeSpeed, MarqueeDirection, DisclosureRole, isExclusiveTrigger, onCompositeItemState, isCompositeItemOn, runCompositeItemAction, shouldScrollToItem, rowIndexOf as rowIndexOf$1, slideIndexForRow, isMarqueeRoot, compositeScopeId, compositeRowKeys, isExclusiveRoot, ExclusiveRuntime, exclusiveDefaultOf, exclusiveLabelOf, exclusiveBehaviourOf, exclusiveKindOf, compositeKindOfRole, AccordionRuntime, isToggleKind, MarqueeRuntime, closestAccordionItem, isAnyCompositeRoot, SceneRole, isExclusiveList, buildCardRecipe, buildBadge, buildChip, buildAccordionRecipe, buildDisclosureRecipe, buildTabsRecipe, buildSegmentedRecipe, buildLightboxRecipe, buildMarqueeRecipe, buildCounterRecipe, buildReadingBarRecipe, NO_ELEMENT_STYLE, CompositeKind, SliderRole, accordionItemLabel, segmentedOptionLabel, isExclusivePanels, isLockedPart, accordionRootOf, accordionItemOf, addAccordionItem, addTabPair, segmentedAddRefusal, addSegmentedOption, isCompositeItem, accordionHeadingLevel, isCompositePanel, isCompositeTrigger, splitSliderChildren, hasSliderParts, SLIDE_CAPTION_TRAIT, SLIDER_SCROLL_PLAYS_TRAIT, SLIDER_PLACEMENT_TRAIT, SliderPlacement, SliderFlag } from '@vectoriox/iox-ui/composite-core';
23
23
  import { formContentInHost, isInteractiveHost, findFormContentInHosts, FORM_CHANNEL_ROLE, channelHref, channelOpensNewTab, FORM_ACTIONS_ROLE, SUBMIT_BUTTON_NODE_TYPE, FORM_AFTER_SEND_TRAIT, FormAfterSend } from '@vectoriox/iox-ui/forms-core';
24
24
  import { Subject, BehaviorSubject, of, forkJoin } from 'rxjs';
25
25
  import interact from 'interactjs';
@@ -1338,6 +1338,9 @@ const ACTION_TYPE_OPTIONS = [
1338
1338
  { value: 'clipReveal', label: 'Reveal' },
1339
1339
  { value: 'zoomOut', label: 'Zoom settle' },
1340
1340
  { value: 'revealText', label: 'Reveal text' },
1341
+ { value: 'countUp', label: 'Count up' },
1342
+ { value: 'parallax', label: 'Depth (parallax)' },
1343
+ { value: 'progressBar', label: 'Fill as read' },
1341
1344
  { value: 'show', label: 'Show' },
1342
1345
  { value: 'hide', label: 'Hide' },
1343
1346
  { value: 'toggleVisibility', label: 'Toggle visibility' },
@@ -1948,6 +1951,18 @@ function buildCompositeBehaviourTraits() {
1948
1951
  new TraitConfig(TabsBehaviourTrait.Label, 'Accessible name', TraitInputType.Text),
1949
1952
  ];
1950
1953
  }
1954
+ /**
1955
+ * A Marquee band's own settings (premium components M6, N6) — on the band's root Container. No default: a plain Container
1956
+ * saves none (the recipe writes them); edited in Bindings ▸ Marquee, never as generic rows.
1957
+ */
1958
+ function buildMarqueeTraits() {
1959
+ return [
1960
+ new TraitConfig(MarqueeTrait.Speed, 'Speed', TraitInputType.Select, Object.values(MarqueeSpeed).map(v => ({ label: v, value: v }))),
1961
+ new TraitConfig(MarqueeTrait.Direction, 'Direction', TraitInputType.Select, Object.values(MarqueeDirection).map(v => ({ label: v, value: v }))),
1962
+ new TraitConfig(MarqueeTrait.Fade, 'Fade the edges', TraitInputType.Checkbox),
1963
+ new TraitConfig(MarqueeTrait.PauseOnHover, 'Pause on hover', TraitInputType.Checkbox),
1964
+ ];
1965
+ }
1951
1966
  /**
1952
1967
  * How a header behaves as the page scrolls (premium components T2, M4) — on the Navigation bar's root. No default: a
1953
1968
  * plain Container saves none; edited in the Navigation bar's Header section, never as generic rows.
@@ -4111,1092 +4126,1277 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
4111
4126
  // Re-exported from the shared iox-ui package so iox-builder consumers
4112
4127
  // don't need to import from two different packages.
4113
4128
 
4114
- /** What the canvas never writes inline (a transform inside the scaled page breaks Lenis). */
4115
- const CANVAS_SKIPPED_PROPS = new Set(['transform']);
4116
4129
  /**
4117
- * Wires ComponentNode interactions to live DOM elements via the
4118
- * OverlayService node-element registry.
4130
+ * Parts that are HIDDEN AT REST (complex-components R2, board 3): on the live site they are not on
4131
+ * screen until something opens them, so on the canvas they show only while selected into or switched
4132
+ * open ("Showing on canvas"). The layer tree marks them with the eye-slash glyph. A Dialog (the mobile
4133
+ * menu's drawer, a consent banner) by its type; an Accordion's PANEL by its role (CC2 — closed until its
4134
+ * item opens); a Disclosure's panel and its "Read less" word (CC2.1 — shown only while it is open).
4135
+ */
4136
+ const HIDDEN_AT_REST_TYPES = ['Dialog', 'Popover'];
4137
+ const HIDDEN_AT_REST_ROLES = [AccordionRole.Panel, DisclosureRole.Panel, DisclosureRole.Less];
4138
+ /** A READING BAR (premium M6, N7): empty until the scroll fills it, so on the canvas the selection holds it full. */
4139
+ function isReadingBar(node) {
4140
+ return PROGRESS_ATTRIBUTE in interactionMarkerAttributes(node);
4141
+ }
4142
+ function isHiddenAtRest(node) {
4143
+ return (!!node?.type && HIDDEN_AT_REST_TYPES.includes(node.type)) || (!!node?.role && HIDDEN_AT_REST_ROLES.includes(node.role)) || isReadingBar(node);
4144
+ }
4145
+ /**
4146
+ * How the selection shows a node hidden at rest — the two approved rules, by what the node IS:
4147
+ * - a PART of a composite (it carries a `role`: the mobile menu's drawer) follows R2 (board 3): the
4148
+ * selection is the reveal, and moving it out of the part hides it again;
4149
+ * - a free-standing overlay (a plain Dialog) follows board A4: selecting it opens it, and it stays open
4150
+ * while the author works elsewhere, until its X, its backdrop or the eye closes it.
4151
+ */
4152
+ var SelectionReveal;
4153
+ (function (SelectionReveal) {
4154
+ SelectionReveal["WhileSelected"] = "while-selected";
4155
+ SelectionReveal["UntilClosed"] = "until-closed";
4156
+ })(SelectionReveal || (SelectionReveal = {}));
4157
+ /**
4158
+ * Parts hidden at rest that live in a composite's TOGGLE, not in its panel (CC2.1 follow-up, approved 2026-10-21): a
4159
+ * click on one on the canvas toggles the composite live — selecting it never forces the reveal, so "Read less"
4160
+ * closes the disclosure it was clicked in. Only a part inside a PANEL is revealed by the selection.
4161
+ */
4162
+ const TOGGLE_PART_ROLES = [DisclosureRole.Less];
4163
+ /**
4164
+ * Parts the selection REVEALS though they are not hidden at rest (CC4a): a tab PANEL — one is always on, so the layer
4165
+ * tree draws it no eye-slash (board 3), but selecting anything inside an inactive one shows its tab while selected.
4166
+ */
4167
+ const REVEALED_WHILE_SELECTED_ROLES = [TabsRole.Panel];
4168
+ function selectionRevealOf(node) {
4169
+ if ((!!node?.role && REVEALED_WHILE_SELECTED_ROLES.includes(node.role)) || isReadingBar(node))
4170
+ return SelectionReveal.WhileSelected;
4171
+ if (!isHiddenAtRest(node) || (!!node?.role && TOGGLE_PART_ROLES.includes(node.role)))
4172
+ return null;
4173
+ return node?.role ? SelectionReveal.WhileSelected : SelectionReveal.UntilClosed;
4174
+ }
4175
+ /** Root → `target`, inclusive, across `roots` (page layout and globals), or [] when it is not there. */
4176
+ function pathToNode(roots, target) {
4177
+ for (const root of roots) {
4178
+ if (root === target)
4179
+ return [root];
4180
+ const below = pathToNode(root.children ?? [], target);
4181
+ if (below.length)
4182
+ return [root, ...below];
4183
+ }
4184
+ return [];
4185
+ }
4186
+
4187
+ const NAV_CONTEXT_ROLES = new Set([NavRole.Root, NavRole.Drawer, NavRole.Bar]);
4188
+ function findRole(nodes, role) {
4189
+ for (const n of nodes) {
4190
+ if (n.role === role)
4191
+ return n;
4192
+ const below = findRole(n.children ?? [], role);
4193
+ if (below)
4194
+ return below;
4195
+ }
4196
+ return null;
4197
+ }
4198
+ /** True when the selection (root → selected) is the Navigation bar, its drawer, its bottom bar, or anything inside them. */
4199
+ function inPhoneMenuContext(path) {
4200
+ return path.some(n => !!n.role && NAV_CONTEXT_ROLES.has(n.role));
4201
+ }
4202
+ /**
4203
+ * The phone menu a selection belongs to, or null when the selection is outside a navigation or the site has no drawer
4204
+ * (no phone shape yet, or the Bottom bar shape). The drawer is looked up in the selection's own Navigation bar first,
4205
+ * then in the globals (the Both shape makes it a global of its own).
4206
+ */
4207
+ function phoneMenuOf(path, globals = []) {
4208
+ if (!inPhoneMenuContext(path))
4209
+ return null;
4210
+ const root = path.find(n => n.role === NavRole.Root) ?? null;
4211
+ const scopes = root ? [root, ...globals] : [...globals];
4212
+ const drawer = path.find(n => n.role === NavRole.Drawer) ?? findRole(scopes, NavRole.Drawer);
4213
+ if (!drawer)
4214
+ return null;
4215
+ const menuButton = findRole(scopes, NavRole.MenuButton);
4216
+ const at = navShapeBreakpoint(scopes);
4217
+ return { root, drawer, menuButton, at };
4218
+ }
4219
+ /** True when the canvas, at `breakpoint`, shows the phone menu that takes over at `at` (that width or narrower). */
4220
+ function phoneMenuShowsAt(at, breakpoint) {
4221
+ return cascadeChain(breakpoint).includes(at);
4222
+ }
4223
+ const OPENER_TRIGGER = 'click';
4224
+ /** The dialog actions a phone menu's opener runs (generation-core stamps Toggle; CC1 menus still hold Open). */
4225
+ var PhoneMenuOpenerAction;
4226
+ (function (PhoneMenuOpenerAction) {
4227
+ PhoneMenuOpenerAction["Open"] = "openDialog";
4228
+ PhoneMenuOpenerAction["Toggle"] = "toggleDialog";
4229
+ })(PhoneMenuOpenerAction || (PhoneMenuOpenerAction = {}));
4230
+ const OPENER_ACTIONS = new Set(Object.values(PhoneMenuOpenerAction));
4231
+ /** True when `interaction` is the phone menu's own opener: the menu button's (or More's) click opening the drawer. */
4232
+ function isPhoneMenuOpener(node, interaction, drawerId) {
4233
+ if (!drawerId || (node?.role !== NavRole.MenuButton && node?.role !== NavRole.More))
4234
+ return false;
4235
+ const actions = interaction?.actions ?? [];
4236
+ return interaction?.trigger === OPENER_TRIGGER && actions.length === 1
4237
+ && OPENER_ACTIONS.has(actions[0].type ?? '') && actions[0].target === drawerId;
4238
+ }
4239
+
4240
+ /** What a hidden-at-rest part shows on the canvas when the author picks it in "Showing on canvas". */
4241
+ var CanvasShowState;
4242
+ (function (CanvasShowState) {
4243
+ CanvasShowState["Closed"] = "closed";
4244
+ CanvasShowState["Open"] = "open";
4245
+ })(CanvasShowState || (CanvasShowState = {}));
4246
+ const EMPTY_SNAPSHOT = () => ({
4247
+ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), triggeredBySelection: new Set(), shown: new Map(), items: new Map(),
4248
+ runtime: new Set(), menu: null, tooltips: new Set(), toast: null,
4249
+ });
4250
+ /**
4251
+ * The canvas's DESIGN state (complex-components R2; composite plan §5a's CompositeDesignStateService,
4252
+ * started here for the navigation and reused by Accordion in CC2): which parts that are hidden at rest
4253
+ * — a closed drawer today, a collapsed panel or an inactive tab next — the canvas shows right now.
4119
4254
  *
4120
- * Call `attach(node)` after the node is rendered and registered.
4121
- * Call `detach(node)` on destroy to tear down listeners/observers.
4255
+ * Two sources, the Dialog's "selection is the reveal" and the Form's switcher, generalised:
4256
+ * - **the selection** — selecting anything inside a hidden part (tree, breadcrumb, an Items row,
4257
+ * SELECT_NODE) shows it; deselecting hides it again (`setSelectionPath`);
4258
+ * - **"Showing on canvas"** — the Bindings panel's switcher, for the one case a selection cannot express:
4259
+ * the page with the menu open while a part OUTSIDE it is selected (`setShown`).
4260
+ *
4261
+ * Canvas-only: never saved, never a history step. Also carries the canvas page's path, so a link to the
4262
+ * page being edited renders as the current page (N2) — the same decision the live site makes.
4122
4263
  */
4123
- class InteractionEngineService {
4124
- constructor(overlayService) {
4125
- this.overlayService = overlayService;
4126
- this.attached = new Map();
4127
- /** Shared with the SSR engine — one store type, one set of semantics. */
4128
- this.contextStore = inject(ContextStore);
4129
- /** Resolved data for the canvas, so a row marker (source + index) resolves back to its item. */
4130
- this.contextData = {};
4131
- /** Elements that have had pre-state applied (hidden by attach). */
4132
- this.preStatedElements = new Set();
4133
- /**
4134
- * Toggle state for click interactions with reverseActions.
4135
- * false = next click runs actions (primary); true = next click runs reverseActions.
4136
- */
4137
- this.toggleStates = new Map();
4138
- /**
4139
- * Pre-states queued for elements that weren't registered yet when the owner
4140
- * node's attach() ran. Keyed by target node ID. Consumed when the target
4141
- * element's own attach() fires.
4142
- */
4143
- this.pendingPreStates = new Map();
4144
- /** Action types that animate an element from hidden → visible / visible → hidden — the SHARED sets
4145
- * (@vectoriox/iox-ui), the same ones the live site's runtime reads. */
4146
- this.ENTRANCE_TYPES = ENTRANCE_ACTION_TYPES;
4147
- this.EXIT_TYPES = EXIT_ACTION_TYPES;
4148
- this.SWIPER_ACTIONS = new Set([
4149
- 'swiperNext', 'swiperPrev', 'swiperGoToSlide',
4150
- 'swiperAutoplayStart', 'swiperAutoplayStop', 'swiperAutoplayPause',
4151
- ]);
4152
- this.instanceRegistry = inject(ComponentInstanceRegistryService);
4153
- this.ngZone = inject(NgZone);
4264
+ class CanvasDesignStateService {
4265
+ constructor() {
4266
+ this._state$ = new BehaviorSubject(EMPTY_SNAPSHOT());
4267
+ this.state$ = this._state$.asObservable();
4268
+ this._pagePath$ = new BehaviorSubject(null);
4269
+ /** The app path of the page on the canvas (`/work`), or null when unknown. */
4270
+ this.pagePath$ = this._pagePath$.asObservable();
4271
+ }
4272
+ get snapshot() {
4273
+ return this._state$.value;
4274
+ }
4275
+ get pagePath() {
4276
+ return this._pagePath$.value;
4277
+ }
4278
+ setPagePath(path) {
4279
+ const next = path ?? null;
4280
+ if (next !== this._pagePath$.value)
4281
+ this._pagePath$.next(next);
4154
4282
  }
4155
4283
  /**
4156
- * Run all DOM-manipulation operations (pre-state, observers, animations)
4157
- * outside the Angular zone so Zone.js does not schedule unnecessary CD ticks
4158
- * for each RAF/IntersectionObserver/animation callback.
4159
- * The caller is responsible for calling markForCheck() / refreshSelect() inside
4160
- * the zone after this returns.
4284
+ * The selection moved. The hidden-at-rest PARTS on its path are revealed and every other part is not
4285
+ * (R2); a free-standing overlay on the path is pinned open until it is closed (A4). `globals` lets it find the
4286
+ * phone menu the selection belongs to when its drawer lives outside the path (the Both shape).
4161
4287
  */
4162
- runOutside(fn) {
4163
- return this.ngZone.runOutsideAngular(fn);
4288
+ setSelectionPath(path, globals = []) {
4289
+ const ids = (reveal) => path.filter(n => selectionRevealOf(n) === reveal).map(n => n.id).filter((id) => !!id);
4290
+ const revealed = new Set(ids(SelectionReveal.WhileSelected));
4291
+ const selectedTriggers = new Set(path.filter(n => isExclusiveTrigger(n)).map(n => n.id).filter((id) => !!id));
4292
+ const triggeredBySelection = new Set(path.map(displayTriggerOf).filter((id) => !!id));
4293
+ const { revealed: was, pinned: held, selectedTriggers: wasTriggers, triggeredBySelection: wasTriggered } = this._state$.value;
4294
+ const pinned = new Set([...held, ...ids(SelectionReveal.UntilClosed)]);
4295
+ const menu = phoneMenuOf(path, globals);
4296
+ if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers) && sameSet(triggeredBySelection, wasTriggered)
4297
+ && sameMenu(menu, this._state$.value.menu))
4298
+ return;
4299
+ this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers, triggeredBySelection, menu });
4164
4300
  }
4165
- /** Wire all interactions for a node to its rendered DOM element. */
4166
- attach(node) { this.runOutside(() => this._attach(node)); }
4167
- /** Attach interactions to a node whose element is NOT overlay-registered — e.g. repeater/slider
4168
- * PREVIEW CLONES, which are non-selectable but must still animate (parity with the SSR runtime,
4169
- * which animates every repeated row). See RenderDirective. */
4170
- attachTo(node, element) { this.runOutside(() => this._attach(node, element)); }
4171
- _attach(node, elementOverride) {
4172
- const element = elementOverride ?? this.overlayService.getNodeRef(node)?.element;
4173
- if (!element)
4301
+ /** Closed on the canvas (its X, its backdrop, the eye): no longer pinned, switched open or revealed by the
4302
+ * selection — selecting into it again reveals it again. */
4303
+ dismiss(nodeId) {
4304
+ if (!nodeId)
4174
4305
  return;
4175
- const inBuilder = this.isBuilderMode();
4176
- // Apply any pre-states queued for THIS element by earlier-registered nodes.
4177
- // Pending entries are only added when appropriate (see loop below), so always consume.
4178
- if (node.id) {
4179
- const pending = this.pendingPreStates.get(node.id);
4180
- if (pending) {
4181
- for (const action of pending) {
4182
- this.applyPreState(element, action);
4183
- }
4184
- this.pendingPreStates.delete(node.id);
4185
- }
4186
- }
4187
- if (!node.interactions?.length)
4306
+ const { pinned, shown, revealed, runtime } = this._state$.value;
4307
+ if (!pinned.has(nodeId) && !shown.has(nodeId) && !revealed.has(nodeId) && !runtime.has(nodeId))
4188
4308
  return;
4189
- const cleanups = [];
4190
- for (const ix of node.interactions) {
4191
- // Pre-state: hide target elements before their entrance animation fires so
4192
- // the user sees a real hidden→visible animation on the canvas. Every trigger
4193
- // now runs live in the builder (see attachInteraction), so pre-state is applied
4194
- // for all of them EXCEPT scrollProgress — that trigger is unimplemented (no-op),
4195
- // so pre-stating it would leave the element stuck at opacity:0 forever.
4196
- // Under reduced motion a self-playing entrance starts shown and never moves (shared `entranceStartsHidden`).
4197
- const skipPreState = (inBuilder && ix.trigger === 'scrollProgress') || !entranceStartsHidden(ix.trigger);
4198
- if (!skipPreState) {
4199
- for (const action of ix.actions) {
4200
- // A disabled action must not pre-state: it would hide the element with nothing
4201
- // left to reveal it.
4202
- if (!isActionEnabled(action))
4203
- continue;
4204
- if (this.ENTRANCE_TYPES.has(action.type)) {
4205
- const target = this.resolveTarget(node, action, element);
4206
- if (target) {
4207
- this.applyPreState(target, action);
4208
- }
4209
- else if (action.target && action.target !== 'self') {
4210
- const list = this.pendingPreStates.get(action.target) ?? [];
4211
- list.push(action);
4212
- this.pendingPreStates.set(action.target, list);
4213
- }
4214
- }
4215
- }
4216
- }
4217
- const cleanup = this.attachInteraction(node, ix, element);
4218
- cleanups.push({ node, interaction: ix, cleanup });
4219
- }
4220
- this.attached.set(node, cleanups);
4309
+ const nextPinned = new Set(pinned);
4310
+ nextPinned.delete(nodeId);
4311
+ const nextShown = new Map(shown);
4312
+ nextShown.delete(nodeId);
4313
+ const nextRevealed = new Set(revealed);
4314
+ nextRevealed.delete(nodeId);
4315
+ const nextRuntime = new Set(runtime);
4316
+ nextRuntime.delete(nodeId);
4317
+ this._state$.next({ ...this._state$.value, pinned: nextPinned, shown: nextShown, revealed: nextRevealed, runtime: nextRuntime });
4221
4318
  }
4222
- /** Tear down all interaction listeners for a node and reset its animated state. */
4223
- detach(node) { this.runOutside(() => this._detach(node)); }
4224
- _detach(node) {
4225
- const entries = this.attached.get(node);
4226
- if (!entries)
4319
+ /** An action opened the overlay on the canvas (a click on the menu button): shown until something closes it. */
4320
+ markOpened(nodeId) {
4321
+ if (!nodeId || this._state$.value.runtime.has(nodeId))
4227
4322
  return;
4228
- for (const entry of entries) {
4229
- entry.cleanup();
4230
- }
4231
- this.attached.delete(node);
4232
- // Reset toggle state so re-attach starts from the primary actions.
4233
- for (const entry of entries) {
4234
- if (entry.interaction.reverseActions?.length) {
4235
- this.toggleStates.delete(entry.interaction.id);
4236
- }
4237
- }
4238
- // Cancel Web Animations and strip any inline pre-state styles that
4239
- // were applied by applyPreState() so the element returns to its natural CSS state.
4240
- const ref = this.overlayService.getNodeRef(node);
4241
- if (ref) {
4242
- ref.element.getAnimations().forEach(a => a.cancel());
4243
- this.clearInlineAnimationStyles(ref.element);
4244
- }
4245
- // Also clean up pre-state on any target elements this node was animating.
4246
- if (node.interactions) {
4247
- for (const ix of node.interactions) {
4248
- for (const action of ix.actions) {
4249
- // Remove any pending pre-states for unregistered targets.
4250
- if (action.target && action.target !== 'self') {
4251
- const pending = this.pendingPreStates.get(action.target);
4252
- if (pending) {
4253
- const filtered = pending.filter(a => a !== action);
4254
- if (filtered.length) {
4255
- this.pendingPreStates.set(action.target, filtered);
4256
- }
4257
- else {
4258
- this.pendingPreStates.delete(action.target);
4259
- }
4260
- }
4261
- }
4262
- // Clean up inline styles on already-registered target elements.
4263
- const target = this.resolveTarget(node, action);
4264
- if (target && this.preStatedElements.has(target)) {
4265
- target.getAnimations().forEach(a => a.cancel());
4266
- this.clearInlineAnimationStyles(target);
4267
- this.preStatedElements.delete(target);
4268
- }
4269
- }
4270
- }
4271
- }
4323
+ this._state$.next({ ...this._state$.value, runtime: new Set([...this._state$.value.runtime, nodeId]) });
4272
4324
  }
4273
- /** Re-attach interactions after they have been edited in the panel. */
4274
- refresh(node) {
4275
- this.runOutside(() => { this._detach(node); this._attach(node); });
4325
+ /** The switcher's choice for one part (null = back to the saved default). */
4326
+ setShown(nodeId, state) {
4327
+ const shown = new Map(this._state$.value.shown);
4328
+ if (state && state !== CanvasShowState.Closed)
4329
+ shown.set(nodeId, state);
4330
+ else
4331
+ shown.delete(nodeId);
4332
+ this._state$.next({ ...this._state$.value, shown });
4276
4333
  }
4277
- /**
4278
- * Replay all interactions on a node immediately, regardless of their trigger.
4279
- * Used by the builder overlay play button for in-editor animation preview.
4280
- * Uses fill:'none' so the element returns to its natural state after the preview.
4281
- */
4282
- replay(node) { this.runOutside(() => this._replay(node)); }
4283
- _replay(node) {
4284
- if (!node.interactions?.length)
4334
+ /** Tabs' "Showing on canvas": the item a root shows (null = whatever the canvas shows live). */
4335
+ setShownItem(rootId, key) {
4336
+ if ((this._state$.value.items.get(rootId) ?? null) === key)
4285
4337
  return;
4286
- const ref = this.overlayService.getNodeRef(node);
4287
- if (ref) {
4288
- ref.element.getAnimations().forEach(a => a.cancel());
4289
- // Clear any leftover inline styles so the pre-state applies cleanly.
4290
- this.clearInlineAnimationStyles(ref.element);
4291
- }
4292
- // In builder mode pre-state is never applied on attach, so apply it now
4293
- // to give an accurate hidden→visible preview. It is cleaned up via
4294
- // executeAction's anim.finished handler once the animation completes.
4295
- for (const ix of node.interactions) {
4296
- for (const action of ix.actions) {
4297
- if (this.ENTRANCE_TYPES.has(action.type)) {
4298
- const target = this.resolveTarget(node, action);
4299
- if (target)
4300
- this.applyPreState(target, action);
4301
- }
4302
- }
4303
- }
4304
- for (const ix of node.interactions) {
4305
- this.runActions(node, ix.actions);
4306
- }
4338
+ const items = new Map(this._state$.value.items);
4339
+ if (key)
4340
+ items.set(rootId, key);
4341
+ else
4342
+ items.delete(rootId);
4343
+ this._state$.next({ ...this._state$.value, items });
4307
4344
  }
4308
- /**
4309
- * Fire all interactions on the given node that match the specified trigger.
4310
- * Used by Swiper (and future components) to dispatch their events into the
4311
- * interaction engine without coupling to the engine's attach/detach lifecycle.
4312
- */
4313
- replayTrigger(nodeId, trigger) { this.runOutside(() => this._replayTrigger(nodeId, trigger)); }
4314
- _replayTrigger(nodeId, trigger) {
4315
- for (const [node] of this.attached) {
4316
- if (node.id !== nodeId)
4317
- continue;
4318
- for (const ix of (node.interactions ?? [])) {
4319
- if (ix.trigger === trigger) {
4320
- this.runActions(node, ix.actions);
4321
- }
4322
- }
4345
+ /** The item a root's "Showing on canvas" shows, or null. */
4346
+ shownItem(rootId) {
4347
+ return (rootId && this._state$.value.items.get(rootId)) || null;
4348
+ }
4349
+ /** The switcher's current choice for one part. */
4350
+ shownState(nodeId) {
4351
+ return (nodeId && this._state$.value.shown.get(nodeId)) || CanvasShowState.Closed;
4352
+ }
4353
+ /** True when the canvas shows the part: selected into, switched open, or opened by an action. */
4354
+ isShown(nodeId) {
4355
+ if (!nodeId)
4356
+ return false;
4357
+ const { revealed, pinned, shown, runtime } = this._state$.value;
4358
+ return revealed.has(nodeId) || pinned.has(nodeId) || runtime.has(nodeId) || shown.get(nodeId) === CanvasShowState.Open;
4359
+ }
4360
+ /** True when the canvas shows an item because the selection is styled "when it is on" (the switcher's hint). */
4361
+ isTriggeredBySelection(triggerId) {
4362
+ return !!triggerId && this._state$.value.triggeredBySelection.has(triggerId);
4363
+ }
4364
+ /** True when the part is shown only because the selection is inside it (the switcher's hint line). */
4365
+ isRevealedBySelection(nodeId) {
4366
+ return !!nodeId && this._state$.value.revealed.has(nodeId);
4367
+ }
4368
+ /** Hold a control's tooltip open on the canvas (true), or let it go. */
4369
+ setTooltipShown(nodeId, on) {
4370
+ if (!nodeId || this._state$.value.tooltips.has(nodeId) === on)
4323
4371
  return;
4324
- }
4372
+ const tooltips = new Set(this._state$.value.tooltips);
4373
+ if (on)
4374
+ tooltips.add(nodeId);
4375
+ else
4376
+ tooltips.delete(nodeId);
4377
+ this._state$.next({ ...this._state$.value, tooltips });
4325
4378
  }
4326
- // ── Private ──────────────────────────────────────────────
4327
- attachInteraction(node, ix, element) {
4328
- // All triggers run live in the builder canvas so the user sees the real
4329
- // animation (pageLoad on attach, viewportEnter on scroll-into-view, click/hover).
4330
- // The former NG0103 crash was caused by (a) Swiper's ResizeObserver created inside
4331
- // the Angular zone — fixed in SliderContainerComponent via runOutsideAngular — and
4332
- // (b) the overlay's Resize/MutationObservers synchronously re-dirtying the view
4333
- // during synchronize() — fixed by coalescing overlay updates through a RAF.
4334
- // scrollProgress is still unimplemented and returns a no-op below.
4335
- switch (ix.trigger) {
4336
- case 'pageLoad':
4337
- return this.attachPageLoad(node, ix, element);
4338
- case 'pageEnter':
4339
- // In the builder canvas there is no navigation, so preview a page-enter the same
4340
- // way as page-load (pre-stated hidden → animate in). In production the SSR engine
4341
- // fires it only after the page transition completes (ClientInteractionService).
4342
- return this.attachPageLoad(node, ix, element);
4343
- case 'pageLeave':
4344
- // Exit choreography that plays on navigate-away. There is no navigation in the
4345
- // builder, so it does not auto-fire (element stays visible) — the overlay play
4346
- // button previews it via replay(). In production the orchestrator awaits it.
4347
- return () => { };
4348
- case 'viewportEnter':
4349
- return this.attachViewportEnter(node, ix, element);
4350
- case 'click':
4351
- return this.attachClick(node, ix, element);
4352
- case 'hover':
4353
- return this.attachHover(node, ix, element);
4354
- case 'scrollProgress':
4355
- // Scroll-progress is complex — deferred.
4356
- return () => { };
4357
- case 'itemOn':
4358
- case 'itemOff':
4359
- return this.attachItemState(node, ix, element, ix.trigger === 'itemOn');
4360
- default:
4361
- return () => { };
4362
- }
4379
+ /** True while the canvas holds the control's tooltip open. */
4380
+ tooltipShown(nodeId) {
4381
+ return !!nodeId && this._state$.value.tooltips.has(nodeId);
4363
4382
  }
4364
- /**
4365
- * `itemOn` / `itemOff` (CC2): run when the part's accordion ITEM opens / closes — announced by the shared
4366
- * composite-core runtime on the item, on the canvas as on the site. An item already open when the
4367
- * listener attaches has opened: its `itemOn` runs at once (its entrance was pre-stated hidden).
4368
- */
4369
- attachItemState(node, ix, element, on) {
4370
- const run = () => this.runActions(node, ix.actions, element);
4371
- const off = onCompositeItemState(element, on, run);
4372
- const timer = on && isCompositeItemOn(element) ? setTimeout(run, 50) : null;
4373
- return () => { off(); if (timer)
4374
- clearTimeout(timer); };
4383
+ /** Hold a toast up on the canvas with these words, or take it down (null). */
4384
+ setToastShown(words) {
4385
+ const next = words === null ? null : String(words);
4386
+ if (this._state$.value.toast === next)
4387
+ return;
4388
+ this._state$.next({ ...this._state$.value, toast: next });
4375
4389
  }
4376
- // ── Triggers ─────────────────────────────────────────────
4377
- attachPageLoad(node, ix, element) {
4378
- const timer = setTimeout(() => {
4379
- if (this.hasPreStatedAncestor(element)) {
4380
- this.deferUntilVisible(element, () => this.runActions(node, ix.actions, element));
4381
- }
4382
- else {
4383
- this.runActions(node, ix.actions, element);
4384
- }
4385
- }, 50);
4386
- return () => clearTimeout(timer);
4390
+ /** The words of the toast the canvas holds up, or null. */
4391
+ get toastShown() {
4392
+ return this._state$.value.toast;
4387
4393
  }
4388
- attachViewportEnter(node, ix, element) {
4389
- const firedSet = new Set();
4390
- const fire = () => {
4391
- const key = ix.id;
4392
- if (ix.actions.some(a => a.once) && firedSet.has(key))
4393
- return;
4394
- if (this.hasPreStatedAncestor(element)) {
4395
- this.deferUntilVisible(element, () => {
4396
- if (ix.actions.some(a => a.once) && firedSet.has(key))
4397
- return;
4398
- firedSet.add(key);
4399
- this.runActions(node, ix.actions, element);
4400
- });
4401
- return;
4402
- }
4403
- firedSet.add(key);
4404
- this.runActions(node, ix.actions, element);
4405
- };
4406
- // IntersectionObserver with no root uses the browser viewport.
4407
- // It respects overflow:hidden clipping from .preview-scroll so it fires
4408
- // correctly as Lenis scrolls (Lenis changes scrollTop on .preview-scroll).
4409
- const observer = new IntersectionObserver((entries) => {
4410
- for (const entry of entries) {
4411
- if (entry.isIntersecting) {
4412
- cleanup();
4413
- fire();
4414
- }
4415
- }
4416
- }, { threshold: 0.1 });
4417
- observer.observe(element);
4418
- // Builder: also listen to Lenis scroll events on .preview-lenis-content
4419
- // as a fallback in case IntersectionObserver misses elements behind a
4420
- // CSS transform (scale) on iox-page-component.
4421
- const lenisEl = this.overlayService.getScrollContainer();
4422
- let scrollFired = false;
4423
- const onScroll = () => {
4424
- if (scrollFired)
4425
- return;
4426
- const rootEl = this.overlayService.getIntersectionRoot();
4427
- if (!rootEl)
4428
- return;
4429
- const rootRect = rootEl.getBoundingClientRect();
4430
- const elRect = element.getBoundingClientRect();
4431
- const overlapH = Math.min(elRect.bottom, rootRect.bottom) - Math.max(elRect.top, rootRect.top);
4432
- const elH = elRect.bottom - elRect.top;
4433
- if (elH > 0 && overlapH / elH >= 0.1) {
4434
- scrollFired = true;
4435
- cleanup();
4436
- fire();
4437
- }
4438
- };
4439
- if (lenisEl)
4440
- lenisEl.addEventListener('scroll', onScroll, { passive: true });
4441
- const cleanup = () => {
4442
- observer.disconnect();
4443
- scrollFired = true;
4444
- if (lenisEl)
4445
- lenisEl.removeEventListener('scroll', onScroll);
4446
- };
4447
- return cleanup;
4394
+ /** A new page on the canvas: nothing is revealed or switched open. */
4395
+ reset() {
4396
+ this._state$.next(EMPTY_SNAPSHOT());
4448
4397
  }
4449
- isBuilderMode() {
4450
- return document.body.classList.contains('builder-active');
4398
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4399
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService }); }
4400
+ }
4401
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService, decorators: [{
4402
+ type: Injectable
4403
+ }] });
4404
+ function sameMenu(a, b) {
4405
+ if (!a || !b)
4406
+ return a === b;
4407
+ return a.drawer === b.drawer && a.menuButton === b.menuButton && a.root === b.root && a.at === b.at;
4408
+ }
4409
+ function sameSet(a, b) {
4410
+ return a.size === b.size && [...a].every(id => b.has(id));
4411
+ }
4412
+ /**
4413
+ * Show or hide an overlay part (the Menu drawer) for DESIGN — ONE move for every control that previews it (the
4414
+ * Bindings ▸ Phone menu row, the canvas's Menu chip). Opening switches it open; closing closes its canvas twin the way
4415
+ * its own Close does (the exit plays, the reveal and the switch are cleared, a selection inside it moves to its
4416
+ * opener), so a drawer shown because the selection sits in it closes too.
4417
+ */
4418
+ function setOverlayPreview(state, twins, id, open) {
4419
+ if (!id)
4420
+ return;
4421
+ if (open) {
4422
+ state.setShown(id, CanvasShowState.Open);
4423
+ return;
4451
4424
  }
4452
- /** Returns true if any DOM ancestor of element is currently faded out via pre-state. */
4453
- hasPreStatedAncestor(element) {
4454
- let el = element.parentElement;
4455
- while (el && el !== document.body) {
4456
- if (this.preStatedElements.has(el))
4457
- return true;
4458
- el = el.parentElement;
4425
+ const twin = twins?.get(id);
4426
+ if (twin?.close)
4427
+ twin.close();
4428
+ else
4429
+ state.dismiss(id);
4430
+ }
4431
+
4432
+ const CANVAS_SCROLLER = '.preview-scroll';
4433
+ const PREVIEW_SCROLLER = '.iox-page-wrapper';
4434
+ /** What a page-long reading bar measures: the canvas's page frame, or Preview's page inside its scroller. */
4435
+ const CANVAS_PAGE = '#canvas-preview';
4436
+ const PREVIEW_PAGE = '.iox-page-wrapper > .page-mask';
4437
+ /**
4438
+ * Scenes on the canvas and in Preview: the SAME scene-core driver the live site runs, so a layer stands at the same
4439
+ * value at the same progress in every renderer (premium-sites plan §5b). The canvas's page never scrolls itself (its
4440
+ * Lenis scrolls a wrapper around a zoomed frame), so `position: sticky` cannot hold the stage there and the driver does;
4441
+ * Preview's page is a real scroller, and sticky works. Canvas-only state: it never writes a node, so nothing here is
4442
+ * saved or becomes an undo step.
4443
+ */
4444
+ class SceneCanvasService {
4445
+ constructor() {
4446
+ this.zone = inject(NgZone);
4447
+ this.overlay = inject(OverlayService, { optional: true });
4448
+ /** The selection holds a reading bar full (R2): a design-state change plays the next frame. */
4449
+ this.designSub = inject(CanvasDesignStateService, { optional: true })?.state$.subscribe(() => this.wake()) ?? null;
4450
+ this.driver = null;
4451
+ this.rootsById = new Map();
4452
+ this.idsByRoot = new Map();
4453
+ /** Every frame on every Scene: the selection box and the timeline follow on it. */
4454
+ this.frames$ = new Subject();
4455
+ }
4456
+ /**
4457
+ * Drive a root. `onCanvas` comes from the caller, because only the render directive can read IOX_BUILDER_CANVAS: the
4458
+ * token is provided on the canvas's element, below where this service lives. Read here, it answered false, and the
4459
+ * canvas measured its Scenes against a page that moves with them, so they never left progress 0.
4460
+ */
4461
+ attach(root, onCanvas, nodeId) {
4462
+ this.zone.runOutsideAngular(() => this.driverFor(onCanvas).add(root));
4463
+ if (nodeId) {
4464
+ this.rootsById.set(nodeId, root);
4465
+ this.idsByRoot.set(root, nodeId);
4459
4466
  }
4460
- return false;
4461
4467
  }
4462
- /** Polls via RAF until no ancestor is in pre-state, then fires callback. */
4463
- deferUntilVisible(element, callback) {
4464
- const poll = () => {
4465
- if (!this.hasPreStatedAncestor(element)) {
4466
- callback();
4467
- }
4468
- else {
4469
- requestAnimationFrame(poll);
4470
- }
4471
- };
4472
- requestAnimationFrame(poll);
4468
+ /**
4469
+ * Play an element's `scrollProgress` interaction on THIS driver — the Scenes' — never a loop of its own (premium M6,
4470
+ * N7). The live site makes the same call (`attachScrollProgress`) on its own driver. Returns the way to let go.
4471
+ */
4472
+ linkScroll(owner, interaction, options) {
4473
+ return this.zone.runOutsideAngular(() => {
4474
+ const driver = this.driverFor(options.onCanvas);
4475
+ const page = owner.closest(options.onCanvas ? CANVAS_PAGE : PREVIEW_PAGE)
4476
+ ?? owner.ownerDocument.querySelector(options.onCanvas ? CANVAS_PAGE : PREVIEW_PAGE)
4477
+ ?? owner.ownerDocument.documentElement;
4478
+ return attachScrollProgress(owner, interaction, { driver, page, targetsOf: action => options.targetsOf(action), held: options.held });
4479
+ });
4473
4480
  }
4474
- attachClick(node, ix, element) {
4475
- // G7: press FEEDBACK (an animation of the pressed element that hides and shows nothing) lands on pointer-down,
4476
- // at once; the rest commits on the click. A toggle keeps its whole list on the click (the shared split, iox-ui).
4477
- const { onPress, onClick } = ix.reverseActions?.length ? { onPress: [], onClick: ix.actions } : splitPressFeedback(ix.actions);
4478
- const handler = () => {
4479
- if (ix.reverseActions?.length) {
4480
- // Toggle: alternate between primary and reverse actions on each click.
4481
- const isToggled = this.toggleStates.get(ix.id) ?? false;
4482
- this.runActions(node, isToggled ? ix.reverseActions : ix.actions, element);
4483
- this.toggleStates.set(ix.id, !isToggled);
4484
- }
4485
- else {
4486
- this.runActions(node, onClick, element);
4487
- }
4488
- };
4489
- const press = (e) => { if (e.button === 0 || e.pointerType !== 'mouse')
4490
- this.runActions(node, onPress, element); };
4491
- // CAPTURE, like the composite runtime: on the canvas every part's own listener stops the click to select
4492
- // that part, so a click on a button's LABEL never bubbled to the button's click interaction — the live
4493
- // site runs it (CC2.1: "Open an item" on a ButtonBlock did nothing on the canvas).
4494
- element.addEventListener('click', handler, true);
4495
- if (onPress.length)
4496
- element.addEventListener('pointerdown', press, true);
4497
- return () => {
4498
- element.removeEventListener('click', handler, true);
4499
- element.removeEventListener('pointerdown', press, true);
4500
- };
4481
+ /** Something only the canvas knows moved (the selection held a reading bar full): play the next frame. */
4482
+ wake() {
4483
+ this.driver?.wake();
4501
4484
  }
4502
- attachHover(node, ix, element) {
4503
- // G7: a hover trigger answers a mouse or a pen — a finger's emulated hover stuck until the next tap elsewhere.
4504
- const enterHandler = (e) => { if (isHoverPointer(e))
4505
- this.runActions(node, ix.actions, element); };
4506
- const leaveHandler = (e) => {
4507
- if (!isHoverPointer(e))
4508
- return;
4509
- // Reverse animations on leave for hover triggers
4510
- for (const action of ix.actions) {
4511
- if (!isActionEnabled(action))
4512
- continue;
4513
- // Context actions have no "backwards" — the shared table decides what leave does
4514
- // (clear, unless the action sets noReverse). Same call the SSR engine makes.
4515
- if (isContextAction(action)) {
4516
- this.applyContextAction(action, element, 'reverse');
4517
- continue;
4518
- }
4519
- const target = this.resolveTarget(node, action, element);
4520
- if (!target)
4521
- continue;
4522
- this.reverseAction(target, action);
4523
- }
4524
- };
4525
- element.addEventListener('pointerenter', enterHandler);
4526
- element.addEventListener('pointerleave', leaveHandler);
4527
- return () => {
4528
- element.removeEventListener('pointerenter', enterHandler);
4529
- element.removeEventListener('pointerleave', leaveHandler);
4530
- };
4485
+ driverFor(onCanvas) {
4486
+ this.driver ??= new ScrollDriver({
4487
+ pin: onCanvas,
4488
+ observe: true,
4489
+ originOf: el => el.closest(onCanvas ? CANVAS_SCROLLER : PREVIEW_SCROLLER),
4490
+ view: () => viewOf(document.querySelector(onCanvas ? CANVAS_SCROLLER : PREVIEW_SCROLLER)),
4491
+ onFrame: (el, state) => this.frames$.next({ nodeId: this.idsByRoot.get(el) ?? null, ...state }),
4492
+ });
4493
+ return this.driver;
4531
4494
  }
4532
- // ── Actions ──────────────────────────────────────────────
4533
- runActions(node, actions, selfEl) {
4534
- for (const action of actions) {
4535
- if (!isActionEnabled(action))
4536
- continue; // switched off by the author
4537
- if (this.SWIPER_ACTIONS.has(action.type)) {
4538
- this.executeSwiperAction(action);
4539
- continue;
4540
- }
4541
- // Dialog actions (A1b): the canvas RUNS them — styling a dialog you cannot open is
4542
- // impossible. The canvas twin registered open/close/toggle under its nodeId, exactly
4543
- // the Swiper-method pattern; a missing/mistargeted id is a silent no-op, never a throw.
4544
- if (DIALOG_ACTION_TYPES.has(action.type)) {
4545
- this.executeDialogAction(action, selfEl);
4546
- continue;
4547
- }
4548
- // Popover actions (premium M5): the canvas RUNS them too, through the twin's registry — beside the element
4549
- // whose click ran them (the opener the live runtime anchors to).
4550
- if (POPOVER_ACTION_TYPE_SET.has(action.type)) {
4551
- this.executePopoverAction(action, selfEl);
4552
- continue;
4553
- }
4554
- // A toast (premium M5): RUN inside the page's frame — the layer of the token scope the element sits in (the
4555
- // page, or the globals' overlay), so it wears the look and zooms with the canvas.
4556
- if (TOAST_ACTION_TYPES.has(action.type)) {
4557
- showCanvasToast(action.params?.['text'], action.params?.['edge'], selfEl ?? null);
4558
- continue;
4559
- }
4560
- // Item actions (CC2.1 — composite T14): the canvas RUNS them on the target item's own runtime — the
4561
- // one the live site runs — and brings an item that opened out of view into it.
4562
- const itemAction = itemActionOf(action.type);
4563
- if (itemAction) {
4564
- this.executeItemAction(node, action, itemAction, selfEl);
4565
- continue;
4566
- }
4567
- // Tier-A behaviour (A1c). The canvas-is-live rules are catalog DATA
4568
- // (ELEMENT_ACTION_DESCRIPTORS): copy runs, scrollTo runs (scrollIntoView — the frame
4569
- // scrolls natively here; the Lenis rule is the LIVE side's), navigate and submitForm
4570
- // SIMULATE — a dispatched event the host surfaces as a toast, never a route change
4571
- // or a fake submit under the author's feet.
4572
- if (BEHAVIOUR_ACTION_TYPES.has(action.type)) {
4573
- this.executeBehaviourAction(node, action, selfEl);
4574
- continue;
4575
- }
4576
- // Tier-B server actions (A2). ALWAYS a simulation here: the canvas is the ADMIN's
4577
- // browser and a server action is a public-site write (an email, a webhook, a content
4578
- // row) — the toast says what the LIVE site would do.
4579
- if (SERVER_ELEMENT_ACTION_TYPES.has(action.type)) {
4580
- const detail = { type: action.type, params: action.params ?? {}, target: action.target };
4581
- window.dispatchEvent(new CustomEvent('iox-action-simulated', { detail }));
4582
- continue;
4583
- }
4584
- // Consent actions (C3). Recording ones SIMULATE — the canvas is the ADMIN's browser,
4585
- // and an admin's cookie is not a visitor's decision; the toast says what would be
4586
- // recorded. Opening the preferences dialog is dialog-opening and honestly RUNS.
4587
- if (CONSENT_ACTION_TYPES.has(action.type)) {
4588
- this.executeConsentAction(action);
4589
- continue;
4590
- }
4591
- // Context actions write STATE, not animation. `selfEl` is deliberate: $item is read by
4592
- // walking up from the element actually hovered/clicked, which for a repeater PREVIEW ROW
4593
- // is the clone — so each row publishes its own item, exactly as on the live site.
4594
- if (isContextAction(action)) {
4595
- this.applyContextAction(action, selfEl ?? null, 'forward');
4596
- continue;
4597
- }
4598
- // selfEl lets 'self'-targeted actions resolve for preview CLONES, which aren't
4599
- // overlay-registered (getNodeRef would return undefined) — see attachTo. (issue #2)
4600
- const target = this.resolveTarget(node, action, selfEl);
4601
- if (!target)
4602
- continue;
4603
- this.executeAction(target, action);
4604
- }
4495
+ detach(root) {
4496
+ this.driver?.remove(root);
4497
+ const id = this.idsByRoot.get(root);
4498
+ // A re-render attaches the new element before the old one detaches: it must not lose the new one.
4499
+ if (id && this.rootsById.get(id) === root)
4500
+ this.rootsById.delete(id);
4501
+ this.idsByRoot.delete(root);
4502
+ }
4503
+ /** The canvas's Lenis just moved the scroll, inside this frame: hold every stage in this same frame. */
4504
+ pinNow() {
4505
+ this.driver?.pinNow();
4506
+ }
4507
+ /** The damped progress a root's layers stand at. */
4508
+ progressOf(root) {
4509
+ return this.driver?.progressOf(root) ?? null;
4510
+ }
4511
+ /** Where the scroll stands in a Scene, 0 to 1. Null for a Scene that is not on the canvas. */
4512
+ positionOf(nodeId) {
4513
+ const root = nodeId ? this.rootsById.get(nodeId) : null;
4514
+ return root ? this.driver?.targetOf(root) ?? null : null;
4605
4515
  }
4606
4516
  /**
4607
- * Execute a context action against the shared store. The forward/reverse semantics come from
4608
- * `contextActionEffect` in @vectoriox/iox-ui — the same function the SSR engine calls, so the
4609
- * builder canvas and the live site behave identically.
4517
+ * Stand a Scene at `p` by really scrolling the canvas: there is one position, so the scrubber, the wheel, the pin and
4518
+ * the selection box can never disagree. It lands with no glide, because the author is waiting to see that moment.
4610
4519
  */
4611
- applyContextAction(action, el, pass) {
4612
- const effect = contextActionEffect(action, pass);
4613
- if (!effect)
4614
- return;
4615
- if (effect.op === 'clear') {
4616
- this.contextStore.clear(effect.slot);
4520
+ goTo(nodeId, p) {
4521
+ const root = nodeId ? this.rootsById.get(nodeId) : null;
4522
+ const offset = root ? this.driver?.offsetFor(root, p) : null;
4523
+ const scroller = root?.closest(CANVAS_SCROLLER);
4524
+ if (!root || offset == null || !scroller)
4617
4525
  return;
4618
- }
4619
- const value = resolveContextValue(effect.value, {
4620
- item: this.rowItemFor(el),
4621
- resolvedData: this.contextData,
4622
- });
4623
- this.contextStore.set(effect.slot, value);
4526
+ const rect = root.getBoundingClientRect();
4527
+ const zoom = root.offsetHeight ? rect.height / root.offsetHeight : 1;
4528
+ scroller.scrollTop += rect.top - scroller.getBoundingClientRect().top + offset * zoom;
4529
+ this.driver?.settle(root);
4624
4530
  }
4625
- /** The repeater item this element belongs to — the nearest marked row ancestor. */
4626
- rowItemFor(el) {
4627
- const row = el?.closest?.('[data-iox-row-idx]');
4628
- if (!row)
4629
- return undefined;
4630
- const source = row.getAttribute('data-iox-row-src') ?? '';
4631
- const index = Number(row.getAttribute('data-iox-row-idx'));
4632
- const items = this.contextData?.[source];
4633
- return Array.isArray(items) && Number.isInteger(index) ? items[index] : undefined;
4531
+ /**
4532
+ * A layer's tracks were edited: its element carries them now and its Scene plays them on the next frame, with no
4533
+ * re-render, because a Scene's own DOM is what the author is looking at.
4534
+ */
4535
+ refreshLayer(node, root) {
4536
+ const ref = this.overlay?.getNodeRef(node) ?? (node.id ? this.overlay?.findRefByNodeId(node.id)?.ref : undefined);
4537
+ if (ref?.element && this.overlay)
4538
+ syncSceneAttributes(this.overlay.getStyledElement(ref.element), node);
4539
+ const el = root?.id ? this.rootsById.get(root.id) : null;
4540
+ if (el)
4541
+ this.driver?.reread(el);
4634
4542
  }
4635
- /** Resolved data for the canvas, kept current by the repeater as it fetches. */
4636
- setContextData(data) {
4637
- // MERGE, not replace — a page can hold several repeaters, each publishing its own source.
4638
- this.contextData = { ...this.contextData, ...(data ?? {}) };
4543
+ /**
4544
+ * A value a layer's own component draws changed (a Video's pictures): its instance takes it now. Its Scene reads the new
4545
+ * attribute when it lands, with no re-render of the page.
4546
+ */
4547
+ setLayerInput(node, name, value) {
4548
+ const ref = this.overlay?.getNodeRef(node) ?? (node.id ? this.overlay?.findRefByNodeId(node.id)?.ref : undefined);
4549
+ const component = ref?.componentRef;
4550
+ if (!component?.instance)
4551
+ return;
4552
+ component.instance[name] = value;
4553
+ component.changeDetectorRef.detectChanges();
4639
4554
  }
4640
- executeBehaviourAction(node, action, selfEl) {
4641
- switch (action.type) {
4642
- case 'copyToClipboard': {
4643
- const text = String(action.params?.['text'] ?? '');
4644
- if (text)
4645
- navigator.clipboard?.writeText(text).catch(() => { });
4646
- return;
4647
- }
4648
- case 'scrollTo': {
4649
- const target = this.resolveTarget(node, action, selfEl);
4650
- target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
4651
- return;
4652
- }
4653
- case 'navigate':
4654
- case 'submitForm': {
4655
- // Simulated: the host (the admin app) listens and shows "would …" as a toast.
4656
- const detail = { type: action.type, params: action.params ?? {}, target: action.target };
4657
- window.dispatchEvent(new CustomEvent('iox-action-simulated', { detail }));
4658
- return;
4659
- }
4660
- }
4555
+ ngOnDestroy() {
4556
+ this.designSub?.unsubscribe();
4557
+ this.driver?.destroy();
4558
+ this.driver = null;
4559
+ this.rootsById.clear();
4560
+ this.idsByRoot.clear();
4561
+ this.frames$.complete();
4661
4562
  }
4662
- executeItemAction(node, action, itemAction, selfEl) {
4663
- const target = this.resolveTarget(node, action, selfEl);
4664
- const open = runCompositeItemAction(target, itemAction);
4665
- if (!open || !target)
4666
- return;
4667
- if (shouldScrollToItem(target.getBoundingClientRect().top, window.innerHeight))
4668
- target.scrollIntoView({ behavior: 'smooth', block: 'start' });
4563
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SceneCanvasService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4564
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SceneCanvasService }); }
4565
+ }
4566
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SceneCanvasService, decorators: [{
4567
+ type: Injectable
4568
+ }] });
4569
+ /** The part of the page on screen: the scroller's own rectangle (the canvas's, Preview's). */
4570
+ function viewOf(scroller) {
4571
+ if (!scroller)
4572
+ return null;
4573
+ const rect = scroller.getBoundingClientRect();
4574
+ return { top: rect.top, height: rect.height };
4575
+ }
4576
+
4577
+ /** What the canvas never writes inline (a transform inside the scaled page breaks Lenis). */
4578
+ const CANVAS_SKIPPED_PROPS = new Set(['transform']);
4579
+ /**
4580
+ * Wires ComponentNode interactions to live DOM elements via the
4581
+ * OverlayService node-element registry.
4582
+ *
4583
+ * Call `attach(node)` after the node is rendered and registered.
4584
+ * Call `detach(node)` on destroy to tear down listeners/observers.
4585
+ */
4586
+ class InteractionEngineService {
4587
+ constructor(overlayService) {
4588
+ this.overlayService = overlayService;
4589
+ this.attached = new Map();
4590
+ /** Shared with the SSR engine — one store type, one set of semantics. */
4591
+ this.contextStore = inject(ContextStore);
4592
+ /** Resolved data for the canvas, so a row marker (source + index) resolves back to its item. */
4593
+ this.contextData = {};
4594
+ /** Elements that have had pre-state applied (hidden by attach). */
4595
+ this.preStatedElements = new Set();
4596
+ /**
4597
+ * Toggle state for click interactions with reverseActions.
4598
+ * false = next click runs actions (primary); true = next click runs reverseActions.
4599
+ */
4600
+ this.toggleStates = new Map();
4601
+ /**
4602
+ * Pre-states queued for elements that weren't registered yet when the owner
4603
+ * node's attach() ran. Keyed by target node ID. Consumed when the target
4604
+ * element's own attach() fires.
4605
+ */
4606
+ this.pendingPreStates = new Map();
4607
+ /** Action types that animate an element from hidden → visible / visible → hidden — the SHARED sets
4608
+ * (@vectoriox/iox-ui), the same ones the live site's runtime reads. */
4609
+ this.ENTRANCE_TYPES = ENTRANCE_ACTION_TYPES;
4610
+ this.EXIT_TYPES = EXIT_ACTION_TYPES;
4611
+ this.SWIPER_ACTIONS = new Set([
4612
+ 'swiperNext', 'swiperPrev', 'swiperGoToSlide',
4613
+ 'swiperAutoplayStart', 'swiperAutoplayStop', 'swiperAutoplayPause',
4614
+ ]);
4615
+ this.instanceRegistry = inject(ComponentInstanceRegistryService);
4616
+ this.ngZone = inject(NgZone);
4617
+ /** The ONE scroll loop of this canvas (its Scenes' driver): `scrollProgress` interactions ride it as links (M6, N7). */
4618
+ this.sceneCanvas = inject(SceneCanvasService, { optional: true });
4619
+ this.designState = inject(CanvasDesignStateService, { optional: true });
4620
+ /** Numbers armed to count (M6, N5): put back when the node lets go. */
4621
+ this.armedCounts = new Map();
4669
4622
  }
4670
- executeConsentAction(action) {
4671
- if (action.type === 'openConsentPreferences') {
4672
- const dialog = action.target && action.target !== 'self' ? this.instanceRegistry.get(action.target) : null;
4673
- dialog?.open?.();
4623
+ /**
4624
+ * Run all DOM-manipulation operations (pre-state, observers, animations)
4625
+ * outside the Angular zone so Zone.js does not schedule unnecessary CD ticks
4626
+ * for each RAF/IntersectionObserver/animation callback.
4627
+ * The caller is responsible for calling markForCheck() / refreshSelect() inside
4628
+ * the zone after this returns.
4629
+ */
4630
+ runOutside(fn) {
4631
+ return this.ngZone.runOutsideAngular(fn);
4632
+ }
4633
+ /** Wire all interactions for a node to its rendered DOM element. */
4634
+ attach(node) { this.runOutside(() => this._attach(node)); }
4635
+ /** Attach interactions to a node whose element is NOT overlay-registered — e.g. repeater/slider
4636
+ * PREVIEW CLONES, which are non-selectable but must still animate (parity with the SSR runtime,
4637
+ * which animates every repeated row). See RenderDirective. */
4638
+ attachTo(node, element) { this.runOutside(() => this._attach(node, element)); }
4639
+ _attach(node, elementOverride) {
4640
+ const element = elementOverride ?? this.overlayService.getNodeRef(node)?.element;
4641
+ if (!element)
4674
4642
  return;
4643
+ // Apply any pre-states queued for THIS element by earlier-registered nodes.
4644
+ // Pending entries are only added when appropriate (see loop below), so always consume.
4645
+ if (node.id) {
4646
+ const pending = this.pendingPreStates.get(node.id);
4647
+ if (pending) {
4648
+ for (const action of pending) {
4649
+ this.applyPreState(element, action);
4650
+ }
4651
+ this.pendingPreStates.delete(node.id);
4652
+ }
4675
4653
  }
4676
- const detail = { type: action.type, params: action.params ?? {}, target: action.target };
4677
- window.dispatchEvent(new CustomEvent('iox-action-simulated', { detail }));
4678
- }
4679
- executeDialogAction(action, selfEl) {
4680
- const dialog = action.target && action.target !== 'self' ? this.instanceRegistry.get(action.target) : null;
4681
- if (!dialog)
4654
+ // What its interactions say about its first paint (tabular digits, an empty reading bar) — the same markers every
4655
+ // renderer writes, kept in step with the Interactions panel.
4656
+ syncInteractionMarkers(element, node);
4657
+ if (!node.interactions?.length)
4682
4658
  return;
4683
- switch (action.type) {
4684
- case 'openDialog':
4685
- dialog.open?.();
4686
- // CC3 "Open at the clicked item": the lightbox's slider goes to the clicked row's slide — the same
4687
- // rule the live site runs (composite-core `slideIndexForRow`), once the canvas twin has rendered it.
4688
- if (opensAtClickedItem(action))
4689
- this.slideDialogToRow(action.target, rowIndexOf$1(selfEl));
4690
- break;
4691
- case 'closeDialog':
4692
- dialog.close?.();
4693
- break;
4694
- case 'toggleDialog':
4695
- dialog.toggle?.();
4696
- break;
4659
+ const cleanups = [];
4660
+ for (const ix of node.interactions) {
4661
+ // Pre-state: hide target elements before their entrance animation fires so
4662
+ // the user sees a real hidden→visible animation on the canvas. Every trigger
4663
+ // runs live in the builder (see attachInteraction).
4664
+ // Under reduced motion a self-playing entrance starts shown and never moves (shared `entranceStartsHidden`).
4665
+ // A scroll-played entrance never starts hidden either (shared: the scroll stands it — M6).
4666
+ const skipPreState = !entranceStartsHidden(ix.trigger);
4667
+ if (!skipPreState) {
4668
+ for (const action of ix.actions) {
4669
+ // A disabled action must not pre-state: it would hide the element with nothing
4670
+ // left to reveal it.
4671
+ if (!isActionEnabled(action))
4672
+ continue;
4673
+ if (this.ENTRANCE_TYPES.has(action.type)) {
4674
+ const target = this.resolveTarget(node, action, element);
4675
+ if (target) {
4676
+ this.applyPreState(target, action);
4677
+ }
4678
+ else if (action.target && action.target !== 'self') {
4679
+ const list = this.pendingPreStates.get(action.target) ?? [];
4680
+ list.push(action);
4681
+ this.pendingPreStates.set(action.target, list);
4682
+ }
4683
+ }
4684
+ }
4685
+ }
4686
+ const cleanup = this.attachInteraction(node, ix, element);
4687
+ cleanups.push({ node, interaction: ix, cleanup });
4697
4688
  }
4689
+ this.armCounts(node, element);
4690
+ this.attached.set(node, cleanups);
4698
4691
  }
4699
- executePopoverAction(action, selfEl) {
4700
- const popover = action.target && action.target !== 'self' ? this.instanceRegistry.get(action.target) : null;
4701
- if (!popover)
4702
- return;
4703
- switch (action.type) {
4704
- case 'openPopover':
4705
- popover.open?.(selfEl ?? null);
4706
- break;
4707
- case 'closePopover':
4708
- popover.close?.();
4709
- break;
4710
- case 'togglePopover':
4711
- popover.toggle?.(selfEl ?? null);
4712
- break;
4692
+ /** Tear down all interaction listeners for a node and reset its animated state. */
4693
+ detach(node) { this.runOutside(() => this._detach(node)); }
4694
+ _detach(node) {
4695
+ for (const el of this.armedCounts.get(node) ?? [])
4696
+ restoreCountUp(el);
4697
+ this.armedCounts.delete(node);
4698
+ const entries = this.attached.get(node);
4699
+ if (!entries)
4700
+ return;
4701
+ for (const entry of entries) {
4702
+ entry.cleanup();
4713
4703
  }
4714
- }
4715
- /** The slider inside the opened dialog (its canvas twin renders on the next passes) → the row's slide. */
4716
- slideDialogToRow(dialogId, row) {
4717
- let tries = 0;
4718
- const attempt = () => {
4719
- const host = document.querySelector(`.iox-node-${CSS.escape(dialogId)}`);
4720
- const swiper = host?.querySelector('.swiper')?.swiper;
4721
- if (swiper?.slides?.length) {
4722
- swiper.update?.();
4723
- swiper.slideTo(slideIndexForRow(row, swiper.slides.length), 0);
4724
- return;
4704
+ this.attached.delete(node);
4705
+ // Reset toggle state so re-attach starts from the primary actions.
4706
+ for (const entry of entries) {
4707
+ if (entry.interaction.reverseActions?.length) {
4708
+ this.toggleStates.delete(entry.interaction.id);
4725
4709
  }
4726
- if (++tries < 40)
4727
- requestAnimationFrame(attempt);
4728
- };
4729
- requestAnimationFrame(attempt);
4730
- }
4731
- executeSwiperAction(action) {
4732
- // Target is the nodeId of the SliderContainer to control.
4733
- const swiper = this.instanceRegistry.get(action.target);
4734
- if (!swiper)
4735
- return;
4736
- switch (action.type) {
4737
- case 'swiperNext':
4738
- swiper.slideNext();
4739
- break;
4740
- case 'swiperPrev':
4741
- swiper.slidePrev();
4742
- break;
4743
- case 'swiperGoToSlide':
4744
- swiper.slideTo(action.params?.['index'] ?? 0);
4745
- break;
4746
- case 'swiperAutoplayStart':
4747
- swiper.autoplay?.start();
4748
- break;
4749
- case 'swiperAutoplayStop':
4750
- swiper.autoplay?.stop();
4751
- break;
4752
- case 'swiperAutoplayPause':
4753
- swiper.autoplay?.pause();
4754
- break;
4755
4710
  }
4756
- }
4757
- resolveTarget(ownerNode, action, selfEl) {
4758
- if (action.target === 'self' || !action.target) {
4759
- return selfEl ?? this.overlayService.getNodeRef(ownerNode)?.element ?? null;
4711
+ // Cancel Web Animations and strip any inline pre-state styles that
4712
+ // were applied by applyPreState() so the element returns to its natural CSS state.
4713
+ const ref = this.overlayService.getNodeRef(node);
4714
+ if (ref) {
4715
+ ref.element.getAnimations().forEach(a => a.cancel());
4716
+ this.clearInlineAnimationStyles(ref.element);
4760
4717
  }
4761
- // Find by node ID
4762
- for (const [n, ref] of this.overlayService.getAllNodeEntries()) {
4763
- if (n.id === action.target)
4764
- return ref.element;
4718
+ // Also clean up pre-state on any target elements this node was animating.
4719
+ if (node.interactions) {
4720
+ for (const ix of node.interactions) {
4721
+ for (const action of ix.actions) {
4722
+ // Remove any pending pre-states for unregistered targets.
4723
+ if (action.target && action.target !== 'self') {
4724
+ const pending = this.pendingPreStates.get(action.target);
4725
+ if (pending) {
4726
+ const filtered = pending.filter(a => a !== action);
4727
+ if (filtered.length) {
4728
+ this.pendingPreStates.set(action.target, filtered);
4729
+ }
4730
+ else {
4731
+ this.pendingPreStates.delete(action.target);
4732
+ }
4733
+ }
4734
+ }
4735
+ // Clean up inline styles on already-registered target elements.
4736
+ const target = this.resolveTarget(node, action);
4737
+ if (target && this.preStatedElements.has(target)) {
4738
+ target.getAnimations().forEach(a => a.cancel());
4739
+ this.clearInlineAnimationStyles(target);
4740
+ this.preStatedElements.delete(target);
4741
+ }
4742
+ }
4743
+ }
4765
4744
  }
4766
- return null;
4767
4745
  }
4768
- executeAction(element, action) {
4769
- // toggleVisibility HIDES a visible element and SHOWS a hidden one — resolved against the element's
4770
- // own state, the same rule the live site applies (shared `effectiveActionType`, CC1 N5).
4771
- const type = effectiveActionType(action.type, element.hasAttribute(INTERACTION_HIDDEN_ATTRIBUTE));
4772
- const keyframes = this.keyframesFor(type, action);
4773
- if (!keyframes.length)
4774
- return;
4775
- if (this.ENTRANCE_TYPES.has(type))
4776
- element.removeAttribute(INTERACTION_HIDDEN_ATTRIBUTE);
4777
- else if (this.EXIT_TYPES.has(type))
4778
- element.setAttribute(INTERACTION_HIDDEN_ATTRIBUTE, '');
4779
- // Reduced motion (CC1 R1): no animation runs — the element is left as the run would leave it (the shared settle
4780
- // rule: an entrance releases to the stylesheet, an exit holds its hidden frame).
4781
- if (!motionAllowed()) {
4782
- settleInteraction(element, keyframes, settleModeFor(type), this.settleOptions());
4783
- this.settleAfter(element, type);
4746
+ /** Re-attach interactions after they have been edited in the panel. */
4747
+ refresh(node) {
4748
+ this.runOutside(() => { this._detach(node); this._attach(node); });
4749
+ }
4750
+ /**
4751
+ * Replay all interactions on a node immediately, regardless of their trigger.
4752
+ * Used by the builder overlay play button for in-editor animation preview.
4753
+ * Uses fill:'none' so the element returns to its natural state after the preview.
4754
+ */
4755
+ replay(node) { this.runOutside(() => this._replay(node)); }
4756
+ _replay(node) {
4757
+ if (!node.interactions?.length)
4784
4758
  return;
4759
+ const ref = this.overlayService.getNodeRef(node);
4760
+ if (ref) {
4761
+ ref.element.getAnimations().forEach(a => a.cancel());
4762
+ // Clear any leftover inline styles so the pre-state applies cleanly.
4763
+ this.clearInlineAnimationStyles(ref.element);
4785
4764
  }
4786
- // The ONE call the live site makes too: fill both (holds the first frame through a delay), started from the live
4787
- // value, settled by the shared rule — an element that has entered keeps its hover and press (iox-ui#41). A look
4788
- // entrance on a Repeater's template staggers its rows: each copy adds its row's share (shared `entranceDelay`).
4789
- const anim = playInteraction(element, { type, params: action.params }, keyframes, {
4790
- duration: action.duration,
4791
- delay: entranceDelay(action, repeatedRowIndex(element)),
4792
- // A keyword, a curve, or the look's easing token as this element sees it (G6) — the live site's resolver.
4793
- easing: resolveInteractionEasing(action.easing, element),
4794
- }, this.settleOptions());
4795
- anim.finished.then(() => this.settleAfter(element, type)).catch(() => { });
4796
- }
4797
- /** On the canvas a run never writes a transform inline (one inside the scaled page breaks Lenis). */
4798
- settleOptions() {
4799
- return this.isBuilderMode() ? { skip: CANVAS_SKIPPED_PROPS } : {};
4800
- }
4801
- /** What an element is left as once an action is done: a revealed one interactive, a hidden one inert. */
4802
- settleAfter(element, type) {
4803
- if (this.ENTRANCE_TYPES.has(type)) {
4804
- element.style.removeProperty('pointer-events');
4805
- this.preStatedElements.delete(element);
4806
- // Builder: clear inline pre-state so cancelling animations later
4807
- // doesn't leave the element invisible after a play preview.
4808
- if (this.isBuilderMode())
4809
- this.clearInlineAnimationStyles(element);
4765
+ // In builder mode pre-state is never applied on attach, so apply it now
4766
+ // to give an accurate hidden→visible preview. It is cleaned up via
4767
+ // executeAction's anim.finished handler once the animation completes.
4768
+ for (const ix of node.interactions) {
4769
+ for (const action of ix.actions) {
4770
+ if (this.ENTRANCE_TYPES.has(action.type)) {
4771
+ const target = this.resolveTarget(node, action);
4772
+ if (target)
4773
+ this.applyPreState(target, action);
4774
+ }
4775
+ }
4810
4776
  }
4811
- else if (this.EXIT_TYPES.has(type)) {
4812
- // Once the exit is done the element is invisible — disable pointer events so it doesn't
4813
- // silently block clicks underneath it.
4814
- element.style.setProperty('pointer-events', 'none');
4777
+ for (const ix of node.interactions) {
4778
+ this.runActions(node, ix.actions);
4815
4779
  }
4816
4780
  }
4817
4781
  /**
4818
- * Freeze an element at the animation's starting frame immediately, before any
4819
- * trigger fires. Called for ALL entrance-type actions regardless of trigger so
4820
- * that a click-triggered fadeIn on a menu overlay also starts it hidden.
4821
- *
4822
- * In builder mode only opacity:0 is applied — transforms on canvas elements
4823
- * inside iox-page-component's scale() transform break Lenis scroll. The transform
4824
- * jump at animation t=0 is invisible because the element is already opacity:0.
4825
- * In production the full keyframe is applied + pointer-events:none.
4782
+ * Fire all interactions on the given node that match the specified trigger.
4783
+ * Used by Swiper (and future components) to dispatch their events into the
4784
+ * interaction engine without coupling to the engine's attach/detach lifecycle.
4826
4785
  */
4827
- applyPreState(element, action) {
4828
- const keyframes = this.keyframesFor(action.type, action);
4829
- if (!keyframes.length)
4786
+ replayTrigger(nodeId, trigger) { this.runOutside(() => this._replayTrigger(nodeId, trigger)); }
4787
+ _replayTrigger(nodeId, trigger) {
4788
+ for (const [node] of this.attached) {
4789
+ if (node.id !== nodeId)
4790
+ continue;
4791
+ for (const ix of (node.interactions ?? [])) {
4792
+ if (ix.trigger === trigger) {
4793
+ this.runActions(node, ix.actions);
4794
+ }
4795
+ }
4830
4796
  return;
4831
- const first = entrancePreStateStyle(action.type, action.params);
4832
- if (this.isBuilderMode()) {
4833
- // No transforms — inline transforms on children inside iox-page-component's
4834
- // scale() break Lenis scroll. pointer-events:none lets wheel events and
4835
- // hover fall through to visible elements underneath. An entrance that hides
4836
- // nothing (a picture settling) stays in view.
4837
- if ('opacity' in first || 'visibility' in first)
4838
- element.style.setProperty('opacity', '0');
4839
- element.style.setProperty('pointer-events', 'none');
4840
4797
  }
4841
- else {
4842
- for (const [prop, val] of Object.entries(first))
4843
- element.style.setProperty(prop, val);
4844
- element.style.setProperty('pointer-events', 'none');
4798
+ }
4799
+ // ── Private ──────────────────────────────────────────────
4800
+ attachInteraction(node, ix, element) {
4801
+ // All triggers run live in the builder canvas so the user sees the real
4802
+ // animation (pageLoad on attach, viewportEnter on scroll-into-view, click/hover).
4803
+ // The former NG0103 crash was caused by (a) Swiper's ResizeObserver created inside
4804
+ // the Angular zone — fixed in SliderContainerComponent via runOutsideAngular — and
4805
+ // (b) the overlay's Resize/MutationObservers synchronously re-dirtying the view
4806
+ // during synchronize() — fixed by coalescing overlay updates through a RAF.
4807
+ // scrollProgress is still unimplemented and returns a no-op below.
4808
+ switch (ix.trigger) {
4809
+ case 'pageLoad':
4810
+ return this.attachPageLoad(node, ix, element);
4811
+ case 'pageEnter':
4812
+ // In the builder canvas there is no navigation, so preview a page-enter the same
4813
+ // way as page-load (pre-stated hidden → animate in). In production the SSR engine
4814
+ // fires it only after the page transition completes (ClientInteractionService).
4815
+ return this.attachPageLoad(node, ix, element);
4816
+ case 'pageLeave':
4817
+ // Exit choreography that plays on navigate-away. There is no navigation in the
4818
+ // builder, so it does not auto-fire (element stays visible) — the overlay play
4819
+ // button previews it via replay(). In production the orchestrator awaits it.
4820
+ return () => { };
4821
+ case 'viewportEnter':
4822
+ return this.attachViewportEnter(node, ix, element);
4823
+ case 'click':
4824
+ return this.attachClick(node, ix, element);
4825
+ case 'hover':
4826
+ return this.attachHover(node, ix, element);
4827
+ case 'scrollProgress':
4828
+ return this.attachScrollProgress(node, ix, element);
4829
+ case 'itemOn':
4830
+ case 'itemOff':
4831
+ return this.attachItemState(node, ix, element, ix.trigger === 'itemOn');
4832
+ default:
4833
+ return () => { };
4845
4834
  }
4846
- element.setAttribute(INTERACTION_HIDDEN_ATTRIBUTE, '');
4847
- this.preStatedElements.add(element);
4848
4835
  }
4849
- clearInlineAnimationStyles(element) {
4850
- element.style.removeProperty('opacity');
4851
- element.style.removeProperty('transform');
4852
- element.style.removeProperty('clip-path');
4853
- element.style.removeProperty('visibility');
4854
- element.style.removeProperty('pointer-events');
4855
- element.removeAttribute(INTERACTION_HIDDEN_ATTRIBUTE);
4836
+ /**
4837
+ * `scrollProgress` (premium M6, N7): the actions follow where the element stands in the canvas's page, on the canvas's
4838
+ * ONE scroll loop — the Scenes' driver (iox-ui `attachScrollProgress`, the live site's same call). While a reading bar
4839
+ * is selected (R2) it stands full, so it can be seen and styled.
4840
+ */
4841
+ attachScrollProgress(node, ix, element) {
4842
+ if (!this.sceneCanvas || ix.trigger !== SCROLL_PROGRESS_TRIGGER)
4843
+ return () => { };
4844
+ return this.sceneCanvas.linkScroll(element, ix, {
4845
+ onCanvas: this.isBuilderMode(),
4846
+ targetsOf: action => {
4847
+ const target = this.resolveTarget(node, action, element);
4848
+ return target ? [target] : [];
4849
+ },
4850
+ held: () => (node.id && this.designState?.isShown(node.id) ? 1 : null),
4851
+ });
4856
4852
  }
4857
- /** A hover-out: the action played back from where the element IS (never from its end frame), settled by the shared
4858
- * rule — played back to hidden, it holds hidden; to shown, it releases. */
4859
- reverseAction(element, action) {
4860
- const type = effectiveActionType(action.type, element.hasAttribute(INTERACTION_HIDDEN_ATTRIBUTE));
4861
- const keyframes = this.keyframesFor(type, action);
4862
- if (!keyframes.length)
4863
- return;
4864
- const frames = [...keyframes].reverse();
4865
- if (!motionAllowed()) {
4866
- settleInteraction(element, frames, settleModeFor(type, true), this.settleOptions());
4867
- return;
4868
- }
4869
- playInteraction(element, { type, params: action.params }, frames, {
4870
- duration: action.duration,
4871
- easing: resolveInteractionEasing(action.easing, element),
4872
- }, { ...this.settleOptions(), reverse: true });
4853
+ /**
4854
+ * A count (premium M6, N5) shows its start value once armed, so it can count when it comes into view; armed after
4855
+ * every pre-state, so a number its entrance hides is armed and one the author can already read keeps its value.
4856
+ */
4857
+ armCounts(node, element) {
4858
+ const armed = armInteractionCounts(node.interactions, action => this.resolveTarget(node, action, element));
4859
+ if (armed.length)
4860
+ this.armedCounts.set(node, armed);
4873
4861
  }
4874
- /** The action's keyframes from the ONE shared table both runtimes animate from (composite T2). */
4875
- keyframesFor(type, action) {
4876
- return buildInteractionKeyframes(type, action.params) ?? [];
4862
+ /**
4863
+ * `itemOn` / `itemOff` (CC2): run when the part's accordion ITEM opens / closes — announced by the shared
4864
+ * composite-core runtime on the item, on the canvas as on the site. An item already open when the
4865
+ * listener attaches has opened: its `itemOn` runs at once (its entrance was pre-stated hidden).
4866
+ */
4867
+ attachItemState(node, ix, element, on) {
4868
+ const run = () => this.runActions(node, ix.actions, element);
4869
+ const off = onCompositeItemState(element, on, run);
4870
+ const timer = on && isCompositeItemOn(element) ? setTimeout(run, 50) : null;
4871
+ return () => { off(); if (timer)
4872
+ clearTimeout(timer); };
4877
4873
  }
4878
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService, deps: [{ token: OverlayService }], target: i0.ɵɵFactoryTarget.Injectable }); }
4879
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService }); }
4880
- }
4881
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService, decorators: [{
4882
- type: Injectable
4883
- }], ctorParameters: () => [{ type: OverlayService }] });
4884
- /** The canvas's token scopes: the page frame and the globals' overlay (each declares the org's tokens). */
4885
- const CANVAS_TOKEN_SCOPE = '.iox-token-scope';
4886
- /** The floating layer's home on the canvas for an element: the token scope (the zoomed frame) it sits in. */
4887
- function canvasFloatingContainer(from) {
4888
- return from?.closest?.(CANVAS_TOKEN_SCOPE) ?? from?.ownerDocument?.querySelector(CANVAS_TOKEN_SCOPE) ?? null;
4889
- }
4890
- /** Raise a toast on the canvas (a showToast action the canvas runs): inside the frame, never over the builder. */
4891
- function showCanvasToast(text, edge, from) {
4892
- const doc = from?.ownerDocument ?? (typeof document !== 'undefined' ? document : null);
4893
- showToast(doc, typeof text === 'string' ? text : '', { edge: typeof edge === 'string' ? edge : null, from, topLayer: false, container: canvasFloatingContainer(from) });
4894
- }
4895
-
4896
- // Virtual trait composition now lives in @vectoriox/iox-ui so the SSR engine (which does NOT depend
4897
- // on iox-builder) can use the SAME logic — otherwise the engine emitted raw virtual keys
4898
- // (translate-x, filter-blur…) as invalid CSS. This is a re-export passthrough to keep existing
4899
- // iox-builder imports (`../style-compose`) working with no duplicated implementation.
4900
-
4901
- /**
4902
- * Parts that are HIDDEN AT REST (complex-components R2, board 3): on the live site they are not on
4903
- * screen until something opens them, so on the canvas they show only while selected into or switched
4904
- * open ("Showing on canvas"). The layer tree marks them with the eye-slash glyph. A Dialog (the mobile
4905
- * menu's drawer, a consent banner) by its type; an Accordion's PANEL by its role (CC2 — closed until its
4906
- * item opens); a Disclosure's panel and its "Read less" word (CC2.1 — shown only while it is open).
4907
- */
4908
- const HIDDEN_AT_REST_TYPES = ['Dialog', 'Popover'];
4909
- const HIDDEN_AT_REST_ROLES = [AccordionRole.Panel, DisclosureRole.Panel, DisclosureRole.Less];
4910
- function isHiddenAtRest(node) {
4911
- return (!!node?.type && HIDDEN_AT_REST_TYPES.includes(node.type)) || (!!node?.role && HIDDEN_AT_REST_ROLES.includes(node.role));
4912
- }
4913
- /**
4914
- * How the selection shows a node hidden at rest — the two approved rules, by what the node IS:
4915
- * - a PART of a composite (it carries a `role`: the mobile menu's drawer) follows R2 (board 3): the
4916
- * selection is the reveal, and moving it out of the part hides it again;
4917
- * - a free-standing overlay (a plain Dialog) follows board A4: selecting it opens it, and it stays open
4918
- * while the author works elsewhere, until its X, its backdrop or the eye closes it.
4919
- */
4920
- var SelectionReveal;
4921
- (function (SelectionReveal) {
4922
- SelectionReveal["WhileSelected"] = "while-selected";
4923
- SelectionReveal["UntilClosed"] = "until-closed";
4924
- })(SelectionReveal || (SelectionReveal = {}));
4925
- /**
4926
- * Parts hidden at rest that live in a composite's TOGGLE, not in its panel (CC2.1 follow-up, approved 2026-10-21): a
4927
- * click on one on the canvas toggles the composite live — selecting it never forces the reveal, so "Read less"
4928
- * closes the disclosure it was clicked in. Only a part inside a PANEL is revealed by the selection.
4929
- */
4930
- const TOGGLE_PART_ROLES = [DisclosureRole.Less];
4931
- /**
4932
- * Parts the selection REVEALS though they are not hidden at rest (CC4a): a tab PANEL — one is always on, so the layer
4933
- * tree draws it no eye-slash (board 3), but selecting anything inside an inactive one shows its tab while selected.
4934
- */
4935
- const REVEALED_WHILE_SELECTED_ROLES = [TabsRole.Panel];
4936
- function selectionRevealOf(node) {
4937
- if (!!node?.role && REVEALED_WHILE_SELECTED_ROLES.includes(node.role))
4938
- return SelectionReveal.WhileSelected;
4939
- if (!isHiddenAtRest(node) || (!!node?.role && TOGGLE_PART_ROLES.includes(node.role)))
4940
- return null;
4941
- return node?.role ? SelectionReveal.WhileSelected : SelectionReveal.UntilClosed;
4942
- }
4943
- /** Root → `target`, inclusive, across `roots` (page layout and globals), or [] when it is not there. */
4944
- function pathToNode(roots, target) {
4945
- for (const root of roots) {
4946
- if (root === target)
4947
- return [root];
4948
- const below = pathToNode(root.children ?? [], target);
4949
- if (below.length)
4950
- return [root, ...below];
4874
+ // ── Triggers ─────────────────────────────────────────────
4875
+ attachPageLoad(node, ix, element) {
4876
+ const timer = setTimeout(() => {
4877
+ if (this.hasPreStatedAncestor(element)) {
4878
+ this.deferUntilVisible(element, () => this.runActions(node, ix.actions, element));
4879
+ }
4880
+ else {
4881
+ this.runActions(node, ix.actions, element);
4882
+ }
4883
+ }, 50);
4884
+ return () => clearTimeout(timer);
4951
4885
  }
4952
- return [];
4953
- }
4954
-
4955
- const NAV_CONTEXT_ROLES = new Set([NavRole.Root, NavRole.Drawer, NavRole.Bar]);
4956
- function findRole(nodes, role) {
4957
- for (const n of nodes) {
4958
- if (n.role === role)
4959
- return n;
4960
- const below = findRole(n.children ?? [], role);
4961
- if (below)
4962
- return below;
4886
+ attachViewportEnter(node, ix, element) {
4887
+ const firedSet = new Set();
4888
+ const fire = () => {
4889
+ const key = ix.id;
4890
+ if (ix.actions.some(a => a.once) && firedSet.has(key))
4891
+ return;
4892
+ if (this.hasPreStatedAncestor(element)) {
4893
+ this.deferUntilVisible(element, () => {
4894
+ if (ix.actions.some(a => a.once) && firedSet.has(key))
4895
+ return;
4896
+ firedSet.add(key);
4897
+ this.runActions(node, ix.actions, element);
4898
+ });
4899
+ return;
4900
+ }
4901
+ firedSet.add(key);
4902
+ this.runActions(node, ix.actions, element);
4903
+ };
4904
+ // IntersectionObserver with no root uses the browser viewport.
4905
+ // It respects overflow:hidden clipping from .preview-scroll so it fires
4906
+ // correctly as Lenis scrolls (Lenis changes scrollTop on .preview-scroll).
4907
+ const observer = new IntersectionObserver((entries) => {
4908
+ for (const entry of entries) {
4909
+ if (entry.isIntersecting) {
4910
+ cleanup();
4911
+ fire();
4912
+ }
4913
+ }
4914
+ }, { threshold: 0.1 });
4915
+ observer.observe(element);
4916
+ // Builder: also listen to Lenis scroll events on .preview-lenis-content
4917
+ // as a fallback in case IntersectionObserver misses elements behind a
4918
+ // CSS transform (scale) on iox-page-component.
4919
+ const lenisEl = this.overlayService.getScrollContainer();
4920
+ let scrollFired = false;
4921
+ const onScroll = () => {
4922
+ if (scrollFired)
4923
+ return;
4924
+ const rootEl = this.overlayService.getIntersectionRoot();
4925
+ if (!rootEl)
4926
+ return;
4927
+ const rootRect = rootEl.getBoundingClientRect();
4928
+ const elRect = element.getBoundingClientRect();
4929
+ const overlapH = Math.min(elRect.bottom, rootRect.bottom) - Math.max(elRect.top, rootRect.top);
4930
+ const elH = elRect.bottom - elRect.top;
4931
+ if (elH > 0 && overlapH / elH >= 0.1) {
4932
+ scrollFired = true;
4933
+ cleanup();
4934
+ fire();
4935
+ }
4936
+ };
4937
+ if (lenisEl)
4938
+ lenisEl.addEventListener('scroll', onScroll, { passive: true });
4939
+ const cleanup = () => {
4940
+ observer.disconnect();
4941
+ scrollFired = true;
4942
+ if (lenisEl)
4943
+ lenisEl.removeEventListener('scroll', onScroll);
4944
+ };
4945
+ return cleanup;
4963
4946
  }
4964
- return null;
4965
- }
4966
- /** True when the selection (root → selected) is the Navigation bar, its drawer, its bottom bar, or anything inside them. */
4967
- function inPhoneMenuContext(path) {
4968
- return path.some(n => !!n.role && NAV_CONTEXT_ROLES.has(n.role));
4969
- }
4970
- /**
4971
- * The phone menu a selection belongs to, or null when the selection is outside a navigation or the site has no drawer
4972
- * (no phone shape yet, or the Bottom bar shape). The drawer is looked up in the selection's own Navigation bar first,
4973
- * then in the globals (the Both shape makes it a global of its own).
4974
- */
4975
- function phoneMenuOf(path, globals = []) {
4976
- if (!inPhoneMenuContext(path))
4977
- return null;
4978
- const root = path.find(n => n.role === NavRole.Root) ?? null;
4979
- const scopes = root ? [root, ...globals] : [...globals];
4980
- const drawer = path.find(n => n.role === NavRole.Drawer) ?? findRole(scopes, NavRole.Drawer);
4981
- if (!drawer)
4982
- return null;
4983
- const menuButton = findRole(scopes, NavRole.MenuButton);
4984
- const at = navShapeBreakpoint(scopes);
4985
- return { root, drawer, menuButton, at };
4986
- }
4987
- /** True when the canvas, at `breakpoint`, shows the phone menu that takes over at `at` (that width or narrower). */
4988
- function phoneMenuShowsAt(at, breakpoint) {
4989
- return cascadeChain(breakpoint).includes(at);
4990
- }
4991
- const OPENER_TRIGGER = 'click';
4992
- /** The dialog actions a phone menu's opener runs (generation-core stamps Toggle; CC1 menus still hold Open). */
4993
- var PhoneMenuOpenerAction;
4994
- (function (PhoneMenuOpenerAction) {
4995
- PhoneMenuOpenerAction["Open"] = "openDialog";
4996
- PhoneMenuOpenerAction["Toggle"] = "toggleDialog";
4997
- })(PhoneMenuOpenerAction || (PhoneMenuOpenerAction = {}));
4998
- const OPENER_ACTIONS = new Set(Object.values(PhoneMenuOpenerAction));
4999
- /** True when `interaction` is the phone menu's own opener: the menu button's (or More's) click opening the drawer. */
5000
- function isPhoneMenuOpener(node, interaction, drawerId) {
5001
- if (!drawerId || (node?.role !== NavRole.MenuButton && node?.role !== NavRole.More))
4947
+ isBuilderMode() {
4948
+ return document.body.classList.contains('builder-active');
4949
+ }
4950
+ /** Returns true if any DOM ancestor of element is currently faded out via pre-state. */
4951
+ hasPreStatedAncestor(element) {
4952
+ let el = element.parentElement;
4953
+ while (el && el !== document.body) {
4954
+ if (this.preStatedElements.has(el))
4955
+ return true;
4956
+ el = el.parentElement;
4957
+ }
5002
4958
  return false;
5003
- const actions = interaction?.actions ?? [];
5004
- return interaction?.trigger === OPENER_TRIGGER && actions.length === 1
5005
- && OPENER_ACTIONS.has(actions[0].type ?? '') && actions[0].target === drawerId;
5006
- }
5007
-
5008
- /** What a hidden-at-rest part shows on the canvas when the author picks it in "Showing on canvas". */
5009
- var CanvasShowState;
5010
- (function (CanvasShowState) {
5011
- CanvasShowState["Closed"] = "closed";
5012
- CanvasShowState["Open"] = "open";
5013
- })(CanvasShowState || (CanvasShowState = {}));
5014
- const EMPTY_SNAPSHOT = () => ({
5015
- revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), triggeredBySelection: new Set(), shown: new Map(), items: new Map(),
5016
- runtime: new Set(), menu: null, tooltips: new Set(), toast: null,
5017
- });
5018
- /**
5019
- * The canvas's DESIGN state (complex-components R2; composite plan §5a's CompositeDesignStateService,
5020
- * started here for the navigation and reused by Accordion in CC2): which parts that are hidden at rest
5021
- * — a closed drawer today, a collapsed panel or an inactive tab next — the canvas shows right now.
5022
- *
5023
- * Two sources, the Dialog's "selection is the reveal" and the Form's switcher, generalised:
5024
- * - **the selection** — selecting anything inside a hidden part (tree, breadcrumb, an Items row,
5025
- * SELECT_NODE) shows it; deselecting hides it again (`setSelectionPath`);
5026
- * - **"Showing on canvas"** — the Bindings panel's switcher, for the one case a selection cannot express:
5027
- * the page with the menu open while a part OUTSIDE it is selected (`setShown`).
5028
- *
5029
- * Canvas-only: never saved, never a history step. Also carries the canvas page's path, so a link to the
5030
- * page being edited renders as the current page (N2) — the same decision the live site makes.
5031
- */
5032
- class CanvasDesignStateService {
5033
- constructor() {
5034
- this._state$ = new BehaviorSubject(EMPTY_SNAPSHOT());
5035
- this.state$ = this._state$.asObservable();
5036
- this._pagePath$ = new BehaviorSubject(null);
5037
- /** The app path of the page on the canvas (`/work`), or null when unknown. */
5038
- this.pagePath$ = this._pagePath$.asObservable();
5039
4959
  }
5040
- get snapshot() {
5041
- return this._state$.value;
4960
+ /** Polls via RAF until no ancestor is in pre-state, then fires callback. */
4961
+ deferUntilVisible(element, callback) {
4962
+ const poll = () => {
4963
+ if (!this.hasPreStatedAncestor(element)) {
4964
+ callback();
4965
+ }
4966
+ else {
4967
+ requestAnimationFrame(poll);
4968
+ }
4969
+ };
4970
+ requestAnimationFrame(poll);
4971
+ }
4972
+ attachClick(node, ix, element) {
4973
+ // G7: press FEEDBACK (an animation of the pressed element that hides and shows nothing) lands on pointer-down,
4974
+ // at once; the rest commits on the click. A toggle keeps its whole list on the click (the shared split, iox-ui).
4975
+ const { onPress, onClick } = ix.reverseActions?.length ? { onPress: [], onClick: ix.actions } : splitPressFeedback(ix.actions);
4976
+ const handler = () => {
4977
+ if (ix.reverseActions?.length) {
4978
+ // Toggle: alternate between primary and reverse actions on each click.
4979
+ const isToggled = this.toggleStates.get(ix.id) ?? false;
4980
+ this.runActions(node, isToggled ? ix.reverseActions : ix.actions, element);
4981
+ this.toggleStates.set(ix.id, !isToggled);
4982
+ }
4983
+ else {
4984
+ this.runActions(node, onClick, element);
4985
+ }
4986
+ };
4987
+ const press = (e) => { if (e.button === 0 || e.pointerType !== 'mouse')
4988
+ this.runActions(node, onPress, element); };
4989
+ // CAPTURE, like the composite runtime: on the canvas every part's own listener stops the click to select
4990
+ // that part, so a click on a button's LABEL never bubbled to the button's click interaction — the live
4991
+ // site runs it (CC2.1: "Open an item" on a ButtonBlock did nothing on the canvas).
4992
+ element.addEventListener('click', handler, true);
4993
+ if (onPress.length)
4994
+ element.addEventListener('pointerdown', press, true);
4995
+ return () => {
4996
+ element.removeEventListener('click', handler, true);
4997
+ element.removeEventListener('pointerdown', press, true);
4998
+ };
4999
+ }
5000
+ attachHover(node, ix, element) {
5001
+ // G7: a hover trigger answers a mouse or a pen — a finger's emulated hover stuck until the next tap elsewhere.
5002
+ const enterHandler = (e) => { if (isHoverPointer(e))
5003
+ this.runActions(node, ix.actions, element); };
5004
+ const leaveHandler = (e) => {
5005
+ if (!isHoverPointer(e))
5006
+ return;
5007
+ // Reverse animations on leave for hover triggers
5008
+ for (const action of ix.actions) {
5009
+ if (!isActionEnabled(action))
5010
+ continue;
5011
+ // Context actions have no "backwards" — the shared table decides what leave does
5012
+ // (clear, unless the action sets noReverse). Same call the SSR engine makes.
5013
+ if (isContextAction(action)) {
5014
+ this.applyContextAction(action, element, 'reverse');
5015
+ continue;
5016
+ }
5017
+ const target = this.resolveTarget(node, action, element);
5018
+ if (!target)
5019
+ continue;
5020
+ this.reverseAction(target, action);
5021
+ }
5022
+ };
5023
+ element.addEventListener('pointerenter', enterHandler);
5024
+ element.addEventListener('pointerleave', leaveHandler);
5025
+ return () => {
5026
+ element.removeEventListener('pointerenter', enterHandler);
5027
+ element.removeEventListener('pointerleave', leaveHandler);
5028
+ };
5029
+ }
5030
+ // ── Actions ──────────────────────────────────────────────
5031
+ runActions(node, actions, selfEl) {
5032
+ for (const action of actions) {
5033
+ if (!isActionEnabled(action))
5034
+ continue; // switched off by the author
5035
+ if (this.SWIPER_ACTIONS.has(action.type)) {
5036
+ this.executeSwiperAction(action);
5037
+ continue;
5038
+ }
5039
+ // Dialog actions (A1b): the canvas RUNS them — styling a dialog you cannot open is
5040
+ // impossible. The canvas twin registered open/close/toggle under its nodeId, exactly
5041
+ // the Swiper-method pattern; a missing/mistargeted id is a silent no-op, never a throw.
5042
+ if (DIALOG_ACTION_TYPES.has(action.type)) {
5043
+ this.executeDialogAction(action, selfEl);
5044
+ continue;
5045
+ }
5046
+ // Popover actions (premium M5): the canvas RUNS them too, through the twin's registry — beside the element
5047
+ // whose click ran them (the opener the live runtime anchors to).
5048
+ if (POPOVER_ACTION_TYPE_SET.has(action.type)) {
5049
+ this.executePopoverAction(action, selfEl);
5050
+ continue;
5051
+ }
5052
+ // A toast (premium M5): RUN inside the page's frame — the layer of the token scope the element sits in (the
5053
+ // page, or the globals' overlay), so it wears the look and zooms with the canvas.
5054
+ if (TOAST_ACTION_TYPES.has(action.type)) {
5055
+ showCanvasToast(action.params?.['text'], action.params?.['edge'], selfEl ?? null);
5056
+ continue;
5057
+ }
5058
+ // Item actions (CC2.1 — composite T14): the canvas RUNS them on the target item's own runtime — the
5059
+ // one the live site runs — and brings an item that opened out of view into it.
5060
+ const itemAction = itemActionOf(action.type);
5061
+ if (itemAction) {
5062
+ this.executeItemAction(node, action, itemAction, selfEl);
5063
+ continue;
5064
+ }
5065
+ // Tier-A behaviour (A1c). The canvas-is-live rules are catalog DATA
5066
+ // (ELEMENT_ACTION_DESCRIPTORS): copy runs, scrollTo runs (scrollIntoView — the frame
5067
+ // scrolls natively here; the Lenis rule is the LIVE side's), navigate and submitForm
5068
+ // SIMULATE — a dispatched event the host surfaces as a toast, never a route change
5069
+ // or a fake submit under the author's feet.
5070
+ if (BEHAVIOUR_ACTION_TYPES.has(action.type)) {
5071
+ this.executeBehaviourAction(node, action, selfEl);
5072
+ continue;
5073
+ }
5074
+ // Tier-B server actions (A2). ALWAYS a simulation here: the canvas is the ADMIN's
5075
+ // browser and a server action is a public-site write (an email, a webhook, a content
5076
+ // row) — the toast says what the LIVE site would do.
5077
+ if (SERVER_ELEMENT_ACTION_TYPES.has(action.type)) {
5078
+ const detail = { type: action.type, params: action.params ?? {}, target: action.target };
5079
+ window.dispatchEvent(new CustomEvent('iox-action-simulated', { detail }));
5080
+ continue;
5081
+ }
5082
+ // Consent actions (C3). Recording ones SIMULATE — the canvas is the ADMIN's browser,
5083
+ // and an admin's cookie is not a visitor's decision; the toast says what would be
5084
+ // recorded. Opening the preferences dialog is dialog-opening and honestly RUNS.
5085
+ if (CONSENT_ACTION_TYPES.has(action.type)) {
5086
+ this.executeConsentAction(action);
5087
+ continue;
5088
+ }
5089
+ // Context actions write STATE, not animation. `selfEl` is deliberate: $item is read by
5090
+ // walking up from the element actually hovered/clicked, which for a repeater PREVIEW ROW
5091
+ // is the clone — so each row publishes its own item, exactly as on the live site.
5092
+ if (isContextAction(action)) {
5093
+ this.applyContextAction(action, selfEl ?? null, 'forward');
5094
+ continue;
5095
+ }
5096
+ // selfEl lets 'self'-targeted actions resolve for preview CLONES, which aren't
5097
+ // overlay-registered (getNodeRef would return undefined) — see attachTo. (issue #2)
5098
+ const target = this.resolveTarget(node, action, selfEl);
5099
+ if (!target)
5100
+ continue;
5101
+ this.executeAction(target, action);
5102
+ }
5103
+ }
5104
+ /**
5105
+ * Execute a context action against the shared store. The forward/reverse semantics come from
5106
+ * `contextActionEffect` in @vectoriox/iox-ui — the same function the SSR engine calls, so the
5107
+ * builder canvas and the live site behave identically.
5108
+ */
5109
+ applyContextAction(action, el, pass) {
5110
+ const effect = contextActionEffect(action, pass);
5111
+ if (!effect)
5112
+ return;
5113
+ if (effect.op === 'clear') {
5114
+ this.contextStore.clear(effect.slot);
5115
+ return;
5116
+ }
5117
+ const value = resolveContextValue(effect.value, {
5118
+ item: this.rowItemFor(el),
5119
+ resolvedData: this.contextData,
5120
+ });
5121
+ this.contextStore.set(effect.slot, value);
5122
+ }
5123
+ /** The repeater item this element belongs to — the nearest marked row ancestor. */
5124
+ rowItemFor(el) {
5125
+ const row = el?.closest?.('[data-iox-row-idx]');
5126
+ if (!row)
5127
+ return undefined;
5128
+ const source = row.getAttribute('data-iox-row-src') ?? '';
5129
+ const index = Number(row.getAttribute('data-iox-row-idx'));
5130
+ const items = this.contextData?.[source];
5131
+ return Array.isArray(items) && Number.isInteger(index) ? items[index] : undefined;
5042
5132
  }
5043
- get pagePath() {
5044
- return this._pagePath$.value;
5133
+ /** Resolved data for the canvas, kept current by the repeater as it fetches. */
5134
+ setContextData(data) {
5135
+ // MERGE, not replace — a page can hold several repeaters, each publishing its own source.
5136
+ this.contextData = { ...this.contextData, ...(data ?? {}) };
5045
5137
  }
5046
- setPagePath(path) {
5047
- const next = path ?? null;
5048
- if (next !== this._pagePath$.value)
5049
- this._pagePath$.next(next);
5138
+ executeBehaviourAction(node, action, selfEl) {
5139
+ switch (action.type) {
5140
+ case 'copyToClipboard': {
5141
+ const text = String(action.params?.['text'] ?? '');
5142
+ if (text)
5143
+ navigator.clipboard?.writeText(text).catch(() => { });
5144
+ return;
5145
+ }
5146
+ case 'scrollTo': {
5147
+ const target = this.resolveTarget(node, action, selfEl);
5148
+ target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
5149
+ return;
5150
+ }
5151
+ case 'navigate':
5152
+ case 'submitForm': {
5153
+ // Simulated: the host (the admin app) listens and shows "would …" as a toast.
5154
+ const detail = { type: action.type, params: action.params ?? {}, target: action.target };
5155
+ window.dispatchEvent(new CustomEvent('iox-action-simulated', { detail }));
5156
+ return;
5157
+ }
5158
+ }
5050
5159
  }
5051
- /**
5052
- * The selection moved. The hidden-at-rest PARTS on its path are revealed and every other part is not
5053
- * (R2); a free-standing overlay on the path is pinned open until it is closed (A4). `globals` lets it find the
5054
- * phone menu the selection belongs to when its drawer lives outside the path (the Both shape).
5055
- */
5056
- setSelectionPath(path, globals = []) {
5057
- const ids = (reveal) => path.filter(n => selectionRevealOf(n) === reveal).map(n => n.id).filter((id) => !!id);
5058
- const revealed = new Set(ids(SelectionReveal.WhileSelected));
5059
- const selectedTriggers = new Set(path.filter(n => isExclusiveTrigger(n)).map(n => n.id).filter((id) => !!id));
5060
- const triggeredBySelection = new Set(path.map(displayTriggerOf).filter((id) => !!id));
5061
- const { revealed: was, pinned: held, selectedTriggers: wasTriggers, triggeredBySelection: wasTriggered } = this._state$.value;
5062
- const pinned = new Set([...held, ...ids(SelectionReveal.UntilClosed)]);
5063
- const menu = phoneMenuOf(path, globals);
5064
- if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers) && sameSet(triggeredBySelection, wasTriggered)
5065
- && sameMenu(menu, this._state$.value.menu))
5160
+ executeItemAction(node, action, itemAction, selfEl) {
5161
+ const target = this.resolveTarget(node, action, selfEl);
5162
+ const open = runCompositeItemAction(target, itemAction);
5163
+ if (!open || !target)
5066
5164
  return;
5067
- this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers, triggeredBySelection, menu });
5165
+ if (shouldScrollToItem(target.getBoundingClientRect().top, window.innerHeight))
5166
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
5068
5167
  }
5069
- /** Closed on the canvas (its X, its backdrop, the eye): no longer pinned, switched open or revealed by the
5070
- * selection — selecting into it again reveals it again. */
5071
- dismiss(nodeId) {
5072
- if (!nodeId)
5168
+ executeConsentAction(action) {
5169
+ if (action.type === 'openConsentPreferences') {
5170
+ const dialog = action.target && action.target !== 'self' ? this.instanceRegistry.get(action.target) : null;
5171
+ dialog?.open?.();
5073
5172
  return;
5074
- const { pinned, shown, revealed, runtime } = this._state$.value;
5075
- if (!pinned.has(nodeId) && !shown.has(nodeId) && !revealed.has(nodeId) && !runtime.has(nodeId))
5173
+ }
5174
+ const detail = { type: action.type, params: action.params ?? {}, target: action.target };
5175
+ window.dispatchEvent(new CustomEvent('iox-action-simulated', { detail }));
5176
+ }
5177
+ executeDialogAction(action, selfEl) {
5178
+ const dialog = action.target && action.target !== 'self' ? this.instanceRegistry.get(action.target) : null;
5179
+ if (!dialog)
5076
5180
  return;
5077
- const nextPinned = new Set(pinned);
5078
- nextPinned.delete(nodeId);
5079
- const nextShown = new Map(shown);
5080
- nextShown.delete(nodeId);
5081
- const nextRevealed = new Set(revealed);
5082
- nextRevealed.delete(nodeId);
5083
- const nextRuntime = new Set(runtime);
5084
- nextRuntime.delete(nodeId);
5085
- this._state$.next({ ...this._state$.value, pinned: nextPinned, shown: nextShown, revealed: nextRevealed, runtime: nextRuntime });
5181
+ switch (action.type) {
5182
+ case 'openDialog':
5183
+ dialog.open?.();
5184
+ // CC3 "Open at the clicked item": the lightbox's slider goes to the clicked row's slide — the same
5185
+ // rule the live site runs (composite-core `slideIndexForRow`), once the canvas twin has rendered it.
5186
+ if (opensAtClickedItem(action))
5187
+ this.slideDialogToRow(action.target, rowIndexOf$1(selfEl));
5188
+ break;
5189
+ case 'closeDialog':
5190
+ dialog.close?.();
5191
+ break;
5192
+ case 'toggleDialog':
5193
+ dialog.toggle?.();
5194
+ break;
5195
+ }
5086
5196
  }
5087
- /** An action opened the overlay on the canvas (a click on the menu button): shown until something closes it. */
5088
- markOpened(nodeId) {
5089
- if (!nodeId || this._state$.value.runtime.has(nodeId))
5197
+ executePopoverAction(action, selfEl) {
5198
+ const popover = action.target && action.target !== 'self' ? this.instanceRegistry.get(action.target) : null;
5199
+ if (!popover)
5090
5200
  return;
5091
- this._state$.next({ ...this._state$.value, runtime: new Set([...this._state$.value.runtime, nodeId]) });
5201
+ switch (action.type) {
5202
+ case 'openPopover':
5203
+ popover.open?.(selfEl ?? null);
5204
+ break;
5205
+ case 'closePopover':
5206
+ popover.close?.();
5207
+ break;
5208
+ case 'togglePopover':
5209
+ popover.toggle?.(selfEl ?? null);
5210
+ break;
5211
+ }
5092
5212
  }
5093
- /** The switcher's choice for one part (null = back to the saved default). */
5094
- setShown(nodeId, state) {
5095
- const shown = new Map(this._state$.value.shown);
5096
- if (state && state !== CanvasShowState.Closed)
5097
- shown.set(nodeId, state);
5098
- else
5099
- shown.delete(nodeId);
5100
- this._state$.next({ ...this._state$.value, shown });
5213
+ /** The slider inside the opened dialog (its canvas twin renders on the next passes) → the row's slide. */
5214
+ slideDialogToRow(dialogId, row) {
5215
+ let tries = 0;
5216
+ const attempt = () => {
5217
+ const host = document.querySelector(`.iox-node-${CSS.escape(dialogId)}`);
5218
+ const swiper = host?.querySelector('.swiper')?.swiper;
5219
+ if (swiper?.slides?.length) {
5220
+ swiper.update?.();
5221
+ swiper.slideTo(slideIndexForRow(row, swiper.slides.length), 0);
5222
+ return;
5223
+ }
5224
+ if (++tries < 40)
5225
+ requestAnimationFrame(attempt);
5226
+ };
5227
+ requestAnimationFrame(attempt);
5101
5228
  }
5102
- /** Tabs' "Showing on canvas": the item a root shows (null = whatever the canvas shows live). */
5103
- setShownItem(rootId, key) {
5104
- if ((this._state$.value.items.get(rootId) ?? null) === key)
5229
+ executeSwiperAction(action) {
5230
+ // Target is the nodeId of the SliderContainer to control.
5231
+ const swiper = this.instanceRegistry.get(action.target);
5232
+ if (!swiper)
5105
5233
  return;
5106
- const items = new Map(this._state$.value.items);
5107
- if (key)
5108
- items.set(rootId, key);
5109
- else
5110
- items.delete(rootId);
5111
- this._state$.next({ ...this._state$.value, items });
5112
- }
5113
- /** The item a root's "Showing on canvas" shows, or null. */
5114
- shownItem(rootId) {
5115
- return (rootId && this._state$.value.items.get(rootId)) || null;
5234
+ switch (action.type) {
5235
+ case 'swiperNext':
5236
+ swiper.slideNext();
5237
+ break;
5238
+ case 'swiperPrev':
5239
+ swiper.slidePrev();
5240
+ break;
5241
+ case 'swiperGoToSlide':
5242
+ swiper.slideTo(action.params?.['index'] ?? 0);
5243
+ break;
5244
+ case 'swiperAutoplayStart':
5245
+ swiper.autoplay?.start();
5246
+ break;
5247
+ case 'swiperAutoplayStop':
5248
+ swiper.autoplay?.stop();
5249
+ break;
5250
+ case 'swiperAutoplayPause':
5251
+ swiper.autoplay?.pause();
5252
+ break;
5253
+ }
5116
5254
  }
5117
- /** The switcher's current choice for one part. */
5118
- shownState(nodeId) {
5119
- return (nodeId && this._state$.value.shown.get(nodeId)) || CanvasShowState.Closed;
5255
+ resolveTarget(ownerNode, action, selfEl) {
5256
+ if (action.target === 'self' || !action.target) {
5257
+ return selfEl ?? this.overlayService.getNodeRef(ownerNode)?.element ?? null;
5258
+ }
5259
+ // Find by node ID
5260
+ for (const [n, ref] of this.overlayService.getAllNodeEntries()) {
5261
+ if (n.id === action.target)
5262
+ return ref.element;
5263
+ }
5264
+ return null;
5120
5265
  }
5121
- /** True when the canvas shows the part: selected into, switched open, or opened by an action. */
5122
- isShown(nodeId) {
5123
- if (!nodeId)
5124
- return false;
5125
- const { revealed, pinned, shown, runtime } = this._state$.value;
5126
- return revealed.has(nodeId) || pinned.has(nodeId) || runtime.has(nodeId) || shown.get(nodeId) === CanvasShowState.Open;
5266
+ executeAction(element, action) {
5267
+ // toggleVisibility HIDES a visible element and SHOWS a hidden one — resolved against the element's
5268
+ // own state, the same rule the live site applies (shared `effectiveActionType`, CC1 N5).
5269
+ const type = effectiveActionType(action.type, element.hasAttribute(INTERACTION_HIDDEN_ATTRIBUTE));
5270
+ const keyframes = this.keyframesFor(type, action);
5271
+ // A count has no frames (its digits are written from its own clock) and is still an action.
5272
+ if (!keyframes.length && type !== COUNT_UP_ACTION)
5273
+ return;
5274
+ if (this.ENTRANCE_TYPES.has(type))
5275
+ element.removeAttribute(INTERACTION_HIDDEN_ATTRIBUTE);
5276
+ else if (this.EXIT_TYPES.has(type))
5277
+ element.setAttribute(INTERACTION_HIDDEN_ATTRIBUTE, '');
5278
+ // Reduced motion (CC1 R1): no animation runs — the element is left as the run would leave it (the shared settle
5279
+ // rule: an entrance releases to the stylesheet, an exit holds its hidden frame).
5280
+ if (!motionAllowed()) {
5281
+ settleInteraction(element, keyframes, settleModeFor(type), this.settleOptions());
5282
+ this.settleAfter(element, type);
5283
+ return;
5284
+ }
5285
+ // The ONE call the live site makes too: fill both (holds the first frame through a delay), started from the live
5286
+ // value, settled by the shared rule — an element that has entered keeps its hover and press (iox-ui#41). A look
5287
+ // entrance on a Repeater's template staggers its rows: each copy adds its row's share (shared `entranceDelay`).
5288
+ const anim = playInteraction(element, { type, params: action.params }, keyframes, {
5289
+ duration: action.duration,
5290
+ delay: entranceDelay(action, repeatedRowIndex(element)),
5291
+ // A keyword, a curve, or the look's easing token as this element sees it (G6) — the live site's resolver.
5292
+ easing: resolveInteractionEasing(action.easing, element),
5293
+ }, this.settleOptions());
5294
+ anim.finished.then(() => this.settleAfter(element, type)).catch(() => { });
5127
5295
  }
5128
- /** True when the canvas shows an item because the selection is styled "when it is on" (the switcher's hint). */
5129
- isTriggeredBySelection(triggerId) {
5130
- return !!triggerId && this._state$.value.triggeredBySelection.has(triggerId);
5296
+ /** On the canvas a run never writes a transform inline (one inside the scaled page breaks Lenis). */
5297
+ settleOptions() {
5298
+ return this.isBuilderMode() ? { skip: CANVAS_SKIPPED_PROPS } : {};
5131
5299
  }
5132
- /** True when the part is shown only because the selection is inside it (the switcher's hint line). */
5133
- isRevealedBySelection(nodeId) {
5134
- return !!nodeId && this._state$.value.revealed.has(nodeId);
5300
+ /** What an element is left as once an action is done: a revealed one interactive, a hidden one inert. */
5301
+ settleAfter(element, type) {
5302
+ if (this.ENTRANCE_TYPES.has(type)) {
5303
+ element.style.removeProperty('pointer-events');
5304
+ this.preStatedElements.delete(element);
5305
+ // Builder: clear inline pre-state so cancelling animations later
5306
+ // doesn't leave the element invisible after a play preview.
5307
+ if (this.isBuilderMode())
5308
+ this.clearInlineAnimationStyles(element);
5309
+ }
5310
+ else if (this.EXIT_TYPES.has(type)) {
5311
+ // Once the exit is done the element is invisible — disable pointer events so it doesn't
5312
+ // silently block clicks underneath it.
5313
+ element.style.setProperty('pointer-events', 'none');
5314
+ }
5135
5315
  }
5136
- /** Hold a control's tooltip open on the canvas (true), or let it go. */
5137
- setTooltipShown(nodeId, on) {
5138
- if (!nodeId || this._state$.value.tooltips.has(nodeId) === on)
5316
+ /**
5317
+ * Freeze an element at the animation's starting frame immediately, before any
5318
+ * trigger fires. Called for ALL entrance-type actions regardless of trigger so
5319
+ * that a click-triggered fadeIn on a menu overlay also starts it hidden.
5320
+ *
5321
+ * In builder mode only opacity:0 is applied — transforms on canvas elements
5322
+ * inside iox-page-component's scale() transform break Lenis scroll. The transform
5323
+ * jump at animation t=0 is invisible because the element is already opacity:0.
5324
+ * In production the full keyframe is applied + pointer-events:none.
5325
+ */
5326
+ applyPreState(element, action) {
5327
+ const keyframes = this.keyframesFor(action.type, action);
5328
+ if (!keyframes.length)
5139
5329
  return;
5140
- const tooltips = new Set(this._state$.value.tooltips);
5141
- if (on)
5142
- tooltips.add(nodeId);
5143
- else
5144
- tooltips.delete(nodeId);
5145
- this._state$.next({ ...this._state$.value, tooltips });
5330
+ const first = entrancePreStateStyle(action.type, action.params);
5331
+ if (this.isBuilderMode()) {
5332
+ // No transforms — inline transforms on children inside iox-page-component's
5333
+ // scale() break Lenis scroll. pointer-events:none lets wheel events and
5334
+ // hover fall through to visible elements underneath. An entrance that hides
5335
+ // nothing (a picture settling) stays in view.
5336
+ if ('opacity' in first || 'visibility' in first)
5337
+ element.style.setProperty('opacity', '0');
5338
+ element.style.setProperty('pointer-events', 'none');
5339
+ }
5340
+ else {
5341
+ for (const [prop, val] of Object.entries(first))
5342
+ element.style.setProperty(prop, val);
5343
+ element.style.setProperty('pointer-events', 'none');
5344
+ }
5345
+ element.setAttribute(INTERACTION_HIDDEN_ATTRIBUTE, '');
5346
+ this.preStatedElements.add(element);
5146
5347
  }
5147
- /** True while the canvas holds the control's tooltip open. */
5148
- tooltipShown(nodeId) {
5149
- return !!nodeId && this._state$.value.tooltips.has(nodeId);
5348
+ clearInlineAnimationStyles(element) {
5349
+ element.style.removeProperty('opacity');
5350
+ element.style.removeProperty('transform');
5351
+ element.style.removeProperty('clip-path');
5352
+ element.style.removeProperty('visibility');
5353
+ element.style.removeProperty('pointer-events');
5354
+ element.removeAttribute(INTERACTION_HIDDEN_ATTRIBUTE);
5150
5355
  }
5151
- /** Hold a toast up on the canvas with these words, or take it down (null). */
5152
- setToastShown(words) {
5153
- const next = words === null ? null : String(words);
5154
- if (this._state$.value.toast === next)
5356
+ /** A hover-out: the action played back from where the element IS (never from its end frame), settled by the shared
5357
+ * rule — played back to hidden, it holds hidden; to shown, it releases. */
5358
+ reverseAction(element, action) {
5359
+ const type = effectiveActionType(action.type, element.hasAttribute(INTERACTION_HIDDEN_ATTRIBUTE));
5360
+ const keyframes = this.keyframesFor(type, action);
5361
+ if (!keyframes.length)
5155
5362
  return;
5156
- this._state$.next({ ...this._state$.value, toast: next });
5157
- }
5158
- /** The words of the toast the canvas holds up, or null. */
5159
- get toastShown() {
5160
- return this._state$.value.toast;
5363
+ const frames = [...keyframes].reverse();
5364
+ if (!motionAllowed()) {
5365
+ settleInteraction(element, frames, settleModeFor(type, true), this.settleOptions());
5366
+ return;
5367
+ }
5368
+ playInteraction(element, { type, params: action.params }, frames, {
5369
+ duration: action.duration,
5370
+ easing: resolveInteractionEasing(action.easing, element),
5371
+ }, { ...this.settleOptions(), reverse: true });
5161
5372
  }
5162
- /** A new page on the canvas: nothing is revealed or switched open. */
5163
- reset() {
5164
- this._state$.next(EMPTY_SNAPSHOT());
5373
+ /** The action's keyframes from the ONE shared table both runtimes animate from (composite T2). */
5374
+ keyframesFor(type, action) {
5375
+ return buildInteractionKeyframes(type, action.params) ?? [];
5165
5376
  }
5166
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5167
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService }); }
5377
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService, deps: [{ token: OverlayService }], target: i0.ɵɵFactoryTarget.Injectable }); }
5378
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService }); }
5168
5379
  }
5169
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService, decorators: [{
5380
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: InteractionEngineService, decorators: [{
5170
5381
  type: Injectable
5171
- }] });
5172
- function sameMenu(a, b) {
5173
- if (!a || !b)
5174
- return a === b;
5175
- return a.drawer === b.drawer && a.menuButton === b.menuButton && a.root === b.root && a.at === b.at;
5176
- }
5177
- function sameSet(a, b) {
5178
- return a.size === b.size && [...a].every(id => b.has(id));
5382
+ }], ctorParameters: () => [{ type: OverlayService }] });
5383
+ /** The canvas's token scopes: the page frame and the globals' overlay (each declares the org's tokens). */
5384
+ const CANVAS_TOKEN_SCOPE = '.iox-token-scope';
5385
+ /** The floating layer's home on the canvas for an element: the token scope (the zoomed frame) it sits in. */
5386
+ function canvasFloatingContainer(from) {
5387
+ return from?.closest?.(CANVAS_TOKEN_SCOPE) ?? from?.ownerDocument?.querySelector(CANVAS_TOKEN_SCOPE) ?? null;
5179
5388
  }
5180
- /**
5181
- * Show or hide an overlay part (the Menu drawer) for DESIGN — ONE move for every control that previews it (the
5182
- * Bindings ▸ Phone menu row, the canvas's Menu chip). Opening switches it open; closing closes its canvas twin the way
5183
- * its own Close does (the exit plays, the reveal and the switch are cleared, a selection inside it moves to its
5184
- * opener), so a drawer shown because the selection sits in it closes too.
5185
- */
5186
- function setOverlayPreview(state, twins, id, open) {
5187
- if (!id)
5188
- return;
5189
- if (open) {
5190
- state.setShown(id, CanvasShowState.Open);
5191
- return;
5192
- }
5193
- const twin = twins?.get(id);
5194
- if (twin?.close)
5195
- twin.close();
5196
- else
5197
- state.dismiss(id);
5389
+ /** Raise a toast on the canvas (a showToast action the canvas runs): inside the frame, never over the builder. */
5390
+ function showCanvasToast(text, edge, from) {
5391
+ const doc = from?.ownerDocument ?? (typeof document !== 'undefined' ? document : null);
5392
+ showToast(doc, typeof text === 'string' ? text : '', { edge: typeof edge === 'string' ? edge : null, from, topLayer: false, container: canvasFloatingContainer(from) });
5198
5393
  }
5199
5394
 
5395
+ // Virtual trait composition now lives in @vectoriox/iox-ui so the SSR engine (which does NOT depend
5396
+ // on iox-builder) can use the SAME logic — otherwise the engine emitted raw virtual keys
5397
+ // (translate-x, filter-blur…) as invalid CSS. This is a re-export passthrough to keep existing
5398
+ // iox-builder imports (`../style-compose`) working with no duplicated implementation.
5399
+
5200
5400
  /** The node whose "Showing on canvas" choice a rendered root follows: itself, or — a preview row's copy — its
5201
5401
  * template (a copy shares its template's style id). */
5202
5402
  function designIdOf(node) {
@@ -5222,11 +5422,12 @@ class CompositeCanvasService {
5222
5422
  this.zone = zone;
5223
5423
  this.attached = new Map();
5224
5424
  this.exclusive = new Map();
5425
+ this.marquees = new Map();
5225
5426
  this.sub = designState?.state$.subscribe(() => this.applyDesignState()) ?? null;
5226
5427
  }
5227
5428
  ngOnDestroy() {
5228
5429
  this.sub?.unsubscribe();
5229
- for (const id of [...this.attached.keys(), ...this.exclusive.keys()])
5430
+ for (const id of [...this.attached.keys(), ...this.exclusive.keys(), ...this.marquees.keys()])
5230
5431
  this.detach(id);
5231
5432
  }
5232
5433
  /** A composite root rendered on the canvas (the template, or a preview row's copy): run it. `element` is the
@@ -5235,6 +5436,11 @@ class CompositeCanvasService {
5235
5436
  if (!node.id)
5236
5437
  return;
5237
5438
  this.detach(node.id);
5439
+ if (isMarqueeRoot(node)) {
5440
+ this.marquees.set(node.id, { node, element, runtime: null });
5441
+ this.applyMarquee(this.marquees.get(node.id));
5442
+ return;
5443
+ }
5238
5444
  const cssId = node.styleId ?? node.id;
5239
5445
  const rootId = () => compositeScopeId(cssId, compositeRowKeys(element));
5240
5446
  if (isExclusiveRoot(node)) {
@@ -5280,6 +5486,12 @@ class CompositeCanvasService {
5280
5486
  tabs?.runtime.destroy();
5281
5487
  tabs?.release();
5282
5488
  this.exclusive.delete(nodeId);
5489
+ this.marquees.get(nodeId)?.runtime?.destroy();
5490
+ this.marquees.delete(nodeId);
5491
+ }
5492
+ /** The runtime of a Marquee moving on the canvas, or null while it stands still. */
5493
+ marqueeRuntimeOf(nodeId) {
5494
+ return (nodeId && this.marquees.get(nodeId)?.runtime) || null;
5283
5495
  }
5284
5496
  /** The root's behaviour changed in its panel (Allow several open, Open by default; Activate on, Orientation). */
5285
5497
  refreshBehaviour(node) {
@@ -5316,6 +5528,18 @@ class CompositeCanvasService {
5316
5528
  }
5317
5529
  for (const entry of this.exclusive.values())
5318
5530
  this.applyExclusive(entry);
5531
+ for (const entry of this.marquees.values())
5532
+ this.applyMarquee(entry);
5533
+ }
5534
+ /** Moving while "Showing on canvas" says so (the live site's runtime: its copy, its pause control), else still. */
5535
+ applyMarquee(entry) {
5536
+ const moving = this.designState?.shownState(designIdOf(entry.node)) === CanvasShowState.Open;
5537
+ if (moving && !entry.runtime)
5538
+ entry.runtime = this.zone.runOutsideAngular(() => new MarqueeRuntime(entry.element));
5539
+ else if (!moving && entry.runtime) {
5540
+ entry.runtime.destroy();
5541
+ entry.runtime = null;
5542
+ }
5319
5543
  }
5320
5544
  /** A Tabs on the canvas follows "Showing on canvas" and a selected tab (live), then shows any forced panel. */
5321
5545
  applyExclusive(entry) {
@@ -5380,119 +5604,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
5380
5604
  type: Optional
5381
5605
  }] }, { type: i0.NgZone }] });
5382
5606
 
5383
- const CANVAS_SCROLLER = '.preview-scroll';
5384
- const PREVIEW_SCROLLER = '.iox-page-wrapper';
5385
- /**
5386
- * Scenes on the canvas and in Preview: the SAME scene-core driver the live site runs, so a layer stands at the same
5387
- * value at the same progress in every renderer (premium-sites plan §5b). The canvas's page never scrolls itself (its
5388
- * Lenis scrolls a wrapper around a zoomed frame), so `position: sticky` cannot hold the stage there and the driver does;
5389
- * Preview's page is a real scroller, and sticky works. Canvas-only state: it never writes a node, so nothing here is
5390
- * saved or becomes an undo step.
5391
- */
5392
- class SceneCanvasService {
5393
- constructor() {
5394
- this.zone = inject(NgZone);
5395
- this.overlay = inject(OverlayService, { optional: true });
5396
- this.driver = null;
5397
- this.rootsById = new Map();
5398
- this.idsByRoot = new Map();
5399
- /** Every frame on every Scene: the selection box and the timeline follow on it. */
5400
- this.frames$ = new Subject();
5401
- }
5402
- /**
5403
- * Drive a root. `onCanvas` comes from the caller, because only the render directive can read IOX_BUILDER_CANVAS: the
5404
- * token is provided on the canvas's element, below where this service lives. Read here, it answered false, and the
5405
- * canvas measured its Scenes against a page that moves with them, so they never left progress 0.
5406
- */
5407
- attach(root, onCanvas, nodeId) {
5408
- this.zone.runOutsideAngular(() => {
5409
- this.driver ??= new ScrollDriver({
5410
- pin: onCanvas,
5411
- observe: true,
5412
- originOf: el => el.closest(onCanvas ? CANVAS_SCROLLER : PREVIEW_SCROLLER),
5413
- onFrame: (el, state) => this.frames$.next({ nodeId: this.idsByRoot.get(el) ?? null, ...state }),
5414
- });
5415
- this.driver.add(root);
5416
- });
5417
- if (nodeId) {
5418
- this.rootsById.set(nodeId, root);
5419
- this.idsByRoot.set(root, nodeId);
5420
- }
5421
- }
5422
- detach(root) {
5423
- this.driver?.remove(root);
5424
- const id = this.idsByRoot.get(root);
5425
- // A re-render attaches the new element before the old one detaches: it must not lose the new one.
5426
- if (id && this.rootsById.get(id) === root)
5427
- this.rootsById.delete(id);
5428
- this.idsByRoot.delete(root);
5429
- }
5430
- /** The canvas's Lenis just moved the scroll, inside this frame: hold every stage in this same frame. */
5431
- pinNow() {
5432
- this.driver?.pinNow();
5433
- }
5434
- /** The damped progress a root's layers stand at. */
5435
- progressOf(root) {
5436
- return this.driver?.progressOf(root) ?? null;
5437
- }
5438
- /** Where the scroll stands in a Scene, 0 to 1. Null for a Scene that is not on the canvas. */
5439
- positionOf(nodeId) {
5440
- const root = nodeId ? this.rootsById.get(nodeId) : null;
5441
- return root ? this.driver?.targetOf(root) ?? null : null;
5442
- }
5443
- /**
5444
- * Stand a Scene at `p` by really scrolling the canvas: there is one position, so the scrubber, the wheel, the pin and
5445
- * the selection box can never disagree. It lands with no glide, because the author is waiting to see that moment.
5446
- */
5447
- goTo(nodeId, p) {
5448
- const root = nodeId ? this.rootsById.get(nodeId) : null;
5449
- const offset = root ? this.driver?.offsetFor(root, p) : null;
5450
- const scroller = root?.closest(CANVAS_SCROLLER);
5451
- if (!root || offset == null || !scroller)
5452
- return;
5453
- const rect = root.getBoundingClientRect();
5454
- const zoom = root.offsetHeight ? rect.height / root.offsetHeight : 1;
5455
- scroller.scrollTop += rect.top - scroller.getBoundingClientRect().top + offset * zoom;
5456
- this.driver?.settle(root);
5457
- }
5458
- /**
5459
- * A layer's tracks were edited: its element carries them now and its Scene plays them on the next frame, with no
5460
- * re-render, because a Scene's own DOM is what the author is looking at.
5461
- */
5462
- refreshLayer(node, root) {
5463
- const ref = this.overlay?.getNodeRef(node) ?? (node.id ? this.overlay?.findRefByNodeId(node.id)?.ref : undefined);
5464
- if (ref?.element && this.overlay)
5465
- syncSceneAttributes(this.overlay.getStyledElement(ref.element), node);
5466
- const el = root?.id ? this.rootsById.get(root.id) : null;
5467
- if (el)
5468
- this.driver?.reread(el);
5469
- }
5470
- /**
5471
- * A value a layer's own component draws changed (a Video's pictures): its instance takes it now. Its Scene reads the new
5472
- * attribute when it lands, with no re-render of the page.
5473
- */
5474
- setLayerInput(node, name, value) {
5475
- const ref = this.overlay?.getNodeRef(node) ?? (node.id ? this.overlay?.findRefByNodeId(node.id)?.ref : undefined);
5476
- const component = ref?.componentRef;
5477
- if (!component?.instance)
5478
- return;
5479
- component.instance[name] = value;
5480
- component.changeDetectorRef.detectChanges();
5481
- }
5482
- ngOnDestroy() {
5483
- this.driver?.destroy();
5484
- this.driver = null;
5485
- this.rootsById.clear();
5486
- this.idsByRoot.clear();
5487
- this.frames$.complete();
5488
- }
5489
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SceneCanvasService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5490
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SceneCanvasService }); }
5491
- }
5492
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: SceneCanvasService, decorators: [{
5493
- type: Injectable
5494
- }] });
5495
-
5496
5607
  /** The input a component declares to receive its empty-image placeholder (the Image). */
5497
5608
  const EMPTY_SRC_INPUT = 'emptySrc';
5498
5609
  /** The editing canvas's "Add image" label, through the host's i18n. */
@@ -6026,7 +6137,7 @@ class RenderDirective {
6026
6137
  // A composite root runs its behaviour on the canvas — the live site's runtime (CC2; the Disclosure, CC2.1).
6027
6138
  // A preview row's copy runs too, scoped by its row (vectoriox/iox-ui#20): its ids are its own and it shows
6028
6139
  // the saved default, as every row does on the live site.
6029
- if (nativeEl && node.id && isAnyCompositeRoot(node) && this.compositeCanvas) {
6140
+ if (nativeEl && node.id && (isAnyCompositeRoot(node) || isMarqueeRoot(node)) && this.compositeCanvas) {
6030
6141
  this.compositeCanvas.attach(node, nativeEl);
6031
6142
  this.compositeRootId = node.id;
6032
6143
  }
@@ -6603,6 +6714,9 @@ var CompositePresetId;
6603
6714
  CompositePresetId["Segmented"] = "segmented";
6604
6715
  CompositePresetId["Lightbox"] = "lightbox";
6605
6716
  CompositePresetId["Scene"] = "scene";
6717
+ CompositePresetId["Marquee"] = "marquee";
6718
+ CompositePresetId["Counter"] = "counter";
6719
+ CompositePresetId["ReadingBar"] = "reading-bar";
6606
6720
  })(CompositePresetId || (CompositePresetId = {}));
6607
6721
  /** Where a composite sits in the Components panel (composite plan §6d): content blocks, or the interactive
6608
6722
  * composites (the Accordion — CC2, the Disclosure — CC2.1, Tabs — CC4a, the Segmented control — CC4b; the floating
@@ -6658,6 +6772,22 @@ const COMPOSITE_PRESETS = Object.freeze([
6658
6772
  description: 'A stage that holds still while the page scrolls past it, and everything on it moves. Add your pictures and words as layers.',
6659
6773
  build: create => buildSceneRecipe(create),
6660
6774
  },
6775
+ // Premium components M6.
6776
+ {
6777
+ id: CompositePresetId.Marquee, group: CompositePaletteGroup.Interactive, name: 'Marquee', icon: 'ph-thin ph-arrows-left-right',
6778
+ description: 'A band of words that moves on its own, with a pause button. Still when the visitor asks for less motion.',
6779
+ build: create => buildMarqueeRecipe(create),
6780
+ },
6781
+ {
6782
+ id: CompositePresetId.Counter, group: CompositePaletteGroup.Interactive, name: 'Counter', icon: 'ph-thin ph-number-circle-one',
6783
+ description: 'A number that counts up once, the first time it comes into view — the real number is always in the page.',
6784
+ build: create => buildCounterRecipe(create),
6785
+ },
6786
+ {
6787
+ id: CompositePresetId.ReadingBar, group: CompositePaletteGroup.Interactive, name: 'Reading bar', icon: 'ph-thin ph-line-segment',
6788
+ description: 'A thin bar that fills as the page is read — or its section: change it in Interactions.',
6789
+ build: create => buildReadingBarRecipe(create),
6790
+ },
6661
6791
  ]);
6662
6792
  /**
6663
6793
  * The builder's node factory for composite-core recipes: a NEW node as the palette creates one
@@ -10125,10 +10255,13 @@ class BuilderComponent {
10125
10255
  }
10126
10256
  if (toast !== this.shownToast) {
10127
10257
  const page = root.querySelector('iox-page-component.iox-token-scope') ?? root;
10258
+ // The held toast wears the look when the page does (a node wearing the design system's marker) — the same
10259
+ // rule a toast raised by an element follows (it asks the element it came from).
10260
+ const from = page.querySelector('.iox-es') ?? null;
10128
10261
  if (this.shownToast !== null && toast === null)
10129
10262
  showToastDesign(root.ownerDocument, '', false, { topLayer: false, container: page });
10130
10263
  if (toast !== null)
10131
- showToastDesign(root.ownerDocument, toast, true, { topLayer: false, container: page });
10264
+ showToastDesign(root.ownerDocument, toast, true, { topLayer: false, container: page, from });
10132
10265
  this.shownToast = toast;
10133
10266
  }
10134
10267
  }
@@ -14743,6 +14876,7 @@ class BuilderContainerComponentConfig extends ComponentConfig {
14743
14876
  buildColorSchemeTrait(),
14744
14877
  ...buildCompositeBehaviourTraits(),
14745
14878
  ...buildHeaderBehaviourTraits(),
14879
+ ...buildMarqueeTraits(),
14746
14880
  ];
14747
14881
  this.styleTraits = buildFullStyleTraits();
14748
14882
  this.applyStyleDefaults({
@@ -15589,5 +15723,5 @@ class BuilderSpacerComponentConfig extends ComponentConfig {
15589
15723
  * Generated bundle index. Do not edit.
15590
15724
  */
15591
15725
 
15592
- export { ACTION_TYPE_OPTIONS, AuditCode, AuditPanelComponent, AuditSeverity, BODY_CRUMB_LABEL, BOUND_ICON, BREAKPOINT_PRESENTATION, BUILDER_CANVAS_CONTAINER, BUILDER_CHROME_EN, BUILDER_FONT_LINK_ATTR, BUILDER_TOKEN_SCOPE_CLASS, BackgroundGroupStyleConfig, BorderGroupStyleConfig, BreadcrumbBarComponent, BuilderButtonBlockComponent, BuilderButtonComponentConfig, BuilderClipboardService, BuilderComponent, BuilderContainerComponent, BuilderContainerComponentConfig, BuilderDataSourceService, BuilderDialogComponent, BuilderDividerComponentConfig, BuilderHeadingComponentConfig, BuilderHistoryService, BuilderIconComponentConfig, BuilderImageComponentConfig, BuilderLinkComponentConfig, BuilderLinkedContainerComponent, BuilderLinkedContainerConfig, BuilderListComponentConfig, BuilderListItemComponentConfig, BuilderMode, BuilderPopoverComponent, BuilderRepeaterComponent, BuilderSpacerComponentConfig, BusinessSpecimenView, ButtonBlockComponentConfig, CANVAS_EMPTY_IMAGE_LABEL_EN, CANVAS_EMPTY_IMAGE_LABEL_KEY, CANVAS_ROOT_RESET_CSS, CANVAS_TOKEN_SCOPE, COMPOSITE_PRESETS, CanvasDesignStateService, CanvasShowState, CardComponentConfig, ChildLayoutGroupStyleConfig, ComponentConfig, ComponentRegistryService, CompositeCanvasService, CompositePaletteGroup, CompositePresetId, DEFAULT_ESSENTIAL_TRAITS, DESCRIPTOR_MAX_LENGTH, DRAG_HANDLE_ATTR, DROP_REFUSED_CLASS, DataSourceRegistryService, DataSourceType, DialogComponentConfig, DimensionsGroupStyleConfig, DragEngineService, EASING_OPTIONS, EDITOR_CHROME_ATTRIBUTE, ESSENTIAL_TRAITS_BY_TYPE, EffectsGroupStyleConfig, FIELD_KIND_PRESETS, FIELD_PARTS, FORM_STATE_BLOCK_TYPES, FORM_TYPE, FieldComponentConfig, FieldGroupComponentConfig, FieldTrait, FormComponentConfig, FormErrorComponentConfig, FormStateService, FormSuccessComponentConfig, GLOBALS_AFTER_CLASS, GalleryConfig, GlobalCanvasLayer, GroupStyleConfig, HIDDEN_AT_REST_LABEL_KEY, HIDDEN_AT_REST_ROLES, HIDDEN_AT_REST_TYPES, INSERT_REFUSAL_LABEL_KEYS, INTERACTION_STATES, INVERSE_ACTION, IOX_BUILDER_CANVAS, IOX_BUILDER_LABELS, IOX_CONTENT_TYPE_LABELS, IOX_DATA_SOURCE_ENDPOINTS, InsertRefusal, InteractionEngineService, InteractionPresetRegistryService, IoxBuilderModule, IoxDraggableDirective, IoxDropzoneDirective, LAYOUT_PRESETS, LayerTreeComponent, LayoutGroupStyleConfig, ListGroupStyleConfig, NodeAction, OverlayComponent, OverlayService, PAGE_TRANSITION_EASING_OPTIONS, PALETTE_PRESET_DEFS, POPOVER_PLACEMENT_OPTIONS, POPOVER_SHEET_MAX_WIDTH, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, PhoneMenuOpenerAction, PopoverComponentConfig, PositionGroupStyleConfig, PresetRegistryService, RELATIONAL_STATES, RELATIONAL_STATE_BASE, REPEATER_SOURCE_PROP, REPEATER_TYPE, REVEALED_WHILE_SELECTED_ROLES, ROOT_ESCAPE_PX, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, RootZone, SCREEN_WIDTH_OPTIONS, SENT_LABEL_TRAIT, SILENT_REFUSALS, STRUCTURAL_STATES, SUPPORTED_STATES, SceneCanvasService, SectionComponent, SectionComponentConfig, SelectionReveal, SizeGroupStyleConfig, SliderContainerComponent, SliderContainerConfig, SliderSlideComponent, SliderSlideConfig, SpacingGroupStyleConfig, StructureFix, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, SubmitButtonComponentConfig, SymbolRegistryService, TARGETED_STATES, TARGETED_STATE_BASE, TOGGLE_PART_ROLES, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, TypographyGroupStyleConfig, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, VALIDATION_STATES, VALIDATION_STATE_CLASS, VideoConfig, ViewportService, ZOOM_OPTIONS, addFormChannel, arrayItemSchemaFields, arraySchemaFields, auditNodes, bindingLabel, bindingLabels, buildBusinessSpecimen, buildCompositePreset, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormActionsRow, buildFormChannel, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, builderCompositeFactory, canFluidise, canvasEmptyImageLabel, canvasFloatingContainer, canvasHostDisplay, collapseColumnsProps, columnCountOf, compositeStructureFindings, defaultBuilderLabels, defaultPageSettings, deleteTabPair, displayTriggerOf, dropChildren, dropTargetLabel, duplicateTabPair, escapesRoot, essentialTraitsFor, findNodeByIdInEither, findNodeLocation, findParentOf, flattenLayerTree, flattenSchemaFields, fluidiseProps, formChannelOf, generateNodeId, globalCanvasLayer, hiddenAtBase, hiddenUntilItemOn, inPhoneMenuContext, insertRefusal, insertRefusalLabel, interactionTargets, isHiddenAt, isHiddenAtRest, isPhoneMenuOpener, isPinnedGlobal, isSpecimen, itemsOnAtRest, markSpecimen, matchesControlSearch, nearestFormNode, neverShown, nodeAncestorPath, nodeAncestorPathInEither, nodeDescriptor, nodeStyleMap, overlayGlobals, pathToNode, phoneMenuOf, phoneMenuShowsAt, refreshSubtree, regenerateNodeIds, removalRefusal, reorderProps, repeaterSourceAlias, repeaterSubLabel, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, retypeNode, rootEscapeBand, rootZone, selectionRevealOf, setChannelAddress, setOverlayPreview, setStyleTrait, setTrait, shortDescriptor, showCanvasToast, shownBeforeControl, sourceContentType, sourceTypeLabel, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, syncNodeInputs, tabPairLabel, tabsRootForPart, textNode, upsertNodeStyles, visibilityProps, walkNodes, wrapNode };
15726
+ export { ACTION_TYPE_OPTIONS, AuditCode, AuditPanelComponent, AuditSeverity, BODY_CRUMB_LABEL, BOUND_ICON, BREAKPOINT_PRESENTATION, BUILDER_CANVAS_CONTAINER, BUILDER_CHROME_EN, BUILDER_FONT_LINK_ATTR, BUILDER_TOKEN_SCOPE_CLASS, BackgroundGroupStyleConfig, BorderGroupStyleConfig, BreadcrumbBarComponent, BuilderButtonBlockComponent, BuilderButtonComponentConfig, BuilderClipboardService, BuilderComponent, BuilderContainerComponent, BuilderContainerComponentConfig, BuilderDataSourceService, BuilderDialogComponent, BuilderDividerComponentConfig, BuilderHeadingComponentConfig, BuilderHistoryService, BuilderIconComponentConfig, BuilderImageComponentConfig, BuilderLinkComponentConfig, BuilderLinkedContainerComponent, BuilderLinkedContainerConfig, BuilderListComponentConfig, BuilderListItemComponentConfig, BuilderMode, BuilderPopoverComponent, BuilderRepeaterComponent, BuilderSpacerComponentConfig, BusinessSpecimenView, ButtonBlockComponentConfig, CANVAS_EMPTY_IMAGE_LABEL_EN, CANVAS_EMPTY_IMAGE_LABEL_KEY, CANVAS_ROOT_RESET_CSS, CANVAS_TOKEN_SCOPE, COMPOSITE_PRESETS, CanvasDesignStateService, CanvasShowState, CardComponentConfig, ChildLayoutGroupStyleConfig, ComponentConfig, ComponentRegistryService, CompositeCanvasService, CompositePaletteGroup, CompositePresetId, DEFAULT_ESSENTIAL_TRAITS, DESCRIPTOR_MAX_LENGTH, DRAG_HANDLE_ATTR, DROP_REFUSED_CLASS, DataSourceRegistryService, DataSourceType, DialogComponentConfig, DimensionsGroupStyleConfig, DragEngineService, EASING_OPTIONS, EDITOR_CHROME_ATTRIBUTE, ESSENTIAL_TRAITS_BY_TYPE, EffectsGroupStyleConfig, FIELD_KIND_PRESETS, FIELD_PARTS, FORM_STATE_BLOCK_TYPES, FORM_TYPE, FieldComponentConfig, FieldGroupComponentConfig, FieldTrait, FormComponentConfig, FormErrorComponentConfig, FormStateService, FormSuccessComponentConfig, GLOBALS_AFTER_CLASS, GalleryConfig, GlobalCanvasLayer, GroupStyleConfig, HIDDEN_AT_REST_LABEL_KEY, HIDDEN_AT_REST_ROLES, HIDDEN_AT_REST_TYPES, INSERT_REFUSAL_LABEL_KEYS, INTERACTION_STATES, INVERSE_ACTION, IOX_BUILDER_CANVAS, IOX_BUILDER_LABELS, IOX_CONTENT_TYPE_LABELS, IOX_DATA_SOURCE_ENDPOINTS, InsertRefusal, InteractionEngineService, InteractionPresetRegistryService, IoxBuilderModule, IoxDraggableDirective, IoxDropzoneDirective, LAYOUT_PRESETS, LayerTreeComponent, LayoutGroupStyleConfig, ListGroupStyleConfig, NodeAction, OverlayComponent, OverlayService, PAGE_TRANSITION_EASING_OPTIONS, PALETTE_PRESET_DEFS, POPOVER_PLACEMENT_OPTIONS, POPOVER_SHEET_MAX_WIDTH, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, PhoneMenuOpenerAction, PopoverComponentConfig, PositionGroupStyleConfig, PresetRegistryService, RELATIONAL_STATES, RELATIONAL_STATE_BASE, REPEATER_SOURCE_PROP, REPEATER_TYPE, REVEALED_WHILE_SELECTED_ROLES, ROOT_ESCAPE_PX, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, RootZone, SCREEN_WIDTH_OPTIONS, SENT_LABEL_TRAIT, SILENT_REFUSALS, STRUCTURAL_STATES, SUPPORTED_STATES, SceneCanvasService, SectionComponent, SectionComponentConfig, SelectionReveal, SizeGroupStyleConfig, SliderContainerComponent, SliderContainerConfig, SliderSlideComponent, SliderSlideConfig, SpacingGroupStyleConfig, StructureFix, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, SubmitButtonComponentConfig, SymbolRegistryService, TARGETED_STATES, TARGETED_STATE_BASE, TOGGLE_PART_ROLES, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, TypographyGroupStyleConfig, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, VALIDATION_STATES, VALIDATION_STATE_CLASS, VideoConfig, ViewportService, ZOOM_OPTIONS, addFormChannel, arrayItemSchemaFields, arraySchemaFields, auditNodes, bindingLabel, bindingLabels, buildBusinessSpecimen, buildCompositePreset, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormActionsRow, buildFormChannel, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, builderCompositeFactory, canFluidise, canvasEmptyImageLabel, canvasFloatingContainer, canvasHostDisplay, collapseColumnsProps, columnCountOf, compositeStructureFindings, defaultBuilderLabels, defaultPageSettings, deleteTabPair, displayTriggerOf, dropChildren, dropTargetLabel, duplicateTabPair, escapesRoot, essentialTraitsFor, findNodeByIdInEither, findNodeLocation, findParentOf, flattenLayerTree, flattenSchemaFields, fluidiseProps, formChannelOf, generateNodeId, globalCanvasLayer, hiddenAtBase, hiddenUntilItemOn, inPhoneMenuContext, insertRefusal, insertRefusalLabel, interactionTargets, isHiddenAt, isHiddenAtRest, isPhoneMenuOpener, isPinnedGlobal, isReadingBar, isSpecimen, itemsOnAtRest, markSpecimen, matchesControlSearch, nearestFormNode, neverShown, nodeAncestorPath, nodeAncestorPathInEither, nodeDescriptor, nodeStyleMap, overlayGlobals, pathToNode, phoneMenuOf, phoneMenuShowsAt, refreshSubtree, regenerateNodeIds, removalRefusal, reorderProps, repeaterSourceAlias, repeaterSubLabel, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, retypeNode, rootEscapeBand, rootZone, selectionRevealOf, setChannelAddress, setOverlayPreview, setStyleTrait, setTrait, shortDescriptor, showCanvasToast, shownBeforeControl, sourceContentType, sourceTypeLabel, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, syncNodeInputs, tabPairLabel, tabsRootForPart, textNode, upsertNodeStyles, visibilityProps, walkNodes, wrapNode };
15593
15727
  //# sourceMappingURL=vectoriox-iox-builder.mjs.map