@vectoriox/iox-builder 9.9.0 → 9.11.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.
@@ -7,27 +7,131 @@ import { FormsModule } from '@angular/forms';
7
7
  import * as i5 from '@angular/cdk/drag-drop';
8
8
  import { DragDropModule } from '@angular/cdk/drag-drop';
9
9
  import { AccordionModule } from 'primeng/accordion';
10
- import * as i18 from 'primeng/button';
10
+ import * as i19 from 'primeng/button';
11
11
  import { ButtonModule } from 'primeng/button';
12
12
  import * as i2$1 from 'primeng/popover';
13
13
  import { PopoverModule } from 'primeng/popover';
14
14
  import { InputTextModule } from 'primeng/inputtext';
15
15
  import { SelectModule } from 'primeng/select';
16
16
  import { TooltipModule } from 'primeng/tooltip';
17
+ import * as i20 from 'primeng/dialog';
17
18
  import { DialogModule } from 'primeng/dialog';
18
19
  import * as i1$1 from '@vectoriox/iox-ui';
19
- import { resolveBoundValue, VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, TEXT_DIRECTION_KEY, TEXT_ALIGN_KEY, TextAlign, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, LinkType, MAX_LINES_KEY, A11yTrait, BREAKPOINTS, Breakpoint, withSchemeInk, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, targetedStateSelector, stateSelectorSuffix, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, applyElementStyle, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, ContextStore, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, effectiveActionType, INTERACTION_HIDDEN_ATTRIBUTE, motionAllowed, finalKeyframe, keyframeStyleEntries, buildInteractionKeyframes, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, nodeTraitValue, syncNodeA11yAttributes, nodeA11yAttributes, safeSetInput, colorSchemeOf, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, switchElementStyle, countOverrides, setOverride, breakpointDef, breakpointForWidth, FormRenderState, needsAccessibleName, resolveInputs, setInputOverride, barReserveClass, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, IS_PREVIEW, buttonBlockHref, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
20
+ import { resolveBoundValue, VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, TEXT_DIRECTION_KEY, TEXT_ALIGN_KEY, TextAlign, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, LinkType, MAX_LINES_KEY, A11yTrait, BREAKPOINTS, Breakpoint, withSchemeInk, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, targetedStateSelector, stateSelectorSuffix, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, applyElementStyle, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, ContextStore, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, effectiveActionType, INTERACTION_HIDDEN_ATTRIBUTE, motionAllowed, finalKeyframe, keyframeStyleEntries, buildInteractionKeyframes, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, nodeTraitValue, syncNodeA11yAttributes, nodeA11yAttributes, safeSetInput, colorSchemeOf, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, switchElementStyle, releaseElementStyle, countOverrides, setOverride, breakpointDef, breakpointForWidth, FormRenderState, needsAccessibleName, resolveInputs, setInputOverride, barReserveClass, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, IS_PREVIEW, buttonBlockHref, bootSlider, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
20
21
  export { ComponentInstanceRegistryService, VIRTUAL_TRAIT_KEYS, composeVirtualTraits } from '@vectoriox/iox-ui';
21
- import { StructureEdit, isAccordionPart, isDisclosurePart, structureRefusal, AccordionRole, CompositeBehaviourTrait, ACCORDION_DEFAULT_ONS, onAccordionItemState, isAccordionItemOn, runCompositeItemAction, shouldScrollToItem, DisclosureRole, AccordionRuntime, accordionBehaviour, compositeScopeId, compositeRowKeys, compositeKindOfRole, closestAccordionItem, isCompositeRoot, isAccordionRoot, buildCardRecipe, buildBadge, buildChip, buildAccordionRecipe, buildDisclosureRecipe, CompositeKind, accordionItemLabel, isLockedPart, accordionRootOf, isAccordionFromContent, accordionItemOf, addAccordionItem, isCompositeItem, accordionHeadingLevel, isCompositePanel, isCompositeTrigger } from '@vectoriox/iox-ui/composite-core';
22
+ import { isExclusiveItemPart, exclusiveRootOf, isTabsFromContent, TabsRole, tabTriggerOf, tabLabel, removeTabPair, tabPanelOf, tabsListOf, tabsPanelsOf, nextTabKey, StructureEdit, isAccordionPart, isDisclosurePart, isSliderPart, isTabsPart, structureRefusal, StructureRefusal, AccordionRole, 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, tabsLabel, tabsBehaviour, compositeKindOfRole, AccordionRuntime, accordionBehaviour, isToggleKind, closestAccordionItem, isAnyCompositeRoot, isAccordionRoot, isExclusiveList, buildCardRecipe, buildBadge, buildChip, buildAccordionRecipe, buildDisclosureRecipe, buildTabsRecipe, buildLightboxRecipe, NO_ELEMENT_STYLE, CompositeKind, SliderRole, accordionItemLabel, isExclusivePanels, isLockedPart, accordionRootOf, isAccordionFromContent, accordionItemOf, addAccordionItem, addTabPair, isCompositeItem, accordionHeadingLevel, isCompositePanel, isCompositeTrigger, splitSliderChildren, hasSliderParts } from '@vectoriox/iox-ui/composite-core';
22
23
  import { Subject, BehaviorSubject, of, forkJoin } from 'rxjs';
23
24
  import interact from 'interactjs';
24
25
  import { formContentInHost, isInteractiveHost, findFormContentInHosts } from '@vectoriox/iox-ui/forms-core';
25
26
  import { map, catchError } from 'rxjs/operators';
26
27
  import * as i1 from '@angular/common/http';
27
- import { itemActionOf, BEHAVIOUR_ACTION_TYPES, SERVER_ELEMENT_ACTION_TYPES, CONSENT_ACTION_TYPES } from '@vectoriox/iox-ui/actions-core';
28
+ import { itemActionOf, BEHAVIOUR_ACTION_TYPES, SERVER_ELEMENT_ACTION_TYPES, CONSENT_ACTION_TYPES, opensAtClickedItem } from '@vectoriox/iox-ui/actions-core';
28
29
  import Lenis from 'lenis';
29
30
  import Moveable from 'moveable';
30
31
  import Selecto from 'selecto';
32
+ import * as i18 from 'primeng/api';
33
+ import { planVideo, VideoSource, VIDEO_PROVIDER_LABEL, VideoTrait, VideoMode } from '@vectoriox/iox-ui/media-core';
34
+ import { ConsentCategory } from '@vectoriox/iox-ui/consent-core';
35
+
36
+ /**
37
+ * node-path — pure ancestor-chain helpers for the builder's node tree.
38
+ *
39
+ * The builder needs a node's ancestor chain in several places: the breadcrumb bar,
40
+ * "select parent", and the parent-scoped style traits (Child Layout). Each of those
41
+ * used to re-walk the tree with its own private recursion. `nodeAncestorPath` is the
42
+ * single walk they all derive from.
43
+ *
44
+ * Builder-only on purpose: the SSR engine never needs a breadcrumb, so this stays in
45
+ * `iox-builder` rather than `@vectoriox/iox-ui` (which is the home for logic BOTH
46
+ * renderers consume). Structural helpers the two renderers share live in
47
+ * `@vectoriox/iox-ui` `style-compiler/tree-relations.ts`.
48
+ */
49
+ /**
50
+ * The chain of nodes from a root down to (and including) `target`, matched by
51
+ * REFERENCE identity. Returns `null` when `target` is not in the tree.
52
+ *
53
+ * Depth-first, pre-order; first match wins. Reference identity (not `id`) is the right
54
+ * matcher here because the builder mutates nodes in place and repeater/slider clones
55
+ * deliberately share a template's `styleId` — matching by id would return the template's
56
+ * path for a clone.
57
+ */
58
+ function nodeAncestorPath(roots, target) {
59
+ if (!roots?.length || !target)
60
+ return null;
61
+ for (const node of roots) {
62
+ if (node === target)
63
+ return [node];
64
+ const childPath = nodeAncestorPath(node.children, target);
65
+ if (childPath)
66
+ return [node, ...childPath];
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * The immediate parent of `target`, or `null` when it is a root node or not in the tree.
72
+ * Derived from `nodeAncestorPath` so there is exactly one tree walk to keep correct.
73
+ */
74
+ function findParentOf(roots, target) {
75
+ const path = nodeAncestorPath(roots, target);
76
+ return path && path.length >= 2 ? path[path.length - 2] : null;
77
+ }
78
+ /**
79
+ * The ancestor chain across BOTH builder trees — the page layout and the global
80
+ * elements overlay, which are separate roots. Returns the first tree that contains
81
+ * the node, or `null` when neither does.
82
+ */
83
+ function nodeAncestorPathInEither(layout, globalElements, target) {
84
+ return nodeAncestorPath(layout, target) ?? nodeAncestorPath(globalElements, target);
85
+ }
86
+
87
+ /**
88
+ * A tab and its panel go TOGETHER on the canvas (CC4a — composite T18): deleting either deletes both ("Delete tab and
89
+ * its panel"), duplicating either duplicates both under one fresh key. The pairing is composite-core's (`itemKey`);
90
+ * this is the builder's tree surgery over it, shared by every path (toolbar, layer tree, the host's Cmd+D / Delete).
91
+ */
92
+ /** The Tabs a tab or a panel belongs to — a MANUAL one (From content, the rows are not nodes) — or null. */
93
+ function tabsRootForPart(layout, globals, node) {
94
+ if (!isExclusiveItemPart(node))
95
+ return null;
96
+ const path = nodeAncestorPathInEither(layout, globals, node) ?? [];
97
+ const root = exclusiveRootOf(node, path.filter(n => n !== node));
98
+ return root && !isTabsFromContent(root) ? root : null;
99
+ }
100
+ /** The words the pair is named by in the confirm: its tab's. */
101
+ function tabPairLabel(root, node) {
102
+ const tree = root;
103
+ const tab = node.role === TabsRole.Panel ? tabTriggerOf(tree, node) : node;
104
+ return tab ? tabLabel(tab) : '';
105
+ }
106
+ /** Delete a tab and its panel. True when the pair was removed. */
107
+ function deleteTabPair(root, node) {
108
+ return removeTabPair(root, node);
109
+ }
110
+ /**
111
+ * Duplicate a tab and its panel: copies (`clone` gives fresh ids) placed right after the originals, both under ONE new
112
+ * key — so the copy's tab shows the copy's panel. Returns the new tab, or null when `node` is no tab or panel of `root`.
113
+ */
114
+ function duplicateTabPair(root, node, clone) {
115
+ const tree = root;
116
+ const tab = (node.role === TabsRole.Panel ? tabTriggerOf(tree, node) : node);
117
+ const panel = tab ? tabPanelOf(tree, tab) : undefined;
118
+ const list = tabsListOf(tree);
119
+ const panels = tabsPanelsOf(tree);
120
+ if (!tab || !panel || !list?.children || !panels?.children)
121
+ return null;
122
+ const key = nextTabKey(tree);
123
+ const tabCopy = clone(tab);
124
+ const panelCopy = clone(panel);
125
+ tabCopy.itemKey = panelCopy.itemKey = key;
126
+ list.children = insertAfter(list.children, tab, tabCopy);
127
+ panels.children = insertAfter(panels.children, panel, panelCopy);
128
+ return tabCopy;
129
+ }
130
+ function insertAfter(list, anchor, node) {
131
+ const out = [...list];
132
+ out.splice(out.indexOf(anchor) + 1, 0, node);
133
+ return out;
134
+ }
31
135
 
32
136
  var PanelEventTypes;
33
137
  (function (PanelEventTypes) {
@@ -212,12 +316,28 @@ const BUILDER_CHROME_EN = Object.freeze({
212
316
  'builder.chrome.kindCard': 'Card',
213
317
  'builder.chrome.kindAccordion': 'Accordion',
214
318
  'builder.chrome.kindDisclosure': 'Disclosure',
319
+ 'builder.chrome.kindSlider': 'Slider',
320
+ 'builder.chrome.kindVideo': 'Video',
321
+ 'builder.chrome.videoPosterOnly': 'poster only',
322
+ 'builder.chrome.videoFile': 'Uploaded file',
323
+ 'builder.chrome.refuseLockedSliderPartMove': 'This control belongs to its slider — it can’t be moved out.',
324
+ 'builder.chrome.refuseLockedSliderPause': 'The pause button stays while Autoplay is on — moving content must be pausable. Turn Autoplay off in the slider’s Bindings to remove it.',
215
325
  'builder.chrome.refuseLockedDisclosurePart': 'A disclosure keeps its toggle and its panel. Delete the whole disclosure instead.',
216
326
  'builder.chrome.refuseLockedDisclosurePartMove': 'This part belongs to its disclosure — it can’t be moved out.',
217
327
  'builder.chrome.refuseLockedItemPart': 'An item keeps its header and its panel. Delete the whole item, or the accordion.',
218
328
  'builder.chrome.refuseLockedItemPartMove': 'This part belongs to its item — it can’t be moved out.',
219
329
  'builder.chrome.refuseLastItem': 'An accordion keeps at least one item. Delete the whole accordion instead.',
220
330
  'builder.chrome.addItem': 'Add item',
331
+ 'builder.chrome.kindTabs': 'Tabs',
332
+ 'builder.chrome.addTab': 'Add tab',
333
+ 'builder.chrome.refuseLockedTabsPart': 'Tabs keep their tab list and their panels. Delete the whole Tabs instead.',
334
+ 'builder.chrome.refuseLockedTabsPartMove': 'This part belongs to its Tabs — it can’t be moved out.',
335
+ 'builder.chrome.refuseTabsAccepts': 'A tab list holds only tabs, and the panels only panels. Put it inside a panel instead.',
336
+ 'builder.chrome.refuseLastTab': 'Tabs keep at least one tab. Delete the whole Tabs instead.',
337
+ 'builder.chrome.deletePairTitle': 'Delete tab and its panel?',
338
+ 'builder.chrome.deletePairBody': '“{tab}” and the panel it shows go together: deleting one deletes both. Undo brings them back.',
339
+ 'builder.chrome.deletePair': 'Delete tab and its panel',
340
+ 'builder.chrome.cancel': 'Cancel',
221
341
  'builder.chrome.fromContent': 'From {source} · {count} rows',
222
342
  'builder.chrome.emptySource': 'No rows in “{alias}”.',
223
343
  'builder.chrome.emptySourceHint': 'The {type} content type has no published items that match this source’s filters. Add items in Content ▸ {type}, or change the source.',
@@ -470,6 +590,21 @@ var InsertRefusal;
470
590
  InsertRefusal["LockedDisclosurePartMove"] = "lockedDisclosurePartMove";
471
591
  /** A disclosure's toggle or panel (CC2.1) deleted on its own. */
472
592
  InsertRefusal["LockedDisclosurePart"] = "lockedDisclosurePart";
593
+ /** A Slider's control (CC3) works only as its own direct child. */
594
+ InsertRefusal["LockedSliderPartMove"] = "lockedSliderPartMove";
595
+ /** The pause control stays while Autoplay is on (WCAG 2.2.2) — the Autoplay switch removes it. */
596
+ InsertRefusal["LockedSliderPause"] = "lockedSliderPause";
597
+ /** A Tabs' tab list or panels holder (CC4a) moved out of it. */
598
+ InsertRefusal["LockedTabsPartMove"] = "lockedTabsPartMove";
599
+ /** A Tabs' tab list or panels holder (CC4a) deleted on its own. */
600
+ InsertRefusal["LockedTabsPart"] = "lockedTabsPart";
601
+ /** Something other than a tab dropped into a tab list, or other than a panel into the panels holder (CC4a T18). */
602
+ InsertRefusal["TabsAccepts"] = "tabsAccepts";
603
+ /** A Tabs' LAST tab (composite plan T18): it keeps at least one. */
604
+ InsertRefusal["LastTab"] = "lastTab";
605
+ /** A tab or a panel deleted ALONE: the pair goes together — the host offers "Delete tab and its panel". Never said
606
+ * as a refusal toast. */
607
+ InsertRefusal["PairedPart"] = "pairedPart";
473
608
  })(InsertRefusal || (InsertRefusal = {}));
474
609
  const FORM_STATE_BLOCK_TYPES = new Set(['FormSuccess', 'FormError']);
475
610
  /** The builder-chrome label that says each refusal on screen (English in `BUILDER_CHROME_EN`). */
@@ -484,7 +619,16 @@ const INSERT_REFUSAL_LABEL_KEYS = Object.freeze({
484
619
  [InsertRefusal.LastItem]: 'builder.chrome.refuseLastItem',
485
620
  [InsertRefusal.LockedDisclosurePartMove]: 'builder.chrome.refuseLockedDisclosurePartMove',
486
621
  [InsertRefusal.LockedDisclosurePart]: 'builder.chrome.refuseLockedDisclosurePart',
622
+ [InsertRefusal.LockedSliderPartMove]: 'builder.chrome.refuseLockedSliderPartMove',
623
+ [InsertRefusal.LockedSliderPause]: 'builder.chrome.refuseLockedSliderPause',
624
+ [InsertRefusal.LockedTabsPartMove]: 'builder.chrome.refuseLockedTabsPartMove',
625
+ [InsertRefusal.LockedTabsPart]: 'builder.chrome.refuseLockedTabsPart',
626
+ [InsertRefusal.TabsAccepts]: 'builder.chrome.refuseTabsAccepts',
627
+ [InsertRefusal.LastTab]: 'builder.chrome.refuseLastTab',
628
+ [InsertRefusal.PairedPart]: 'builder.chrome.deletePairTitle',
487
629
  });
630
+ /** The refusals that are not said as a toast: the host asks instead (a tab's pair). */
631
+ const SILENT_REFUSALS = new Set([InsertRefusal.PairedPart]);
488
632
  /** Which refusal says a locked part's move / delete, by the composite it belongs to (a card's is the default). */
489
633
  function lockedRefusal(node, edit) {
490
634
  const move = edit === StructureEdit.Move;
@@ -492,6 +636,10 @@ function lockedRefusal(node, edit) {
492
636
  return move ? InsertRefusal.LockedItemPartMove : InsertRefusal.LockedItemPart;
493
637
  if (isDisclosurePart(node))
494
638
  return move ? InsertRefusal.LockedDisclosurePartMove : InsertRefusal.LockedDisclosurePart;
639
+ if (isSliderPart(node))
640
+ return move ? InsertRefusal.LockedSliderPartMove : InsertRefusal.LockedSliderPause;
641
+ if (isTabsPart(node))
642
+ return move ? InsertRefusal.LockedTabsPartMove : InsertRefusal.LockedTabsPart;
495
643
  return move ? InsertRefusal.LockedPartMove : InsertRefusal.LockedPart;
496
644
  }
497
645
  /** The refusal in the host's language (the library's English when no host resolver is provided). */
@@ -519,18 +667,28 @@ function insertRefusal(node, ancestors) {
519
667
  return InsertRefusal.NestedForm;
520
668
  if (formContentInHost(node, ancestors.some(isInteractiveHost)))
521
669
  return InsertRefusal.FormInInteractive;
522
- if (structureRefusal(node, StructureEdit.Move, ancestors))
670
+ const structural = structureRefusal(node, StructureEdit.Move, ancestors);
671
+ if (structural === StructureRefusal.NotAccepted)
672
+ return InsertRefusal.TabsAccepts;
673
+ if (structural)
523
674
  return lockedRefusal(node, StructureEdit.Move);
524
675
  return null;
525
676
  }
526
677
  /** Why `node` may not be REMOVED from where it is (deleted, cut, made global), or null when it may: a
527
- * composite's locked part stays — its Parts switch or deleting the whole composite are the ways out — and an
528
- * accordion keeps its last item. `siblings` = the list `node` sits in (where the caller has it). */
678
+ * composite's locked part stays — its Parts switch or deleting the whole composite are the ways out — an
679
+ * accordion keeps its last item, Tabs their last tab, and a tab or a panel never leaves without its pair (the host
680
+ * offers to delete both). `siblings` = the list `node` sits in (where the caller has it). */
529
681
  function removalRefusal(node, siblings) {
530
682
  if (structureRefusal(node, StructureEdit.Delete))
531
683
  return lockedRefusal(node, StructureEdit.Delete);
532
684
  if (node?.role === AccordionRole.Item && siblings && siblings.filter(s => s?.role === AccordionRole.Item).length <= 1)
533
685
  return InsertRefusal.LastItem;
686
+ if (isExclusiveItemPart(node)) {
687
+ const role = node.role === TabsRole.Trigger ? TabsRole.Trigger : TabsRole.Panel;
688
+ if (siblings && siblings.filter(s => s?.role === role).length <= 1)
689
+ return InsertRefusal.LastTab;
690
+ return InsertRefusal.PairedPart;
691
+ }
534
692
  return null;
535
693
  }
536
694
 
@@ -1231,13 +1389,17 @@ function buildColorSchemeTrait() {
1231
1389
  }
1232
1390
  /**
1233
1391
  * A composite ROOT's behaviour (composite plan §3 — ordinary traits on the root; CC2's Accordion: Allow
1234
- * several open, Open by default). No default value: a plain Container or Repeater saves neither — only a
1235
- * composite root's recipe writes them. Edited in the root's Behaviour section, never as generic rows.
1392
+ * several open, Open by default; CC4a's Tabs: Activate on, Orientation, and the tab list's accessible name). No
1393
+ * default value: a plain Container or Repeater saves none — only a composite root's recipe writes them. Edited in the
1394
+ * root's Behaviour (and Accessibility) section, never as generic rows.
1236
1395
  */
1237
1396
  function buildCompositeBehaviourTraits() {
1238
1397
  return [
1239
1398
  new TraitConfig(CompositeBehaviourTrait.Multiple, 'Allow several open', TraitInputType.Checkbox),
1240
1399
  new TraitConfig(CompositeBehaviourTrait.DefaultOn, 'Open by default', TraitInputType.Select, ACCORDION_DEFAULT_ONS.map(v => ({ label: v, value: v }))),
1400
+ new TraitConfig(TabsBehaviourTrait.ActivateOn, 'Activate on', TraitInputType.Select, TABS_ACTIVATE_ONS.map(v => ({ label: v, value: v }))),
1401
+ new TraitConfig(TabsBehaviourTrait.Orientation, 'Orientation', TraitInputType.Select, TABS_ORIENTATIONS.map(v => ({ label: v, value: v }))),
1402
+ new TraitConfig(TabsBehaviourTrait.Label, 'Accessible name', TraitInputType.Text),
1241
1403
  ];
1242
1404
  }
1243
1405
  const LINK_TARGETS = [
@@ -2347,6 +2509,8 @@ class DragEngineService {
2347
2509
  this.overlayService = overlayService;
2348
2510
  /** Every insert the builder refused, for the host to say on screen (a toast). */
2349
2511
  this.insertRefused$ = new Subject();
2512
+ /** A tab or a panel asked to leave ALONE (CC4a): the builder offers "Delete tab and its panel" for it. */
2513
+ this.pairRemoval$ = new Subject();
2350
2514
  // ── Drop-zone registry ────────────────────────────────────
2351
2515
  this.ids = [];
2352
2516
  this.elMap = new Map();
@@ -2439,7 +2603,9 @@ class DragEngineService {
2439
2603
  */
2440
2604
  refuseRemoval(node, siblings) {
2441
2605
  const reason = removalRefusal(node, siblings);
2442
- if (reason)
2606
+ if (reason === InsertRefusal.PairedPart)
2607
+ this.pairRemoval$.next(node);
2608
+ else if (reason && !SILENT_REFUSALS.has(reason))
2443
2609
  this.insertRefused$.next({ reason, message: insertRefusalLabel(reason, this.labels) });
2444
2610
  return reason;
2445
2611
  }
@@ -3602,8 +3768,8 @@ class InteractionEngineService {
3602
3768
  */
3603
3769
  attachItemState(node, ix, element, on) {
3604
3770
  const run = () => this.runActions(node, ix.actions, element);
3605
- const off = onAccordionItemState(element, on, run);
3606
- const timer = on && isAccordionItemOn(element) ? setTimeout(run, 50) : null;
3771
+ const off = onCompositeItemState(element, on, run);
3772
+ const timer = on && isCompositeItemOn(element) ? setTimeout(run, 50) : null;
3607
3773
  return () => { off(); if (timer)
3608
3774
  clearTimeout(timer); };
3609
3775
  }
@@ -3762,7 +3928,7 @@ class InteractionEngineService {
3762
3928
  // impossible. The canvas twin registered open/close/toggle under its nodeId, exactly
3763
3929
  // the Swiper-method pattern; a missing/mistargeted id is a silent no-op, never a throw.
3764
3930
  if (DIALOG_ACTION_TYPES.has(action.type)) {
3765
- this.executeDialogAction(action);
3931
+ this.executeDialogAction(action, selfEl);
3766
3932
  continue;
3767
3933
  }
3768
3934
  // Item actions (CC2.1 — composite T14): the canvas RUNS them on the target item's own runtime — the
@@ -3884,13 +4050,17 @@ class InteractionEngineService {
3884
4050
  const detail = { type: action.type, params: action.params ?? {}, target: action.target };
3885
4051
  window.dispatchEvent(new CustomEvent('iox-action-simulated', { detail }));
3886
4052
  }
3887
- executeDialogAction(action) {
4053
+ executeDialogAction(action, selfEl) {
3888
4054
  const dialog = action.target && action.target !== 'self' ? this.instanceRegistry.get(action.target) : null;
3889
4055
  if (!dialog)
3890
4056
  return;
3891
4057
  switch (action.type) {
3892
4058
  case 'openDialog':
3893
4059
  dialog.open?.();
4060
+ // CC3 "Open at the clicked item": the lightbox's slider goes to the clicked row's slide — the same
4061
+ // rule the live site runs (composite-core `slideIndexForRow`), once the canvas twin has rendered it.
4062
+ if (opensAtClickedItem(action))
4063
+ this.slideDialogToRow(action.target, rowIndexOf$1(selfEl));
3894
4064
  break;
3895
4065
  case 'closeDialog':
3896
4066
  dialog.close?.();
@@ -3900,6 +4070,22 @@ class InteractionEngineService {
3900
4070
  break;
3901
4071
  }
3902
4072
  }
4073
+ /** The slider inside the opened dialog (its canvas twin renders on the next passes) → the row's slide. */
4074
+ slideDialogToRow(dialogId, row) {
4075
+ let tries = 0;
4076
+ const attempt = () => {
4077
+ const host = document.querySelector(`.iox-node-${CSS.escape(dialogId)}`);
4078
+ const swiper = host?.querySelector('.swiper')?.swiper;
4079
+ if (swiper?.slides?.length) {
4080
+ swiper.update?.();
4081
+ swiper.slideTo(slideIndexForRow(row, swiper.slides.length), 0);
4082
+ return;
4083
+ }
4084
+ if (++tries < 40)
4085
+ requestAnimationFrame(attempt);
4086
+ };
4087
+ requestAnimationFrame(attempt);
4088
+ }
3903
4089
  executeSwiperAction(action) {
3904
4090
  // Target is the nodeId of the SliderContainer to control.
3905
4091
  const swiper = this.instanceRegistry.get(action.target);
@@ -4062,8 +4248,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
4062
4248
  * screen until something opens them, so on the canvas they show only while selected into or switched
4063
4249
  * open ("Showing on canvas"). The layer tree marks them with the eye-slash glyph. A Dialog (the mobile
4064
4250
  * menu's drawer, a consent banner) by its type; an Accordion's PANEL by its role (CC2 — closed until its
4065
- * item opens); a Disclosure's panel and its "Read less" word (CC2.1 — shown only while it is open). An inactive
4066
- * tab joins in CC4.
4251
+ * item opens); a Disclosure's panel and its "Read less" word (CC2.1 — shown only while it is open).
4067
4252
  */
4068
4253
  const HIDDEN_AT_REST_TYPES = ['Dialog'];
4069
4254
  const HIDDEN_AT_REST_ROLES = [AccordionRole.Panel, DisclosureRole.Panel, DisclosureRole.Less];
@@ -4082,8 +4267,21 @@ var SelectionReveal;
4082
4267
  SelectionReveal["WhileSelected"] = "while-selected";
4083
4268
  SelectionReveal["UntilClosed"] = "until-closed";
4084
4269
  })(SelectionReveal || (SelectionReveal = {}));
4270
+ /**
4271
+ * Parts hidden at rest that live in a composite's TOGGLE, not in its panel (CC2.1 follow-up, approved 2026-10-21): a
4272
+ * click on one on the canvas toggles the composite live — selecting it never forces the reveal, so "Read less"
4273
+ * closes the disclosure it was clicked in. Only a part inside a PANEL is revealed by the selection.
4274
+ */
4275
+ const TOGGLE_PART_ROLES = [DisclosureRole.Less];
4276
+ /**
4277
+ * Parts the selection REVEALS though they are not hidden at rest (CC4a): a tab PANEL — one is always on, so the layer
4278
+ * tree draws it no eye-slash (board 3), but selecting anything inside an inactive one shows its tab while selected.
4279
+ */
4280
+ const REVEALED_WHILE_SELECTED_ROLES = [TabsRole.Panel];
4085
4281
  function selectionRevealOf(node) {
4086
- if (!isHiddenAtRest(node))
4282
+ if (!!node?.role && REVEALED_WHILE_SELECTED_ROLES.includes(node.role))
4283
+ return SelectionReveal.WhileSelected;
4284
+ if (!isHiddenAtRest(node) || (!!node?.role && TOGGLE_PART_ROLES.includes(node.role)))
4087
4285
  return null;
4088
4286
  return node?.role ? SelectionReveal.WhileSelected : SelectionReveal.UntilClosed;
4089
4287
  }
@@ -4121,7 +4319,7 @@ var CanvasShowState;
4121
4319
  */
4122
4320
  class CanvasDesignStateService {
4123
4321
  constructor() {
4124
- this._state$ = new BehaviorSubject({ revealed: new Set(), pinned: new Set(), shown: new Map() });
4322
+ this._state$ = new BehaviorSubject({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), shown: new Map(), items: new Map() });
4125
4323
  this.state$ = this._state$.asObservable();
4126
4324
  this._pagePath$ = new BehaviorSubject(null);
4127
4325
  /** The app path of the page on the canvas (`/work`), or null when unknown. */
@@ -4145,11 +4343,12 @@ class CanvasDesignStateService {
4145
4343
  setSelectionPath(path) {
4146
4344
  const ids = (reveal) => path.filter(n => selectionRevealOf(n) === reveal).map(n => n.id).filter((id) => !!id);
4147
4345
  const revealed = new Set(ids(SelectionReveal.WhileSelected));
4148
- const { revealed: was, pinned: held } = this._state$.value;
4346
+ const selectedTriggers = new Set(path.filter(n => isExclusiveTrigger(n)).map(n => n.id).filter((id) => !!id));
4347
+ const { revealed: was, pinned: held, selectedTriggers: wasTriggers } = this._state$.value;
4149
4348
  const pinned = new Set([...held, ...ids(SelectionReveal.UntilClosed)]);
4150
- if (sameSet(revealed, was) && sameSet(pinned, held))
4349
+ if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers))
4151
4350
  return;
4152
- this._state$.next({ ...this._state$.value, revealed, pinned });
4351
+ this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers });
4153
4352
  }
4154
4353
  /** Closed on the canvas (its X, its backdrop, the eye): no longer pinned or switched open. */
4155
4354
  dismiss(nodeId) {
@@ -4173,6 +4372,21 @@ class CanvasDesignStateService {
4173
4372
  shown.delete(nodeId);
4174
4373
  this._state$.next({ ...this._state$.value, shown });
4175
4374
  }
4375
+ /** Tabs' "Showing on canvas": the item a root shows (null = whatever the canvas shows live). */
4376
+ setShownItem(rootId, key) {
4377
+ if ((this._state$.value.items.get(rootId) ?? null) === key)
4378
+ return;
4379
+ const items = new Map(this._state$.value.items);
4380
+ if (key)
4381
+ items.set(rootId, key);
4382
+ else
4383
+ items.delete(rootId);
4384
+ this._state$.next({ ...this._state$.value, items });
4385
+ }
4386
+ /** The item a root's "Showing on canvas" shows, or null. */
4387
+ shownItem(rootId) {
4388
+ return (rootId && this._state$.value.items.get(rootId)) || null;
4389
+ }
4176
4390
  /** The switcher's current choice for one part. */
4177
4391
  shownState(nodeId) {
4178
4392
  return (nodeId && this._state$.value.shown.get(nodeId)) || CanvasShowState.Closed;
@@ -4190,7 +4404,7 @@ class CanvasDesignStateService {
4190
4404
  }
4191
4405
  /** A new page on the canvas: nothing is revealed or switched open. */
4192
4406
  reset() {
4193
- this._state$.next({ revealed: new Set(), pinned: new Set(), shown: new Map() });
4407
+ this._state$.next({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), shown: new Map(), items: new Map() });
4194
4408
  }
4195
4409
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4196
4410
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService }); }
@@ -4208,14 +4422,15 @@ function designIdOf(node) {
4208
4422
  return node.__previewClone ? (node.styleId ?? node.id) : node.id;
4209
4423
  }
4210
4424
  /**
4211
- * The composites' runtime ON THE CANVAS (CC2) — the canvas is a live runtime, so an Accordion (and a Disclosure,
4212
- * CC2.1) opens and closes on the canvas exactly as on the site: the SAME composite-core `AccordionRuntime` the live
4213
- * engine attaches. A composite repeated in a Repeater's preview rows runs one per row, each with its own ARIA ids
4214
- * (its row folded into the scope id — vectoriox/iox-ui#20). On top of the live state it shows what the author is
4215
- * designing (R2, CC1's design state):
4216
- * - selecting anything inside a CLOSED panel (canvas, tree, crumb, an Items row, SELECT_NODE) opens its item
4217
- * while it is selected, and deselecting closes it again;
4218
- * - "Showing on canvas ▸ All open" opens every item; "As saved" goes back to the saved default.
4425
+ * The composites' runtimes ON THE CANVAS (CC2, CC4a) — the canvas is a live runtime, so an Accordion (and a Disclosure,
4426
+ * CC2.1) opens and closes, and Tabs switch, on the canvas exactly as on the site: the SAME composite-core runtimes the
4427
+ * live engine attaches (`AccordionRuntime`, `ExclusiveRuntime`). A composite repeated in a Repeater's preview rows runs
4428
+ * one per row, each with its own ARIA ids (its row folded into the scope id — vectoriox/iox-ui#20). On top of the live
4429
+ * state it shows what the author is designing (R2, CC1's design state):
4430
+ * - selecting anything inside a CLOSED panel or an INACTIVE tab panel (canvas, tree, crumb, an Items row, SELECT_NODE)
4431
+ * shows it while it is selected, and deselecting goes back;
4432
+ * - selecting a TAB (from anywhere) turns it on, live, like a click on it;
4433
+ * - "Showing on canvas": All open / As saved (accordion), one option per tab (Tabs).
4219
4434
  * Canvas-only: it never writes a node, so the saved page is unchanged and nothing becomes an undo step.
4220
4435
  */
4221
4436
  class CompositeCanvasService {
@@ -4223,11 +4438,12 @@ class CompositeCanvasService {
4223
4438
  this.designState = designState;
4224
4439
  this.zone = zone;
4225
4440
  this.attached = new Map();
4441
+ this.exclusive = new Map();
4226
4442
  this.sub = designState?.state$.subscribe(() => this.applyDesignState()) ?? null;
4227
4443
  }
4228
4444
  ngOnDestroy() {
4229
4445
  this.sub?.unsubscribe();
4230
- for (const id of [...this.attached.keys()])
4446
+ for (const id of [...this.attached.keys(), ...this.exclusive.keys()])
4231
4447
  this.detach(id);
4232
4448
  }
4233
4449
  /** A composite root rendered on the canvas (the template, or a preview row's copy): run it. `element` is the
@@ -4237,9 +4453,28 @@ class CompositeCanvasService {
4237
4453
  return;
4238
4454
  this.detach(node.id);
4239
4455
  const cssId = node.styleId ?? node.id;
4456
+ const rootId = () => compositeScopeId(cssId, compositeRowKeys(element));
4457
+ if (isExclusiveRoot(node)) {
4458
+ const runtime = this.zone.runOutsideAngular(() => new ExclusiveRuntime(element, {
4459
+ rootId,
4460
+ behaviour: tabsBehaviour(node),
4461
+ label: () => tabsLabel(node),
4462
+ observe: true,
4463
+ forced: () => this.forcedTab(node, element),
4464
+ // The switcher follows the TEMPLATE's clicks; a preview row's copy switches on its own.
4465
+ onChange: key => { if (!node.__previewClone && node.id)
4466
+ this.designState?.setShownItem(node.id, key); },
4467
+ }));
4468
+ this.exclusive.set(node.id, { node, runtime, selected: new Set() });
4469
+ const shown = this.designState?.shownItem(designIdOf(node));
4470
+ if (shown)
4471
+ runtime.activate(shown);
4472
+ return;
4473
+ }
4474
+ const kind = compositeKindOfRole(node.role);
4240
4475
  const runtime = this.zone.runOutsideAngular(() => new AccordionRuntime(element, {
4241
- kind: compositeKindOfRole(node.role) ?? undefined,
4242
- rootId: () => compositeScopeId(cssId, compositeRowKeys(element)),
4476
+ kind: isToggleKind(kind) ? kind : undefined,
4477
+ rootId,
4243
4478
  behaviour: accordionBehaviour(node),
4244
4479
  observe: true,
4245
4480
  forced: () => this.forcedFor(node, element),
@@ -4251,16 +4486,24 @@ class CompositeCanvasService {
4251
4486
  return;
4252
4487
  this.attached.get(nodeId)?.runtime.destroy();
4253
4488
  this.attached.delete(nodeId);
4489
+ this.exclusive.get(nodeId)?.runtime.destroy();
4490
+ this.exclusive.delete(nodeId);
4254
4491
  }
4255
- /** The root's behaviour changed in its panel (Allow several open, Open by default). */
4492
+ /** The root's behaviour changed in its panel (Allow several open, Open by default; Activate on, Orientation). */
4256
4493
  refreshBehaviour(node) {
4257
4494
  const entry = node.id ? this.attached.get(node.id) : undefined;
4258
4495
  entry?.runtime.setBehaviour(accordionBehaviour(node));
4496
+ const tabs = node.id ? this.exclusive.get(node.id) : undefined;
4497
+ tabs?.runtime.setBehaviour(tabsBehaviour(node));
4259
4498
  }
4260
- /** The runtime of an attached root (the e2e harness and the panels read its state). */
4499
+ /** The runtime of an attached Accordion / Disclosure root (the e2e harness and the panels read its state). */
4261
4500
  runtimeOf(nodeId) {
4262
4501
  return (nodeId && this.attached.get(nodeId)?.runtime) || null;
4263
4502
  }
4503
+ /** The runtime of an attached Tabs root (CC4a — "Showing on canvas" reads its tabs and the one on). */
4504
+ tabsRuntimeOf(nodeId) {
4505
+ return (nodeId && this.exclusive.get(nodeId)?.runtime) || null;
4506
+ }
4264
4507
  applyDesignState() {
4265
4508
  for (const entry of this.attached.values()) {
4266
4509
  const shown = this.designState?.shownState(designIdOf(entry.node)) ?? CanvasShowState.Closed;
@@ -4271,6 +4514,26 @@ class CompositeCanvasService {
4271
4514
  entry.runtime.refresh();
4272
4515
  entry.shown = shown;
4273
4516
  }
4517
+ for (const entry of this.exclusive.values())
4518
+ this.applyExclusive(entry);
4519
+ }
4520
+ /** A Tabs on the canvas follows "Showing on canvas" and a selected tab (live), then shows any forced panel. */
4521
+ applyExclusive(entry) {
4522
+ const state = this.designState;
4523
+ if (!state)
4524
+ return;
4525
+ const selected = state.snapshot.selectedTriggers;
4526
+ const fresh = [...selected].filter(id => !entry.selected.has(id));
4527
+ entry.selected = new Set(selected);
4528
+ for (const id of fresh) {
4529
+ const key = entry.runtime.keyAt(entry.runtime.root.querySelector(`.iox-node-${CSS.escape(id)}`));
4530
+ if (key)
4531
+ entry.runtime.activate(key);
4532
+ }
4533
+ const shown = state.shownItem(designIdOf(entry.node));
4534
+ if (shown && shown !== entry.runtime.liveKey)
4535
+ entry.runtime.activate(shown);
4536
+ entry.runtime.refresh();
4274
4537
  }
4275
4538
  /** The keys the design state holds open: every item ("All open"), or the items whose hidden part the
4276
4539
  * selection sits in. A preview row shares its template's classes, so the template (row 1) answers — and a
@@ -4294,6 +4557,19 @@ class CompositeCanvasService {
4294
4557
  }
4295
4558
  return forced;
4296
4559
  }
4560
+ /** The tab forced on: the one whose panel the selection sits in (the template's, in a preview row's copy). */
4561
+ forcedTab(node, root) {
4562
+ const state = this.designState;
4563
+ const runtime = node.id ? this.exclusive.get(node.id)?.runtime : undefined;
4564
+ if (!state || !runtime)
4565
+ return null;
4566
+ for (const id of state.snapshot.revealed) {
4567
+ const key = runtime.keyAt(root.querySelector(`.iox-node-${CSS.escape(id)}`));
4568
+ if (key)
4569
+ return key;
4570
+ }
4571
+ return null;
4572
+ }
4297
4573
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CompositeCanvasService, deps: [{ token: CanvasDesignStateService, optional: true }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); }
4298
4574
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CompositeCanvasService }); }
4299
4575
  }
@@ -4806,7 +5082,7 @@ class RenderDirective {
4806
5082
  // A composite root runs its behaviour on the canvas — the live site's runtime (CC2; the Disclosure, CC2.1).
4807
5083
  // A preview row's copy runs too, scoped by its row (vectoriox/iox-ui#20): its ids are its own and it shows
4808
5084
  // the saved default, as every row does on the live site.
4809
- if (nativeEl && node.id && isCompositeRoot(node) && this.compositeCanvas) {
5085
+ if (nativeEl && node.id && isAnyCompositeRoot(node) && this.compositeCanvas) {
4810
5086
  this.compositeCanvas.attach(node, nativeEl);
4811
5087
  this.compositeRootId = node.id;
4812
5088
  }
@@ -4946,7 +5222,8 @@ class BuilderRepeaterComponent {
4946
5222
  }
4947
5223
  /** A composite whose source answered with no rows: the placeholder takes the template's place. */
4948
5224
  get showsEmptySource() {
4949
- return isAccordionRoot(this.node) && !!this._source && this.fetched && !this.isLoading && !this.items.length;
5225
+ // An Accordion's — or a Tabs' list (CC4a; its panels holder says nothing twice) — source that came back empty.
5226
+ return (isAccordionRoot(this.node) || isExclusiveList(this.node)) && !!this._source && this.fetched && !this.isLoading && !this.items.length;
4950
5227
  }
4951
5228
  get emptySourceTitle() {
4952
5229
  return this.label(EMPTY_SOURCE_KEY).replace('{alias}', this._source);
@@ -5234,7 +5511,7 @@ class BuilderRepeaterComponent {
5234
5511
  </ng-container>
5235
5512
  </div>
5236
5513
  </div>
5237
- `, isInline: true, styles: [".repeater-wrapper{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-wrapper.has-children>.repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-wrapper.has-children:hover>.repeater-badge{opacity:1}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-root{box-sizing:border-box}.repeater-wrapper:not(.has-children) .repeater-root{border:1px dashed rgba(203,144,144,.4);border-top:none;border-radius:0 0 4px 4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-placeholder--source{border:1px dashed rgba(203,144,144,.5);border-radius:4px;text-align:center;color:#475569d9;font-size:13px;line-height:1.5}.repeater-placeholder--source strong{font-weight:600;color:#1e293be6}.repeater-item-template{min-width:0}.repeater-preview-child{opacity:.55;pointer-events:none;min-width:0}:host(.iox-role-accordion) .repeater-preview-child{pointer-events:auto}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId", "ioxPhrasing"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId", "ioxDragDisabled"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
5514
+ `, isInline: true, styles: [".repeater-wrapper{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-wrapper.has-children>.repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-wrapper.has-children:hover>.repeater-badge{opacity:1}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-root{box-sizing:border-box}.repeater-wrapper:not(.has-children) .repeater-root{border:1px dashed rgba(203,144,144,.4);border-top:none;border-radius:0 0 4px 4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-placeholder--source{border:1px dashed rgba(203,144,144,.5);border-radius:4px;text-align:center;color:#475569d9;font-size:13px;line-height:1.5}.repeater-placeholder--source strong{font-weight:600;color:#1e293be6}.repeater-item-template{min-width:0}.repeater-preview-child{opacity:.55;pointer-events:none;min-width:0}:host(.iox-role-accordion) .repeater-preview-child,:host(.iox-role-tabs-list) .repeater-preview-child,:host(.iox-role-tabs-panels) .repeater-preview-child{pointer-events:auto}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId", "ioxPhrasing"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId", "ioxDragDisabled"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
5238
5515
  }
5239
5516
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderRepeaterComponent, decorators: [{
5240
5517
  type: Component,
@@ -5328,7 +5605,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
5328
5605
  </ng-container>
5329
5606
  </div>
5330
5607
  </div>
5331
- `, standalone: false, styles: [".repeater-wrapper{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-wrapper.has-children>.repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-wrapper.has-children:hover>.repeater-badge{opacity:1}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-root{box-sizing:border-box}.repeater-wrapper:not(.has-children) .repeater-root{border:1px dashed rgba(203,144,144,.4);border-top:none;border-radius:0 0 4px 4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-placeholder--source{border:1px dashed rgba(203,144,144,.5);border-radius:4px;text-align:center;color:#475569d9;font-size:13px;line-height:1.5}.repeater-placeholder--source strong{font-weight:600;color:#1e293be6}.repeater-item-template{min-width:0}.repeater-preview-child{opacity:.55;pointer-events:none;min-width:0}:host(.iox-role-accordion) .repeater-preview-child{pointer-events:auto}\n"] }]
5608
+ `, standalone: false, styles: [".repeater-wrapper{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-wrapper.has-children>.repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-wrapper.has-children:hover>.repeater-badge{opacity:1}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-root{box-sizing:border-box}.repeater-wrapper:not(.has-children) .repeater-root{border:1px dashed rgba(203,144,144,.4);border-top:none;border-radius:0 0 4px 4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-placeholder--source{border:1px dashed rgba(203,144,144,.5);border-radius:4px;text-align:center;color:#475569d9;font-size:13px;line-height:1.5}.repeater-placeholder--source strong{font-weight:600;color:#1e293be6}.repeater-item-template{min-width:0}.repeater-preview-child{opacity:.55;pointer-events:none;min-width:0}:host(.iox-role-accordion) .repeater-preview-child,:host(.iox-role-tabs-list) .repeater-preview-child,:host(.iox-role-tabs-panels) .repeater-preview-child{pointer-events:auto}\n"] }]
5332
5609
  }], ctorParameters: () => [{ type: DragEngineService }, { type: OverlayService }, { type: BuilderDataSourceService }, { type: DataSourceRegistryService }, { type: PanelEventService }, { type: i0.ChangeDetectorRef }, { type: InteractionEngineService }, { type: undefined, decorators: [{
5333
5610
  type: Optional
5334
5611
  }, {
@@ -5373,9 +5650,11 @@ var CompositePresetId;
5373
5650
  CompositePresetId["Chip"] = "chip";
5374
5651
  CompositePresetId["Accordion"] = "accordion";
5375
5652
  CompositePresetId["Disclosure"] = "disclosure";
5653
+ CompositePresetId["Tabs"] = "tabs";
5654
+ CompositePresetId["Lightbox"] = "lightbox";
5376
5655
  })(CompositePresetId || (CompositePresetId = {}));
5377
5656
  /** Where a composite sits in the Components panel (composite plan §6d): content blocks, or the interactive
5378
- * composites (the Accordion — CC2, the Disclosure — CC2.1; Tabs and the floating family later). */
5657
+ * composites (the Accordion — CC2, the Disclosure — CC2.1, Tabs — CC4a; the floating family later). */
5379
5658
  var CompositePaletteGroup;
5380
5659
  (function (CompositePaletteGroup) {
5381
5660
  CompositePaletteGroup["ContentBlocks"] = "content-blocks";
@@ -5407,6 +5686,16 @@ const COMPOSITE_PRESETS = Object.freeze([
5407
5686
  description: '"Read more" — a button that reveals the text below it and reads "Read less" while it is open.',
5408
5687
  build: create => buildDisclosureRecipe(create),
5409
5688
  },
5689
+ {
5690
+ id: CompositePresetId.Tabs, group: CompositePaletteGroup.Interactive, name: 'Tabs', icon: 'ph-thin ph-tabs',
5691
+ description: 'Tabs that switch what shows below them — three tabs, the first on. Every tab and panel is its own element.',
5692
+ build: create => buildTabsRecipe(create),
5693
+ },
5694
+ {
5695
+ id: CompositePresetId.Lightbox, group: CompositePaletteGroup.Interactive, name: 'Lightbox', icon: 'ph-thin ph-frame-corners',
5696
+ description: 'A full-screen dialog with a slider — bind its slider to your gallery\'s source, and each thumbnail opens it at its own image.',
5697
+ build: create => buildLightboxRecipe(create),
5698
+ },
5410
5699
  ]);
5411
5700
  /**
5412
5701
  * The builder's node factory for composite-core recipes: a NEW node as the palette creates one
@@ -5426,6 +5715,11 @@ function builderCompositeFactory(createConfig, baselineStyles) {
5426
5715
  if (spec.elementStyle && spec.elementStyle !== node.elementStyle) {
5427
5716
  switchElementStyle(node, node.elementStyle, spec.elementStyle, baselineStyles(spec.type));
5428
5717
  }
5718
+ else if (spec.elementStyle === NO_ELEMENT_STYLE && node.elementStyle) {
5719
+ // NO_ELEMENT_STYLE asks for no style (undefined / null keep the type's default): a migrated slider's bare
5720
+ // chevrons, a lightbox's slide (CC3).
5721
+ releaseElementStyle(node, node.elementStyle, baselineStyles(spec.type));
5722
+ }
5429
5723
  if (node.children)
5430
5724
  node.children = [];
5431
5725
  return node;
@@ -5543,57 +5837,6 @@ function overlayGlobals(globals) {
5543
5837
  return { before, pinnedAfter };
5544
5838
  }
5545
5839
 
5546
- /**
5547
- * node-path — pure ancestor-chain helpers for the builder's node tree.
5548
- *
5549
- * The builder needs a node's ancestor chain in several places: the breadcrumb bar,
5550
- * "select parent", and the parent-scoped style traits (Child Layout). Each of those
5551
- * used to re-walk the tree with its own private recursion. `nodeAncestorPath` is the
5552
- * single walk they all derive from.
5553
- *
5554
- * Builder-only on purpose: the SSR engine never needs a breadcrumb, so this stays in
5555
- * `iox-builder` rather than `@vectoriox/iox-ui` (which is the home for logic BOTH
5556
- * renderers consume). Structural helpers the two renderers share live in
5557
- * `@vectoriox/iox-ui` `style-compiler/tree-relations.ts`.
5558
- */
5559
- /**
5560
- * The chain of nodes from a root down to (and including) `target`, matched by
5561
- * REFERENCE identity. Returns `null` when `target` is not in the tree.
5562
- *
5563
- * Depth-first, pre-order; first match wins. Reference identity (not `id`) is the right
5564
- * matcher here because the builder mutates nodes in place and repeater/slider clones
5565
- * deliberately share a template's `styleId` — matching by id would return the template's
5566
- * path for a clone.
5567
- */
5568
- function nodeAncestorPath(roots, target) {
5569
- if (!roots?.length || !target)
5570
- return null;
5571
- for (const node of roots) {
5572
- if (node === target)
5573
- return [node];
5574
- const childPath = nodeAncestorPath(node.children, target);
5575
- if (childPath)
5576
- return [node, ...childPath];
5577
- }
5578
- return null;
5579
- }
5580
- /**
5581
- * The immediate parent of `target`, or `null` when it is a root node or not in the tree.
5582
- * Derived from `nodeAncestorPath` so there is exactly one tree walk to keep correct.
5583
- */
5584
- function findParentOf(roots, target) {
5585
- const path = nodeAncestorPath(roots, target);
5586
- return path && path.length >= 2 ? path[path.length - 2] : null;
5587
- }
5588
- /**
5589
- * The ancestor chain across BOTH builder trees — the page layout and the global
5590
- * elements overlay, which are separate roots. Returns the first tree that contains
5591
- * the node, or `null` when neither does.
5592
- */
5593
- function nodeAncestorPathInEither(layout, globalElements, target) {
5594
- return nodeAncestorPath(layout, target) ?? nodeAncestorPath(globalElements, target);
5595
- }
5596
-
5597
5840
  /** Counts for ONE node (the selection). Breakpoints with nothing overridden are omitted. */
5598
5841
  function nodeBreakpointCounts(node) {
5599
5842
  const counts = {};
@@ -6855,10 +7098,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
6855
7098
  * An accordion's and a disclosure's parts are named by the part alone (`trigger`, "Accordion · trigger", board 3);
6856
7099
  * a card keeps its full role (`card.title`, "Card · card.title", board 8).
6857
7100
  */
6858
- const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure]);
7101
+ const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure, CompositeKind.Tabs]);
6859
7102
  /** A composite kind's glyph (Phosphor, weight-less): the badge, the root's crumb and its tree row carry it
6860
7103
  * (board 3: "the composite root carrying its kind icon"). A card keeps the generic stack glyph. */
6861
- const KIND_GLYPHS = { [CompositeKind.Accordion]: 'ph-rows', [CompositeKind.Disclosure]: 'ph-caret-circle-down' };
7104
+ const KIND_GLYPHS = { [CompositeKind.Accordion]: 'ph-rows', [CompositeKind.Disclosure]: 'ph-caret-circle-down', [CompositeKind.Tabs]: 'ph-tabs', slider: 'ph-slideshow' };
6862
7105
  const DEFAULT_KIND_GLYPH = 'ph-stack';
6863
7106
  /** The tree icon of an accordion's structural parts (board 3): rows on the root, a stack on an item and a
6864
7107
  * panel, a cursor on the header, a sparkle on the icon. Other parts keep their type's icon. */
@@ -6874,6 +7117,17 @@ const PART_ICONS = {
6874
7117
  [DisclosureRole.Trigger]: 'ph-thin ph-cursor',
6875
7118
  [DisclosureRole.Panel]: 'ph-thin ph-stack',
6876
7119
  [DisclosureRole.Icon]: 'ph-thin ph-sparkle',
7120
+ // CC4a — Tabs (board 3): the tabs glyph on the root, a stack on the list, the panels and each panel, a cursor on a tab.
7121
+ [TabsRole.Tabs]: 'ph-thin ph-tabs',
7122
+ [TabsRole.List]: 'ph-thin ph-stack',
7123
+ [TabsRole.Trigger]: 'ph-thin ph-cursor',
7124
+ [TabsRole.Panels]: 'ph-thin ph-stack',
7125
+ [TabsRole.Panel]: 'ph-thin ph-stack',
7126
+ // CC3 — a Slider's controls (board 6): a cursor on the buttons, six dots on the pagination.
7127
+ [SliderRole.Pause]: 'ph-thin ph-cursor',
7128
+ [SliderRole.Prev]: 'ph-thin ph-cursor',
7129
+ [SliderRole.Next]: 'ph-thin ph-cursor',
7130
+ [SliderRole.Pagination]: 'ph-thin ph-dots-six',
6877
7131
  };
6878
7132
  /** The glyph of a role's composite kind (`ph-rows` for an accordion and its parts). */
6879
7133
  function compositeKindGlyph(role) {
@@ -6896,11 +7150,20 @@ function roleBadgeLabel(role, kindLabel) {
6896
7150
  return part ? `${kindLabel} · ${part}` : kindLabel;
6897
7151
  }
6898
7152
  /** What a part's row and crumb say after its name: an accordion item names its question ("Item · Do you take
6899
- * projects…", board 3), so three rows called "Item" can be told apart. */
6900
- function compositePartSub(node) {
6901
- if (node.role !== AccordionRole.Item)
6902
- return null;
6903
- return accordionItemLabel(node) || null;
7153
+ * projects…", board 3), and a tab AND its panel name the tab's words ("Tab · Kitchens", "Panel · Kitchens") — so
7154
+ * three rows called "Item" or "Panel" can be told apart. `ancestors` = the node's ancestors, root first (a panel finds
7155
+ * its tab through its Tabs). */
7156
+ function compositePartSub(node, ancestors = []) {
7157
+ if (node.role === AccordionRole.Item)
7158
+ return accordionItemLabel(node) || null;
7159
+ if (node.role === TabsRole.Trigger)
7160
+ return tabLabel(node) || null;
7161
+ if (node.role === TabsRole.Panel) {
7162
+ const root = exclusiveRootOf(node, ancestors);
7163
+ const tab = root ? tabTriggerOf(root, node) : undefined;
7164
+ return tab ? tabLabel(tab) || null : null;
7165
+ }
7166
+ return null;
6904
7167
  }
6905
7168
 
6906
7169
  /**
@@ -6958,9 +7221,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
6958
7221
  const FROM_ROW_KEY = 'builder.chrome.fromRow';
6959
7222
  const ADD_ITEM_KEY = 'builder.chrome.addItem';
6960
7223
  /** The kind label a composite part's selection badge shows (Card v2: "Card · card.title"). */
6961
- const COMPOSITE_KIND_KEYS = { card: 'builder.chrome.kindCard', accordion: 'builder.chrome.kindAccordion', disclosure: 'builder.chrome.kindDisclosure' };
7224
+ const COMPOSITE_KIND_KEYS = { card: 'builder.chrome.kindCard', accordion: 'builder.chrome.kindAccordion', disclosure: 'builder.chrome.kindDisclosure', tabs: 'builder.chrome.kindTabs', slider: 'builder.chrome.kindSlider' };
7225
+ const VIDEO_TYPE = 'Video';
7226
+ const VIDEO_KIND_KEY = 'builder.chrome.kindVideo';
7227
+ const VIDEO_POSTER_ONLY_KEY = 'builder.chrome.videoPosterOnly';
7228
+ const VIDEO_FILE_KEY = 'builder.chrome.videoFile';
6962
7229
  /** The parts that offer "Add item" on the selection toolbar (board 3: the "Add slide" pattern). */
6963
- const ADD_ITEM_ROLES = new Set([AccordionRole.Accordion, AccordionRole.Item, AccordionRole.Trigger, AccordionRole.Panel]);
7230
+ const ADD_ITEM_ROLES = new Set([
7231
+ AccordionRole.Accordion, AccordionRole.Item, AccordionRole.Trigger, AccordionRole.Panel,
7232
+ TabsRole.Tabs, TabsRole.List, TabsRole.Trigger, TabsRole.Panels, TabsRole.Panel,
7233
+ ]);
7234
+ /** Tabs say "Add tab" (board 3) — the button adds a tab AND its panel. */
7235
+ const ADD_TAB_KEY = 'builder.chrome.addTab';
6964
7236
  class OverlayComponent {
6965
7237
  constructor(elRef, instanceRegistry, overlayService, viewportService, presetRegistry, symbolRegistry, interactionPresetRegistry, interactionEngine, clipboard, styleRegistry, panelEventService, cdr, dragEngine, labels) {
6966
7238
  this.elRef = elRef;
@@ -7175,11 +7447,17 @@ class OverlayComponent {
7175
7447
  }
7176
7448
  /** The kind badge's glyph: the composite's kind icon (board 9: "kind icon + 'Accordion · trigger'"). */
7177
7449
  get selectedRoleIcon() {
7450
+ if (this.selectEntry?.node?.type === VIDEO_TYPE && !this.selectEntry.node.role)
7451
+ return 'ph-fill ph-film-strip';
7178
7452
  return `ph-fill ${compositeKindGlyph(this.selectEntry?.node?.role ?? '')}`;
7179
7453
  }
7180
- /** A composite part's kind + role under the selection (board 3/8 "Selection kind badge"), or null. */
7454
+ /** A composite part's kind + role under the selection (board 3/8 "Selection kind badge"), or null. A Video says
7455
+ * where it plays from and that the canvas shows its poster only (board 5: "Video · YouTube · poster only"). */
7181
7456
  get selectedRoleBadge() {
7182
- const role = this.selectEntry?.node?.role;
7457
+ const node = this.selectEntry?.node;
7458
+ if (node?.type === VIDEO_TYPE && !node.role)
7459
+ return this.videoBadge(node);
7460
+ const role = node?.role;
7183
7461
  if (!role)
7184
7462
  return null;
7185
7463
  const prefix = role.split('.')[0];
@@ -7189,16 +7467,24 @@ class OverlayComponent {
7189
7467
  const row = this.overlayService.selectedRow;
7190
7468
  return row ? `${badge} · ${this.builderLabels(FROM_ROW_KEY, BUILDER_CHROME_EN[FROM_ROW_KEY]).replace('{row}', String(row))}` : badge;
7191
7469
  }
7470
+ videoBadge(node) {
7471
+ const plan = planVideo(node, null);
7472
+ const where = plan.source === VideoSource.File
7473
+ ? this.builderLabels(VIDEO_FILE_KEY, BUILDER_CHROME_EN[VIDEO_FILE_KEY])
7474
+ : VIDEO_PROVIDER_LABEL[plan.source];
7475
+ return `${this.builderLabels(VIDEO_KIND_KEY, BUILDER_CHROME_EN[VIDEO_KIND_KEY])} · ${where} · ${this.builderLabels(VIDEO_POSTER_ONLY_KEY, BUILDER_CHROME_EN[VIDEO_POSTER_ONLY_KEY])}`;
7476
+ }
7192
7477
  /** The selection toolbar's name: the part's LAYER name (board 3 — "Header", "Accordion"), else its type. */
7193
7478
  get toolbarName() {
7194
7479
  return this.selectEntry?.node?.label || this.selectEntry?.componentName || '';
7195
7480
  }
7196
- /** An accordion part: the toolbar's "Add item" (CC2). */
7481
+ /** An accordion part: the toolbar's "Add item" (CC2); a Tabs part: "Add tab" (CC4a). */
7197
7482
  get canAddItem() {
7198
7483
  return ADD_ITEM_ROLES.has(this.selectEntry?.node?.role ?? '');
7199
7484
  }
7200
7485
  get addItemLabel() {
7201
- return this.builderLabels(ADD_ITEM_KEY, BUILDER_CHROME_EN[ADD_ITEM_KEY]);
7486
+ const key = isTabsPart(this.selectEntry?.node) ? ADD_TAB_KEY : ADD_ITEM_KEY;
7487
+ return this.builderLabels(key, BUILDER_CHROME_EN[key]);
7202
7488
  }
7203
7489
  onAddItem(event) {
7204
7490
  event.stopPropagation();
@@ -7782,10 +8068,14 @@ function repeaterSubLabel(rows) {
7782
8068
  /** Flatten `nodes` into rows, depth-first, honouring each node's expanded state. */
7783
8069
  function flattenLayerTree(nodes, ctx) {
7784
8070
  const out = [];
7785
- walk(nodes, 0, out, ctx, false);
8071
+ walk(nodes, 0, out, ctx, false, undefined, []);
7786
8072
  return out;
7787
8073
  }
7788
- function walk(nodes, depth, out, ctx, inTemplate, templateOf) {
8074
+ /** A Repeater that is a composite's item source From content (R3): an Accordion's root, a Tabs' list or panels. */
8075
+ function isCompositeFromContent(node) {
8076
+ return isAccordionRoot(node) || isExclusiveList(node) || isExclusivePanels(node);
8077
+ }
8078
+ function walk(nodes, depth, out, ctx, inTemplate, templateOf, ancestors) {
7789
8079
  for (let i = 0; i < nodes.length; i++) {
7790
8080
  const node = nodes[i];
7791
8081
  const hasChildren = !!node.children?.length;
@@ -7795,7 +8085,7 @@ function walk(nodes, depth, out, ctx, inTemplate, templateOf) {
7795
8085
  const bound = inTemplate && !!node.bindings?.some(b => b.prop !== REPEATER_SOURCE_PROP && b.source);
7796
8086
  const label = node.label ?? node.type;
7797
8087
  // An accordion item shows no text of its own: it names its question (board 3 "Item · Do you take pro…").
7798
- const descriptor = rows ? null : (nodeDescriptor(node, label) ?? compositePartSub(node));
8088
+ const descriptor = rows ? null : (nodeDescriptor(node, label) ?? compositePartSub(node, ancestors));
7799
8089
  out.push({
7800
8090
  node,
7801
8091
  icon: bound ? BOUND_ICON : (compositePartIcon(node.role) || ctx.iconOf(node.type) || DEFAULT_ICON),
@@ -7808,7 +8098,7 @@ function walk(nodes, depth, out, ctx, inTemplate, templateOf) {
7808
8098
  indexInParent: i,
7809
8099
  locked: isLockedPart(node),
7810
8100
  template: !!templateOf?.alias,
7811
- sub: rows ? (isAccordionRoot(node) && ctx.fromContentLabel ? ctx.fromContentLabel(rows) : repeaterSubLabel(rows)) : shortDescriptor(descriptor),
8101
+ sub: rows ? (isCompositeFromContent(node) && ctx.fromContentLabel ? ctx.fromContentLabel(rows) : repeaterSubLabel(rows)) : shortDescriptor(descriptor),
7812
8102
  descriptor,
7813
8103
  bound,
7814
8104
  roleChip: node.role ? roleChipLabel(node.role) : null,
@@ -7816,13 +8106,14 @@ function walk(nodes, depth, out, ctx, inTemplate, templateOf) {
7816
8106
  });
7817
8107
  if (!hasChildren || !expanded)
7818
8108
  continue;
8109
+ const below = [...ancestors, node];
7819
8110
  if (isRepeater && rows) {
7820
- walk(node.children, depth + 1, out, ctx, true, rows);
8111
+ walk(node.children, depth + 1, out, ctx, true, rows, below);
7821
8112
  if (rows.alias && rows.count > 1)
7822
- out[out.length - 1].moreRows = { count: rows.count - 1, alias: rows.alias, depth: depth + 1, composite: isAccordionRoot(node) };
8113
+ out[out.length - 1].moreRows = { count: rows.count - 1, alias: rows.alias, depth: depth + 1, composite: isCompositeFromContent(node) };
7823
8114
  }
7824
8115
  else {
7825
- walk(node.children, depth + 1, out, ctx, inTemplate);
8116
+ walk(node.children, depth + 1, out, ctx, inTemplate, undefined, below);
7826
8117
  }
7827
8118
  }
7828
8119
  }
@@ -8071,10 +8362,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
8071
8362
 
8072
8363
  /** Label of the always-present root crumb. The page surface itself, not a layout node. */
8073
8364
  const BODY_CRUMB_LABEL = 'Body';
8074
- /** A node's crumb: its layer name, and an accordion item's question after it ("Item · How long does…"). */
8075
- function crumbLabel(node) {
8365
+ /** A node's crumb: its layer name, and an accordion item's question after it ("Item · How long does…") — a tab's and a
8366
+ * panel's words likewise ("Panel · Kitchens"). `ancestors` = the crumbs before it. */
8367
+ function crumbLabel(node, ancestors = []) {
8076
8368
  const name = node.label || node.type;
8077
- const sub = compositePartSub(node);
8369
+ const sub = compositePartSub(node, ancestors);
8078
8370
  return sub ? `${name} · ${sub}` : name;
8079
8371
  }
8080
8372
  /**
@@ -8112,8 +8404,8 @@ class BreadcrumbBarComponent {
8112
8404
  { node: null, label: BODY_CRUMB_LABEL, icon: null, isCurrent: nodes.length === 0, key: BODY_CRUMB_LABEL },
8113
8405
  ...nodes.map((node, i) => ({
8114
8406
  node,
8115
- label: crumbLabel(node),
8116
- icon: isCompositeRoot(node) ? `ph-thin ${compositeKindGlyph(node.role)}` : null,
8407
+ label: crumbLabel(node, nodes.slice(0, i)),
8408
+ icon: isAnyCompositeRoot(node) ? `ph-thin ${compositeKindGlyph(node.role)}` : null,
8117
8409
  isCurrent: i === last,
8118
8410
  key: node,
8119
8411
  })),
@@ -8332,11 +8624,12 @@ class BuilderComponent {
8332
8624
  this.auditRunning = false;
8333
8625
  this.cdr.detectChanges();
8334
8626
  }
8335
- /** An Accordion from content whose source came back with no rows (CC2): what the audit names. */
8627
+ /** An Accordion — or Tabs (CC4a: its list) — from content whose source came back with no rows: what the audit names. */
8336
8628
  emptySourceOf(node) {
8337
- if (!isAccordionRoot(node))
8629
+ const holder = isAccordionRoot(node) ? node : isExclusiveRoot(node) ? tabsListOf(node) : undefined;
8630
+ if (!holder)
8338
8631
  return undefined;
8339
- const instance = this.overlayService.getNodeRef(node)?.componentRef?.instance;
8632
+ const instance = this.overlayService.getNodeRef(holder)?.componentRef?.instance;
8340
8633
  if (!(instance instanceof BuilderRepeaterComponent) || !instance.source || instance.isLoading || instance.items.length)
8341
8634
  return undefined;
8342
8635
  const path = nodeAncestorPathInEither(this.layout, this.globalElements, node) ?? [];
@@ -8714,6 +9007,8 @@ class BuilderComponent {
8714
9007
  this.scrollbarTranslate = 0; // counteracts Lenis wrapper scrollTop shifting position:absolute children
8715
9008
  this.activeSidebar = 'components';
8716
9009
  this.showSaveBlockDialog = false;
9010
+ /** A tab or a panel asked to be deleted alone (CC4a): "Delete tab and its panel" is offered for it. */
9011
+ this.pendingPairRemoval = null;
8717
9012
  this.saveBlockName = '';
8718
9013
  this.pendingBlockNode = null;
8719
9014
  this.isPanning = false;
@@ -8769,6 +9064,7 @@ class BuilderComponent {
8769
9064
  this.isDragging = v;
8770
9065
  this.cdr.markForCheck();
8771
9066
  }));
9067
+ this.sub.add(this.dragEngine.pairRemoval$.subscribe((node) => this.offerPairRemoval(node)));
8772
9068
  this.sub.add(this.dragEngine.dropComplete$.subscribe((node) => {
8773
9069
  this._selectAfterDrop(node);
8774
9070
  this.panelEventService.emit(PanelEventTypes.LAYOUT_CHANGED, this.layout);
@@ -9314,6 +9610,8 @@ class BuilderComponent {
9314
9610
  this.panelEventService.emit(PanelEventTypes.LAYOUT_CHANGED, this.layout);
9315
9611
  }
9316
9612
  else if (action === NodeAction.Duplicate) {
9613
+ if (this.duplicateTabPairOf(node))
9614
+ return;
9317
9615
  const clone = this.deepCloneWithNewIds(node);
9318
9616
  result.parent.splice(result.index + 1, 0, clone);
9319
9617
  this.cdr.detectChanges();
@@ -9353,6 +9651,8 @@ class BuilderComponent {
9353
9651
  break;
9354
9652
  }
9355
9653
  case ToolbarAction.Duplicate: {
9654
+ if (this.duplicateTabPairOf(node))
9655
+ break;
9356
9656
  const result = this.findNodeInTree(node, this.layout) ?? this.findNodeInTree(node, this.globalElements);
9357
9657
  if (result && !this.dragEngine.refuseInsert(node, this.siblingAncestors(node))) {
9358
9658
  const clone = this.deepCloneWithNewIds(node);
@@ -9533,7 +9833,8 @@ class BuilderComponent {
9533
9833
  break;
9534
9834
  }
9535
9835
  case ToolbarAction.AddItem: {
9536
- this.addAccordionItemFrom(node);
9836
+ if (!this.addTabPairFrom(node))
9837
+ this.addAccordionItemFrom(node);
9537
9838
  break;
9538
9839
  }
9539
9840
  case ToolbarAction.AddSlide: {
@@ -9568,6 +9869,62 @@ class BuilderComponent {
9568
9869
  if (trigger)
9569
9870
  this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: trigger });
9570
9871
  }
9872
+ /** Toolbar ▸ Add tab on a Tabs part (CC4a): a fresh tab AND its panel after the selection's own tab (or at the end
9873
+ * for the root, the list, the holder) — ONE undo step, then the new tab is selected. False when `node` is no Tabs part. */
9874
+ addTabPairFrom(node) {
9875
+ const path = nodeAncestorPathInEither(this.layout, this.globalElements, node) ?? [node];
9876
+ const root = exclusiveRootOf(node, path);
9877
+ if (!root)
9878
+ return false;
9879
+ if (isTabsFromContent(root))
9880
+ return true;
9881
+ const anchor = path.find(n => isExclusiveItemPart(n)) ?? null;
9882
+ const factory = builderCompositeFactory(t => this.registry.createConfig(t), t => this.registry.baselineStyleProps(t));
9883
+ const pair = addTabPair(root, factory, anchor);
9884
+ if (!pair)
9885
+ return true;
9886
+ this.panelEventService.emit(PanelEventTypes.NODE_STRUCTURE_CHANGED, { node: root });
9887
+ this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: pair.tab });
9888
+ return true;
9889
+ }
9890
+ /** Duplicate on a tab or a panel (toolbar, layer tree): the PAIR is duplicated under one new key. True when handled. */
9891
+ duplicateTabPairOf(node) {
9892
+ const root = tabsRootForPart(this.layout, this.globalElements, node);
9893
+ if (!root)
9894
+ return false;
9895
+ const copy = duplicateTabPair(root, node, n => this.deepCloneWithNewIds(n));
9896
+ if (!copy)
9897
+ return false;
9898
+ this.panelEventService.emit(PanelEventTypes.NODE_STRUCTURE_CHANGED, { node: root });
9899
+ this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: copy });
9900
+ return true;
9901
+ }
9902
+ /** A tab or a panel asked to be deleted ALONE (every delete path asks the one guard): offer the pair. */
9903
+ offerPairRemoval(node) {
9904
+ const root = tabsRootForPart(this.layout, this.globalElements, node);
9905
+ if (!root)
9906
+ return;
9907
+ this.pendingPairRemoval = { node, root, label: tabPairLabel(root, node) };
9908
+ this.cdr.markForCheck();
9909
+ }
9910
+ /** "Delete tab and its panel": both go, ONE undo step. */
9911
+ confirmPairRemoval() {
9912
+ const pending = this.pendingPairRemoval;
9913
+ this.pendingPairRemoval = null;
9914
+ if (!pending || !deleteTabPair(pending.root, pending.node))
9915
+ return;
9916
+ this.selectedItem = null;
9917
+ this.overlayService.clearSelect();
9918
+ this.panelEventService.emit(PanelEventTypes.ELEMENT_DESELECT, null);
9919
+ this.panelEventService.emit(PanelEventTypes.NODE_STRUCTURE_CHANGED, { node: pending.root });
9920
+ }
9921
+ cancelPairRemoval() {
9922
+ this.pendingPairRemoval = null;
9923
+ }
9924
+ /** The pair-removal confirm's words, in the host's language. */
9925
+ pairRemovalText(key) {
9926
+ return this.labels(key, BUILDER_CHROME_EN[key]).replace('{tab}', this.pendingPairRemoval?.label ?? '');
9927
+ }
9571
9928
  confirmSaveBlock() {
9572
9929
  if (!this.saveBlockName.trim() || !this.pendingBlockNode)
9573
9930
  return;
@@ -9808,11 +10165,11 @@ class BuilderComponent {
9808
10165
  }
9809
10166
  }
9810
10167
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderComponent, deps: [{ token: ComponentRegistryService }, { token: OverlayService }, { token: PanelEventService }, { token: DragEngineService }, { token: DataSourceRegistryService }, { token: ViewportService }, { token: FormStateService }, { token: CanvasDesignStateService }, { token: InteractionEngineService }, { token: InteractionPresetRegistryService }, { token: BuilderClipboardService }, { token: SymbolRegistryService }, { token: i0.ChangeDetectorRef }, { token: i0.ApplicationRef }, { token: MoveableCanvasService }, { token: SelectoCanvasService }, { token: StyleRegistryService }], target: i0.ɵɵFactoryTarget.Component }); }
9811
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: BuilderComponent, isStandalone: false, selector: "app-builder", inputs: { layout: "layout", dataSources: "dataSources", orgId: "orgId", routeParams: "routeParams", pageName: "pageName", isSaving: "isSaving", pageStatus: "pageStatus", pageSettings: "pageSettings", globalElements: "globalElements", pagePath: "pagePath", canUndo: "canUndo", canRedo: "canRedo", moveableMode: "moveableMode" }, outputs: { save: "save", undo: "undo", redo: "redo", back: "back", preview: "preview", publishToggle: "publishToggle", pageSettingsChange: "pageSettingsChange", applyAnimationToAll: "applyAnimationToAll", saveBlock: "saveBlock", globalElementsChange: "globalElementsChange" }, providers: [{ provide: IOX_BUILDER_CANVAS, useValue: true }], viewQueries: [{ propertyName: "previewCanvas", first: true, predicate: ["previewCanvas"], descendants: true }, { propertyName: "previewScrollRef", first: true, predicate: ["previewScroll"], descendants: true }, { propertyName: "lenisContentRef", first: true, predicate: ["lenisContent"], descendants: true }, { propertyName: "globalOverlayRef", first: true, predicate: ["globalOverlay"], descendants: true }, { propertyName: "globalsBeforeRef", first: true, predicate: ["globalsBefore"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"builder\">\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <!-- The switcher is the panel's OWN tab bar. It used to live in the header, at the\n maximum possible distance from the thing it controls; attached, the column and the\n panel are one card with one shadow. See board D2. -->\n <aside class=\"sidebar\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'pages'\" [class.active]=\"activeSidebar === 'pages'\" title=\"Pages\" aria-label=\"Pages\">\n <i class=\"ph-thin ph-browsers\"></i>\n </button>\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\" title=\"Components\" aria-label=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\" aria-label=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\" aria-label=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n <div class=\"sidebar-body\">\n @if (activeSidebar === 'pages') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderPages]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'components') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'tokens') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'tree') {\n <app-layer-tree\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n }\n </div>\n </aside>\n\n <!-- \u26A0\uFE0F The strips are SIBLINGS of .preview, not children of it. Board D2 floated them\n over the canvas and that is what they did \u2014 over the page being edited, which is the\n one thing in the builder that must stay clear. They keep their pill shape and their\n centre alignment; they just take their own row.\n\n Siblings, specifically, rather than a wrapper INSIDE .preview: `previewCanvas` is the\n overlay container and the coordinate origin for the selection box, the drag clone,\n Lenis and the zoom maths (nine call sites). Nesting anything between it and the canvas\n would shift every one of them by the strip's height. -->\n <div class=\"canvas-column\">\n <!-- \u2460 THE ACTION STRIP \u2014 floating, top-centre (board D2). It was a solid\n full-width header bar costing 56px of height on every screen; the canvas now\n starts at the top of the viewport and the strip hovers over it.\n stopPropagation for the same reason the breadcrumb does: .preview's (click)\n is \"you clicked empty canvas, drop the selection\". -->\n <div class=\"toolbar-strip\" (click)=\"$event.stopPropagation()\">\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeBreakpoint]=\"activeBreakpoint\"\n [breakpointCounts]=\"breakpointCounts\"\n [auditCount]=\"auditFindings.length || structureFindings.length\"\n [auditOpen]=\"auditOpen\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (breakpointChange)=\"handleToolbarBreakpointChange($event)\"\n (auditToggle)=\"toggleAudit()\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n <ng-content select=\"[builderToolbarActions]\"></ng-content>\n </app-toolbar>\n </div>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\"\n (dragstart)=\"onNativeDragStart($event)\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <!-- Responsive audit (A5) \u2014 floats over the canvas, right gutter. -->\n @if (auditOpen) {\n <div class=\"audit-dock\" (click)=\"$event.stopPropagation()\">\n <app-audit-panel\n [findings]=\"auditFindings\"\n [breakpointLabel]=\"auditBreakpointLabel\"\n [running]=\"auditRunning\"\n (select)=\"selectAuditNode($event)\"\n (fix)=\"applyAuditFix($event)\"\n (fixAll)=\"applyAllAuditFixes()\"\n (rerun)=\"runAudit()\"\n (closePanel)=\"toggleAudit()\"></app-audit-panel>\n </div>\n }\n\n <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height is the frame a pinned global sits in (globalOverlayHeight: the canvas\n slot, or the page on a short page), bound in px so position:fixed + inset:0\n fills it (CSS calc(100%) would give the wrong containing-block height). -->\n @if (globalElements.length) {\n <div\n #globalOverlay\n class=\"global-elements-overlay iox-token-scope\"\n [ngStyle]=\"globalLayerCss\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"globalOverlayHeight\"\n [style.transform]=\"viewportTransform\">\n <!-- BEFORE globals stay here, in the frame (a Dialog inside a header must fill the frame,\n not scroll away with the page). Their wrapper's height is what the in-flow ones take,\n and the page reserves it at its top (canvasHeaderReserve, iox-ui#19). -->\n @if (globalElementsBefore.length) {\n <div #globalsBefore class=\"iox-canvas-globals-before\">\n @for (item of globalElementsBefore; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n <!-- The PINNED after globals (a fixed bottom bar, a Dialog). An in-flow AFTER global (a\n footer) is drawn in the page, after its content (iox-ui#12). -->\n @for (item of globalElementsPinnedAfter; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <!-- iox-token-scope: the ORG's design tokens are declared on this class, not :root\n (StyleRegistryService.upsertTokens), so they never repaint the admin chrome. -->\n <iox-page-component class=\"iox-token-scope\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n\n <!-- ngStyle carries the page's own background/typography (pageSettings.style),\n compiled by the SAME function the engine applies to <body>. Inline wins over\n the #canvas-preview white default, which stays as the unset fallback. -->\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [attr.data-bar-reserve]=\"canvasBarReserve\"\n [style.padding-block-start.px]=\"canvasHeaderReserve || null\"\n [ngStyle]=\"pageStyleCss\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <!-- Empty page (B4): a starting point beats a blank canvas. Each starter is\n a responsive layout preset \u2014 ordinary nodes, editable afterwards. -->\n @if (isPageEmpty) {\n <div class=\"empty-canvas\" (click)=\"$event.stopPropagation()\">\n <h2 class=\"empty-title\">Start with a layout</h2>\n <p class=\"empty-sub\">Each one already adapts to tablet and phone. You can edit or take it apart afterwards.</p>\n <div class=\"empty-grid\">\n @for (starter of starterLayouts; track starter.id) {\n <button type=\"button\" class=\"empty-card\" (click)=\"startWithLayout(starter)\">\n <i [class]=\"starter.icon\" aria-hidden=\"true\"></i>\n <span class=\"empty-card-name\">{{ starter.name }}</span>\n <span class=\"empty-card-desc\">{{ starter.description }}</span>\n </button>\n }\n </div>\n <button type=\"button\" class=\"empty-link\" (click)=\"browseComponents()\">Or start from a single element</button>\n </div>\n }\n\n <!-- WRAPPER COLLAPSE (task 9c): top-level nodes render DIRECTLY into the\n canvas viewport \u2014 no .preview-node wrapper; iox-outer + drag move to the\n host (RenderDirective, via ioxDragSourceId). -->\n @for (item of layout; track item) {\n <ng-container\n [ioxRender]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n\n <!-- In-flow AFTER globals (a footer) follow the page's content, inside the page's\n ground, as on the live site (iox-ui#12). Not part of the layout: the drag\n engine skips this wrapper (GLOBALS_AFTER_CLASS). -->\n @if (globalElementsInPage.length) {\n <div [class]=\"globalsAfterClass\">\n @for (item of globalElementsInPage; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n <!-- \u2462 THE ELEMENT PATH \u2014 a thin floating strip, BOTTOM-CENTRE (board D2). It is\n pinned to the PREVIEW VIEWPORT, not to the canvas frame: the frame pans and\n scales, so a breadcrumb tracking its edge would drift off-screen and shrink with\n zoom. It used to sit in the top gutter, which is where the eye is least often \u2014\n and where the floating action strip now lives.\n\n \u26A0\uFE0F stopPropagation is REQUIRED, not defensive. This element lives inside .preview,\n whose (click) is handleCanvasClick() \u2014 \"you clicked empty canvas, drop the\n selection\". Without it, picking an ancestor crumb selects that node and then\n immediately bubbles into the canvas handler, which clears it: the selection and the\n layer-tree highlight both fall back to Body. Same reason .audit-dock stops its own. -->\n <app-breadcrumb-bar\n (click)=\"$event.stopPropagation()\"\n [path]=\"selectedPath\"\n (nodeSelect)=\"onCrumbSelect($event)\"\n (rootSelect)=\"onCrumbRootSelect()\"\n (nodePeek)=\"onCrumbPeek($event)\">\n </app-breadcrumb-bar>\n\n </div>\n\n <!-- Same rule as the sidebar: the switcher belongs to the panel it drives, as a\n tab ROW across its top. -->\n <div class=\"panel-dock\">\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\" aria-label=\"Page settings\">\n <i class=\"ph-thin ph-gear-six\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></i>\n </button>\n </div>\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n }\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .toolbar-strip{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:6px 8px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 40px);overflow-x:auto;scrollbar-width:none}.builder .toolbar-strip::-webkit-scrollbar{display:none}.builder .sidebar-switcher,.builder .panel-switcher{flex:none;display:flex;flex-direction:row;align-items:center;gap:.375rem;padding:7px 10px;background:var(--p-surface-0);border-block-end:1px solid var(--iox-neutral-100, #EDEFF3)}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;border:none;background:var(--iox-surface-3, #F6F7F9);color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--iox-rose-100, #F8EAEA);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:var(--iox-grad-brand, linear-gradient(135deg, #9B4458 0%, #8A3A72 50%, #63347F 100%));color:#fff}.builder .panel-dock{display:grid;grid-template-rows:auto minmax(0,1fr);flex:none;min-height:0;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden;gap:12px;padding:14px;background:var(--iox-surface-2, #EDEFF3)}.builder .sidebar{display:flex;flex-direction:column;inline-size:260px;min-inline-size:260px;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden;flex-shrink:0;min-block-size:0}.builder .sidebar .sidebar-body{flex:1;min-inline-size:0;overflow-y:auto;overflow-x:hidden}.builder .canvas-column{flex:1 1 0;min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden;background:transparent}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;direction:ltr;container:iox-canvas/inline-size;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:visible!important}.builder #canvas-preview{background:#fff}.builder #canvas-preview[data-bar-reserve=iox-bar-reserve]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}@container iox-canvas (max-width: 1200px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-laptop]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 991px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-tablet]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 767px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-mobile]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 479px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-small]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}.builder .empty-canvas{max-inline-size:560px;margin:0 auto;padding:48px 24px;text-align:center;color:#4b5563}.builder .empty-title{margin:0;font-size:18px;font-weight:600;color:#111827}.builder .empty-sub{margin:6px 0 20px;font-size:12px;line-height:1.5;color:#6b7280}.builder .empty-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.builder .empty-card{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 10px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.builder .empty-card:hover{border-color:#cb9090;box-shadow:0 2px 10px #0000000f}.builder .empty-card:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder .empty-card i{font-size:20px;color:#a05c5c}.builder .empty-card-name{font-size:11.5px;font-weight:600;color:#111827}.builder .empty-card-desc{font-size:9.5px;line-height:1.35;color:#6b7280}.builder .empty-link{margin-block-start:16px;padding:0;border:none;background:none;font-size:11px;color:#a05c5c;cursor:pointer}.builder .empty-link:hover{text-decoration:underline}.builder .empty-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder app-breadcrumb-bar{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:5px 13px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 112px);pointer-events:none}.builder .audit-dock{position:absolute;inset-block-start:20px;inset-inline-end:16px;z-index:20}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden;container:iox-canvas/inline-size;direction:ltr}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.builder .global-elements-overlay .iox-canvas-globals-before{display:flow-root;pointer-events:none}.builder .global-elements-overlay .iox-canvas-globals-before ::ng-deep>*{pointer-events:auto}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-inline-start:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\n"], dependencies: [{ kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i17.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i17.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i17.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i18.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i1$1.IoxPageComponent, selector: "iox-page-component", inputs: ["class"], outputs: ["scrollEvent"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId", "ioxPhrasing"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "component", type: OverlayComponent, selector: "app-overlay", outputs: ["toolbarAction"] }, { kind: "component", type: ToolbarComponent, selector: "app-toolbar", inputs: ["activeMode", "activeBreakpoint", "breakpointCounts", "auditCount", "auditOpen", "activeZoom", "activeScreenWidth", "canUndo", "canRedo", "isSaving", "pageStatus"], outputs: ["modeChange", "breakpointChange", "auditToggle", "zoomChange", "screenWidthChange", "undo", "redo", "save", "preview", "publishToggle"] }, { kind: "component", type: LayerTreeComponent, selector: "app-layer-tree", inputs: ["layout", "globalElements"], outputs: ["nodeSelect", "nodeAction", "nodeMove"] }, { kind: "component", type: BreadcrumbBarComponent, selector: "app-breadcrumb-bar", inputs: ["path"], outputs: ["nodeSelect", "rootSelect", "nodePeek"] }, { kind: "component", type: AuditPanelComponent, selector: "app-audit-panel", inputs: ["findings", "breakpointLabel", "running"], outputs: ["select", "fix", "fixAll", "rerun", "closePanel"] }] }); }
10168
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: BuilderComponent, isStandalone: false, selector: "app-builder", inputs: { layout: "layout", dataSources: "dataSources", orgId: "orgId", routeParams: "routeParams", pageName: "pageName", isSaving: "isSaving", pageStatus: "pageStatus", pageSettings: "pageSettings", globalElements: "globalElements", pagePath: "pagePath", canUndo: "canUndo", canRedo: "canRedo", moveableMode: "moveableMode" }, outputs: { save: "save", undo: "undo", redo: "redo", back: "back", preview: "preview", publishToggle: "publishToggle", pageSettingsChange: "pageSettingsChange", applyAnimationToAll: "applyAnimationToAll", saveBlock: "saveBlock", globalElementsChange: "globalElementsChange" }, providers: [{ provide: IOX_BUILDER_CANVAS, useValue: true }], viewQueries: [{ propertyName: "previewCanvas", first: true, predicate: ["previewCanvas"], descendants: true }, { propertyName: "previewScrollRef", first: true, predicate: ["previewScroll"], descendants: true }, { propertyName: "lenisContentRef", first: true, predicate: ["lenisContent"], descendants: true }, { propertyName: "globalOverlayRef", first: true, predicate: ["globalOverlay"], descendants: true }, { propertyName: "globalsBeforeRef", first: true, predicate: ["globalsBefore"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"builder\">\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <!-- The switcher is the panel's OWN tab bar. It used to live in the header, at the\n maximum possible distance from the thing it controls; attached, the column and the\n panel are one card with one shadow. See board D2. -->\n <aside class=\"sidebar\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'pages'\" [class.active]=\"activeSidebar === 'pages'\" title=\"Pages\" aria-label=\"Pages\">\n <i class=\"ph-thin ph-browsers\"></i>\n </button>\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\" title=\"Components\" aria-label=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\" aria-label=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\" aria-label=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n <div class=\"sidebar-body\">\n @if (activeSidebar === 'pages') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderPages]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'components') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'tokens') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'tree') {\n <app-layer-tree\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n }\n </div>\n </aside>\n\n <!-- \u26A0\uFE0F The strips are SIBLINGS of .preview, not children of it. Board D2 floated them\n over the canvas and that is what they did \u2014 over the page being edited, which is the\n one thing in the builder that must stay clear. They keep their pill shape and their\n centre alignment; they just take their own row.\n\n Siblings, specifically, rather than a wrapper INSIDE .preview: `previewCanvas` is the\n overlay container and the coordinate origin for the selection box, the drag clone,\n Lenis and the zoom maths (nine call sites). Nesting anything between it and the canvas\n would shift every one of them by the strip's height. -->\n <div class=\"canvas-column\">\n <!-- \u2460 THE ACTION STRIP \u2014 floating, top-centre (board D2). It was a solid\n full-width header bar costing 56px of height on every screen; the canvas now\n starts at the top of the viewport and the strip hovers over it.\n stopPropagation for the same reason the breadcrumb does: .preview's (click)\n is \"you clicked empty canvas, drop the selection\". -->\n <div class=\"toolbar-strip\" (click)=\"$event.stopPropagation()\">\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeBreakpoint]=\"activeBreakpoint\"\n [breakpointCounts]=\"breakpointCounts\"\n [auditCount]=\"auditFindings.length || structureFindings.length\"\n [auditOpen]=\"auditOpen\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (breakpointChange)=\"handleToolbarBreakpointChange($event)\"\n (auditToggle)=\"toggleAudit()\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n <ng-content select=\"[builderToolbarActions]\"></ng-content>\n </app-toolbar>\n </div>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\"\n (dragstart)=\"onNativeDragStart($event)\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <!-- Responsive audit (A5) \u2014 floats over the canvas, right gutter. -->\n @if (auditOpen) {\n <div class=\"audit-dock\" (click)=\"$event.stopPropagation()\">\n <app-audit-panel\n [findings]=\"auditFindings\"\n [breakpointLabel]=\"auditBreakpointLabel\"\n [running]=\"auditRunning\"\n (select)=\"selectAuditNode($event)\"\n (fix)=\"applyAuditFix($event)\"\n (fixAll)=\"applyAllAuditFixes()\"\n (rerun)=\"runAudit()\"\n (closePanel)=\"toggleAudit()\"></app-audit-panel>\n </div>\n }\n\n <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height is the frame a pinned global sits in (globalOverlayHeight: the canvas\n slot, or the page on a short page), bound in px so position:fixed + inset:0\n fills it (CSS calc(100%) would give the wrong containing-block height). -->\n @if (globalElements.length) {\n <div\n #globalOverlay\n class=\"global-elements-overlay iox-token-scope\"\n [ngStyle]=\"globalLayerCss\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"globalOverlayHeight\"\n [style.transform]=\"viewportTransform\">\n <!-- BEFORE globals stay here, in the frame (a Dialog inside a header must fill the frame,\n not scroll away with the page). Their wrapper's height is what the in-flow ones take,\n and the page reserves it at its top (canvasHeaderReserve, iox-ui#19). -->\n @if (globalElementsBefore.length) {\n <div #globalsBefore class=\"iox-canvas-globals-before\">\n @for (item of globalElementsBefore; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n <!-- The PINNED after globals (a fixed bottom bar, a Dialog). An in-flow AFTER global (a\n footer) is drawn in the page, after its content (iox-ui#12). -->\n @for (item of globalElementsPinnedAfter; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <!-- iox-token-scope: the ORG's design tokens are declared on this class, not :root\n (StyleRegistryService.upsertTokens), so they never repaint the admin chrome. -->\n <iox-page-component class=\"iox-token-scope\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n\n <!-- ngStyle carries the page's own background/typography (pageSettings.style),\n compiled by the SAME function the engine applies to <body>. Inline wins over\n the #canvas-preview white default, which stays as the unset fallback. -->\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [attr.data-bar-reserve]=\"canvasBarReserve\"\n [style.padding-block-start.px]=\"canvasHeaderReserve || null\"\n [ngStyle]=\"pageStyleCss\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <!-- Empty page (B4): a starting point beats a blank canvas. Each starter is\n a responsive layout preset \u2014 ordinary nodes, editable afterwards. -->\n @if (isPageEmpty) {\n <div class=\"empty-canvas\" (click)=\"$event.stopPropagation()\">\n <h2 class=\"empty-title\">Start with a layout</h2>\n <p class=\"empty-sub\">Each one already adapts to tablet and phone. You can edit or take it apart afterwards.</p>\n <div class=\"empty-grid\">\n @for (starter of starterLayouts; track starter.id) {\n <button type=\"button\" class=\"empty-card\" (click)=\"startWithLayout(starter)\">\n <i [class]=\"starter.icon\" aria-hidden=\"true\"></i>\n <span class=\"empty-card-name\">{{ starter.name }}</span>\n <span class=\"empty-card-desc\">{{ starter.description }}</span>\n </button>\n }\n </div>\n <button type=\"button\" class=\"empty-link\" (click)=\"browseComponents()\">Or start from a single element</button>\n </div>\n }\n\n <!-- WRAPPER COLLAPSE (task 9c): top-level nodes render DIRECTLY into the\n canvas viewport \u2014 no .preview-node wrapper; iox-outer + drag move to the\n host (RenderDirective, via ioxDragSourceId). -->\n @for (item of layout; track item) {\n <ng-container\n [ioxRender]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n\n <!-- In-flow AFTER globals (a footer) follow the page's content, inside the page's\n ground, as on the live site (iox-ui#12). Not part of the layout: the drag\n engine skips this wrapper (GLOBALS_AFTER_CLASS). -->\n @if (globalElementsInPage.length) {\n <div [class]=\"globalsAfterClass\">\n @for (item of globalElementsInPage; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n <!-- \u2462 THE ELEMENT PATH \u2014 a thin floating strip, BOTTOM-CENTRE (board D2). It is\n pinned to the PREVIEW VIEWPORT, not to the canvas frame: the frame pans and\n scales, so a breadcrumb tracking its edge would drift off-screen and shrink with\n zoom. It used to sit in the top gutter, which is where the eye is least often \u2014\n and where the floating action strip now lives.\n\n \u26A0\uFE0F stopPropagation is REQUIRED, not defensive. This element lives inside .preview,\n whose (click) is handleCanvasClick() \u2014 \"you clicked empty canvas, drop the\n selection\". Without it, picking an ancestor crumb selects that node and then\n immediately bubbles into the canvas handler, which clears it: the selection and the\n layer-tree highlight both fall back to Body. Same reason .audit-dock stops its own. -->\n <app-breadcrumb-bar\n (click)=\"$event.stopPropagation()\"\n [path]=\"selectedPath\"\n (nodeSelect)=\"onCrumbSelect($event)\"\n (rootSelect)=\"onCrumbRootSelect()\"\n (nodePeek)=\"onCrumbPeek($event)\">\n </app-breadcrumb-bar>\n\n </div>\n\n <!-- Same rule as the sidebar: the switcher belongs to the panel it drives, as a\n tab ROW across its top. -->\n <div class=\"panel-dock\">\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\" aria-label=\"Page settings\">\n <i class=\"ph-thin ph-gear-six\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></i>\n </button>\n </div>\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n <!-- A tab or a panel deleted alone (CC4a \u2014 composite T18): the pair goes together, so the delete is offered for\n both. Every delete path asks the one guard; this is its answer. -->\n <p-dialog [visible]=\"!!pendingPairRemoval\" (visibleChange)=\"$event || cancelPairRemoval()\" [modal]=\"true\" [closable]=\"true\"\n [draggable]=\"false\" [resizable]=\"false\" [header]=\"pairRemovalText('builder.chrome.deletePairTitle')\" styleClass=\"iox-pair-removal-dialog\"\n appendTo=\"body\" [style]=\"{ width: '400px' }\">\n <p class=\"pair-removal-body\">{{ pairRemovalText('builder.chrome.deletePairBody') }}</p>\n <ng-template pTemplate=\"footer\">\n <p-button size=\"small\" severity=\"secondary\" [label]=\"pairRemovalText('builder.chrome.cancel')\" (onClick)=\"cancelPairRemoval()\"></p-button>\n <p-button size=\"small\" severity=\"danger\" [label]=\"pairRemovalText('builder.chrome.deletePair')\" (onClick)=\"confirmPairRemoval()\"></p-button>\n </ng-template>\n </p-dialog>\n\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n }\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .toolbar-strip{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:6px 8px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 40px);overflow-x:auto;scrollbar-width:none}.builder .toolbar-strip::-webkit-scrollbar{display:none}.builder .sidebar-switcher,.builder .panel-switcher{flex:none;display:flex;flex-direction:row;align-items:center;gap:.375rem;padding:7px 10px;background:var(--p-surface-0);border-block-end:1px solid var(--iox-neutral-100, #EDEFF3)}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;border:none;background:var(--iox-surface-3, #F6F7F9);color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--iox-rose-100, #F8EAEA);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:var(--iox-grad-brand, linear-gradient(135deg, #9B4458 0%, #8A3A72 50%, #63347F 100%));color:#fff}.builder .panel-dock{display:grid;grid-template-rows:auto minmax(0,1fr);flex:none;min-height:0;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden;gap:12px;padding:14px;background:var(--iox-surface-2, #EDEFF3)}.builder .sidebar{display:flex;flex-direction:column;inline-size:260px;min-inline-size:260px;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden;flex-shrink:0;min-block-size:0}.builder .sidebar .sidebar-body{flex:1;min-inline-size:0;overflow-y:auto;overflow-x:hidden}.builder .canvas-column{flex:1 1 0;min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden;background:transparent}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;direction:ltr;container:iox-canvas/inline-size;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:visible!important}.builder #canvas-preview{background:#fff}.builder #canvas-preview[data-bar-reserve=iox-bar-reserve]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}@container iox-canvas (max-width: 1200px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-laptop]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 991px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-tablet]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 767px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-mobile]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 479px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-small]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}.builder .empty-canvas{max-inline-size:560px;margin:0 auto;padding:48px 24px;text-align:center;color:#4b5563}.builder .empty-title{margin:0;font-size:18px;font-weight:600;color:#111827}.builder .empty-sub{margin:6px 0 20px;font-size:12px;line-height:1.5;color:#6b7280}.builder .empty-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.builder .empty-card{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 10px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.builder .empty-card:hover{border-color:#cb9090;box-shadow:0 2px 10px #0000000f}.builder .empty-card:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder .empty-card i{font-size:20px;color:#a05c5c}.builder .empty-card-name{font-size:11.5px;font-weight:600;color:#111827}.builder .empty-card-desc{font-size:9.5px;line-height:1.35;color:#6b7280}.builder .empty-link{margin-block-start:16px;padding:0;border:none;background:none;font-size:11px;color:#a05c5c;cursor:pointer}.builder .empty-link:hover{text-decoration:underline}.builder .empty-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder app-breadcrumb-bar{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:5px 13px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 112px);pointer-events:none}.builder .audit-dock{position:absolute;inset-block-start:20px;inset-inline-end:16px;z-index:20}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden;container:iox-canvas/inline-size;direction:ltr}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.builder .global-elements-overlay .iox-canvas-globals-before{display:flow-root;pointer-events:none}.builder .global-elements-overlay .iox-canvas-globals-before ::ng-deep>*{pointer-events:auto}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-inline-start:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\n"], dependencies: [{ kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i17.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i17.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i17.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i18.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i19.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i20.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "component", type: i1$1.IoxPageComponent, selector: "iox-page-component", inputs: ["class"], outputs: ["scrollEvent"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId", "ioxPhrasing"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "component", type: OverlayComponent, selector: "app-overlay", outputs: ["toolbarAction"] }, { kind: "component", type: ToolbarComponent, selector: "app-toolbar", inputs: ["activeMode", "activeBreakpoint", "breakpointCounts", "auditCount", "auditOpen", "activeZoom", "activeScreenWidth", "canUndo", "canRedo", "isSaving", "pageStatus"], outputs: ["modeChange", "breakpointChange", "auditToggle", "zoomChange", "screenWidthChange", "undo", "redo", "save", "preview", "publishToggle"] }, { kind: "component", type: LayerTreeComponent, selector: "app-layer-tree", inputs: ["layout", "globalElements"], outputs: ["nodeSelect", "nodeAction", "nodeMove"] }, { kind: "component", type: BreadcrumbBarComponent, selector: "app-breadcrumb-bar", inputs: ["path"], outputs: ["nodeSelect", "rootSelect", "nodePeek"] }, { kind: "component", type: AuditPanelComponent, selector: "app-audit-panel", inputs: ["findings", "breakpointLabel", "running"], outputs: ["select", "fix", "fixAll", "rerun", "closePanel"] }] }); }
9812
10169
  }
9813
10170
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderComponent, decorators: [{
9814
10171
  type: Component,
9815
- args: [{ selector: 'app-builder', standalone: false, providers: [{ provide: IOX_BUILDER_CANVAS, useValue: true }], template: "<div class=\"builder\">\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <!-- The switcher is the panel's OWN tab bar. It used to live in the header, at the\n maximum possible distance from the thing it controls; attached, the column and the\n panel are one card with one shadow. See board D2. -->\n <aside class=\"sidebar\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'pages'\" [class.active]=\"activeSidebar === 'pages'\" title=\"Pages\" aria-label=\"Pages\">\n <i class=\"ph-thin ph-browsers\"></i>\n </button>\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\" title=\"Components\" aria-label=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\" aria-label=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\" aria-label=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n <div class=\"sidebar-body\">\n @if (activeSidebar === 'pages') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderPages]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'components') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'tokens') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'tree') {\n <app-layer-tree\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n }\n </div>\n </aside>\n\n <!-- \u26A0\uFE0F The strips are SIBLINGS of .preview, not children of it. Board D2 floated them\n over the canvas and that is what they did \u2014 over the page being edited, which is the\n one thing in the builder that must stay clear. They keep their pill shape and their\n centre alignment; they just take their own row.\n\n Siblings, specifically, rather than a wrapper INSIDE .preview: `previewCanvas` is the\n overlay container and the coordinate origin for the selection box, the drag clone,\n Lenis and the zoom maths (nine call sites). Nesting anything between it and the canvas\n would shift every one of them by the strip's height. -->\n <div class=\"canvas-column\">\n <!-- \u2460 THE ACTION STRIP \u2014 floating, top-centre (board D2). It was a solid\n full-width header bar costing 56px of height on every screen; the canvas now\n starts at the top of the viewport and the strip hovers over it.\n stopPropagation for the same reason the breadcrumb does: .preview's (click)\n is \"you clicked empty canvas, drop the selection\". -->\n <div class=\"toolbar-strip\" (click)=\"$event.stopPropagation()\">\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeBreakpoint]=\"activeBreakpoint\"\n [breakpointCounts]=\"breakpointCounts\"\n [auditCount]=\"auditFindings.length || structureFindings.length\"\n [auditOpen]=\"auditOpen\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (breakpointChange)=\"handleToolbarBreakpointChange($event)\"\n (auditToggle)=\"toggleAudit()\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n <ng-content select=\"[builderToolbarActions]\"></ng-content>\n </app-toolbar>\n </div>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\"\n (dragstart)=\"onNativeDragStart($event)\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <!-- Responsive audit (A5) \u2014 floats over the canvas, right gutter. -->\n @if (auditOpen) {\n <div class=\"audit-dock\" (click)=\"$event.stopPropagation()\">\n <app-audit-panel\n [findings]=\"auditFindings\"\n [breakpointLabel]=\"auditBreakpointLabel\"\n [running]=\"auditRunning\"\n (select)=\"selectAuditNode($event)\"\n (fix)=\"applyAuditFix($event)\"\n (fixAll)=\"applyAllAuditFixes()\"\n (rerun)=\"runAudit()\"\n (closePanel)=\"toggleAudit()\"></app-audit-panel>\n </div>\n }\n\n <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height is the frame a pinned global sits in (globalOverlayHeight: the canvas\n slot, or the page on a short page), bound in px so position:fixed + inset:0\n fills it (CSS calc(100%) would give the wrong containing-block height). -->\n @if (globalElements.length) {\n <div\n #globalOverlay\n class=\"global-elements-overlay iox-token-scope\"\n [ngStyle]=\"globalLayerCss\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"globalOverlayHeight\"\n [style.transform]=\"viewportTransform\">\n <!-- BEFORE globals stay here, in the frame (a Dialog inside a header must fill the frame,\n not scroll away with the page). Their wrapper's height is what the in-flow ones take,\n and the page reserves it at its top (canvasHeaderReserve, iox-ui#19). -->\n @if (globalElementsBefore.length) {\n <div #globalsBefore class=\"iox-canvas-globals-before\">\n @for (item of globalElementsBefore; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n <!-- The PINNED after globals (a fixed bottom bar, a Dialog). An in-flow AFTER global (a\n footer) is drawn in the page, after its content (iox-ui#12). -->\n @for (item of globalElementsPinnedAfter; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <!-- iox-token-scope: the ORG's design tokens are declared on this class, not :root\n (StyleRegistryService.upsertTokens), so they never repaint the admin chrome. -->\n <iox-page-component class=\"iox-token-scope\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n\n <!-- ngStyle carries the page's own background/typography (pageSettings.style),\n compiled by the SAME function the engine applies to <body>. Inline wins over\n the #canvas-preview white default, which stays as the unset fallback. -->\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [attr.data-bar-reserve]=\"canvasBarReserve\"\n [style.padding-block-start.px]=\"canvasHeaderReserve || null\"\n [ngStyle]=\"pageStyleCss\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <!-- Empty page (B4): a starting point beats a blank canvas. Each starter is\n a responsive layout preset \u2014 ordinary nodes, editable afterwards. -->\n @if (isPageEmpty) {\n <div class=\"empty-canvas\" (click)=\"$event.stopPropagation()\">\n <h2 class=\"empty-title\">Start with a layout</h2>\n <p class=\"empty-sub\">Each one already adapts to tablet and phone. You can edit or take it apart afterwards.</p>\n <div class=\"empty-grid\">\n @for (starter of starterLayouts; track starter.id) {\n <button type=\"button\" class=\"empty-card\" (click)=\"startWithLayout(starter)\">\n <i [class]=\"starter.icon\" aria-hidden=\"true\"></i>\n <span class=\"empty-card-name\">{{ starter.name }}</span>\n <span class=\"empty-card-desc\">{{ starter.description }}</span>\n </button>\n }\n </div>\n <button type=\"button\" class=\"empty-link\" (click)=\"browseComponents()\">Or start from a single element</button>\n </div>\n }\n\n <!-- WRAPPER COLLAPSE (task 9c): top-level nodes render DIRECTLY into the\n canvas viewport \u2014 no .preview-node wrapper; iox-outer + drag move to the\n host (RenderDirective, via ioxDragSourceId). -->\n @for (item of layout; track item) {\n <ng-container\n [ioxRender]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n\n <!-- In-flow AFTER globals (a footer) follow the page's content, inside the page's\n ground, as on the live site (iox-ui#12). Not part of the layout: the drag\n engine skips this wrapper (GLOBALS_AFTER_CLASS). -->\n @if (globalElementsInPage.length) {\n <div [class]=\"globalsAfterClass\">\n @for (item of globalElementsInPage; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n <!-- \u2462 THE ELEMENT PATH \u2014 a thin floating strip, BOTTOM-CENTRE (board D2). It is\n pinned to the PREVIEW VIEWPORT, not to the canvas frame: the frame pans and\n scales, so a breadcrumb tracking its edge would drift off-screen and shrink with\n zoom. It used to sit in the top gutter, which is where the eye is least often \u2014\n and where the floating action strip now lives.\n\n \u26A0\uFE0F stopPropagation is REQUIRED, not defensive. This element lives inside .preview,\n whose (click) is handleCanvasClick() \u2014 \"you clicked empty canvas, drop the\n selection\". Without it, picking an ancestor crumb selects that node and then\n immediately bubbles into the canvas handler, which clears it: the selection and the\n layer-tree highlight both fall back to Body. Same reason .audit-dock stops its own. -->\n <app-breadcrumb-bar\n (click)=\"$event.stopPropagation()\"\n [path]=\"selectedPath\"\n (nodeSelect)=\"onCrumbSelect($event)\"\n (rootSelect)=\"onCrumbRootSelect()\"\n (nodePeek)=\"onCrumbPeek($event)\">\n </app-breadcrumb-bar>\n\n </div>\n\n <!-- Same rule as the sidebar: the switcher belongs to the panel it drives, as a\n tab ROW across its top. -->\n <div class=\"panel-dock\">\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\" aria-label=\"Page settings\">\n <i class=\"ph-thin ph-gear-six\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></i>\n </button>\n </div>\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n }\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .toolbar-strip{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:6px 8px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 40px);overflow-x:auto;scrollbar-width:none}.builder .toolbar-strip::-webkit-scrollbar{display:none}.builder .sidebar-switcher,.builder .panel-switcher{flex:none;display:flex;flex-direction:row;align-items:center;gap:.375rem;padding:7px 10px;background:var(--p-surface-0);border-block-end:1px solid var(--iox-neutral-100, #EDEFF3)}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;border:none;background:var(--iox-surface-3, #F6F7F9);color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--iox-rose-100, #F8EAEA);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:var(--iox-grad-brand, linear-gradient(135deg, #9B4458 0%, #8A3A72 50%, #63347F 100%));color:#fff}.builder .panel-dock{display:grid;grid-template-rows:auto minmax(0,1fr);flex:none;min-height:0;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden;gap:12px;padding:14px;background:var(--iox-surface-2, #EDEFF3)}.builder .sidebar{display:flex;flex-direction:column;inline-size:260px;min-inline-size:260px;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden;flex-shrink:0;min-block-size:0}.builder .sidebar .sidebar-body{flex:1;min-inline-size:0;overflow-y:auto;overflow-x:hidden}.builder .canvas-column{flex:1 1 0;min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden;background:transparent}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;direction:ltr;container:iox-canvas/inline-size;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:visible!important}.builder #canvas-preview{background:#fff}.builder #canvas-preview[data-bar-reserve=iox-bar-reserve]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}@container iox-canvas (max-width: 1200px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-laptop]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 991px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-tablet]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 767px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-mobile]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 479px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-small]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}.builder .empty-canvas{max-inline-size:560px;margin:0 auto;padding:48px 24px;text-align:center;color:#4b5563}.builder .empty-title{margin:0;font-size:18px;font-weight:600;color:#111827}.builder .empty-sub{margin:6px 0 20px;font-size:12px;line-height:1.5;color:#6b7280}.builder .empty-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.builder .empty-card{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 10px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.builder .empty-card:hover{border-color:#cb9090;box-shadow:0 2px 10px #0000000f}.builder .empty-card:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder .empty-card i{font-size:20px;color:#a05c5c}.builder .empty-card-name{font-size:11.5px;font-weight:600;color:#111827}.builder .empty-card-desc{font-size:9.5px;line-height:1.35;color:#6b7280}.builder .empty-link{margin-block-start:16px;padding:0;border:none;background:none;font-size:11px;color:#a05c5c;cursor:pointer}.builder .empty-link:hover{text-decoration:underline}.builder .empty-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder app-breadcrumb-bar{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:5px 13px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 112px);pointer-events:none}.builder .audit-dock{position:absolute;inset-block-start:20px;inset-inline-end:16px;z-index:20}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden;container:iox-canvas/inline-size;direction:ltr}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.builder .global-elements-overlay .iox-canvas-globals-before{display:flow-root;pointer-events:none}.builder .global-elements-overlay .iox-canvas-globals-before ::ng-deep>*{pointer-events:auto}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-inline-start:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\n"] }]
10172
+ args: [{ selector: 'app-builder', standalone: false, providers: [{ provide: IOX_BUILDER_CANVAS, useValue: true }], template: "<div class=\"builder\">\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <!-- The switcher is the panel's OWN tab bar. It used to live in the header, at the\n maximum possible distance from the thing it controls; attached, the column and the\n panel are one card with one shadow. See board D2. -->\n <aside class=\"sidebar\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'pages'\" [class.active]=\"activeSidebar === 'pages'\" title=\"Pages\" aria-label=\"Pages\">\n <i class=\"ph-thin ph-browsers\"></i>\n </button>\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\" title=\"Components\" aria-label=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\" aria-label=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\" aria-label=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n <div class=\"sidebar-body\">\n @if (activeSidebar === 'pages') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderPages]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'components') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'tokens') {\n <div class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n }\n @if (activeSidebar === 'tree') {\n <app-layer-tree\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n }\n </div>\n </aside>\n\n <!-- \u26A0\uFE0F The strips are SIBLINGS of .preview, not children of it. Board D2 floated them\n over the canvas and that is what they did \u2014 over the page being edited, which is the\n one thing in the builder that must stay clear. They keep their pill shape and their\n centre alignment; they just take their own row.\n\n Siblings, specifically, rather than a wrapper INSIDE .preview: `previewCanvas` is the\n overlay container and the coordinate origin for the selection box, the drag clone,\n Lenis and the zoom maths (nine call sites). Nesting anything between it and the canvas\n would shift every one of them by the strip's height. -->\n <div class=\"canvas-column\">\n <!-- \u2460 THE ACTION STRIP \u2014 floating, top-centre (board D2). It was a solid\n full-width header bar costing 56px of height on every screen; the canvas now\n starts at the top of the viewport and the strip hovers over it.\n stopPropagation for the same reason the breadcrumb does: .preview's (click)\n is \"you clicked empty canvas, drop the selection\". -->\n <div class=\"toolbar-strip\" (click)=\"$event.stopPropagation()\">\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeBreakpoint]=\"activeBreakpoint\"\n [breakpointCounts]=\"breakpointCounts\"\n [auditCount]=\"auditFindings.length || structureFindings.length\"\n [auditOpen]=\"auditOpen\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (breakpointChange)=\"handleToolbarBreakpointChange($event)\"\n (auditToggle)=\"toggleAudit()\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n <ng-content select=\"[builderToolbarActions]\"></ng-content>\n </app-toolbar>\n </div>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\"\n (dragstart)=\"onNativeDragStart($event)\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <!-- Responsive audit (A5) \u2014 floats over the canvas, right gutter. -->\n @if (auditOpen) {\n <div class=\"audit-dock\" (click)=\"$event.stopPropagation()\">\n <app-audit-panel\n [findings]=\"auditFindings\"\n [breakpointLabel]=\"auditBreakpointLabel\"\n [running]=\"auditRunning\"\n (select)=\"selectAuditNode($event)\"\n (fix)=\"applyAuditFix($event)\"\n (fixAll)=\"applyAllAuditFixes()\"\n (rerun)=\"runAudit()\"\n (closePanel)=\"toggleAudit()\"></app-audit-panel>\n </div>\n }\n\n <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height is the frame a pinned global sits in (globalOverlayHeight: the canvas\n slot, or the page on a short page), bound in px so position:fixed + inset:0\n fills it (CSS calc(100%) would give the wrong containing-block height). -->\n @if (globalElements.length) {\n <div\n #globalOverlay\n class=\"global-elements-overlay iox-token-scope\"\n [ngStyle]=\"globalLayerCss\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"globalOverlayHeight\"\n [style.transform]=\"viewportTransform\">\n <!-- BEFORE globals stay here, in the frame (a Dialog inside a header must fill the frame,\n not scroll away with the page). Their wrapper's height is what the in-flow ones take,\n and the page reserves it at its top (canvasHeaderReserve, iox-ui#19). -->\n @if (globalElementsBefore.length) {\n <div #globalsBefore class=\"iox-canvas-globals-before\">\n @for (item of globalElementsBefore; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n <!-- The PINNED after globals (a fixed bottom bar, a Dialog). An in-flow AFTER global (a\n footer) is drawn in the page, after its content (iox-ui#12). -->\n @for (item of globalElementsPinnedAfter; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <!-- iox-token-scope: the ORG's design tokens are declared on this class, not :root\n (StyleRegistryService.upsertTokens), so they never repaint the admin chrome. -->\n <iox-page-component class=\"iox-token-scope\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n\n <!-- ngStyle carries the page's own background/typography (pageSettings.style),\n compiled by the SAME function the engine applies to <body>. Inline wins over\n the #canvas-preview white default, which stays as the unset fallback. -->\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [attr.data-bar-reserve]=\"canvasBarReserve\"\n [style.padding-block-start.px]=\"canvasHeaderReserve || null\"\n [ngStyle]=\"pageStyleCss\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <!-- Empty page (B4): a starting point beats a blank canvas. Each starter is\n a responsive layout preset \u2014 ordinary nodes, editable afterwards. -->\n @if (isPageEmpty) {\n <div class=\"empty-canvas\" (click)=\"$event.stopPropagation()\">\n <h2 class=\"empty-title\">Start with a layout</h2>\n <p class=\"empty-sub\">Each one already adapts to tablet and phone. You can edit or take it apart afterwards.</p>\n <div class=\"empty-grid\">\n @for (starter of starterLayouts; track starter.id) {\n <button type=\"button\" class=\"empty-card\" (click)=\"startWithLayout(starter)\">\n <i [class]=\"starter.icon\" aria-hidden=\"true\"></i>\n <span class=\"empty-card-name\">{{ starter.name }}</span>\n <span class=\"empty-card-desc\">{{ starter.description }}</span>\n </button>\n }\n </div>\n <button type=\"button\" class=\"empty-link\" (click)=\"browseComponents()\">Or start from a single element</button>\n </div>\n }\n\n <!-- WRAPPER COLLAPSE (task 9c): top-level nodes render DIRECTLY into the\n canvas viewport \u2014 no .preview-node wrapper; iox-outer + drag move to the\n host (RenderDirective, via ioxDragSourceId). -->\n @for (item of layout; track item) {\n <ng-container\n [ioxRender]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n\n <!-- In-flow AFTER globals (a footer) follow the page's content, inside the page's\n ground, as on the live site (iox-ui#12). Not part of the layout: the drag\n engine skips this wrapper (GLOBALS_AFTER_CLASS). -->\n @if (globalElementsInPage.length) {\n <div [class]=\"globalsAfterClass\">\n @for (item of globalElementsInPage; track item) {\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n }\n </div>\n }\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n <!-- \u2462 THE ELEMENT PATH \u2014 a thin floating strip, BOTTOM-CENTRE (board D2). It is\n pinned to the PREVIEW VIEWPORT, not to the canvas frame: the frame pans and\n scales, so a breadcrumb tracking its edge would drift off-screen and shrink with\n zoom. It used to sit in the top gutter, which is where the eye is least often \u2014\n and where the floating action strip now lives.\n\n \u26A0\uFE0F stopPropagation is REQUIRED, not defensive. This element lives inside .preview,\n whose (click) is handleCanvasClick() \u2014 \"you clicked empty canvas, drop the\n selection\". Without it, picking an ancestor crumb selects that node and then\n immediately bubbles into the canvas handler, which clears it: the selection and the\n layer-tree highlight both fall back to Body. Same reason .audit-dock stops its own. -->\n <app-breadcrumb-bar\n (click)=\"$event.stopPropagation()\"\n [path]=\"selectedPath\"\n (nodeSelect)=\"onCrumbSelect($event)\"\n (rootSelect)=\"onCrumbRootSelect()\"\n (nodePeek)=\"onCrumbPeek($event)\">\n </app-breadcrumb-bar>\n\n </div>\n\n <!-- Same rule as the sidebar: the switcher belongs to the panel it drives, as a\n tab ROW across its top. -->\n <div class=\"panel-dock\">\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\" aria-label=\"Page settings\">\n <i class=\"ph-thin ph-gear-six\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></i>\n </button>\n </div>\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n <!-- A tab or a panel deleted alone (CC4a \u2014 composite T18): the pair goes together, so the delete is offered for\n both. Every delete path asks the one guard; this is its answer. -->\n <p-dialog [visible]=\"!!pendingPairRemoval\" (visibleChange)=\"$event || cancelPairRemoval()\" [modal]=\"true\" [closable]=\"true\"\n [draggable]=\"false\" [resizable]=\"false\" [header]=\"pairRemovalText('builder.chrome.deletePairTitle')\" styleClass=\"iox-pair-removal-dialog\"\n appendTo=\"body\" [style]=\"{ width: '400px' }\">\n <p class=\"pair-removal-body\">{{ pairRemovalText('builder.chrome.deletePairBody') }}</p>\n <ng-template pTemplate=\"footer\">\n <p-button size=\"small\" severity=\"secondary\" [label]=\"pairRemovalText('builder.chrome.cancel')\" (onClick)=\"cancelPairRemoval()\"></p-button>\n <p-button size=\"small\" severity=\"danger\" [label]=\"pairRemovalText('builder.chrome.deletePair')\" (onClick)=\"confirmPairRemoval()\"></p-button>\n </ng-template>\n </p-dialog>\n\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n }\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .toolbar-strip{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:6px 8px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 40px);overflow-x:auto;scrollbar-width:none}.builder .toolbar-strip::-webkit-scrollbar{display:none}.builder .sidebar-switcher,.builder .panel-switcher{flex:none;display:flex;flex-direction:row;align-items:center;gap:.375rem;padding:7px 10px;background:var(--p-surface-0);border-block-end:1px solid var(--iox-neutral-100, #EDEFF3)}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;border:none;background:var(--iox-surface-3, #F6F7F9);color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--iox-rose-100, #F8EAEA);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:var(--iox-grad-brand, linear-gradient(135deg, #9B4458 0%, #8A3A72 50%, #63347F 100%));color:#fff}.builder .panel-dock{display:grid;grid-template-rows:auto minmax(0,1fr);flex:none;min-height:0;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden;gap:12px;padding:14px;background:var(--iox-surface-2, #EDEFF3)}.builder .sidebar{display:flex;flex-direction:column;inline-size:260px;min-inline-size:260px;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden;flex-shrink:0;min-block-size:0}.builder .sidebar .sidebar-body{flex:1;min-inline-size:0;overflow-y:auto;overflow-x:hidden}.builder .canvas-column{flex:1 1 0;min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden;background:transparent}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;direction:ltr;container:iox-canvas/inline-size;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:visible!important}.builder #canvas-preview{background:#fff}.builder #canvas-preview[data-bar-reserve=iox-bar-reserve]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}@container iox-canvas (max-width: 1200px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-laptop]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 991px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-tablet]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 767px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-mobile]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}@container iox-canvas (max-width: 479px){.builder #canvas-preview[data-bar-reserve=iox-bar-reserve-small]{padding-block-end:calc(var(--iox-tabbar-height, 64px) + var(--iox-border-width, 1px) + var(--iox-tabbar-inset, 0px) * 2)}}.builder .empty-canvas{max-inline-size:560px;margin:0 auto;padding:48px 24px;text-align:center;color:#4b5563}.builder .empty-title{margin:0;font-size:18px;font-weight:600;color:#111827}.builder .empty-sub{margin:6px 0 20px;font-size:12px;line-height:1.5;color:#6b7280}.builder .empty-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.builder .empty-card{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 10px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.builder .empty-card:hover{border-color:#cb9090;box-shadow:0 2px 10px #0000000f}.builder .empty-card:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder .empty-card i{font-size:20px;color:#a05c5c}.builder .empty-card-name{font-size:11.5px;font-weight:600;color:#111827}.builder .empty-card-desc{font-size:9.5px;line-height:1.35;color:#6b7280}.builder .empty-link{margin-block-start:16px;padding:0;border:none;background:none;font-size:11px;color:#a05c5c;cursor:pointer}.builder .empty-link:hover{text-decoration:underline}.builder .empty-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder app-breadcrumb-bar{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:5px 13px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 112px);pointer-events:none}.builder .audit-dock{position:absolute;inset-block-start:20px;inset-inline-end:16px;z-index:20}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden;container:iox-canvas/inline-size;direction:ltr}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.builder .global-elements-overlay .iox-canvas-globals-before{display:flow-root;pointer-events:none}.builder .global-elements-overlay .iox-canvas-globals-before ::ng-deep>*{pointer-events:auto}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-inline-start:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\n"] }]
9816
10173
  }], ctorParameters: () => [{ type: ComponentRegistryService }, { type: OverlayService }, { type: PanelEventService }, { type: DragEngineService }, { type: DataSourceRegistryService }, { type: ViewportService }, { type: FormStateService }, { type: CanvasDesignStateService }, { type: InteractionEngineService }, { type: InteractionPresetRegistryService }, { type: BuilderClipboardService }, { type: SymbolRegistryService }, { type: i0.ChangeDetectorRef }, { type: i0.ApplicationRef }, { type: MoveableCanvasService }, { type: SelectoCanvasService }, { type: StyleRegistryService }], propDecorators: { layout: [{
9817
10174
  type: Input
9818
10175
  }], dataSources: [{
@@ -10654,7 +11011,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
10654
11011
  type: Output
10655
11012
  }] } });
10656
11013
 
11014
+ /** Swiper's event → the interaction trigger it replays (componentEvents). */
11015
+ const SWIPER_TRIGGER = {
11016
+ slideChange: 'swiperSlideChange', reachBeginning: 'swiperReachBeginning', reachEnd: 'swiperReachEnd',
11017
+ autoplayStart: 'swiperAutoplayStart', autoplayStop: 'swiperAutoplayStop',
11018
+ };
10657
11019
  class SliderContainerComponent {
11020
+ /** Every child — the slides AND the control parts (CC3). Split once per change, never in a getter. */
11021
+ set children(value) {
11022
+ this._children = value ?? [];
11023
+ const { lead, slides, trail } = splitSliderChildren(this._children);
11024
+ this.leadParts = lead;
11025
+ this.slides = slides;
11026
+ this.trailParts = trail;
11027
+ this.controlParts = hasSliderParts({ children: this._children });
11028
+ }
11029
+ get children() { return this._children; }
11030
+ /** A saved slider whose controls are not parts yet keeps Swiper-drawn ones. */
11031
+ get legacyControls() { return !this.controlParts; }
10658
11032
  set style(v) {
10659
11033
  if (v === this._style)
10660
11034
  return;
@@ -10701,7 +11075,14 @@ class SliderContainerComponent {
10701
11075
  this.dataSourceService = dataSourceService;
10702
11076
  this.dsRegistry = dsRegistry;
10703
11077
  this.panelEventService = panelEventService;
10704
- this.children = [];
11078
+ this._children = [];
11079
+ /** The slides (into `.swiper-wrapper`); the template slide of a bound slider is `slides[0]`. */
11080
+ this.slides = [];
11081
+ /** Control parts before the first slide / after it — rendered beside the wrapper, inside `.swiper`. */
11082
+ this.leadParts = [];
11083
+ this.trailParts = [];
11084
+ /** The controls are parts: Swiper draws none of its own. */
11085
+ this.controlParts = false;
10705
11086
  this._style = {};
10706
11087
  this._previewStyle = {};
10707
11088
  this._editShellStyle = { position: 'relative' };
@@ -10852,7 +11233,7 @@ class SliderContainerComponent {
10852
11233
  });
10853
11234
  }
10854
11235
  refreshPreviews() {
10855
- if (!this.items.length || !this.children.length) {
11236
+ if (!this.items.length || !this.slides.length) {
10856
11237
  this.previewSlides = [];
10857
11238
  this.cdr.markForCheck();
10858
11239
  this._scheduleReinit();
@@ -10860,16 +11241,16 @@ class SliderContainerComponent {
10860
11241
  }
10861
11242
  this.applyFirstItemToTemplate();
10862
11243
  // Each preview slide = one cloned SliderSlide node with item data resolved
10863
- this.previewSlides = this.items.slice(1).map(item => cloneNodesForItem([this.children[0]], item)[0]);
11244
+ this.previewSlides = this.items.slice(1).map(item => cloneNodesForItem([this.slides[0]], item)[0]);
10864
11245
  this.cdr.markForCheck();
10865
11246
  this._scheduleReinit();
10866
11247
  }
10867
11248
  applyFirstItemToTemplate() {
10868
11249
  const item = this.items[0];
10869
- if (!item || !this.children.length || !this.overlayService)
11250
+ if (!item || !this.slides.length || !this.overlayService)
10870
11251
  return;
10871
11252
  // Bindings live on leaf nodes inside the template slide, so recurse into the tree
10872
- this.applyItemToNodes([this.children[0]], item);
11253
+ this.applyItemToNodes([this.slides[0]], item);
10873
11254
  }
10874
11255
  applyItemToNodes(nodes, item) {
10875
11256
  for (const node of nodes) {
@@ -10895,7 +11276,7 @@ class SliderContainerComponent {
10895
11276
  _isInTemplate(target) {
10896
11277
  const search = (nodes) => nodes.some(c => c === target || c.id === target.id ||
10897
11278
  (c.children?.length ? search(c.children) : false));
10898
- return this.children.length > 0 && search([this.children[0]]);
11279
+ return this.slides.length > 0 && search([this.slides[0]]);
10899
11280
  }
10900
11281
  _scheduleReinit() {
10901
11282
  this._destroySwiper();
@@ -10908,123 +11289,31 @@ class SliderContainerComponent {
10908
11289
  const el = this.isPreview ? this.swiperEl?.nativeElement : this.editSwiperEl?.nativeElement;
10909
11290
  if (!el)
10910
11291
  return;
10911
- const [{ default: Swiper }, swiperModules] = await Promise.all([
10912
- import('swiper'),
10913
- import('swiper/modules'),
10914
- ]);
10915
- const { Navigation, Pagination, Scrollbar, Autoplay, Keyboard, Mousewheel, Zoom, Grid, Parallax, FreeMode, EffectFade, EffectCube, EffectFlip, EffectCoverflow, EffectCards, EffectCreative, } = swiperModules;
10916
- const modules = [];
10917
- if (this.enableNavigation)
10918
- modules.push(Navigation);
10919
- if (this.enablePagination)
10920
- modules.push(Pagination);
10921
- if (this.enableScrollbar)
10922
- modules.push(Scrollbar);
10923
- const config = {
10924
- modules,
10925
- direction: this.direction,
10926
- slidesPerView: this.slidesPerView,
10927
- slidesPerGroup: this.slidesPerGroup > 1 ? this.slidesPerGroup : 1,
10928
- spaceBetween: this.spaceBetween,
10929
- speed: this.isPreview ? this.speed : 300,
10930
- // Always slide effect in edit mode — fade/cube etc. hide all but active slide
10931
- effect: this.isPreview ? this.effect : 'slide',
10932
- centeredSlides: this.centeredSlides,
10933
- initialSlide: this.initialSlide,
10934
- autoHeight: this.autoHeight,
10935
- };
10936
- if (this.isPreview) {
10937
- // Full interactive config for preview
10938
- config['grabCursor'] = this.grabCursor;
10939
- config['slideToClickedSlide'] = this.slideToClickedSlide;
10940
- config['loop'] = this.loop === 'loop';
10941
- config['rewind'] = this.loop === 'rewind';
10942
- if (this.freeMode) {
10943
- modules.push(FreeMode);
10944
- config['freeMode'] = true;
10945
- }
10946
- if (this.enableAutoplay) {
10947
- modules.push(Autoplay);
10948
- }
10949
- if (this.enableKeyboard) {
10950
- modules.push(Keyboard);
10951
- }
10952
- if (this.enableMousewheel) {
10953
- modules.push(Mousewheel);
10954
- }
10955
- if (this.enableZoom) {
10956
- modules.push(Zoom);
10957
- }
10958
- if (this.enableGrid) {
10959
- modules.push(Grid);
10960
- }
10961
- if (this.enableParallax) {
10962
- modules.push(Parallax);
10963
- }
10964
- const effectMap = {
10965
- fade: EffectFade, cube: EffectCube, flip: EffectFlip,
10966
- coverflow: EffectCoverflow, cards: EffectCards, creative: EffectCreative,
10967
- };
10968
- if (this.effect !== 'slide' && effectMap[this.effect]) {
10969
- modules.push(effectMap[this.effect]);
10970
- }
10971
- if (this.enableNavigation) {
10972
- config['navigation'] = { prevEl: '.swiper-button-prev', nextEl: '.swiper-button-next' };
10973
- }
10974
- if (this.enablePagination) {
10975
- config['pagination'] = {
10976
- el: '.swiper-pagination',
10977
- type: this.paginationType,
10978
- clickable: this.paginationClickable,
10979
- dynamicBullets: this.paginationDynamicBullets,
10980
- };
10981
- }
10982
- if (this.enableScrollbar)
10983
- config['scrollbar'] = { el: '.swiper-scrollbar', draggable: true };
10984
- if (this.enableAutoplay) {
10985
- config['autoplay'] = {
10986
- delay: this.autoplayDelay,
10987
- pauseOnMouseEnter: this.autoplayPauseOnMouseEnter,
10988
- disableOnInteraction: this.autoplayDisableOnInteraction,
10989
- };
10990
- }
10991
- if (this.enableGrid && this.gridRows > 1)
10992
- config['grid'] = { rows: this.gridRows };
10993
- if (this.breakpoints) {
10994
- try {
10995
- config['breakpoints'] = JSON.parse(this.breakpoints);
10996
- }
10997
- catch { /* ignore */ }
10998
- }
10999
- config['on'] = {
11000
- slideChange: (swiper) => this._onSwiperEvent('swiperSlideChange', swiper),
11001
- reachBeginning: (swiper) => this._onSwiperEvent('swiperReachBeginning', swiper),
11002
- reachEnd: (swiper) => this._onSwiperEvent('swiperReachEnd', swiper),
11003
- autoplayStart: (swiper) => this._onSwiperEvent('swiperAutoplayStart', swiper),
11004
- autoplayStop: (swiper) => this._onSwiperEvent('swiperAutoplayStop', swiper),
11005
- };
11006
- }
11007
- else {
11008
- // Edit mode: disable touch/swipe so builder click handlers work normally
11009
- config['allowTouchMove'] = false;
11010
- config['simulateTouch'] = false;
11011
- if (this.enableNavigation) {
11012
- config['navigation'] = { prevEl: '.swiper-button-prev', nextEl: '.swiper-button-next' };
11013
- }
11014
- if (this.enablePagination) {
11015
- config['pagination'] = {
11016
- el: '.swiper-pagination',
11017
- type: this.paginationType,
11018
- clickable: this.paginationClickable,
11019
- dynamicBullets: this.paginationDynamicBullets,
11020
- };
11021
- }
11022
- if (this.enableScrollbar)
11023
- config['scrollbar'] = { el: '.swiper-scrollbar', draggable: false };
11024
- }
11025
- this._swiper = new Swiper(el, config);
11292
+ // The ONE Swiper decision (iox-ui `bootSlider` → `planSwiperOptions`): the EDIT canvas is tamed (no touch,
11293
+ // loop or autoplay; the parts still navigate); Preview is the live site's — the A11y module, the pause
11294
+ // control, the live region (CC3; Preview used to build its own config and skipped A11y).
11295
+ const swiper = await bootSlider(el, this.swiperInputs(), {
11296
+ edit: !this.isPreview,
11297
+ onEvent: this.isPreview ? (event, sw) => this._onSwiperEvent(SWIPER_TRIGGER[event] ?? event, sw) : undefined,
11298
+ });
11299
+ if (!swiper)
11300
+ return;
11301
+ this._swiper = swiper;
11026
11302
  this.instanceRegistry.register(this.nodeId, this._swiper);
11027
11303
  }
11304
+ swiperInputs() {
11305
+ return {
11306
+ dir: this.dir, direction: this.direction, loop: this.loop, slidesPerView: this.slidesPerView, slidesPerGroup: this.slidesPerGroup,
11307
+ spaceBetween: this.spaceBetween, speed: this.speed, effect: this.effect, grabCursor: this.grabCursor, centeredSlides: this.centeredSlides,
11308
+ initialSlide: this.initialSlide, autoHeight: this.autoHeight, slideToClickedSlide: this.slideToClickedSlide, freeMode: this.freeMode,
11309
+ enableNavigation: this.enableNavigation, enablePagination: this.enablePagination, enableScrollbar: this.enableScrollbar,
11310
+ enableAutoplay: this.enableAutoplay, enableKeyboard: this.enableKeyboard, enableMousewheel: this.enableMousewheel,
11311
+ enableZoom: this.enableZoom, enableGrid: this.enableGrid, enableParallax: this.enableParallax, autoplayDelay: this.autoplayDelay,
11312
+ autoplayPauseOnMouseEnter: this.autoplayPauseOnMouseEnter, autoplayDisableOnInteraction: this.autoplayDisableOnInteraction,
11313
+ paginationType: this.paginationType, paginationClickable: this.paginationClickable, paginationDynamicBullets: this.paginationDynamicBullets,
11314
+ gridRows: this.gridRows, breakpoints: this.breakpoints,
11315
+ };
11316
+ }
11028
11317
  _destroySwiper() {
11029
11318
  if (this._swiper) {
11030
11319
  this._swiper.destroy(true, true);
@@ -11037,7 +11326,7 @@ class SliderContainerComponent {
11037
11326
  if (eventName === 'swiperSlideChange') {
11038
11327
  const prevIdx = swiper.previousIndex;
11039
11328
  const activeIdx = swiper.activeIndex;
11040
- const allSlides = this._source ? [this.children[0], ...this.previewSlides] : this.children;
11329
+ const allSlides = this._source ? [this.slides[0], ...this.previewSlides] : this.slides;
11041
11330
  if (prevIdx !== undefined && allSlides[prevIdx]) {
11042
11331
  this.interactionEngine.replayTrigger(allSlides[prevIdx].id, 'swiperSlideInactive');
11043
11332
  }
@@ -11067,11 +11356,17 @@ class SliderContainerComponent {
11067
11356
  </div>
11068
11357
 
11069
11358
  <div #editSwiperEl class="swiper swiper-edit" [attr.dir]="dir" [style.direction]="dir">
11359
+ <!-- CC3: the control PARTS (Pause, Previous, Next, Pagination) render inside .swiper, beside the
11360
+ wrapper (never in it — slider.md); selectable and styled on their own (R2). -->
11361
+ <ng-container *ngFor="let part of leadParts; trackBy: trackByNodeId">
11362
+ <ng-container [ioxRender]="part" (onClick)="childClick.emit($event)"
11363
+ (onMouseEnter)="childMouseEnter.emit($event)" (onMouseLeave)="childMouseLeave.emit()"></ng-container>
11364
+ </ng-container>
11070
11365
  <div class="swiper-wrapper">
11071
11366
 
11072
- <!-- NORMAL MODE: all children as draggable slides -->
11367
+ <!-- NORMAL MODE: all slides draggable -->
11073
11368
  <ng-container *ngIf="!source">
11074
- <div *ngFor="let child of children"
11369
+ <div *ngFor="let child of slides; trackBy: trackByNodeId"
11075
11370
  class="swiper-slide"
11076
11371
  [ngClass]="['iox-outer-' + (child.styleId ?? child.id)]"
11077
11372
  ioxDraggable
@@ -11089,22 +11384,24 @@ class SliderContainerComponent {
11089
11384
  <!-- REPEAT MODE: template slide (item 0) + dimmed preview slides (items 1..N) -->
11090
11385
  <ng-container *ngIf="source">
11091
11386
  <!-- Template slide — draggable/editable, receives item[0] data -->
11092
- <div *ngIf="children.length"
11387
+ <div *ngIf="slides.length"
11093
11388
  class="swiper-slide"
11094
- [ngClass]="['iox-outer-' + (children[0].styleId ?? children[0].id)]"
11389
+ [ngClass]="['iox-outer-' + (slides[0].styleId ?? slides[0].id)]"
11390
+ [attr.data-iox-row-idx]="0"
11095
11391
  ioxDraggable
11096
- [ioxDragData]="children[0]"
11392
+ [ioxDragData]="slides[0]"
11097
11393
  [ioxDragSourceId]="dropListId">
11098
11394
  <ng-container
11099
- [ioxRender]="children[0]"
11395
+ [ioxRender]="slides[0]"
11100
11396
  (onClick)="childClick.emit($event)"
11101
11397
  (onMouseEnter)="childMouseEnter.emit($event)"
11102
11398
  (onMouseLeave)="childMouseLeave.emit()">
11103
11399
  </ng-container>
11104
11400
  </div>
11105
11401
  <!-- Preview slides — dimmed, non-interactive, data resolved from items 1..N -->
11106
- <div *ngFor="let slide of previewSlides; trackBy: trackByNodeId"
11402
+ <div *ngFor="let slide of previewSlides; let i = index; trackBy: trackByNodeId"
11107
11403
  class="swiper-slide slider-preview-slide"
11404
+ [attr.data-iox-row-idx]="i + 1"
11108
11405
  [ngClass]="['iox-outer-' + (slide.styleId ?? slide.id)]">
11109
11406
  <ng-container
11110
11407
  [ioxRender]="slide"
@@ -11116,23 +11413,28 @@ class SliderContainerComponent {
11116
11413
  </ng-container>
11117
11414
 
11118
11415
  </div>
11416
+ <ng-container *ngFor="let part of trailParts; trackBy: trackByNodeId">
11417
+ <ng-container [ioxRender]="part" (onClick)="childClick.emit($event)"
11418
+ (onMouseEnter)="childMouseEnter.emit($event)" (onMouseLeave)="childMouseLeave.emit()"></ng-container>
11419
+ </ng-container>
11119
11420
 
11120
- <div *ngIf="!source && !children.length" class="slider-edit-placeholder" data-iox-chrome>
11421
+ <div *ngIf="!source && !slides.length" class="slider-edit-placeholder" data-iox-chrome>
11121
11422
  <i class="ph-thin ph-sliders-horizontal"></i>
11122
11423
  <span>Click + in toolbar to add a slide</span>
11123
11424
  </div>
11124
11425
 
11125
- <div *ngIf="enablePagination" class="swiper-pagination"></div>
11426
+ <div *ngIf="legacyControls && enablePagination" class="swiper-pagination"></div>
11126
11427
  <div *ngIf="enableScrollbar" class="swiper-scrollbar"></div>
11127
11428
  </div>
11128
11429
 
11129
- <!-- Nav buttons live on the shell, not inside .swiper, so overflow:hidden never clips them
11130
- and top:50%+transform always centers on the real slide height -->
11131
- <button *ngIf="enableNavigation" class="slider-edit-nav slider-edit-nav--prev" data-iox-chrome
11430
+ <!-- Legacy (Swiper-drawn) nav buttons live on the shell, not inside .swiper, so overflow:hidden never
11431
+ clips them and top:50%+transform always centers on the real slide height. A slider whose controls
11432
+ are PARTS (CC3) draws none: the parts are the controls. -->
11433
+ <button *ngIf="legacyControls && enableNavigation" class="slider-edit-nav slider-edit-nav--prev" data-iox-chrome
11132
11434
  (click)="_swiper?.slidePrev()" type="button" title="Previous slide">
11133
11435
  <i class="ph-thin ph-caret-left"></i>
11134
11436
  </button>
11135
- <button *ngIf="enableNavigation" class="slider-edit-nav slider-edit-nav--next" data-iox-chrome
11437
+ <button *ngIf="legacyControls && enableNavigation" class="slider-edit-nav slider-edit-nav--next" data-iox-chrome
11136
11438
  (click)="_swiper?.slideNext()" type="button" title="Next slide">
11137
11439
  <i class="ph-thin ph-caret-right"></i>
11138
11440
  </button>
@@ -11149,12 +11451,16 @@ class SliderContainerComponent {
11149
11451
  [style.--swiper-navigation-color]="navigationColor || null"
11150
11452
  [style.--swiper-navigation-size.px]="navigationSize || null"
11151
11453
  [style.--swiper-pagination-color]="paginationColor || null">
11454
+ <ng-container *ngFor="let part of leadParts; trackBy: trackByNodeId">
11455
+ <ng-container [ioxRender]="part" (onClick)="childClick.emit($event)"
11456
+ (onMouseEnter)="childMouseEnter.emit($event)" (onMouseLeave)="childMouseLeave.emit()"></ng-container>
11457
+ </ng-container>
11152
11458
  <div class="swiper-wrapper">
11153
11459
  <!-- Swiper requires .swiper-slide as a DIRECT child of .swiper-wrapper.
11154
11460
  Wrap each rendered slide in one (same structure as edit mode) so the
11155
11461
  app-slider-slide host sits INSIDE the slide, not between it and the wrapper. -->
11156
11462
  <ng-container *ngIf="!source">
11157
- <div *ngFor="let child of children"
11463
+ <div *ngFor="let child of slides; trackBy: trackByNodeId"
11158
11464
  class="swiper-slide"
11159
11465
  [ngClass]="['iox-outer-' + (child.styleId ?? child.id)]">
11160
11466
  <ng-container
@@ -11165,20 +11471,22 @@ class SliderContainerComponent {
11165
11471
  </ng-container>
11166
11472
  </div>
11167
11473
  </ng-container>
11168
- <!-- REPEAT: template slide + preview slides -->
11474
+ <!-- REPEAT: template slide + preview slides; each is a row (a lightbox opens at the clicked one) -->
11169
11475
  <ng-container *ngIf="source">
11170
- <div *ngIf="children.length"
11476
+ <div *ngIf="slides.length"
11171
11477
  class="swiper-slide"
11172
- [ngClass]="['iox-outer-' + (children[0].styleId ?? children[0].id)]">
11478
+ [attr.data-iox-row-idx]="0"
11479
+ [ngClass]="['iox-outer-' + (slides[0].styleId ?? slides[0].id)]">
11173
11480
  <ng-container
11174
- [ioxRender]="children[0]"
11481
+ [ioxRender]="slides[0]"
11175
11482
  (onClick)="childClick.emit($event)"
11176
11483
  (onMouseEnter)="childMouseEnter.emit($event)"
11177
11484
  (onMouseLeave)="childMouseLeave.emit()">
11178
11485
  </ng-container>
11179
11486
  </div>
11180
- <div *ngFor="let slide of previewSlides; trackBy: trackByNodeId"
11487
+ <div *ngFor="let slide of previewSlides; let i = index; trackBy: trackByNodeId"
11181
11488
  class="swiper-slide"
11489
+ [attr.data-iox-row-idx]="i + 1"
11182
11490
  [ngClass]="['iox-outer-' + (slide.styleId ?? slide.id)]">
11183
11491
  <ng-container
11184
11492
  [ioxRender]="slide"
@@ -11189,9 +11497,13 @@ class SliderContainerComponent {
11189
11497
  </div>
11190
11498
  </ng-container>
11191
11499
  </div>
11192
- <div *ngIf="enableNavigation" class="swiper-button-prev"></div>
11193
- <div *ngIf="enableNavigation" class="swiper-button-next"></div>
11194
- <div *ngIf="enablePagination" class="swiper-pagination"></div>
11500
+ <ng-container *ngFor="let part of trailParts; trackBy: trackByNodeId">
11501
+ <ng-container [ioxRender]="part" (onClick)="childClick.emit($event)"
11502
+ (onMouseEnter)="childMouseEnter.emit($event)" (onMouseLeave)="childMouseLeave.emit()"></ng-container>
11503
+ </ng-container>
11504
+ <div *ngIf="legacyControls && enableNavigation" class="swiper-button-prev"></div>
11505
+ <div *ngIf="legacyControls && enableNavigation" class="swiper-button-next"></div>
11506
+ <div *ngIf="legacyControls && enablePagination" class="swiper-pagination"></div>
11195
11507
  <div *ngIf="enableScrollbar" class="swiper-scrollbar"></div>
11196
11508
  </div>
11197
11509
  `, isInline: true, styles: [":host{display:block}.swiper-wrapper{height:auto}.slider-edit-shell{position:relative;display:block}.swiper-edit{width:100%}.swiper-edit .swiper-slide{position:relative}.slider-edit-nav{position:absolute;top:50%;transform:translateY(-50%);width:32px;height:32px;border-radius:50%;border:1px solid rgba(255,255,255,.5);background:#00000038;color:#fff;font-size:16px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:10;transition:background .15s}.slider-edit-nav:hover{background:#00000073}.slider-edit-nav--prev{left:8px}.slider-edit-nav--next{right:8px}.slider-edit-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.slider-edit-placeholder i{font-size:22px}.slider-repeat-badge{position:absolute;top:0;left:0;z-index:10;display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px solid rgba(203,144,144,.5);border-radius:0 0 4px;opacity:0;transition:opacity .15s;-webkit-user-select:none;user-select:none;pointer-events:none}.slider-edit-shell:hover .slider-repeat-badge{opacity:1}.slider-repeat-badge-count{font-weight:600}.slider-badge-spin{animation:slider-badge-spin 1s linear infinite}@keyframes slider-badge-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.slider-preview-slide{opacity:.55;pointer-events:none}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId", "ioxPhrasing"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId", "ioxDragDisabled"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
@@ -11218,11 +11530,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
11218
11530
  </div>
11219
11531
 
11220
11532
  <div #editSwiperEl class="swiper swiper-edit" [attr.dir]="dir" [style.direction]="dir">
11533
+ <!-- CC3: the control PARTS (Pause, Previous, Next, Pagination) render inside .swiper, beside the
11534
+ wrapper (never in it — slider.md); selectable and styled on their own (R2). -->
11535
+ <ng-container *ngFor="let part of leadParts; trackBy: trackByNodeId">
11536
+ <ng-container [ioxRender]="part" (onClick)="childClick.emit($event)"
11537
+ (onMouseEnter)="childMouseEnter.emit($event)" (onMouseLeave)="childMouseLeave.emit()"></ng-container>
11538
+ </ng-container>
11221
11539
  <div class="swiper-wrapper">
11222
11540
 
11223
- <!-- NORMAL MODE: all children as draggable slides -->
11541
+ <!-- NORMAL MODE: all slides draggable -->
11224
11542
  <ng-container *ngIf="!source">
11225
- <div *ngFor="let child of children"
11543
+ <div *ngFor="let child of slides; trackBy: trackByNodeId"
11226
11544
  class="swiper-slide"
11227
11545
  [ngClass]="['iox-outer-' + (child.styleId ?? child.id)]"
11228
11546
  ioxDraggable
@@ -11240,22 +11558,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
11240
11558
  <!-- REPEAT MODE: template slide (item 0) + dimmed preview slides (items 1..N) -->
11241
11559
  <ng-container *ngIf="source">
11242
11560
  <!-- Template slide — draggable/editable, receives item[0] data -->
11243
- <div *ngIf="children.length"
11561
+ <div *ngIf="slides.length"
11244
11562
  class="swiper-slide"
11245
- [ngClass]="['iox-outer-' + (children[0].styleId ?? children[0].id)]"
11563
+ [ngClass]="['iox-outer-' + (slides[0].styleId ?? slides[0].id)]"
11564
+ [attr.data-iox-row-idx]="0"
11246
11565
  ioxDraggable
11247
- [ioxDragData]="children[0]"
11566
+ [ioxDragData]="slides[0]"
11248
11567
  [ioxDragSourceId]="dropListId">
11249
11568
  <ng-container
11250
- [ioxRender]="children[0]"
11569
+ [ioxRender]="slides[0]"
11251
11570
  (onClick)="childClick.emit($event)"
11252
11571
  (onMouseEnter)="childMouseEnter.emit($event)"
11253
11572
  (onMouseLeave)="childMouseLeave.emit()">
11254
11573
  </ng-container>
11255
11574
  </div>
11256
11575
  <!-- Preview slides — dimmed, non-interactive, data resolved from items 1..N -->
11257
- <div *ngFor="let slide of previewSlides; trackBy: trackByNodeId"
11576
+ <div *ngFor="let slide of previewSlides; let i = index; trackBy: trackByNodeId"
11258
11577
  class="swiper-slide slider-preview-slide"
11578
+ [attr.data-iox-row-idx]="i + 1"
11259
11579
  [ngClass]="['iox-outer-' + (slide.styleId ?? slide.id)]">
11260
11580
  <ng-container
11261
11581
  [ioxRender]="slide"
@@ -11267,23 +11587,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
11267
11587
  </ng-container>
11268
11588
 
11269
11589
  </div>
11590
+ <ng-container *ngFor="let part of trailParts; trackBy: trackByNodeId">
11591
+ <ng-container [ioxRender]="part" (onClick)="childClick.emit($event)"
11592
+ (onMouseEnter)="childMouseEnter.emit($event)" (onMouseLeave)="childMouseLeave.emit()"></ng-container>
11593
+ </ng-container>
11270
11594
 
11271
- <div *ngIf="!source && !children.length" class="slider-edit-placeholder" data-iox-chrome>
11595
+ <div *ngIf="!source && !slides.length" class="slider-edit-placeholder" data-iox-chrome>
11272
11596
  <i class="ph-thin ph-sliders-horizontal"></i>
11273
11597
  <span>Click + in toolbar to add a slide</span>
11274
11598
  </div>
11275
11599
 
11276
- <div *ngIf="enablePagination" class="swiper-pagination"></div>
11600
+ <div *ngIf="legacyControls && enablePagination" class="swiper-pagination"></div>
11277
11601
  <div *ngIf="enableScrollbar" class="swiper-scrollbar"></div>
11278
11602
  </div>
11279
11603
 
11280
- <!-- Nav buttons live on the shell, not inside .swiper, so overflow:hidden never clips them
11281
- and top:50%+transform always centers on the real slide height -->
11282
- <button *ngIf="enableNavigation" class="slider-edit-nav slider-edit-nav--prev" data-iox-chrome
11604
+ <!-- Legacy (Swiper-drawn) nav buttons live on the shell, not inside .swiper, so overflow:hidden never
11605
+ clips them and top:50%+transform always centers on the real slide height. A slider whose controls
11606
+ are PARTS (CC3) draws none: the parts are the controls. -->
11607
+ <button *ngIf="legacyControls && enableNavigation" class="slider-edit-nav slider-edit-nav--prev" data-iox-chrome
11283
11608
  (click)="_swiper?.slidePrev()" type="button" title="Previous slide">
11284
11609
  <i class="ph-thin ph-caret-left"></i>
11285
11610
  </button>
11286
- <button *ngIf="enableNavigation" class="slider-edit-nav slider-edit-nav--next" data-iox-chrome
11611
+ <button *ngIf="legacyControls && enableNavigation" class="slider-edit-nav slider-edit-nav--next" data-iox-chrome
11287
11612
  (click)="_swiper?.slideNext()" type="button" title="Next slide">
11288
11613
  <i class="ph-thin ph-caret-right"></i>
11289
11614
  </button>
@@ -11300,12 +11625,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
11300
11625
  [style.--swiper-navigation-color]="navigationColor || null"
11301
11626
  [style.--swiper-navigation-size.px]="navigationSize || null"
11302
11627
  [style.--swiper-pagination-color]="paginationColor || null">
11628
+ <ng-container *ngFor="let part of leadParts; trackBy: trackByNodeId">
11629
+ <ng-container [ioxRender]="part" (onClick)="childClick.emit($event)"
11630
+ (onMouseEnter)="childMouseEnter.emit($event)" (onMouseLeave)="childMouseLeave.emit()"></ng-container>
11631
+ </ng-container>
11303
11632
  <div class="swiper-wrapper">
11304
11633
  <!-- Swiper requires .swiper-slide as a DIRECT child of .swiper-wrapper.
11305
11634
  Wrap each rendered slide in one (same structure as edit mode) so the
11306
11635
  app-slider-slide host sits INSIDE the slide, not between it and the wrapper. -->
11307
11636
  <ng-container *ngIf="!source">
11308
- <div *ngFor="let child of children"
11637
+ <div *ngFor="let child of slides; trackBy: trackByNodeId"
11309
11638
  class="swiper-slide"
11310
11639
  [ngClass]="['iox-outer-' + (child.styleId ?? child.id)]">
11311
11640
  <ng-container
@@ -11316,20 +11645,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
11316
11645
  </ng-container>
11317
11646
  </div>
11318
11647
  </ng-container>
11319
- <!-- REPEAT: template slide + preview slides -->
11648
+ <!-- REPEAT: template slide + preview slides; each is a row (a lightbox opens at the clicked one) -->
11320
11649
  <ng-container *ngIf="source">
11321
- <div *ngIf="children.length"
11650
+ <div *ngIf="slides.length"
11322
11651
  class="swiper-slide"
11323
- [ngClass]="['iox-outer-' + (children[0].styleId ?? children[0].id)]">
11652
+ [attr.data-iox-row-idx]="0"
11653
+ [ngClass]="['iox-outer-' + (slides[0].styleId ?? slides[0].id)]">
11324
11654
  <ng-container
11325
- [ioxRender]="children[0]"
11655
+ [ioxRender]="slides[0]"
11326
11656
  (onClick)="childClick.emit($event)"
11327
11657
  (onMouseEnter)="childMouseEnter.emit($event)"
11328
11658
  (onMouseLeave)="childMouseLeave.emit()">
11329
11659
  </ng-container>
11330
11660
  </div>
11331
- <div *ngFor="let slide of previewSlides; trackBy: trackByNodeId"
11661
+ <div *ngFor="let slide of previewSlides; let i = index; trackBy: trackByNodeId"
11332
11662
  class="swiper-slide"
11663
+ [attr.data-iox-row-idx]="i + 1"
11333
11664
  [ngClass]="['iox-outer-' + (slide.styleId ?? slide.id)]">
11334
11665
  <ng-container
11335
11666
  [ioxRender]="slide"
@@ -11340,9 +11671,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
11340
11671
  </div>
11341
11672
  </ng-container>
11342
11673
  </div>
11343
- <div *ngIf="enableNavigation" class="swiper-button-prev"></div>
11344
- <div *ngIf="enableNavigation" class="swiper-button-next"></div>
11345
- <div *ngIf="enablePagination" class="swiper-pagination"></div>
11674
+ <ng-container *ngFor="let part of trailParts; trackBy: trackByNodeId">
11675
+ <ng-container [ioxRender]="part" (onClick)="childClick.emit($event)"
11676
+ (onMouseEnter)="childMouseEnter.emit($event)" (onMouseLeave)="childMouseLeave.emit()"></ng-container>
11677
+ </ng-container>
11678
+ <div *ngIf="legacyControls && enableNavigation" class="swiper-button-prev"></div>
11679
+ <div *ngIf="legacyControls && enableNavigation" class="swiper-button-next"></div>
11680
+ <div *ngIf="legacyControls && enablePagination" class="swiper-pagination"></div>
11346
11681
  <div *ngIf="enableScrollbar" class="swiper-scrollbar"></div>
11347
11682
  </div>
11348
11683
  `, standalone: false, styles: [":host{display:block}.swiper-wrapper{height:auto}.slider-edit-shell{position:relative;display:block}.swiper-edit{width:100%}.swiper-edit .swiper-slide{position:relative}.slider-edit-nav{position:absolute;top:50%;transform:translateY(-50%);width:32px;height:32px;border-radius:50%;border:1px solid rgba(255,255,255,.5);background:#00000038;color:#fff;font-size:16px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:10;transition:background .15s}.slider-edit-nav:hover{background:#00000073}.slider-edit-nav--prev{left:8px}.slider-edit-nav--next{right:8px}.slider-edit-placeholder{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:1.5px dashed rgba(100,116,139,.35);border-radius:4px;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.slider-edit-placeholder i{font-size:22px}.slider-repeat-badge{position:absolute;top:0;left:0;z-index:10;display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px solid rgba(203,144,144,.5);border-radius:0 0 4px;opacity:0;transition:opacity .15s;-webkit-user-select:none;user-select:none;pointer-events:none}.slider-edit-shell:hover .slider-repeat-badge{opacity:1}.slider-repeat-badge-count{font-weight:600}.slider-badge-spin{animation:slider-badge-spin 1s linear infinite}@keyframes slider-badge-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.slider-preview-slide{opacity:.55;pointer-events:none}\n"] }]
@@ -12501,6 +12836,7 @@ class DialogComponentConfig extends ComponentConfig {
12501
12836
  showCloseButton: 'checkbox',
12502
12837
  consentRole: 'checkbox',
12503
12838
  closeAbove: 'select',
12839
+ colorScheme: 'select',
12504
12840
  ariaLabel: 'text',
12505
12841
  };
12506
12842
  this.traits = [
@@ -12513,6 +12849,8 @@ class DialogComponentConfig extends ComponentConfig {
12513
12849
  new TraitConfig('consentRole', 'Consent banner role', TraitInputType.Checkbox, undefined, false),
12514
12850
  ...buildDialogCloseAboveTraits(),
12515
12851
  ...buildAccessibleNameTraits(),
12852
+ // CC3 (board 6): a dialog may wear a section colour scheme — the Lightbox is Inverse.
12853
+ buildColorSchemeTrait(),
12516
12854
  ];
12517
12855
  this.styleTraits = buildFullStyleTraits();
12518
12856
  this.applyStyleDefaults({
@@ -12707,6 +13045,64 @@ class GalleryConfig extends ComponentConfig {
12707
13045
  }
12708
13046
  }
12709
13047
 
13048
+ /**
13049
+ * Video (CC3 — complex-components plan §7b, board 5): a LEAF. The RENDER component is the shared `iox-video`
13050
+ * (`IoxVideoComponent` in @vectoriox/iox-ui) — the canvas shows its poster only; Preview and the live site run the
13051
+ * media-core runtime (the YouTube / Vimeo facade behind consent, the background video's pause button).
13052
+ *
13053
+ * Bindings ▸ Media · Behaviour · Accessibility · Privacy (boards 5 and 9). The address and the poster are bindable (a
13054
+ * project's video field) and work inside a Repeater. Background mode and autoplay act on an uploaded file; a YouTube
13055
+ * or Vimeo video is always the facade (D5) and plays on a click, after consent — the panel says so.
13056
+ */
13057
+ class VideoConfig extends ComponentConfig {
13058
+ constructor() {
13059
+ super('Video', 'iox-video', 'ph-thin ph-film-strip', 'Media');
13060
+ const embed = { trait: VideoTrait.Source, values: [VideoSource.YouTube, VideoSource.Vimeo] };
13061
+ const file = { trait: VideoTrait.Source, values: VideoSource.File };
13062
+ this.traits = [
13063
+ // ── Media ─────────────────────────────────────────────────────
13064
+ new TraitConfig('__mediaLabel', 'Media', TraitInputType.SectionLabel),
13065
+ new TraitConfig(VideoTrait.Source, 'Source', TraitInputType.Select, [
13066
+ { label: 'YouTube', value: VideoSource.YouTube },
13067
+ { label: 'Vimeo', value: VideoSource.Vimeo },
13068
+ { label: 'Uploaded file', value: VideoSource.File },
13069
+ ], VideoSource.YouTube),
13070
+ new TraitConfig(VideoTrait.Src, 'Video address', TraitInputType.Media, { mode: 'compact' }, ''),
13071
+ new TraitConfig(VideoTrait.Poster, 'Poster', TraitInputType.Media, { mode: 'compact' }, ''),
13072
+ // ── Behaviour ─────────────────────────────────────────────────
13073
+ new TraitConfig('__behaviourLabel', 'Behaviour', TraitInputType.SectionLabel),
13074
+ new TraitConfig(VideoTrait.Mode, 'Mode', TraitInputType.Select, [
13075
+ { label: 'Inline · with controls', value: VideoMode.Inline },
13076
+ { label: 'Background · muted loop', value: VideoMode.Background },
13077
+ ], VideoMode.Inline),
13078
+ new TraitConfig(VideoTrait.Autoplay, 'Autoplay (muted)', TraitInputType.Checkbox, undefined, false),
13079
+ // ── Accessibility ─────────────────────────────────────────────
13080
+ new TraitConfig('__accessibilityLabel', 'Accessibility', TraitInputType.SectionLabel),
13081
+ new TraitConfig(VideoTrait.Title, 'Title (names the play button)', TraitInputType.Text, undefined, ''),
13082
+ new TraitConfig(VideoTrait.Captions, 'Captions (.vtt)', TraitInputType.Media, { mode: 'compact' }, '', undefined, file),
13083
+ new TraitConfig(VideoTrait.CaptionsLang, 'Captions language', TraitInputType.Text, undefined, 'en', undefined, file),
13084
+ // ── Privacy ───────────────────────────────────────────────────
13085
+ new TraitConfig('__privacyLabel', 'Privacy', TraitInputType.SectionLabel),
13086
+ new TraitConfig(VideoTrait.Consent, 'Consent', TraitInputType.Select, [
13087
+ { label: 'Marketing', value: ConsentCategory.Marketing },
13088
+ ], ConsentCategory.Marketing, undefined, embed),
13089
+ ];
13090
+ // A media frame: size, spacing, border (radius), effects (the `media` style's transition and hover scale).
13091
+ this.styleTraits = [
13092
+ new PositionGroupStyleConfig(),
13093
+ new SizeGroupStyleConfig(),
13094
+ new SpacingGroupStyleConfig(),
13095
+ new BorderGroupStyleConfig(),
13096
+ new EffectsGroupStyleConfig(),
13097
+ ];
13098
+ // A 16 / 9 frame that fills its column — the size the poster and the player take.
13099
+ this.applyStyleDefaults({
13100
+ width: '100%',
13101
+ aspectRatio: '16 / 9',
13102
+ });
13103
+ }
13104
+ }
13105
+
12710
13106
  /**
12711
13107
  * Field's styleable sub-elements (board 7 — "the failure design"), for the Style panel's picker.
12712
13108
  * Class names come from the SHARED `PART_CLASS_NAMES` (`@vectoriox/iox-ui`) — the same map the
@@ -13329,6 +13725,11 @@ class SliderContainerConfig extends ComponentConfig {
13329
13725
  // ── Responsive ────────────────────────────────────────────────────
13330
13726
  new TraitConfig('__responsiveLabel', 'Responsive', TraitInputType.SectionLabel),
13331
13727
  new TraitConfig('breakpoints', 'Breakpoints (JSON)', TraitInputType.Textarea, undefined, ''),
13728
+ // ── Accessibility (CC3, board 6) ──────────────────────────────────
13729
+ // The carousel's name: a named carousel is a `region` landmark ("What clients say"). The pause control
13730
+ // is a part added while Autoplay is on (WCAG 2.2.2) — the panel says so under this group.
13731
+ new TraitConfig('__accessibilityLabel', 'Accessibility', TraitInputType.SectionLabel),
13732
+ new TraitConfig(A11yTrait.AccessibleName, 'Carousel name', TraitInputType.Text, undefined, ''),
13332
13733
  ];
13333
13734
  this.styleTraits = [
13334
13735
  new PositionGroupStyleConfig(),
@@ -13362,5 +13763,5 @@ class BuilderSpacerComponentConfig extends ComponentConfig {
13362
13763
  * Generated bundle index. Do not edit.
13363
13764
  */
13364
13765
 
13365
- 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, BuilderRepeaterComponent, BuilderSpacerComponentConfig, ButtonBlockComponentConfig, CANVAS_EMPTY_IMAGE_LABEL_EN, CANVAS_EMPTY_IMAGE_LABEL_KEY, CANVAS_ROOT_RESET_CSS, 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$1 as EASING_OPTIONS, EDITOR_CHROME_ATTRIBUTE, ESSENTIAL_TRAITS_BY_TYPE, EffectsGroupStyleConfig, FIELD_KIND_PRESETS, FIELD_PARTS, FORM_STATE_BLOCK_TYPES, FORM_TYPE, FieldComponentConfig, FieldGroupComponentConfig, 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, PALETTE_PRESET_DEFS, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, PositionGroupStyleConfig, PresetRegistryService, RELATIONAL_STATES, RELATIONAL_STATE_BASE, REPEATER_SOURCE_PROP, REPEATER_TYPE, ROOT_ESCAPE_PX, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, RootZone, SCREEN_WIDTH_OPTIONS, STRUCTURAL_STATES, SUPPORTED_STATES, SectionComponent, SectionComponentConfig, SelectionReveal, SizeGroupStyleConfig, SliderContainerComponent, SliderContainerConfig, SliderSlideComponent, SliderSlideConfig, SpacingGroupStyleConfig, StructureFix, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, SubmitButtonComponentConfig, SymbolRegistryService, TARGETED_STATES, TARGETED_STATE_BASE, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, TypographyGroupStyleConfig, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, VALIDATION_STATES, VALIDATION_STATE_CLASS, ViewportService, ZOOM_OPTIONS, arrayItemSchemaFields, arraySchemaFields, auditNodes, bindingLabel, bindingLabels, buildCompositePreset, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, builderCompositeFactory, canFluidise, canvasEmptyImageLabel, collapseColumnsProps, columnCountOf, defaultBuilderLabels, defaultPageSettings, dropChildren, dropTargetLabel, escapesRoot, essentialTraitsFor, findNodeLocation, findParentOf, flattenLayerTree, flattenSchemaFields, fluidiseProps, generateNodeId, globalCanvasLayer, insertRefusal, insertRefusalLabel, isHiddenAt, isHiddenAtRest, isPinnedGlobal, isSpecimen, markSpecimen, matchesControlSearch, nearestFormNode, nodeAncestorPath, nodeAncestorPathInEither, nodeDescriptor, nodeStyleMap, overlayGlobals, pathToNode, refreshSubtree, regenerateNodeIds, removalRefusal, reorderProps, repeaterSourceAlias, repeaterSubLabel, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, retypeNode, rootEscapeBand, rootZone, selectionRevealOf, setStyleTrait, setTrait, shortDescriptor, sourceContentType, sourceTypeLabel, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, syncNodeInputs, textNode, upsertNodeStyles, visibilityProps, walkNodes, wrapNode };
13766
+ 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, BuilderRepeaterComponent, BuilderSpacerComponentConfig, ButtonBlockComponentConfig, CANVAS_EMPTY_IMAGE_LABEL_EN, CANVAS_EMPTY_IMAGE_LABEL_KEY, CANVAS_ROOT_RESET_CSS, 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$1 as EASING_OPTIONS, EDITOR_CHROME_ATTRIBUTE, ESSENTIAL_TRAITS_BY_TYPE, EffectsGroupStyleConfig, FIELD_KIND_PRESETS, FIELD_PARTS, FORM_STATE_BLOCK_TYPES, FORM_TYPE, FieldComponentConfig, FieldGroupComponentConfig, 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, PALETTE_PRESET_DEFS, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, 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, SILENT_REFUSALS, STRUCTURAL_STATES, SUPPORTED_STATES, 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, arrayItemSchemaFields, arraySchemaFields, auditNodes, bindingLabel, bindingLabels, buildCompositePreset, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, builderCompositeFactory, canFluidise, canvasEmptyImageLabel, collapseColumnsProps, columnCountOf, defaultBuilderLabels, defaultPageSettings, deleteTabPair, dropChildren, dropTargetLabel, duplicateTabPair, escapesRoot, essentialTraitsFor, findNodeLocation, findParentOf, flattenLayerTree, flattenSchemaFields, fluidiseProps, generateNodeId, globalCanvasLayer, insertRefusal, insertRefusalLabel, isHiddenAt, isHiddenAtRest, isPinnedGlobal, isSpecimen, markSpecimen, matchesControlSearch, nearestFormNode, nodeAncestorPath, nodeAncestorPathInEither, nodeDescriptor, nodeStyleMap, overlayGlobals, pathToNode, refreshSubtree, regenerateNodeIds, removalRefusal, reorderProps, repeaterSourceAlias, repeaterSubLabel, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, retypeNode, rootEscapeBand, rootZone, selectionRevealOf, setStyleTrait, setTrait, shortDescriptor, sourceContentType, sourceTypeLabel, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, syncNodeInputs, tabPairLabel, tabsRootForPart, textNode, upsertNodeStyles, visibilityProps, walkNodes, wrapNode };
13366
13767
  //# sourceMappingURL=vectoriox-iox-builder.mjs.map