@vectoriox/iox-builder 9.10.0 → 9.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/vectoriox-iox-builder.mjs +403 -117
- package/fesm2022/vectoriox-iox-builder.mjs.map +1 -1
- package/index.d.ts +96 -20
- package/package.json +2 -2
|
@@ -7,18 +7,19 @@ 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
|
|
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
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, isSliderPart, structureRefusal, AccordionRole, CompositeBehaviourTrait, ACCORDION_DEFAULT_ONS,
|
|
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';
|
|
@@ -28,9 +29,110 @@ import { itemActionOf, BEHAVIOUR_ACTION_TYPES, SERVER_ELEMENT_ACTION_TYPES, CONS
|
|
|
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';
|
|
31
33
|
import { planVideo, VideoSource, VIDEO_PROVIDER_LABEL, VideoTrait, VideoMode } from '@vectoriox/iox-ui/media-core';
|
|
32
34
|
import { ConsentCategory } from '@vectoriox/iox-ui/consent-core';
|
|
33
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
|
+
}
|
|
135
|
+
|
|
34
136
|
var PanelEventTypes;
|
|
35
137
|
(function (PanelEventTypes) {
|
|
36
138
|
PanelEventTypes["ELEMENT_SELECT"] = "elementselect";
|
|
@@ -226,6 +328,16 @@ const BUILDER_CHROME_EN = Object.freeze({
|
|
|
226
328
|
'builder.chrome.refuseLockedItemPartMove': 'This part belongs to its item — it can’t be moved out.',
|
|
227
329
|
'builder.chrome.refuseLastItem': 'An accordion keeps at least one item. Delete the whole accordion instead.',
|
|
228
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',
|
|
229
341
|
'builder.chrome.fromContent': 'From {source} · {count} rows',
|
|
230
342
|
'builder.chrome.emptySource': 'No rows in “{alias}”.',
|
|
231
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.',
|
|
@@ -482,6 +594,17 @@ var InsertRefusal;
|
|
|
482
594
|
InsertRefusal["LockedSliderPartMove"] = "lockedSliderPartMove";
|
|
483
595
|
/** The pause control stays while Autoplay is on (WCAG 2.2.2) — the Autoplay switch removes it. */
|
|
484
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";
|
|
485
608
|
})(InsertRefusal || (InsertRefusal = {}));
|
|
486
609
|
const FORM_STATE_BLOCK_TYPES = new Set(['FormSuccess', 'FormError']);
|
|
487
610
|
/** The builder-chrome label that says each refusal on screen (English in `BUILDER_CHROME_EN`). */
|
|
@@ -498,7 +621,14 @@ const INSERT_REFUSAL_LABEL_KEYS = Object.freeze({
|
|
|
498
621
|
[InsertRefusal.LockedDisclosurePart]: 'builder.chrome.refuseLockedDisclosurePart',
|
|
499
622
|
[InsertRefusal.LockedSliderPartMove]: 'builder.chrome.refuseLockedSliderPartMove',
|
|
500
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',
|
|
501
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]);
|
|
502
632
|
/** Which refusal says a locked part's move / delete, by the composite it belongs to (a card's is the default). */
|
|
503
633
|
function lockedRefusal(node, edit) {
|
|
504
634
|
const move = edit === StructureEdit.Move;
|
|
@@ -508,6 +638,8 @@ function lockedRefusal(node, edit) {
|
|
|
508
638
|
return move ? InsertRefusal.LockedDisclosurePartMove : InsertRefusal.LockedDisclosurePart;
|
|
509
639
|
if (isSliderPart(node))
|
|
510
640
|
return move ? InsertRefusal.LockedSliderPartMove : InsertRefusal.LockedSliderPause;
|
|
641
|
+
if (isTabsPart(node))
|
|
642
|
+
return move ? InsertRefusal.LockedTabsPartMove : InsertRefusal.LockedTabsPart;
|
|
511
643
|
return move ? InsertRefusal.LockedPartMove : InsertRefusal.LockedPart;
|
|
512
644
|
}
|
|
513
645
|
/** The refusal in the host's language (the library's English when no host resolver is provided). */
|
|
@@ -535,18 +667,28 @@ function insertRefusal(node, ancestors) {
|
|
|
535
667
|
return InsertRefusal.NestedForm;
|
|
536
668
|
if (formContentInHost(node, ancestors.some(isInteractiveHost)))
|
|
537
669
|
return InsertRefusal.FormInInteractive;
|
|
538
|
-
|
|
670
|
+
const structural = structureRefusal(node, StructureEdit.Move, ancestors);
|
|
671
|
+
if (structural === StructureRefusal.NotAccepted)
|
|
672
|
+
return InsertRefusal.TabsAccepts;
|
|
673
|
+
if (structural)
|
|
539
674
|
return lockedRefusal(node, StructureEdit.Move);
|
|
540
675
|
return null;
|
|
541
676
|
}
|
|
542
677
|
/** Why `node` may not be REMOVED from where it is (deleted, cut, made global), or null when it may: a
|
|
543
|
-
* composite's locked part stays — its Parts switch or deleting the whole composite are the ways out —
|
|
544
|
-
* accordion keeps its last item
|
|
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). */
|
|
545
681
|
function removalRefusal(node, siblings) {
|
|
546
682
|
if (structureRefusal(node, StructureEdit.Delete))
|
|
547
683
|
return lockedRefusal(node, StructureEdit.Delete);
|
|
548
684
|
if (node?.role === AccordionRole.Item && siblings && siblings.filter(s => s?.role === AccordionRole.Item).length <= 1)
|
|
549
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
|
+
}
|
|
550
692
|
return null;
|
|
551
693
|
}
|
|
552
694
|
|
|
@@ -1247,13 +1389,17 @@ function buildColorSchemeTrait() {
|
|
|
1247
1389
|
}
|
|
1248
1390
|
/**
|
|
1249
1391
|
* A composite ROOT's behaviour (composite plan §3 — ordinary traits on the root; CC2's Accordion: Allow
|
|
1250
|
-
* several open, Open by default
|
|
1251
|
-
* composite root's recipe writes them. Edited in the
|
|
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.
|
|
1252
1395
|
*/
|
|
1253
1396
|
function buildCompositeBehaviourTraits() {
|
|
1254
1397
|
return [
|
|
1255
1398
|
new TraitConfig(CompositeBehaviourTrait.Multiple, 'Allow several open', TraitInputType.Checkbox),
|
|
1256
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),
|
|
1257
1403
|
];
|
|
1258
1404
|
}
|
|
1259
1405
|
const LINK_TARGETS = [
|
|
@@ -2363,6 +2509,8 @@ class DragEngineService {
|
|
|
2363
2509
|
this.overlayService = overlayService;
|
|
2364
2510
|
/** Every insert the builder refused, for the host to say on screen (a toast). */
|
|
2365
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();
|
|
2366
2514
|
// ── Drop-zone registry ────────────────────────────────────
|
|
2367
2515
|
this.ids = [];
|
|
2368
2516
|
this.elMap = new Map();
|
|
@@ -2455,7 +2603,9 @@ class DragEngineService {
|
|
|
2455
2603
|
*/
|
|
2456
2604
|
refuseRemoval(node, siblings) {
|
|
2457
2605
|
const reason = removalRefusal(node, siblings);
|
|
2458
|
-
if (reason)
|
|
2606
|
+
if (reason === InsertRefusal.PairedPart)
|
|
2607
|
+
this.pairRemoval$.next(node);
|
|
2608
|
+
else if (reason && !SILENT_REFUSALS.has(reason))
|
|
2459
2609
|
this.insertRefused$.next({ reason, message: insertRefusalLabel(reason, this.labels) });
|
|
2460
2610
|
return reason;
|
|
2461
2611
|
}
|
|
@@ -3618,8 +3768,8 @@ class InteractionEngineService {
|
|
|
3618
3768
|
*/
|
|
3619
3769
|
attachItemState(node, ix, element, on) {
|
|
3620
3770
|
const run = () => this.runActions(node, ix.actions, element);
|
|
3621
|
-
const off =
|
|
3622
|
-
const timer = on &&
|
|
3771
|
+
const off = onCompositeItemState(element, on, run);
|
|
3772
|
+
const timer = on && isCompositeItemOn(element) ? setTimeout(run, 50) : null;
|
|
3623
3773
|
return () => { off(); if (timer)
|
|
3624
3774
|
clearTimeout(timer); };
|
|
3625
3775
|
}
|
|
@@ -4098,8 +4248,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
4098
4248
|
* screen until something opens them, so on the canvas they show only while selected into or switched
|
|
4099
4249
|
* open ("Showing on canvas"). The layer tree marks them with the eye-slash glyph. A Dialog (the mobile
|
|
4100
4250
|
* menu's drawer, a consent banner) by its type; an Accordion's PANEL by its role (CC2 — closed until its
|
|
4101
|
-
* item opens); a Disclosure's panel and its "Read less" word (CC2.1 — shown only while it is open).
|
|
4102
|
-
* tab joins in CC4.
|
|
4251
|
+
* item opens); a Disclosure's panel and its "Read less" word (CC2.1 — shown only while it is open).
|
|
4103
4252
|
*/
|
|
4104
4253
|
const HIDDEN_AT_REST_TYPES = ['Dialog'];
|
|
4105
4254
|
const HIDDEN_AT_REST_ROLES = [AccordionRole.Panel, DisclosureRole.Panel, DisclosureRole.Less];
|
|
@@ -4124,7 +4273,14 @@ var SelectionReveal;
|
|
|
4124
4273
|
* closes the disclosure it was clicked in. Only a part inside a PANEL is revealed by the selection.
|
|
4125
4274
|
*/
|
|
4126
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];
|
|
4127
4281
|
function selectionRevealOf(node) {
|
|
4282
|
+
if (!!node?.role && REVEALED_WHILE_SELECTED_ROLES.includes(node.role))
|
|
4283
|
+
return SelectionReveal.WhileSelected;
|
|
4128
4284
|
if (!isHiddenAtRest(node) || (!!node?.role && TOGGLE_PART_ROLES.includes(node.role)))
|
|
4129
4285
|
return null;
|
|
4130
4286
|
return node?.role ? SelectionReveal.WhileSelected : SelectionReveal.UntilClosed;
|
|
@@ -4163,7 +4319,7 @@ var CanvasShowState;
|
|
|
4163
4319
|
*/
|
|
4164
4320
|
class CanvasDesignStateService {
|
|
4165
4321
|
constructor() {
|
|
4166
|
-
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() });
|
|
4167
4323
|
this.state$ = this._state$.asObservable();
|
|
4168
4324
|
this._pagePath$ = new BehaviorSubject(null);
|
|
4169
4325
|
/** The app path of the page on the canvas (`/work`), or null when unknown. */
|
|
@@ -4187,11 +4343,12 @@ class CanvasDesignStateService {
|
|
|
4187
4343
|
setSelectionPath(path) {
|
|
4188
4344
|
const ids = (reveal) => path.filter(n => selectionRevealOf(n) === reveal).map(n => n.id).filter((id) => !!id);
|
|
4189
4345
|
const revealed = new Set(ids(SelectionReveal.WhileSelected));
|
|
4190
|
-
const
|
|
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;
|
|
4191
4348
|
const pinned = new Set([...held, ...ids(SelectionReveal.UntilClosed)]);
|
|
4192
|
-
if (sameSet(revealed, was) && sameSet(pinned, held))
|
|
4349
|
+
if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers))
|
|
4193
4350
|
return;
|
|
4194
|
-
this._state$.next({ ...this._state$.value, revealed, pinned });
|
|
4351
|
+
this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers });
|
|
4195
4352
|
}
|
|
4196
4353
|
/** Closed on the canvas (its X, its backdrop, the eye): no longer pinned or switched open. */
|
|
4197
4354
|
dismiss(nodeId) {
|
|
@@ -4215,6 +4372,21 @@ class CanvasDesignStateService {
|
|
|
4215
4372
|
shown.delete(nodeId);
|
|
4216
4373
|
this._state$.next({ ...this._state$.value, shown });
|
|
4217
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
|
+
}
|
|
4218
4390
|
/** The switcher's current choice for one part. */
|
|
4219
4391
|
shownState(nodeId) {
|
|
4220
4392
|
return (nodeId && this._state$.value.shown.get(nodeId)) || CanvasShowState.Closed;
|
|
@@ -4232,7 +4404,7 @@ class CanvasDesignStateService {
|
|
|
4232
4404
|
}
|
|
4233
4405
|
/** A new page on the canvas: nothing is revealed or switched open. */
|
|
4234
4406
|
reset() {
|
|
4235
|
-
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() });
|
|
4236
4408
|
}
|
|
4237
4409
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4238
4410
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService }); }
|
|
@@ -4250,14 +4422,15 @@ function designIdOf(node) {
|
|
|
4250
4422
|
return node.__previewClone ? (node.styleId ?? node.id) : node.id;
|
|
4251
4423
|
}
|
|
4252
4424
|
/**
|
|
4253
|
-
* The composites'
|
|
4254
|
-
* CC2.1) opens and closes on the canvas exactly as on the site: the SAME composite-core
|
|
4255
|
-
* engine attaches. A composite repeated in a Repeater's preview rows runs
|
|
4256
|
-
* (its row folded into the scope id — vectoriox/iox-ui#20). On top of the live
|
|
4257
|
-
* designing (R2, CC1's design state):
|
|
4258
|
-
* - selecting anything inside a CLOSED panel (canvas, tree, crumb, an Items row, SELECT_NODE)
|
|
4259
|
-
* while it is selected, and deselecting
|
|
4260
|
-
* -
|
|
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).
|
|
4261
4434
|
* Canvas-only: it never writes a node, so the saved page is unchanged and nothing becomes an undo step.
|
|
4262
4435
|
*/
|
|
4263
4436
|
class CompositeCanvasService {
|
|
@@ -4265,11 +4438,12 @@ class CompositeCanvasService {
|
|
|
4265
4438
|
this.designState = designState;
|
|
4266
4439
|
this.zone = zone;
|
|
4267
4440
|
this.attached = new Map();
|
|
4441
|
+
this.exclusive = new Map();
|
|
4268
4442
|
this.sub = designState?.state$.subscribe(() => this.applyDesignState()) ?? null;
|
|
4269
4443
|
}
|
|
4270
4444
|
ngOnDestroy() {
|
|
4271
4445
|
this.sub?.unsubscribe();
|
|
4272
|
-
for (const id of [...this.attached.keys()])
|
|
4446
|
+
for (const id of [...this.attached.keys(), ...this.exclusive.keys()])
|
|
4273
4447
|
this.detach(id);
|
|
4274
4448
|
}
|
|
4275
4449
|
/** A composite root rendered on the canvas (the template, or a preview row's copy): run it. `element` is the
|
|
@@ -4279,9 +4453,28 @@ class CompositeCanvasService {
|
|
|
4279
4453
|
return;
|
|
4280
4454
|
this.detach(node.id);
|
|
4281
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);
|
|
4282
4475
|
const runtime = this.zone.runOutsideAngular(() => new AccordionRuntime(element, {
|
|
4283
|
-
kind:
|
|
4284
|
-
rootId
|
|
4476
|
+
kind: isToggleKind(kind) ? kind : undefined,
|
|
4477
|
+
rootId,
|
|
4285
4478
|
behaviour: accordionBehaviour(node),
|
|
4286
4479
|
observe: true,
|
|
4287
4480
|
forced: () => this.forcedFor(node, element),
|
|
@@ -4293,16 +4486,24 @@ class CompositeCanvasService {
|
|
|
4293
4486
|
return;
|
|
4294
4487
|
this.attached.get(nodeId)?.runtime.destroy();
|
|
4295
4488
|
this.attached.delete(nodeId);
|
|
4489
|
+
this.exclusive.get(nodeId)?.runtime.destroy();
|
|
4490
|
+
this.exclusive.delete(nodeId);
|
|
4296
4491
|
}
|
|
4297
|
-
/** 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). */
|
|
4298
4493
|
refreshBehaviour(node) {
|
|
4299
4494
|
const entry = node.id ? this.attached.get(node.id) : undefined;
|
|
4300
4495
|
entry?.runtime.setBehaviour(accordionBehaviour(node));
|
|
4496
|
+
const tabs = node.id ? this.exclusive.get(node.id) : undefined;
|
|
4497
|
+
tabs?.runtime.setBehaviour(tabsBehaviour(node));
|
|
4301
4498
|
}
|
|
4302
|
-
/** 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). */
|
|
4303
4500
|
runtimeOf(nodeId) {
|
|
4304
4501
|
return (nodeId && this.attached.get(nodeId)?.runtime) || null;
|
|
4305
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
|
+
}
|
|
4306
4507
|
applyDesignState() {
|
|
4307
4508
|
for (const entry of this.attached.values()) {
|
|
4308
4509
|
const shown = this.designState?.shownState(designIdOf(entry.node)) ?? CanvasShowState.Closed;
|
|
@@ -4313,6 +4514,26 @@ class CompositeCanvasService {
|
|
|
4313
4514
|
entry.runtime.refresh();
|
|
4314
4515
|
entry.shown = shown;
|
|
4315
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();
|
|
4316
4537
|
}
|
|
4317
4538
|
/** The keys the design state holds open: every item ("All open"), or the items whose hidden part the
|
|
4318
4539
|
* selection sits in. A preview row shares its template's classes, so the template (row 1) answers — and a
|
|
@@ -4336,6 +4557,19 @@ class CompositeCanvasService {
|
|
|
4336
4557
|
}
|
|
4337
4558
|
return forced;
|
|
4338
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
|
+
}
|
|
4339
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 }); }
|
|
4340
4574
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CompositeCanvasService }); }
|
|
4341
4575
|
}
|
|
@@ -4848,7 +5082,7 @@ class RenderDirective {
|
|
|
4848
5082
|
// A composite root runs its behaviour on the canvas — the live site's runtime (CC2; the Disclosure, CC2.1).
|
|
4849
5083
|
// A preview row's copy runs too, scoped by its row (vectoriox/iox-ui#20): its ids are its own and it shows
|
|
4850
5084
|
// the saved default, as every row does on the live site.
|
|
4851
|
-
if (nativeEl && node.id &&
|
|
5085
|
+
if (nativeEl && node.id && isAnyCompositeRoot(node) && this.compositeCanvas) {
|
|
4852
5086
|
this.compositeCanvas.attach(node, nativeEl);
|
|
4853
5087
|
this.compositeRootId = node.id;
|
|
4854
5088
|
}
|
|
@@ -4988,7 +5222,8 @@ class BuilderRepeaterComponent {
|
|
|
4988
5222
|
}
|
|
4989
5223
|
/** A composite whose source answered with no rows: the placeholder takes the template's place. */
|
|
4990
5224
|
get showsEmptySource() {
|
|
4991
|
-
|
|
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;
|
|
4992
5227
|
}
|
|
4993
5228
|
get emptySourceTitle() {
|
|
4994
5229
|
return this.label(EMPTY_SOURCE_KEY).replace('{alias}', this._source);
|
|
@@ -5276,7 +5511,7 @@ class BuilderRepeaterComponent {
|
|
|
5276
5511
|
</ng-container>
|
|
5277
5512
|
</div>
|
|
5278
5513
|
</div>
|
|
5279
|
-
`, 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"] }] }); }
|
|
5280
5515
|
}
|
|
5281
5516
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderRepeaterComponent, decorators: [{
|
|
5282
5517
|
type: Component,
|
|
@@ -5370,7 +5605,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5370
5605
|
</ng-container>
|
|
5371
5606
|
</div>
|
|
5372
5607
|
</div>
|
|
5373
|
-
`, 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"] }]
|
|
5374
5609
|
}], ctorParameters: () => [{ type: DragEngineService }, { type: OverlayService }, { type: BuilderDataSourceService }, { type: DataSourceRegistryService }, { type: PanelEventService }, { type: i0.ChangeDetectorRef }, { type: InteractionEngineService }, { type: undefined, decorators: [{
|
|
5375
5610
|
type: Optional
|
|
5376
5611
|
}, {
|
|
@@ -5415,10 +5650,11 @@ var CompositePresetId;
|
|
|
5415
5650
|
CompositePresetId["Chip"] = "chip";
|
|
5416
5651
|
CompositePresetId["Accordion"] = "accordion";
|
|
5417
5652
|
CompositePresetId["Disclosure"] = "disclosure";
|
|
5653
|
+
CompositePresetId["Tabs"] = "tabs";
|
|
5418
5654
|
CompositePresetId["Lightbox"] = "lightbox";
|
|
5419
5655
|
})(CompositePresetId || (CompositePresetId = {}));
|
|
5420
5656
|
/** Where a composite sits in the Components panel (composite plan §6d): content blocks, or the interactive
|
|
5421
|
-
* composites (the Accordion — CC2, the Disclosure — CC2.1
|
|
5657
|
+
* composites (the Accordion — CC2, the Disclosure — CC2.1, Tabs — CC4a; the floating family later). */
|
|
5422
5658
|
var CompositePaletteGroup;
|
|
5423
5659
|
(function (CompositePaletteGroup) {
|
|
5424
5660
|
CompositePaletteGroup["ContentBlocks"] = "content-blocks";
|
|
@@ -5450,6 +5686,11 @@ const COMPOSITE_PRESETS = Object.freeze([
|
|
|
5450
5686
|
description: '"Read more" — a button that reveals the text below it and reads "Read less" while it is open.',
|
|
5451
5687
|
build: create => buildDisclosureRecipe(create),
|
|
5452
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
|
+
},
|
|
5453
5694
|
{
|
|
5454
5695
|
id: CompositePresetId.Lightbox, group: CompositePaletteGroup.Interactive, name: 'Lightbox', icon: 'ph-thin ph-frame-corners',
|
|
5455
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.',
|
|
@@ -5596,57 +5837,6 @@ function overlayGlobals(globals) {
|
|
|
5596
5837
|
return { before, pinnedAfter };
|
|
5597
5838
|
}
|
|
5598
5839
|
|
|
5599
|
-
/**
|
|
5600
|
-
* node-path — pure ancestor-chain helpers for the builder's node tree.
|
|
5601
|
-
*
|
|
5602
|
-
* The builder needs a node's ancestor chain in several places: the breadcrumb bar,
|
|
5603
|
-
* "select parent", and the parent-scoped style traits (Child Layout). Each of those
|
|
5604
|
-
* used to re-walk the tree with its own private recursion. `nodeAncestorPath` is the
|
|
5605
|
-
* single walk they all derive from.
|
|
5606
|
-
*
|
|
5607
|
-
* Builder-only on purpose: the SSR engine never needs a breadcrumb, so this stays in
|
|
5608
|
-
* `iox-builder` rather than `@vectoriox/iox-ui` (which is the home for logic BOTH
|
|
5609
|
-
* renderers consume). Structural helpers the two renderers share live in
|
|
5610
|
-
* `@vectoriox/iox-ui` `style-compiler/tree-relations.ts`.
|
|
5611
|
-
*/
|
|
5612
|
-
/**
|
|
5613
|
-
* The chain of nodes from a root down to (and including) `target`, matched by
|
|
5614
|
-
* REFERENCE identity. Returns `null` when `target` is not in the tree.
|
|
5615
|
-
*
|
|
5616
|
-
* Depth-first, pre-order; first match wins. Reference identity (not `id`) is the right
|
|
5617
|
-
* matcher here because the builder mutates nodes in place and repeater/slider clones
|
|
5618
|
-
* deliberately share a template's `styleId` — matching by id would return the template's
|
|
5619
|
-
* path for a clone.
|
|
5620
|
-
*/
|
|
5621
|
-
function nodeAncestorPath(roots, target) {
|
|
5622
|
-
if (!roots?.length || !target)
|
|
5623
|
-
return null;
|
|
5624
|
-
for (const node of roots) {
|
|
5625
|
-
if (node === target)
|
|
5626
|
-
return [node];
|
|
5627
|
-
const childPath = nodeAncestorPath(node.children, target);
|
|
5628
|
-
if (childPath)
|
|
5629
|
-
return [node, ...childPath];
|
|
5630
|
-
}
|
|
5631
|
-
return null;
|
|
5632
|
-
}
|
|
5633
|
-
/**
|
|
5634
|
-
* The immediate parent of `target`, or `null` when it is a root node or not in the tree.
|
|
5635
|
-
* Derived from `nodeAncestorPath` so there is exactly one tree walk to keep correct.
|
|
5636
|
-
*/
|
|
5637
|
-
function findParentOf(roots, target) {
|
|
5638
|
-
const path = nodeAncestorPath(roots, target);
|
|
5639
|
-
return path && path.length >= 2 ? path[path.length - 2] : null;
|
|
5640
|
-
}
|
|
5641
|
-
/**
|
|
5642
|
-
* The ancestor chain across BOTH builder trees — the page layout and the global
|
|
5643
|
-
* elements overlay, which are separate roots. Returns the first tree that contains
|
|
5644
|
-
* the node, or `null` when neither does.
|
|
5645
|
-
*/
|
|
5646
|
-
function nodeAncestorPathInEither(layout, globalElements, target) {
|
|
5647
|
-
return nodeAncestorPath(layout, target) ?? nodeAncestorPath(globalElements, target);
|
|
5648
|
-
}
|
|
5649
|
-
|
|
5650
5840
|
/** Counts for ONE node (the selection). Breakpoints with nothing overridden are omitted. */
|
|
5651
5841
|
function nodeBreakpointCounts(node) {
|
|
5652
5842
|
const counts = {};
|
|
@@ -6908,10 +7098,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
6908
7098
|
* An accordion's and a disclosure's parts are named by the part alone (`trigger`, "Accordion · trigger", board 3);
|
|
6909
7099
|
* a card keeps its full role (`card.title`, "Card · card.title", board 8).
|
|
6910
7100
|
*/
|
|
6911
|
-
const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure]);
|
|
7101
|
+
const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure, CompositeKind.Tabs]);
|
|
6912
7102
|
/** A composite kind's glyph (Phosphor, weight-less): the badge, the root's crumb and its tree row carry it
|
|
6913
7103
|
* (board 3: "the composite root carrying its kind icon"). A card keeps the generic stack glyph. */
|
|
6914
|
-
const KIND_GLYPHS = { [CompositeKind.Accordion]: 'ph-rows', [CompositeKind.Disclosure]: 'ph-caret-circle-down', slider: 'ph-slideshow' };
|
|
7104
|
+
const KIND_GLYPHS = { [CompositeKind.Accordion]: 'ph-rows', [CompositeKind.Disclosure]: 'ph-caret-circle-down', [CompositeKind.Tabs]: 'ph-tabs', slider: 'ph-slideshow' };
|
|
6915
7105
|
const DEFAULT_KIND_GLYPH = 'ph-stack';
|
|
6916
7106
|
/** The tree icon of an accordion's structural parts (board 3): rows on the root, a stack on an item and a
|
|
6917
7107
|
* panel, a cursor on the header, a sparkle on the icon. Other parts keep their type's icon. */
|
|
@@ -6927,6 +7117,12 @@ const PART_ICONS = {
|
|
|
6927
7117
|
[DisclosureRole.Trigger]: 'ph-thin ph-cursor',
|
|
6928
7118
|
[DisclosureRole.Panel]: 'ph-thin ph-stack',
|
|
6929
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',
|
|
6930
7126
|
// CC3 — a Slider's controls (board 6): a cursor on the buttons, six dots on the pagination.
|
|
6931
7127
|
[SliderRole.Pause]: 'ph-thin ph-cursor',
|
|
6932
7128
|
[SliderRole.Prev]: 'ph-thin ph-cursor',
|
|
@@ -6954,11 +7150,20 @@ function roleBadgeLabel(role, kindLabel) {
|
|
|
6954
7150
|
return part ? `${kindLabel} · ${part}` : kindLabel;
|
|
6955
7151
|
}
|
|
6956
7152
|
/** What a part's row and crumb say after its name: an accordion item names its question ("Item · Do you take
|
|
6957
|
-
* projects…", board 3),
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
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;
|
|
6962
7167
|
}
|
|
6963
7168
|
|
|
6964
7169
|
/**
|
|
@@ -7016,13 +7221,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
7016
7221
|
const FROM_ROW_KEY = 'builder.chrome.fromRow';
|
|
7017
7222
|
const ADD_ITEM_KEY = 'builder.chrome.addItem';
|
|
7018
7223
|
/** The kind label a composite part's selection badge shows (Card v2: "Card · card.title"). */
|
|
7019
|
-
const COMPOSITE_KIND_KEYS = { card: 'builder.chrome.kindCard', accordion: 'builder.chrome.kindAccordion', disclosure: 'builder.chrome.kindDisclosure', slider: 'builder.chrome.kindSlider' };
|
|
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' };
|
|
7020
7225
|
const VIDEO_TYPE = 'Video';
|
|
7021
7226
|
const VIDEO_KIND_KEY = 'builder.chrome.kindVideo';
|
|
7022
7227
|
const VIDEO_POSTER_ONLY_KEY = 'builder.chrome.videoPosterOnly';
|
|
7023
7228
|
const VIDEO_FILE_KEY = 'builder.chrome.videoFile';
|
|
7024
7229
|
/** The parts that offer "Add item" on the selection toolbar (board 3: the "Add slide" pattern). */
|
|
7025
|
-
const ADD_ITEM_ROLES = new Set([
|
|
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';
|
|
7026
7236
|
class OverlayComponent {
|
|
7027
7237
|
constructor(elRef, instanceRegistry, overlayService, viewportService, presetRegistry, symbolRegistry, interactionPresetRegistry, interactionEngine, clipboard, styleRegistry, panelEventService, cdr, dragEngine, labels) {
|
|
7028
7238
|
this.elRef = elRef;
|
|
@@ -7268,12 +7478,13 @@ class OverlayComponent {
|
|
|
7268
7478
|
get toolbarName() {
|
|
7269
7479
|
return this.selectEntry?.node?.label || this.selectEntry?.componentName || '';
|
|
7270
7480
|
}
|
|
7271
|
-
/** 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). */
|
|
7272
7482
|
get canAddItem() {
|
|
7273
7483
|
return ADD_ITEM_ROLES.has(this.selectEntry?.node?.role ?? '');
|
|
7274
7484
|
}
|
|
7275
7485
|
get addItemLabel() {
|
|
7276
|
-
|
|
7486
|
+
const key = isTabsPart(this.selectEntry?.node) ? ADD_TAB_KEY : ADD_ITEM_KEY;
|
|
7487
|
+
return this.builderLabels(key, BUILDER_CHROME_EN[key]);
|
|
7277
7488
|
}
|
|
7278
7489
|
onAddItem(event) {
|
|
7279
7490
|
event.stopPropagation();
|
|
@@ -7857,10 +8068,14 @@ function repeaterSubLabel(rows) {
|
|
|
7857
8068
|
/** Flatten `nodes` into rows, depth-first, honouring each node's expanded state. */
|
|
7858
8069
|
function flattenLayerTree(nodes, ctx) {
|
|
7859
8070
|
const out = [];
|
|
7860
|
-
walk(nodes, 0, out, ctx, false);
|
|
8071
|
+
walk(nodes, 0, out, ctx, false, undefined, []);
|
|
7861
8072
|
return out;
|
|
7862
8073
|
}
|
|
7863
|
-
|
|
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) {
|
|
7864
8079
|
for (let i = 0; i < nodes.length; i++) {
|
|
7865
8080
|
const node = nodes[i];
|
|
7866
8081
|
const hasChildren = !!node.children?.length;
|
|
@@ -7870,7 +8085,7 @@ function walk(nodes, depth, out, ctx, inTemplate, templateOf) {
|
|
|
7870
8085
|
const bound = inTemplate && !!node.bindings?.some(b => b.prop !== REPEATER_SOURCE_PROP && b.source);
|
|
7871
8086
|
const label = node.label ?? node.type;
|
|
7872
8087
|
// An accordion item shows no text of its own: it names its question (board 3 "Item · Do you take pro…").
|
|
7873
|
-
const descriptor = rows ? null : (nodeDescriptor(node, label) ?? compositePartSub(node));
|
|
8088
|
+
const descriptor = rows ? null : (nodeDescriptor(node, label) ?? compositePartSub(node, ancestors));
|
|
7874
8089
|
out.push({
|
|
7875
8090
|
node,
|
|
7876
8091
|
icon: bound ? BOUND_ICON : (compositePartIcon(node.role) || ctx.iconOf(node.type) || DEFAULT_ICON),
|
|
@@ -7883,7 +8098,7 @@ function walk(nodes, depth, out, ctx, inTemplate, templateOf) {
|
|
|
7883
8098
|
indexInParent: i,
|
|
7884
8099
|
locked: isLockedPart(node),
|
|
7885
8100
|
template: !!templateOf?.alias,
|
|
7886
|
-
sub: rows ? (
|
|
8101
|
+
sub: rows ? (isCompositeFromContent(node) && ctx.fromContentLabel ? ctx.fromContentLabel(rows) : repeaterSubLabel(rows)) : shortDescriptor(descriptor),
|
|
7887
8102
|
descriptor,
|
|
7888
8103
|
bound,
|
|
7889
8104
|
roleChip: node.role ? roleChipLabel(node.role) : null,
|
|
@@ -7891,13 +8106,14 @@ function walk(nodes, depth, out, ctx, inTemplate, templateOf) {
|
|
|
7891
8106
|
});
|
|
7892
8107
|
if (!hasChildren || !expanded)
|
|
7893
8108
|
continue;
|
|
8109
|
+
const below = [...ancestors, node];
|
|
7894
8110
|
if (isRepeater && rows) {
|
|
7895
|
-
walk(node.children, depth + 1, out, ctx, true, rows);
|
|
8111
|
+
walk(node.children, depth + 1, out, ctx, true, rows, below);
|
|
7896
8112
|
if (rows.alias && rows.count > 1)
|
|
7897
|
-
out[out.length - 1].moreRows = { count: rows.count - 1, alias: rows.alias, depth: depth + 1, composite:
|
|
8113
|
+
out[out.length - 1].moreRows = { count: rows.count - 1, alias: rows.alias, depth: depth + 1, composite: isCompositeFromContent(node) };
|
|
7898
8114
|
}
|
|
7899
8115
|
else {
|
|
7900
|
-
walk(node.children, depth + 1, out, ctx, inTemplate);
|
|
8116
|
+
walk(node.children, depth + 1, out, ctx, inTemplate, undefined, below);
|
|
7901
8117
|
}
|
|
7902
8118
|
}
|
|
7903
8119
|
}
|
|
@@ -8146,10 +8362,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
8146
8362
|
|
|
8147
8363
|
/** Label of the always-present root crumb. The page surface itself, not a layout node. */
|
|
8148
8364
|
const BODY_CRUMB_LABEL = 'Body';
|
|
8149
|
-
/** A node's crumb: its layer name, and an accordion item's question after it ("Item · How long does…")
|
|
8150
|
-
|
|
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 = []) {
|
|
8151
8368
|
const name = node.label || node.type;
|
|
8152
|
-
const sub = compositePartSub(node);
|
|
8369
|
+
const sub = compositePartSub(node, ancestors);
|
|
8153
8370
|
return sub ? `${name} · ${sub}` : name;
|
|
8154
8371
|
}
|
|
8155
8372
|
/**
|
|
@@ -8187,8 +8404,8 @@ class BreadcrumbBarComponent {
|
|
|
8187
8404
|
{ node: null, label: BODY_CRUMB_LABEL, icon: null, isCurrent: nodes.length === 0, key: BODY_CRUMB_LABEL },
|
|
8188
8405
|
...nodes.map((node, i) => ({
|
|
8189
8406
|
node,
|
|
8190
|
-
label: crumbLabel(node),
|
|
8191
|
-
icon:
|
|
8407
|
+
label: crumbLabel(node, nodes.slice(0, i)),
|
|
8408
|
+
icon: isAnyCompositeRoot(node) ? `ph-thin ${compositeKindGlyph(node.role)}` : null,
|
|
8192
8409
|
isCurrent: i === last,
|
|
8193
8410
|
key: node,
|
|
8194
8411
|
})),
|
|
@@ -8407,11 +8624,12 @@ class BuilderComponent {
|
|
|
8407
8624
|
this.auditRunning = false;
|
|
8408
8625
|
this.cdr.detectChanges();
|
|
8409
8626
|
}
|
|
8410
|
-
/** An Accordion from content whose source came back with no rows
|
|
8627
|
+
/** An Accordion — or Tabs (CC4a: its list) — from content whose source came back with no rows: what the audit names. */
|
|
8411
8628
|
emptySourceOf(node) {
|
|
8412
|
-
|
|
8629
|
+
const holder = isAccordionRoot(node) ? node : isExclusiveRoot(node) ? tabsListOf(node) : undefined;
|
|
8630
|
+
if (!holder)
|
|
8413
8631
|
return undefined;
|
|
8414
|
-
const instance = this.overlayService.getNodeRef(
|
|
8632
|
+
const instance = this.overlayService.getNodeRef(holder)?.componentRef?.instance;
|
|
8415
8633
|
if (!(instance instanceof BuilderRepeaterComponent) || !instance.source || instance.isLoading || instance.items.length)
|
|
8416
8634
|
return undefined;
|
|
8417
8635
|
const path = nodeAncestorPathInEither(this.layout, this.globalElements, node) ?? [];
|
|
@@ -8789,6 +9007,8 @@ class BuilderComponent {
|
|
|
8789
9007
|
this.scrollbarTranslate = 0; // counteracts Lenis wrapper scrollTop shifting position:absolute children
|
|
8790
9008
|
this.activeSidebar = 'components';
|
|
8791
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;
|
|
8792
9012
|
this.saveBlockName = '';
|
|
8793
9013
|
this.pendingBlockNode = null;
|
|
8794
9014
|
this.isPanning = false;
|
|
@@ -8844,6 +9064,7 @@ class BuilderComponent {
|
|
|
8844
9064
|
this.isDragging = v;
|
|
8845
9065
|
this.cdr.markForCheck();
|
|
8846
9066
|
}));
|
|
9067
|
+
this.sub.add(this.dragEngine.pairRemoval$.subscribe((node) => this.offerPairRemoval(node)));
|
|
8847
9068
|
this.sub.add(this.dragEngine.dropComplete$.subscribe((node) => {
|
|
8848
9069
|
this._selectAfterDrop(node);
|
|
8849
9070
|
this.panelEventService.emit(PanelEventTypes.LAYOUT_CHANGED, this.layout);
|
|
@@ -9389,6 +9610,8 @@ class BuilderComponent {
|
|
|
9389
9610
|
this.panelEventService.emit(PanelEventTypes.LAYOUT_CHANGED, this.layout);
|
|
9390
9611
|
}
|
|
9391
9612
|
else if (action === NodeAction.Duplicate) {
|
|
9613
|
+
if (this.duplicateTabPairOf(node))
|
|
9614
|
+
return;
|
|
9392
9615
|
const clone = this.deepCloneWithNewIds(node);
|
|
9393
9616
|
result.parent.splice(result.index + 1, 0, clone);
|
|
9394
9617
|
this.cdr.detectChanges();
|
|
@@ -9428,6 +9651,8 @@ class BuilderComponent {
|
|
|
9428
9651
|
break;
|
|
9429
9652
|
}
|
|
9430
9653
|
case ToolbarAction.Duplicate: {
|
|
9654
|
+
if (this.duplicateTabPairOf(node))
|
|
9655
|
+
break;
|
|
9431
9656
|
const result = this.findNodeInTree(node, this.layout) ?? this.findNodeInTree(node, this.globalElements);
|
|
9432
9657
|
if (result && !this.dragEngine.refuseInsert(node, this.siblingAncestors(node))) {
|
|
9433
9658
|
const clone = this.deepCloneWithNewIds(node);
|
|
@@ -9608,7 +9833,8 @@ class BuilderComponent {
|
|
|
9608
9833
|
break;
|
|
9609
9834
|
}
|
|
9610
9835
|
case ToolbarAction.AddItem: {
|
|
9611
|
-
this.
|
|
9836
|
+
if (!this.addTabPairFrom(node))
|
|
9837
|
+
this.addAccordionItemFrom(node);
|
|
9612
9838
|
break;
|
|
9613
9839
|
}
|
|
9614
9840
|
case ToolbarAction.AddSlide: {
|
|
@@ -9643,6 +9869,62 @@ class BuilderComponent {
|
|
|
9643
9869
|
if (trigger)
|
|
9644
9870
|
this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: trigger });
|
|
9645
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
|
+
}
|
|
9646
9928
|
confirmSaveBlock() {
|
|
9647
9929
|
if (!this.saveBlockName.trim() || !this.pendingBlockNode)
|
|
9648
9930
|
return;
|
|
@@ -9883,11 +10165,11 @@ class BuilderComponent {
|
|
|
9883
10165
|
}
|
|
9884
10166
|
}
|
|
9885
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 }); }
|
|
9886
|
-
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"] }] }); }
|
|
9887
10169
|
}
|
|
9888
10170
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderComponent, decorators: [{
|
|
9889
10171
|
type: Component,
|
|
9890
|
-
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"] }]
|
|
9891
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: [{
|
|
9892
10174
|
type: Input
|
|
9893
10175
|
}], dataSources: [{
|
|
@@ -10423,6 +10705,8 @@ class BuilderButtonBlockComponent {
|
|
|
10423
10705
|
this.elRef = elRef;
|
|
10424
10706
|
this.dragEngine = dragEngine;
|
|
10425
10707
|
this.cdr = cdr;
|
|
10708
|
+
/** The node this button renders (assigned by RenderDirective, never an input). */
|
|
10709
|
+
this.node = null;
|
|
10426
10710
|
this.children = [];
|
|
10427
10711
|
this.style = {};
|
|
10428
10712
|
this.tag = 'button';
|
|
@@ -10452,7 +10736,7 @@ class BuilderButtonBlockComponent {
|
|
|
10452
10736
|
this.dragEngine.handleDrop(this.children, event, this.dropListId, this.cdr);
|
|
10453
10737
|
}
|
|
10454
10738
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderButtonBlockComponent, deps: [{ token: i0.ElementRef }, { token: DragEngineService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
10455
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: BuilderButtonBlockComponent, isStandalone: false, selector: "button[iox-button-block],a[iox-button-block]", inputs: { children: "children", style: "style", tag: "tag", linkType: "linkType", href: "href", pageRoute: "pageRoute", target: "target", type: "type", nodeId: "nodeId", dropListId: "dropListId" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, host: { properties: { "class": "'button-block-root iox-node-' + nodeId", "attr.href": "resolvedHref", "attr.target": "tag === 'a' ? (target || null) : null", "attr.type": "tag === 'a' ? null : type" } }, ngImport: i0, template: `
|
|
10739
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: BuilderButtonBlockComponent, isStandalone: false, selector: "button[iox-button-block],a[iox-button-block]", inputs: { children: "children", style: "style", tag: "tag", linkType: "linkType", href: "href", pageRoute: "pageRoute", target: "target", type: "type", nodeId: "nodeId", dropListId: "dropListId" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, host: { properties: { "class": "'button-block-root iox-node-' + nodeId", "attr.href": "resolvedHref", "attr.target": "tag === 'a' ? (target || null) : null", "attr.type": "tag === 'a' ? null : type", "attr.data-iox-item-key": "node?.itemKey ?? null" } }, ngImport: i0, template: `
|
|
10456
10740
|
<span *ngIf="!children.length" class="button-block-placeholder" data-iox-chrome>Button</span>
|
|
10457
10741
|
|
|
10458
10742
|
<!-- WRAPPER COLLAPSE (task 9c): render children directly into the host; iox-outer + drag land
|
|
@@ -10485,6 +10769,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
10485
10769
|
'[attr.href]': 'resolvedHref',
|
|
10486
10770
|
'[attr.target]': "tag === 'a' ? (target || null) : null",
|
|
10487
10771
|
'[attr.type]': "tag === 'a' ? null : type",
|
|
10772
|
+
// A tab's identity (CC4a): the exclusive runtime pairs it with its panel by this key, as an accordion item's.
|
|
10773
|
+
'[attr.data-iox-item-key]': 'node?.itemKey ?? null',
|
|
10488
10774
|
}, standalone: false, styles: [":host{box-sizing:border-box}.button-block-placeholder{flex:1;display:flex;align-items:center;justify-content:center;color:#64748b66;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}:host ::ng-deep>[class*=iox-outer-]{min-width:0}\n"] }]
|
|
10489
10775
|
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: DragEngineService }, { type: i0.ChangeDetectorRef }], propDecorators: { children: [{
|
|
10490
10776
|
type: Input
|
|
@@ -13481,5 +13767,5 @@ class BuilderSpacerComponentConfig extends ComponentConfig {
|
|
|
13481
13767
|
* Generated bundle index. Do not edit.
|
|
13482
13768
|
*/
|
|
13483
13769
|
|
|
13484
|
-
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, 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, 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 };
|
|
13770
|
+
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 };
|
|
13485
13771
|
//# sourceMappingURL=vectoriox-iox-builder.mjs.map
|