@vectoriox/iox-builder 10.0.2 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, Optional, Inject, inject, NgZone, EventEmitter, createComponent, Output, Input, Directive, Component, ChangeDetectionStrategy, ViewChild, effect, ContentChildren, NO_ERRORS_SCHEMA, NgModule } from '@angular/core';
2
+ import { InjectionToken, Injectable, Optional, Inject, inject, NgZone, EventEmitter, createComponent, Output, Input, Directive, Component, ChangeDetectionStrategy, ViewChild, effect, ContentChildren, NO_ERRORS_SCHEMA, NgModule } from '@angular/core';
3
3
  import * as i2 from '@angular/common';
4
4
  import { DOCUMENT, CommonModule } from '@angular/common';
5
5
  import * as i17 from '@angular/forms';
@@ -17,12 +17,12 @@ import { TooltipModule } from 'primeng/tooltip';
17
17
  import * as i20 from 'primeng/dialog';
18
18
  import { DialogModule } from 'primeng/dialog';
19
19
  import * as i1$1 from '@vectoriox/iox-ui';
20
- import { 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
+ import { nodeCssId, needsGroupName, resolveBoundValue, VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, TEXT_DIRECTION_KEY, TEXT_ALIGN_KEY, TextAlign, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, LinkType, MAX_LINES_KEY, A11yTrait, BREAKPOINTS, Breakpoint, withSchemeInk, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, targetedStateSelector, stateSelectorSuffix, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, 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';
21
21
  export { ComponentInstanceRegistryService, VIRTUAL_TRAIT_KEYS, composeVirtualTraits } from '@vectoriox/iox-ui';
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
+ import { StructureEdit, isAccordionPart, isDisclosurePart, isSliderPart, isTabsPart, isSegmentedPart, structureRefusal, StructureRefusal, SegmentedRole, SEGMENTED_MAX_OPTIONS, AccordionRole, SEGMENTED_MIN_OPTIONS, isExclusiveItemPart, TabsRole, ITEM_OPEN_STATE, readStyle, isSegmentedRoot, segmentedDefaultOn, segmentedOptions, isTabsRoot, isTabsFromContent, tabsTriggers, isAccordionRoot, isAccordionFromContent, accordionItems, initialAccordionOpen, accordionBehaviour, exclusiveItemLabel, exclusiveRootOf, tabTriggerOf, tabLabel, removeTabPair, tabPanelOf, tabsListOf, tabsPanelsOf, nextTabKey, CompositeBehaviourTrait, ACCORDION_DEFAULT_ONS, TabsBehaviourTrait, TABS_ACTIVATE_ONS, TABS_ORIENTATIONS, onCompositeItemState, isCompositeItemOn, runCompositeItemAction, shouldScrollToItem, rowIndexOf as rowIndexOf$1, slideIndexForRow, DisclosureRole, isExclusiveTrigger, compositeScopeId, compositeRowKeys, isExclusiveRoot, ExclusiveRuntime, exclusiveDefaultOf, exclusiveLabelOf, exclusiveBehaviourOf, exclusiveKindOf, compositeKindOfRole, AccordionRuntime, isToggleKind, closestAccordionItem, isAnyCompositeRoot, isExclusiveList, buildCardRecipe, buildBadge, buildChip, buildAccordionRecipe, buildDisclosureRecipe, buildTabsRecipe, buildSegmentedRecipe, buildLightboxRecipe, NO_ELEMENT_STYLE, CompositeKind, SliderRole, accordionItemLabel, segmentedOptionLabel, isExclusivePanels, isLockedPart, accordionRootOf, accordionItemOf, addAccordionItem, addTabPair, segmentedAddRefusal, addSegmentedOption, isCompositeItem, accordionHeadingLevel, isCompositePanel, isCompositeTrigger, splitSliderChildren, hasSliderParts } from '@vectoriox/iox-ui/composite-core';
23
+ import { formContentInHost, isInteractiveHost, findFormContentInHosts } from '@vectoriox/iox-ui/forms-core';
23
24
  import { Subject, BehaviorSubject, of, forkJoin } from 'rxjs';
24
25
  import interact from 'interactjs';
25
- import { formContentInHost, isInteractiveHost, findFormContentInHosts } from '@vectoriox/iox-ui/forms-core';
26
26
  import { map, catchError } from 'rxjs/operators';
27
27
  import * as i1 from '@angular/common/http';
28
28
  import { itemActionOf, BEHAVIOUR_ACTION_TYPES, SERVER_ELEMENT_ACTION_TYPES, CONSENT_ACTION_TYPES, opensAtClickedItem } from '@vectoriox/iox-ui/actions-core';
@@ -33,230 +33,6 @@ import * as i18 from 'primeng/api';
33
33
  import { planVideo, VideoSource, VIDEO_PROVIDER_LABEL, VideoTrait, VideoMode } from '@vectoriox/iox-ui/media-core';
34
34
  import { ConsentCategory } from '@vectoriox/iox-ui/consent-core';
35
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
-
136
- var PanelEventTypes;
137
- (function (PanelEventTypes) {
138
- PanelEventTypes["ELEMENT_SELECT"] = "elementselect";
139
- PanelEventTypes["ELEMENT_DESELECT"] = "elementdeselect";
140
- PanelEventTypes["PANEL_SELECTED"] = "panelselect";
141
- PanelEventTypes["PANEL_OPEN"] = "panelopen";
142
- PanelEventTypes["PANEL_CLOSE"] = "panelclose";
143
- PanelEventTypes["BINDING_CHANGED"] = "bindingchanged";
144
- PanelEventTypes["NODE_RENDERED"] = "noderendered";
145
- /** Open the Style panel in preset-edit mode for the given preset ID. */
146
- PanelEventTypes["PRESET_EDIT"] = "presetedit";
147
- /** Fired after a preset is created, updated, or deleted so panels refresh their list. */
148
- PanelEventTypes["PRESETS_CHANGED"] = "presetschanged";
149
- /** Fired when a linked symbol instance's styles change — propagates to all instances. */
150
- PanelEventTypes["SYMBOLS_CHANGED"] = "symbolschanged";
151
- /** Fired on every structural or style mutation — used by BuilderHistoryService to snapshot. */
152
- PanelEventTypes["LAYOUT_CHANGED"] = "layoutchanged";
153
- /** Open the Interactions panel in preset-edit mode for the given preset ID. */
154
- PanelEventTypes["INTERACTION_PRESET_EDIT"] = "interactionpresetedit";
155
- /** Fired after an interaction preset is created, updated, or deleted so panels refresh. */
156
- PanelEventTypes["INTERACTION_PRESETS_CHANGED"] = "interactionpresetschanged";
157
- /** Open the Interactions panel and EXPAND a specific interaction for editing (data = interaction
158
- * id). Emitted when the overlay interaction badge is clicked so that badge → "edit THIS
159
- * interaction", without firing the element's (live) animation. */
160
- PanelEventTypes["INTERACTION_EDIT"] = "interactionedit";
161
- /** A node's `interactions` were added/removed/edited. Carries `{ node }`.
162
- * Repeater/Slider listen so their PREVIEW CLONES are re-cloned with the new interactions —
163
- * clones are attached by ELEMENT at render time, so without this a new interaction only starts
164
- * working after a save + reload. */
165
- PanelEventTypes["INTERACTION_CHANGED"] = "interactionchanged";
166
- /** CLICK-TO-INSERT (B2): a components-panel entry was clicked. Carries the same payload a drag
167
- * would (`string` component type, or a block / symbol object). The builder resolves WHERE from
168
- * the current selection — see `resolveInsertTarget`. */
169
- PanelEventTypes["INSERT_COMPONENT"] = "insertcomponent";
170
- /** Insert prebuilt node(s) as GLOBAL elements (data = ComponentNode[]) — the consent preset's click and
171
- * the site header's click or drop (CC1.1). The HOST handles it (it owns the globals, their undo step and
172
- * the one-header rule); the builder only routes a dropped global payload here (`globalInsert$`). */
173
- PanelEventTypes["INSERT_GLOBAL"] = "insertglobal";
174
- /** A panel makes `data.node` a GLOBAL element at `data.position` (default before) — the same move as the
175
- * toolbar's Make Global (the Style panel's Global switch, Bindings ▸ Phone menu ▸ "Make it the site header"). */
176
- PanelEventTypes["MAKE_GLOBAL"] = "make-global";
177
- /** A panel returns the global `data.node` to the page — the toolbar's Remove Global. */
178
- PanelEventTypes["REMOVE_GLOBAL"] = "remove-global";
179
- /** A panel moved the global `data.node` to `data.position` (before / after the page content). */
180
- PanelEventTypes["GLOBAL_POSITION"] = "update-global-position";
181
- /** A click on a compound component's PART (Field label/control/help/…) — the style panel
182
- * activates that part (data = { part }). Doron 2026-10-10: "both shall be selectable". */
183
- PanelEventTypes["PART_SELECT"] = "partselect";
184
- /** A panel wants to select a NODE it already has a reference to (e.g. the Form's nested field
185
- * list). Carries `{ node }`. `OverlayService.setSelect()` alone only updates the overlay's own
186
- * visual state — `selectedItem`/`selectedPath` (breadcrumb) and every other panel's idea of
187
- * "what's selected" stay stale unless the SAME `_emitElementSelect` a real canvas click goes
188
- * through also runs. This event is that path for code outside `BuilderComponent` itself,
189
- * mirroring what `onTreeNodeSelect`/`onCrumbSelect` already do internally. */
190
- PanelEventTypes["SELECT_NODE"] = "selectnode";
191
- /** A panel rewrote a node's SUBTREE in place — its children (a Card's Parts), its layout styles (Card ▸
192
- * Layout), a part's element style or scheme. Carries `{ node }`. The builder re-renders the subtree,
193
- * re-registers every node's rules and design classes, refreshes a Repeater holding it, and records ONE
194
- * undo step (LAYOUT_CHANGED). */
195
- PanelEventTypes["NODE_STRUCTURE_CHANGED"] = "nodestructurechanged";
196
- /** A Repeater's rows changed on the canvas (fetched, re-fetched, its source cleared). Carries `{ nodeId,
197
- * count }`. Not a page edit — never an undo step: the layer tree redraws its "N more rows" line. */
198
- PanelEventTypes["REPEATER_ROWS_CHANGED"] = "repeaterrowschanged";
199
- })(PanelEventTypes || (PanelEventTypes = {}));
200
- class PanelEventService {
201
- constructor() {
202
- this.panelEventSubject = new Subject();
203
- }
204
- subscribe(next) {
205
- return this.panelEventSubject.asObservable().subscribe(next);
206
- }
207
- emit(type, data) {
208
- this.panelEventSubject.next({ type, data });
209
- }
210
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
211
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService }); }
212
- }
213
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService, decorators: [{
214
- type: Injectable
215
- }] });
216
-
217
- /**
218
- * Deep-clones a ComponentNode tree and overrides trait defaults
219
- * according to each node's bindings resolved against `item`.
220
- *
221
- * Each clone gets a new `id` (for Angular tracking / overlay registration)
222
- * but shares the original node's CSS class by setting `styleId = originalId`.
223
- * This means all repeated preview rows point to the same CSS rule block as the
224
- * template node — the style engine only needs one rule per logical slot.
225
- */
226
- function cloneNodesForItem(nodes, item) {
227
- return nodes.map(node => {
228
- // Capture the CSS-owning id before clone
229
- const cssId = node.styleId ?? node.id;
230
- const clone = JSON.parse(JSON.stringify(node));
231
- clone.id = Math.random().toString(36).substr(2, 9);
232
- // Point this clone and all its descendants at the original CSS rule.
233
- clone.styleId = cssId;
234
- // Mark as a preview clone: display-only, not selectable/editable on the canvas.
235
- // (Distinct from styleId, which duplication/paste also uses.) The render directive
236
- // skips overlay registration for nodes carrying this flag.
237
- clone.__previewClone = true;
238
- if (clone.bindings?.length && item) {
239
- for (const binding of clone.bindings) {
240
- // Path lookup AND pipes (UI "Filters"). Resolving the path alone left a `date` or
241
- // `uppercase` filter unapplied on every preview row, so the canvas showed the raw
242
- // value while the SSR engine showed the formatted one — see binding-filters-plan.md.
243
- const value = resolveBoundValue(item, binding);
244
- if (value !== undefined) {
245
- for (const trait of clone.traits || []) {
246
- if (trait.name === binding.prop) {
247
- trait.default = value;
248
- }
249
- }
250
- }
251
- }
252
- }
253
- if (clone.children?.length) {
254
- clone.children = cloneNodesForItem(clone.children, item);
255
- }
256
- return clone;
257
- });
258
- }
259
-
260
36
  /**
261
37
  * Canvas data preview used to come from host-provided content services (IOX_CONTENT_SERVICE /
262
38
  * IOX_SHOP_SERVICE), whose `getContent(contentType, orgId)` hit the ADMIN resources endpoint and
@@ -346,6 +122,14 @@ const BUILDER_CHROME_EN = Object.freeze({
346
122
  'builder.chrome.deletePairTitle': 'Delete tab and its panel?',
347
123
  'builder.chrome.deletePairBody': '“{tab}” and the panel it shows go together: deleting one deletes both. Undo brings them back.',
348
124
  'builder.chrome.deletePair': 'Delete tab and its panel',
125
+ 'builder.chrome.kindSegmented': 'Segmented',
126
+ 'builder.chrome.addOption': 'Add option',
127
+ 'builder.chrome.refuseLockedSegmentedPartMove': 'This part belongs to its Segmented control — an option stays in it, and its badge in its option.',
128
+ 'builder.chrome.refuseSegmentedAccepts': 'A Segmented control holds only its options. Put it beside the control instead.',
129
+ 'builder.chrome.refuseSegmentedMinOptions': 'A Segmented control keeps at least two options. Delete the whole control instead.',
130
+ 'builder.chrome.refuseSegmentedMaxOptions': 'A Segmented control holds four options at most — past four, use Tabs or a dropdown.',
131
+ 'builder.chrome.auditShownBeforeControl': 'Shows “when an item is on” but comes before {control} in reading order — a screen reader reads it before the choice that changes it. Move it after the control.',
132
+ 'builder.chrome.auditUnnamedGroup': 'A Segmented control with no accessible name — a screen reader says “radio group” and nothing more. Name it in Bindings ▸ Accessibility.',
349
133
  'builder.chrome.cancel': 'Cancel',
350
134
  'builder.chrome.fromContent': 'From {source} · {count} rows',
351
135
  'builder.chrome.emptySource': 'No rows in “{alias}”.',
@@ -380,194 +164,26 @@ const CANVAS_EMPTY_IMAGE_LABEL_EN = BUILDER_CHROME_EN[CANVAS_EMPTY_IMAGE_LABEL_K
380
164
  */
381
165
  const EDITOR_CHROME_ATTRIBUTE = 'data-iox-chrome';
382
166
 
383
- /** The content type a data source reads (`faqs`), or '' for a source that reads none (external, static). */
384
- function sourceContentType(ds) {
385
- return ds?.request?.cms?.contentType || ds?.request?.shop?.contentType || '';
386
- }
167
+ /** The node type that scopes a form. Kept here so the rule has one name, not a scattered literal. */
168
+ const FORM_TYPE = 'Form';
387
169
  /**
388
- * What the builder's chrome calls the list a source reads — the CONTENT TYPE's label ("From FAQ", board 3), as the
389
- * host knows it (`IOX_CONTENT_TYPE_LABELS`), else the source's alias. ONE answer for the layer tree's
390
- * "From FAQ · 3 rows", the Repeater's empty-source hint and the Items section's source options.
170
+ * The form the current selection belongs to — the NEAREST Form ancestor, or the selection itself
171
+ * when a Form is selected.
172
+ *
173
+ * This is the selection-side expression of decision D1: a field belongs to its nearest ancestor
174
+ * form. Searching the path from the deepest node outward is what makes "nearest" true, and it is
175
+ * why a nested form (were one ever allowed) could not capture an outer form's toolbar.
176
+ *
177
+ * @param path Ancestor chain from the root down to the selected node, as `selectedPath` holds it.
391
178
  */
392
- function sourceTypeLabel(alias, ds, resolve) {
393
- const type = sourceContentType(ds);
394
- return (type && resolve(type)) || alias;
395
- }
396
-
397
- const freshId = () => Math.random().toString(36).substr(2, 9);
398
- /**
399
- * Assign fresh ids to a cloned ComponentNode subtree AND remap intra-subtree references to the
400
- * new ids. Used by every author-time clone path (duplicate, paste, block/external drop) so a copied
401
- * structure stays self-consistent.
402
- *
403
- * Absolute node-id references that break under duplication if left unmapped:
404
- * - `stateTriggers[state]` — the trigger-hover target (see style-engine.md → Targeted states)
405
- * - interaction `action.target` / `reverseActions.target`
406
- * A reference pointing OUTSIDE the cloned subtree (id not in the map) is left untouched — the copy
407
- * intentionally still points at the external node.
408
- *
409
- * TRAIT values are NOT ids and are copied verbatim — with one exception. A trait that carries an
410
- * IDENTITY rather than a setting must be reissued, or the copy silently shares the original's
411
- * identity. `formId` is the first: two forms with the same id share one submissions stream, and the
412
- * server resolves a submission against whichever page it reaches first. See REGENERATED_TRAITS.
413
- *
414
- * `styleId` strategy:
415
- * - `'clear'` (default) — drop styleId so the clone OWNS its own CSS rule (independent copy).
416
- * - `'regenerate'` — keep a styleId but give it a fresh value (legacy builder.component path).
417
- */
418
- /**
419
- * Trait names whose value is an IDENTITY, not a setting, and must therefore be reissued on every
420
- * clone. Add a name here the moment a trait starts identifying something rather than describing it.
421
- *
422
- * `formId` — a form's submissions stream. Duplicating a page, pasting a form or dropping a saved
423
- * block twice used to produce two forms claiming the same stream.
424
- */
425
- const REGENERATED_TRAITS = new Set(['formId']);
426
- /** Reissue identity-carrying traits on a node, preserving the original's prefix where it had one. */
427
- function reissueIdentityTraits(node) {
428
- for (const trait of (node.traits ?? [])) {
429
- if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
430
- continue;
431
- if (trait.default === undefined || trait.default === '')
432
- continue;
433
- // Keep the human-readable prefix a preset gave it (`contact-a1b2c3` → `contact-<new>`), so a
434
- // duplicated contact form is still recognisably a contact form in the inbox.
435
- const prefix = String(trait.default).split('-')[0];
436
- trait.default = `${prefix}-${freshId()}`;
437
- }
438
- }
439
- /**
440
- * A NEW node is born with its identity: an empty identity trait (a palette Form's `formId`) is issued,
441
- * one already set (a preset's `contact-…`) is kept. A Form inserted from the palette used to keep
442
- * `formId: ''` — its submissions posted to `forms//submit` and nothing was ever stored.
443
- */
444
- function issueIdentityTraits(root) {
445
- for (const trait of (root.traits ?? [])) {
446
- if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
447
- continue;
448
- if (trait.default === undefined || trait.default === '')
449
- trait.default = `${IDENTITY_PREFIX[trait.name] ?? trait.name}-${freshId()}`;
450
- }
451
- for (const child of root.children ?? [])
452
- if (child)
453
- issueIdentityTraits(child);
454
- }
455
- /** The readable prefix a freshly issued identity carries (`form-a1b2c3`). */
456
- const IDENTITY_PREFIX = { formId: 'form' };
457
- function regenerateNodeIds(root, opts = {}) {
458
- const idMap = new Map();
459
- // A composite item's identity (CC2 `itemKey`) is reissued per copy, consistently inside it: a duplicated
460
- // accordion item must not share its original's ARIA ids (composite plan §3).
461
- const keyMap = new Map();
462
- const assign = (node) => {
463
- const oldId = node.id;
464
- node.id = freshId();
465
- reissueIdentityTraits(node);
466
- if (node.itemKey) {
467
- if (!keyMap.has(node.itemKey))
468
- keyMap.set(node.itemKey, `k-${freshId()}`);
469
- node.itemKey = keyMap.get(node.itemKey);
470
- }
471
- if (opts.styleId === 'regenerate') {
472
- if (node.styleId)
473
- node.styleId = freshId();
474
- }
475
- else {
476
- node.styleId = undefined;
477
- }
478
- if (oldId)
479
- idMap.set(oldId, node.id);
480
- (node.children || []).forEach(assign);
481
- };
482
- assign(root);
483
- remapNodeReferences(root, idMap);
484
- }
485
- /** Remap absolute node-id references inside a subtree to their new ids (per `idMap`). Ids not in
486
- * the map (references to nodes outside the cloned subtree) are preserved. */
487
- function remapNodeReferences(node, idMap) {
488
- const triggers = node.stateTriggers;
489
- if (triggers) {
490
- for (const state of Object.keys(triggers)) {
491
- const mapped = idMap.get(triggers[state]);
492
- if (mapped)
493
- triggers[state] = mapped;
494
- }
495
- }
496
- for (const interaction of node.interactions || []) {
497
- const actions = [...(interaction.actions || []), ...(interaction.reverseActions || [])];
498
- for (const action of actions) {
499
- if (action?.target && action.target !== 'self') {
500
- const mapped = idMap.get(action.target);
501
- if (mapped)
502
- action.target = mapped;
503
- }
504
- }
505
- }
506
- (node.children || []).forEach(child => remapNodeReferences(child, idMap));
507
- }
508
-
509
- /**
510
- * The text on the drop indicator's chip — "into Card grid · position 2 of 3" (B2).
511
- *
512
- * The most common drag failure is landing a node one level off and not noticing; naming the PARENT
513
- * and the slot makes the target unambiguous before the pointer is released.
514
- *
515
- * @param containerName The target container's label or type; blank / unknown → "the page".
516
- * @param index Insert index within the container (0-based).
517
- * @param childCount How many children it has now — the node being dropped makes `childCount + 1`.
518
- */
519
- function dropTargetLabel(containerName, index, childCount) {
520
- const name = (containerName ?? '').trim() || 'the page';
521
- const total = Math.max(0, childCount) + 1;
522
- const position = Math.min(Math.max(index, 0), total - 1) + 1;
523
- return `into ${name} · position ${position} of ${total}`;
524
- }
525
-
526
- /**
527
- * Per-frame exponential smoothing step — moves `current` a fraction of the way
528
- * toward `target`. Used to ease the drag ghost toward the pointer instead of
529
- * snapping to it 1:1 on every dragmove (see DragEngineService._ghostFollowStep).
530
- */
531
- function easeToward(current, target, smoothing) {
532
- return current + (target - current) * smoothing;
533
- }
534
-
535
- /**
536
- * Decides which element a drag gesture should actually operate on.
537
- *
538
- * A drag must never silently descend into a nested child just because the pointer happened to
539
- * be over it while its ANCESTOR is the currently selected node, and the overlay's drag-handle
540
- * (which carries no node of its own — it lives outside the canvas DOM entirely) must always
541
- * redirect to whatever is currently selected. In both cases the redirect target is the
542
- * currently-selected element; otherwise the gesture proceeds on its natural origin unchanged.
543
- */
544
- function resolveDragTarget(el, selectedElement, isDragHandle) {
545
- if (!selectedElement || el === selectedElement)
546
- return el;
547
- const isDescendantOfSelected = selectedElement.contains(el);
548
- return (isDragHandle || isDescendantOfSelected) ? selectedElement : el;
549
- }
550
-
551
- /** The node type that scopes a form. Kept here so the rule has one name, not a scattered literal. */
552
- const FORM_TYPE = 'Form';
553
- /**
554
- * The form the current selection belongs to — the NEAREST Form ancestor, or the selection itself
555
- * when a Form is selected.
556
- *
557
- * This is the selection-side expression of decision D1: a field belongs to its nearest ancestor
558
- * form. Searching the path from the deepest node outward is what makes "nearest" true, and it is
559
- * why a nested form (were one ever allowed) could not capture an outer form's toolbar.
560
- *
561
- * @param path Ancestor chain from the root down to the selected node, as `selectedPath` holds it.
562
- */
563
- function nearestFormNode(path) {
564
- if (!path?.length)
565
- return null;
566
- for (let i = path.length - 1; i >= 0; i--) {
567
- if (path[i]?.type === FORM_TYPE)
568
- return path[i];
569
- }
570
- return null;
179
+ function nearestFormNode(path) {
180
+ if (!path?.length)
181
+ return null;
182
+ for (let i = path.length - 1; i >= 0; i--) {
183
+ if (path[i]?.type === FORM_TYPE)
184
+ return path[i];
185
+ }
186
+ return null;
571
187
  }
572
188
 
573
189
  /**
@@ -614,6 +230,14 @@ var InsertRefusal;
614
230
  /** A tab or a panel deleted ALONE: the pair goes together — the host offers "Delete tab and its panel". Never said
615
231
  * as a refusal toast. */
616
232
  InsertRefusal["PairedPart"] = "pairedPart";
233
+ /** A Segmented control's option (or its badge) moved out of it (CC4b). */
234
+ InsertRefusal["LockedSegmentedPartMove"] = "lockedSegmentedPartMove";
235
+ /** Something other than an option dropped into a Segmented control (CC4b). */
236
+ InsertRefusal["SegmentedAccepts"] = "segmentedAccepts";
237
+ /** A Segmented control's last two options (Q40): it keeps at least two. */
238
+ InsertRefusal["SegmentedMinOptions"] = "segmentedMinOptions";
239
+ /** A fifth option (Q40): a Segmented control holds four at most. */
240
+ InsertRefusal["SegmentedMaxOptions"] = "segmentedMaxOptions";
617
241
  })(InsertRefusal || (InsertRefusal = {}));
618
242
  const FORM_STATE_BLOCK_TYPES = new Set(['FormSuccess', 'FormError']);
619
243
  /** The builder-chrome label that says each refusal on screen (English in `BUILDER_CHROME_EN`). */
@@ -635,6 +259,10 @@ const INSERT_REFUSAL_LABEL_KEYS = Object.freeze({
635
259
  [InsertRefusal.TabsAccepts]: 'builder.chrome.refuseTabsAccepts',
636
260
  [InsertRefusal.LastTab]: 'builder.chrome.refuseLastTab',
637
261
  [InsertRefusal.PairedPart]: 'builder.chrome.deletePairTitle',
262
+ [InsertRefusal.LockedSegmentedPartMove]: 'builder.chrome.refuseLockedSegmentedPartMove',
263
+ [InsertRefusal.SegmentedAccepts]: 'builder.chrome.refuseSegmentedAccepts',
264
+ [InsertRefusal.SegmentedMinOptions]: 'builder.chrome.refuseSegmentedMinOptions',
265
+ [InsertRefusal.SegmentedMaxOptions]: 'builder.chrome.refuseSegmentedMaxOptions',
638
266
  });
639
267
  /** The refusals that are not said as a toast: the host asks instead (a tab's pair). */
640
268
  const SILENT_REFUSALS = new Set([InsertRefusal.PairedPart]);
@@ -649,56 +277,894 @@ function lockedRefusal(node, edit) {
649
277
  return move ? InsertRefusal.LockedSliderPartMove : InsertRefusal.LockedSliderPause;
650
278
  if (isTabsPart(node))
651
279
  return move ? InsertRefusal.LockedTabsPartMove : InsertRefusal.LockedTabsPart;
280
+ if (isSegmentedPart(node))
281
+ return InsertRefusal.LockedSegmentedPartMove;
652
282
  return move ? InsertRefusal.LockedPartMove : InsertRefusal.LockedPart;
653
283
  }
654
- /** The refusal in the host's language (the library's English when no host resolver is provided). */
655
- function insertRefusalLabel(reason, labels = defaultBuilderLabels) {
656
- const key = INSERT_REFUSAL_LABEL_KEYS[reason];
657
- return labels(key, BUILDER_CHROME_EN[key]);
284
+ /** The refusal in the host's language (the library's English when no host resolver is provided). */
285
+ function insertRefusalLabel(reason, labels = defaultBuilderLabels) {
286
+ const key = INSERT_REFUSAL_LABEL_KEYS[reason];
287
+ return labels(key, BUILDER_CHROME_EN[key]);
288
+ }
289
+ function containsType(node, type) {
290
+ if (node.type === type)
291
+ return true;
292
+ return (node.children ?? []).some(child => containsType(child, type));
293
+ }
294
+ /**
295
+ * Why `node` may not be inserted under `ancestors` (outermost first, the new parent last; empty = the page),
296
+ * or null when it may. `node` is the whole subtree being placed — a Container holding a Form is refused
297
+ * inside a Form just as a bare Form is, and a link that holds a form is refused wherever it goes. Reordering a node inside its CURRENT parent is not an insert and
298
+ * never asks.
299
+ */
300
+ function insertRefusal(node, ancestors) {
301
+ if (!node)
302
+ return null;
303
+ if (node.type && FORM_STATE_BLOCK_TYPES.has(node.type))
304
+ return InsertRefusal.StateBlockAlone;
305
+ if (ancestors.some(a => a?.type === FORM_TYPE) && containsType(node, FORM_TYPE))
306
+ return InsertRefusal.NestedForm;
307
+ if (formContentInHost(node, ancestors.some(isInteractiveHost)))
308
+ return InsertRefusal.FormInInteractive;
309
+ const parent = ancestors[ancestors.length - 1];
310
+ const structural = structureRefusal(node, StructureEdit.Move, ancestors);
311
+ if (structural === StructureRefusal.NotAccepted)
312
+ return isSegmentedPart(parent) ? InsertRefusal.SegmentedAccepts : InsertRefusal.TabsAccepts;
313
+ if (structural)
314
+ return lockedRefusal(node, StructureEdit.Move);
315
+ // Q40: a fifth option (a paste, a duplicate, a drop from another control) is refused.
316
+ if (node.role === SegmentedRole.Option && parent?.role === SegmentedRole.Segmented
317
+ && (parent.children ?? []).filter(c => c?.role === SegmentedRole.Option).length >= SEGMENTED_MAX_OPTIONS)
318
+ return InsertRefusal.SegmentedMaxOptions;
319
+ return null;
320
+ }
321
+ /** Why `node` may not be REMOVED from where it is (deleted, cut, made global), or null when it may: a
322
+ * composite's locked part stays — its Parts switch or deleting the whole composite are the ways out — an
323
+ * accordion keeps its last item, Tabs their last tab, and a tab or a panel never leaves without its pair (the host
324
+ * offers to delete both). `siblings` = the list `node` sits in (where the caller has it). */
325
+ function removalRefusal(node, siblings) {
326
+ if (structureRefusal(node, StructureEdit.Delete))
327
+ return lockedRefusal(node, StructureEdit.Delete);
328
+ if (node?.role === AccordionRole.Item && siblings && siblings.filter(s => s?.role === AccordionRole.Item).length <= 1)
329
+ return InsertRefusal.LastItem;
330
+ // A Segmented control's option goes alone (it has no panel) — and the last two stay (Q40).
331
+ if (node?.role === SegmentedRole.Option) {
332
+ return siblings && siblings.filter(s => s?.role === SegmentedRole.Option).length <= SEGMENTED_MIN_OPTIONS ? InsertRefusal.SegmentedMinOptions : null;
333
+ }
334
+ if (isExclusiveItemPart(node)) {
335
+ const role = node.role === TabsRole.Trigger ? TabsRole.Trigger : TabsRole.Panel;
336
+ if (siblings && siblings.filter(s => s?.role === role).length <= 1)
337
+ return InsertRefusal.LastTab;
338
+ return InsertRefusal.PairedPart;
339
+ }
340
+ return null;
341
+ }
342
+
343
+ /**
344
+ * Nodes styled "WHEN AN ITEM IS ON" from outside it (composite T20; CC4b's price "when Yearly is on") — what the builder
345
+ * says about them: hidden at rest in the layer tree (the eye-slash, board 10), the reading-order audit finding (Q34) and
346
+ * the Style panel's "never shown" warning (Q38). Pure: every caller passes the trees.
347
+ */
348
+ const HIDDEN = 'none';
349
+ const DISPLAY = 'display';
350
+ const tree = (n) => n;
351
+ const cssId = (n) => nodeCssId(n) ?? n.id ?? '';
352
+ const stateOf = (n, state) => n.stateStyles?.[state];
353
+ /** The item a node's `trigger-on` state SHOWS it by — its state changes `display` — as that item's css id, else null. */
354
+ function displayTriggerOf(node) {
355
+ const trigger = node.stateTriggers?.[ITEM_OPEN_STATE];
356
+ const display = stateOf(node, ITEM_OPEN_STATE)?.[DISPLAY];
357
+ return trigger && display !== undefined && display !== '' ? trigger : null;
358
+ }
359
+ /** True when the node is hidden at its base (its own `display: none`). */
360
+ function hiddenAtBase(node) {
361
+ return String(readStyle(tree(node), DISPLAY) ?? '') === HIDDEN;
362
+ }
363
+ /** The css ids of every Manual composite item ON at rest — a Segmented control's default option, a Tabs' first tab, an
364
+ * accordion's items open by default. From-content rows are not nodes and are left out. */
365
+ function itemsOnAtRest(roots) {
366
+ const on = new Set();
367
+ const visit = (n) => {
368
+ if (isSegmentedRoot(n)) {
369
+ const key = segmentedDefaultOn(tree(n));
370
+ const option = segmentedOptions(tree(n)).find((o, i) => (o.itemKey || `item-${i}`) === key);
371
+ if (option)
372
+ on.add(cssId(option));
373
+ }
374
+ else if (isTabsRoot(n) && !isTabsFromContent(tree(n))) {
375
+ const [first] = tabsTriggers(tree(n));
376
+ if (first)
377
+ on.add(cssId(first));
378
+ }
379
+ else if (isAccordionRoot(n) && !isAccordionFromContent(tree(n))) {
380
+ const items = accordionItems(tree(n));
381
+ const open = initialAccordionOpen(items.map((it, i) => it.itemKey || `item-${i}`), accordionBehaviour(tree(n)).defaultOn);
382
+ items.forEach((it, i) => { if (open.has(it.itemKey || `item-${i}`))
383
+ on.add(cssId(it)); });
384
+ }
385
+ (n.children ?? []).forEach(visit);
386
+ };
387
+ roots.forEach(visit);
388
+ return on;
389
+ }
390
+ /** Hidden at rest because it shows only "when an item is on" and that item is not on at rest (board 10's eye-slash on
391
+ * Price · Yearly; Price · Monthly is on screen at rest, Monthly being the default). */
392
+ function hiddenUntilItemOn(node, onAtRest) {
393
+ const trigger = displayTriggerOf(node);
394
+ if (!trigger || !hiddenAtBase(node))
395
+ return false;
396
+ return String(stateOf(node, ITEM_OPEN_STATE)?.[DISPLAY]) !== HIDDEN && !onAtRest.has(trigger);
397
+ }
398
+ /**
399
+ * Q38: a node whose base is `display: none` and that NOTHING shows — no state, no breakpoint layer, no interaction that
400
+ * targets it. Deleting the "when Yearly is on" state of a price makes the price disappear for good; the Style panel
401
+ * says so. `targeted` = the ids interactions target anywhere on the page.
402
+ */
403
+ function neverShown(node, targeted = new Set()) {
404
+ if (!hiddenAtBase(node))
405
+ return false;
406
+ if (node.id && targeted.has(node.id))
407
+ return false;
408
+ const shows = (map) => !!map && map[DISPLAY] !== undefined && map[DISPLAY] !== '' && map[DISPLAY] !== HIDDEN;
409
+ for (const state of Object.values((node.stateStyles ?? {})))
410
+ if (shows(state))
411
+ return false;
412
+ for (const layer of Object.values(node.responsive ?? {})) {
413
+ if (shows(layer?.styleProps))
414
+ return false;
415
+ for (const state of Object.values((layer?.stateStyles ?? {})))
416
+ if (shows(state))
417
+ return false;
418
+ }
419
+ return true;
420
+ }
421
+ /** Every node id an interaction on the page targets (its actions' and reverse actions' `target`). */
422
+ function interactionTargets(roots) {
423
+ const out = new Set();
424
+ const visit = (n) => {
425
+ for (const ix of (n.interactions ?? [])) {
426
+ for (const a of [...(ix.actions ?? []), ...(ix.reverseActions ?? [])])
427
+ if (a.target)
428
+ out.add(a.target);
429
+ }
430
+ (n.children ?? []).forEach(visit);
431
+ };
432
+ roots.forEach(visit);
433
+ return out;
434
+ }
435
+ /**
436
+ * Q34 (no live region): a screen reader hears the new price because the control comes BEFORE it in reading order — so
437
+ * a node shown "when X is on" that sits before X (outside X) is read before the choice that changes it. One finding per
438
+ * such node. Reading order = document order across `roots` (the page's globals before / after it as given).
439
+ */
440
+ function shownBeforeControl(roots) {
441
+ const order = new Map();
442
+ const byCss = new Map();
443
+ const nodes = [];
444
+ const visit = (n) => {
445
+ order.set(cssId(n), nodes.length);
446
+ if (!byCss.has(cssId(n)))
447
+ byCss.set(cssId(n), n);
448
+ nodes.push(n);
449
+ (n.children ?? []).forEach(visit);
450
+ };
451
+ roots.forEach(visit);
452
+ const out = [];
453
+ for (const node of nodes) {
454
+ const trigger = displayTriggerOf(node);
455
+ const control = trigger ? byCss.get(trigger) : undefined;
456
+ if (!control || contains(control, node) || contains(node, control))
457
+ continue;
458
+ if ((order.get(cssId(node)) ?? 0) < (order.get(trigger) ?? 0))
459
+ out.push({ node, control });
460
+ }
461
+ return out;
462
+ }
463
+ function contains(outer, inner) {
464
+ return (outer.children ?? []).some(c => c === inner || contains(c, inner));
465
+ }
466
+
467
+ /**
468
+ * STRUCTURE OPS (A4) — the one-click layout changes people actually make at a narrow width, as pure
469
+ * "what to write" functions. Each returns a flat style map the caller stores in the node's
470
+ * `responsive[bp]` layer (or in the base styles at Base) through the shared `setOverride`.
471
+ *
472
+ * All of it is ordinary CSS the SSR engine already renders — there is no second tree and no
473
+ * per-width HTML. See responsive-breakpoints-plan.md (D5).
474
+ */
475
+ /** CSS lengths that are a FIXED pixel size — the ones that stop a layout fitting a phone. */
476
+ const PX_LENGTH = /^-?\d+(\.\d+)?px$/i;
477
+ /**
478
+ * Hide / show at a width. Hiding is `display: none` INSIDE that breakpoint's rules — the node stays
479
+ * in the DOM and in the SSR output (one HTML document serves every width), so hidden content still
480
+ * ships bytes and is still indexed. Showing again clears the override rather than guessing a value.
481
+ *
482
+ * @returns the props to write, or `null` meaning "clear the display override at this breakpoint".
483
+ */
484
+ function visibilityProps(hidden) {
485
+ return hidden ? { display: 'none' } : null;
486
+ }
487
+ /** True when this node is hidden by the resolved styles at some width. */
488
+ function isHiddenAt(resolvedStyleProps) {
489
+ return String(resolvedStyleProps?.['display'] ?? '') === 'none';
490
+ }
491
+ /**
492
+ * Stack a container's children in one column — the most common mobile edit, and four separate
493
+ * controls today (display, direction, wrap, columns). A grid becomes a single-column grid; anything
494
+ * else becomes a column flex box.
495
+ */
496
+ function stackChildrenProps(resolvedStyleProps) {
497
+ const display = String(resolvedStyleProps?.['display'] ?? '');
498
+ if (display === 'grid' || display === 'inline-grid') {
499
+ return { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))' };
500
+ }
501
+ return { display: 'flex', flexDirection: 'column', flexWrap: 'nowrap' };
502
+ }
503
+ /**
504
+ * Collapse a grid to `count` columns (a flex row is stacked instead — it has no column track).
505
+ * `count` is clamped to at least 1.
506
+ */
507
+ function collapseColumnsProps(resolvedStyleProps, count) {
508
+ const columns = Math.max(1, Math.floor(count));
509
+ const display = String(resolvedStyleProps?.['display'] ?? '');
510
+ if (display === 'grid' || display === 'inline-grid') {
511
+ return { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` };
512
+ }
513
+ return columns === 1 ? stackChildrenProps(resolvedStyleProps) : { flexWrap: 'wrap' };
514
+ }
515
+ /** How many equal columns a `repeat(N, …)` track list describes, or null when it isn't one. */
516
+ function columnCountOf(resolvedStyleProps) {
517
+ const match = /^repeat\(\s*(\d+)\s*,/.exec(String(resolvedStyleProps?.['gridTemplateColumns'] ?? ''));
518
+ return match ? Number(match[1]) : null;
519
+ }
520
+ /**
521
+ * Make fixed pixel sizes fluid: `width: 520px` → `width: 100%` with the original kept as
522
+ * `max-width`, so the element still never exceeds its designed size on a wide screen. `minWidth` in
523
+ * px is dropped to 0 — it is what actually forces horizontal scrolling on a phone.
524
+ *
525
+ * @returns the props to write; EMPTY when there is nothing fixed to fluidise.
526
+ */
527
+ function fluidiseProps(resolvedStyleProps) {
528
+ const out = {};
529
+ const width = String(resolvedStyleProps?.['width'] ?? '');
530
+ const minWidth = String(resolvedStyleProps?.['minWidth'] ?? '');
531
+ if (PX_LENGTH.test(width)) {
532
+ out['width'] = '100%';
533
+ const maxWidth = String(resolvedStyleProps?.['maxWidth'] ?? '');
534
+ if (!maxWidth || maxWidth === 'none')
535
+ out['maxWidth'] = width;
536
+ }
537
+ if (PX_LENGTH.test(minWidth) && parseFloat(minWidth) > 0)
538
+ out['minWidth'] = '0px';
539
+ return out;
540
+ }
541
+ /** True when `fluidiseProps` would change anything — drives the button's disabled state. */
542
+ function canFluidise(resolvedStyleProps) {
543
+ return Object.keys(fluidiseProps(resolvedStyleProps)).length > 0;
544
+ }
545
+ /**
546
+ * Reorder children AT ONE WIDTH using CSS `order`, leaving the tree — and so the DOM and
547
+ * screen-reader order — untouched. Returns one value per child BY CHILD INDEX: the `order` it must
548
+ * take, or `null` for "no override needed".
549
+ *
550
+ * Either every child gets an explicit `order` or none does: CSS defaults `order` to 0, so a partial
551
+ * set would mix overridden children with unordered ones and read as an arbitrary interleave.
552
+ *
553
+ * @param count How many children there are.
554
+ * @param from Index of the child being moved.
555
+ * @param to Index it should occupy.
556
+ */
557
+ function reorderProps(count, from, to) {
558
+ const none = Array.from({ length: Math.max(0, count) }, () => null);
559
+ if (from < 0 || from >= count || to < 0 || to >= count || from === to)
560
+ return none;
561
+ // Visual order after the move, as a list of CHILD indices.
562
+ const sequence = Array.from({ length: count }, (_, i) => i);
563
+ sequence.splice(to, 0, ...sequence.splice(from, 1));
564
+ const orderByChild = none.slice();
565
+ sequence.forEach((childIndex, position) => { orderByChild[childIndex] = position; });
566
+ return orderByChild;
567
+ }
568
+
569
+ /**
570
+ * RESPONSIVE AUDIT (A5) — find what is broken at a width, from the node tree plus ONE measurement
571
+ * pass. A builder that lets you author five widths must also tell you which of them are broken.
572
+ *
573
+ * Pure: the caller measures (one `getBoundingClientRect` per node, off the animation path) and
574
+ * passes the numbers in. Every finding that can be repaired carries the exact props to write, so a
575
+ * fix is one `setOverride` per key — one undo step, and it shows as an ordinary override.
576
+ */
577
+ var AuditCode;
578
+ (function (AuditCode) {
579
+ AuditCode["Overflow"] = "overflow";
580
+ AuditCode["FixedWidth"] = "fixed-width";
581
+ AuditCode["TapTarget"] = "tap-target";
582
+ AuditCode["TinyText"] = "tiny-text";
583
+ AuditCode["UnboundedImage"] = "unbounded-image";
584
+ /** A slider showing more slides than the width can carry — each one unreadably narrow. */
585
+ AuditCode["SlidesOverflow"] = "slides-overflow";
586
+ /** An out-of-flow node whose box lands outside the frame, so it is unreachable at this width. */
587
+ AuditCode["OffscreenAbsolute"] = "offscreen-absolute";
588
+ /** Form content inside a link or a button — a STRUCTURE finding, true at every width (form-nesting-audit.util). */
589
+ AuditCode["FormInLink"] = "form-in-link";
590
+ /** An icon-only button or link with no Accessible name — announced as an unnamed control (CC1 N6). */
591
+ AuditCode["UnnamedControl"] = "unnamed-control";
592
+ /** A composite whose items come from content, and its source returned no rows (CC2, board 9): the
593
+ * canvas shows the placeholder, the live site renders nothing. */
594
+ AuditCode["EmptySource"] = "empty-source";
595
+ /** CC4b (Q34): a node shown "when X is on" placed BEFORE its control X in reading order — a STRUCTURE finding. */
596
+ AuditCode["ShownBeforeControl"] = "shown-before-control";
597
+ /** CC4b: a Segmented control's radio group with no accessible name — a STRUCTURE finding. */
598
+ AuditCode["UnnamedGroup"] = "unnamed-group";
599
+ })(AuditCode || (AuditCode = {}));
600
+ /** A fix that changes the tree, not a style or a trait — applied as one page-history step. */
601
+ var StructureFix;
602
+ (function (StructureFix) {
603
+ /** Move the form out of the link or button holding it, to right after it. */
604
+ StructureFix["MoveFormOut"] = "move-form-out";
605
+ })(StructureFix || (StructureFix = {}));
606
+ var AuditSeverity;
607
+ (function (AuditSeverity) {
608
+ AuditSeverity["Error"] = "error";
609
+ AuditSeverity["Warning"] = "warning";
610
+ AuditSeverity["Info"] = "info";
611
+ })(AuditSeverity || (AuditSeverity = {}));
612
+ /** WCAG 2.5.8 minimum target size. */
613
+ const MIN_TAP_PX = 44;
614
+ /** Below this, iOS Safari zooms the page when an adjacent input is focused. */
615
+ const MIN_TEXT_PX = 12;
616
+ /** Types a visitor taps. */
617
+ const TAPPABLE = new Set(['Button', 'ButtonBlock', 'Link', 'LinkedContainer']);
618
+ /** Narrower than this, a slide shows too little of its content to be worth showing at all. */
619
+ const MIN_SLIDE_PX = 200;
620
+ /** Out-of-flow positioning that can put a node outside the frame. */
621
+ const OUT_OF_FLOW = new Set(['absolute', 'fixed']);
622
+ const nameOf = (node) => node.label || node.type;
623
+ const pxOf = (value) => {
624
+ const match = /^(-?\d+(?:\.\d+)?)px$/i.exec(String(value ?? ''));
625
+ return match ? Number(match[1]) : null;
626
+ };
627
+ /**
628
+ * Audit nodes laid out at `viewportWidth`. Returns findings ordered by severity (errors first),
629
+ * then by the order the nodes were given — so the worst thing is at the top of the list.
630
+ */
631
+ function auditNodes(nodes, viewportWidth) {
632
+ const findings = [];
633
+ for (const node of nodes) {
634
+ const name = nameOf(node);
635
+ const styles = node.styleProps ?? {};
636
+ if (String(styles['display'] ?? '') === 'none')
637
+ continue; // hidden here — nothing to report
638
+ // 1. Wider than the viewport: the page scrolls sideways.
639
+ if (node.width > viewportWidth + 0.5) {
640
+ findings.push({
641
+ nodeId: node.id, nodeName: name, code: AuditCode.Overflow, severity: AuditSeverity.Error,
642
+ message: `${name} is ${Math.round(node.width)}px wide in a ${viewportWidth}px viewport — the page scrolls sideways.`,
643
+ fix: canFluidise(styles) ? fluidiseProps(styles) : { maxWidth: '100%' },
644
+ });
645
+ }
646
+ else if (canFluidise(styles)) {
647
+ // 2. A fixed px size that still fits, but will not on a narrower phone.
648
+ const width = pxOf(styles['width']);
649
+ findings.push({
650
+ nodeId: node.id, nodeName: name, code: AuditCode.FixedWidth, severity: AuditSeverity.Warning,
651
+ message: width !== null
652
+ ? `${name} has a fixed width of ${width}px — it cannot shrink on a narrower screen.`
653
+ : `${name} has a fixed minimum width — it forces sideways scrolling below that size.`,
654
+ fix: fluidiseProps(styles),
655
+ });
656
+ }
657
+ // 3. Tap targets below the WCAG minimum.
658
+ if (TAPPABLE.has(node.type) && node.width > 0 && node.height > 0
659
+ && (node.width < MIN_TAP_PX || node.height < MIN_TAP_PX)) {
660
+ findings.push({
661
+ nodeId: node.id, nodeName: name, code: AuditCode.TapTarget, severity: AuditSeverity.Warning,
662
+ message: `${name} is ${Math.round(node.width)}×${Math.round(node.height)}px — below the ${MIN_TAP_PX}px minimum tap target.`,
663
+ fix: { minHeight: `${MIN_TAP_PX}px`, minWidth: `${MIN_TAP_PX}px` },
664
+ });
665
+ }
666
+ // 4. Text too small to read (and small enough to trigger iOS zoom).
667
+ const fontSize = pxOf(styles['fontSize']);
668
+ if (fontSize !== null && fontSize < MIN_TEXT_PX) {
669
+ findings.push({
670
+ nodeId: node.id, nodeName: name, code: AuditCode.TinyText, severity: AuditSeverity.Warning,
671
+ message: `${name} renders at ${fontSize}px — under the ${MIN_TEXT_PX}px readable minimum.`,
672
+ fix: { fontSize: `${MIN_TEXT_PX}px` },
673
+ });
674
+ }
675
+ // 5. An image with a fixed height and no ratio crops unpredictably as the width changes.
676
+ if (node.type === 'Image' && pxOf(styles['height']) !== null
677
+ && !String(styles['aspectRatio'] ?? '').trim().replace(/^auto$/, '')) {
678
+ findings.push({
679
+ nodeId: node.id, nodeName: name, code: AuditCode.UnboundedImage, severity: AuditSeverity.Info,
680
+ message: `${name} has a fixed height and no aspect ratio — it crops unpredictably as the width changes.`,
681
+ fix: { height: 'auto', aspectRatio: '16 / 9' },
682
+ });
683
+ }
684
+ // 6. A slider showing more slides than this width can carry. NOT a CSS problem: slide count
685
+ // is a component trait, so the fix is a per-breakpoint TRAIT override (P3-G1), not a style.
686
+ if (node.type === 'SliderContainer' && node.width > 0) {
687
+ const perView = Number(node.inputs?.['slidesPerView']);
688
+ if (Number.isFinite(perView) && perView > 1) {
689
+ const slideWidth = node.width / perView;
690
+ if (slideWidth < MIN_SLIDE_PX) {
691
+ const fits = Math.max(1, Math.floor(node.width / MIN_SLIDE_PX));
692
+ findings.push({
693
+ nodeId: node.id, nodeName: name, code: AuditCode.SlidesOverflow, severity: AuditSeverity.Warning,
694
+ message: `${name} shows ${perView} slides across ${Math.round(node.width)}px — ${Math.round(slideWidth)}px each. Show ${fits} at this width.`,
695
+ inputFix: { slidesPerView: fits },
696
+ });
697
+ }
698
+ }
699
+ }
700
+ // 7. An icon-only control with no name (WCAG 4.1.2): a screen reader says "button" and nothing
701
+ // else. No auto-fix — only the author knows what it does; the finding selects it.
702
+ if (node.unnamedControl) {
703
+ findings.push({
704
+ nodeId: node.id, nodeName: name, code: AuditCode.UnnamedControl, severity: AuditSeverity.Error,
705
+ message: `${name} shows only an icon and has no accessible name — screen readers announce it as an unnamed control. Name it in Bindings ▸ Accessibility.`,
706
+ });
707
+ }
708
+ // 7b. A composite from content whose source has no rows (CC2 R3): the visitor sees nothing there.
709
+ if (node.emptySource) {
710
+ findings.push({
711
+ nodeId: node.id, nodeName: name, code: AuditCode.EmptySource, severity: AuditSeverity.Warning,
712
+ message: `${name} in ${node.emptySource.section}: its source ${node.emptySource.alias} has no rows.`,
713
+ });
714
+ }
715
+ // 8. An out-of-flow node that has left the frame. Measured, not inferred: a negative offset
716
+ // is legitimate design until it actually puts the content beyond the edge.
717
+ if (OUT_OF_FLOW.has(String(styles['position'] ?? '')) && node.left !== undefined && node.width > 0) {
718
+ const right = node.left + node.width;
719
+ const offLeft = right <= 0.5;
720
+ const offRight = node.left >= viewportWidth - 0.5;
721
+ if (offLeft || offRight) {
722
+ findings.push({
723
+ nodeId: node.id, nodeName: name, code: AuditCode.OffscreenAbsolute, severity: AuditSeverity.Error,
724
+ message: `${name} is positioned ${offLeft ? 'left of' : 'beyond'} the ${viewportWidth}px viewport — it cannot be seen or tapped at this width.`,
725
+ // No blind auto-fix: the author's intent (a decorative overflow vs a mistake)
726
+ // is not knowable from geometry. Selecting the node is the useful action.
727
+ });
728
+ }
729
+ }
730
+ }
731
+ const rank = { [AuditSeverity.Error]: 0, [AuditSeverity.Warning]: 1, [AuditSeverity.Info]: 2 };
732
+ return findings
733
+ .map((finding, index) => ({ finding, index }))
734
+ .sort((a, b) => rank[a.finding.severity] - rank[b.finding.severity] || a.index - b.index)
735
+ .map(entry => entry.finding);
736
+ }
737
+ /** Counts per severity — the per-breakpoint summary strip. */
738
+ function summarise(findings) {
739
+ const out = { [AuditSeverity.Error]: 0, [AuditSeverity.Warning]: 0, [AuditSeverity.Info]: 0 };
740
+ for (const finding of findings)
741
+ out[finding.severity]++;
742
+ return out;
743
+ }
744
+
745
+ const SHOWN_BEFORE_KEY = 'builder.chrome.auditShownBeforeControl';
746
+ const UNNAMED_GROUP_KEY = 'builder.chrome.auditUnnamedGroup';
747
+ const CONTROL_PLACEHOLDER = '{control}';
748
+ /**
749
+ * The composites' STRUCTURE findings (CC4b, board 10) — true at every width, so the audit lists them beside the form
750
+ * nesting ones:
751
+ * - Q34: a node shown "when X is on" that sits BEFORE its control X in reading order — a screen reader reads it before
752
+ * the choice that changes it (there is no live region, on purpose);
753
+ * - a Segmented control's radio group with no accessible name.
754
+ * No auto-fix: selecting the finding selects the node.
755
+ */
756
+ function compositeStructureFindings(page, globals, labels = defaultBuilderLabels) {
757
+ const say = (key) => labels(key, BUILDER_CHROME_EN[key]);
758
+ const findings = [];
759
+ for (const { node, control } of shownBeforeControl([...page, ...globals])) {
760
+ if (!node.id)
761
+ continue;
762
+ findings.push({
763
+ nodeId: node.id, nodeName: node.label || node.type, code: AuditCode.ShownBeforeControl, severity: AuditSeverity.Warning,
764
+ message: say(SHOWN_BEFORE_KEY).replace(CONTROL_PLACEHOLDER, exclusiveItemLabel(control) || control.label || control.type),
765
+ });
766
+ }
767
+ const visit = (n) => {
768
+ if (n.id && needsGroupName(n)) {
769
+ findings.push({ nodeId: n.id, nodeName: n.label || n.type, code: AuditCode.UnnamedGroup, severity: AuditSeverity.Error, message: say(UNNAMED_GROUP_KEY) });
770
+ }
771
+ (n.children ?? []).forEach(visit);
772
+ };
773
+ [...page, ...globals].forEach(visit);
774
+ return findings;
775
+ }
776
+
777
+ /**
778
+ * node-path — pure ancestor-chain helpers for the builder's node tree.
779
+ *
780
+ * The builder needs a node's ancestor chain in several places: the breadcrumb bar,
781
+ * "select parent", and the parent-scoped style traits (Child Layout). Each of those
782
+ * used to re-walk the tree with its own private recursion. `nodeAncestorPath` is the
783
+ * single walk they all derive from.
784
+ *
785
+ * Builder-only on purpose: the SSR engine never needs a breadcrumb, so this stays in
786
+ * `iox-builder` rather than `@vectoriox/iox-ui` (which is the home for logic BOTH
787
+ * renderers consume). Structural helpers the two renderers share live in
788
+ * `@vectoriox/iox-ui` `style-compiler/tree-relations.ts`.
789
+ */
790
+ /**
791
+ * The chain of nodes from a root down to (and including) `target`, matched by
792
+ * REFERENCE identity. Returns `null` when `target` is not in the tree.
793
+ *
794
+ * Depth-first, pre-order; first match wins. Reference identity (not `id`) is the right
795
+ * matcher here because the builder mutates nodes in place and repeater/slider clones
796
+ * deliberately share a template's `styleId` — matching by id would return the template's
797
+ * path for a clone.
798
+ */
799
+ function nodeAncestorPath(roots, target) {
800
+ if (!roots?.length || !target)
801
+ return null;
802
+ for (const node of roots) {
803
+ if (node === target)
804
+ return [node];
805
+ const childPath = nodeAncestorPath(node.children, target);
806
+ if (childPath)
807
+ return [node, ...childPath];
808
+ }
809
+ return null;
810
+ }
811
+ /**
812
+ * The immediate parent of `target`, or `null` when it is a root node or not in the tree.
813
+ * Derived from `nodeAncestorPath` so there is exactly one tree walk to keep correct.
814
+ */
815
+ function findParentOf(roots, target) {
816
+ const path = nodeAncestorPath(roots, target);
817
+ return path && path.length >= 2 ? path[path.length - 2] : null;
818
+ }
819
+ /**
820
+ * The ancestor chain across BOTH builder trees — the page layout and the global
821
+ * elements overlay, which are separate roots. Returns the first tree that contains
822
+ * the node, or `null` when neither does.
823
+ */
824
+ function nodeAncestorPathInEither(layout, globalElements, target) {
825
+ return nodeAncestorPath(layout, target) ?? nodeAncestorPath(globalElements, target);
826
+ }
827
+
828
+ /**
829
+ * A tab and its panel go TOGETHER on the canvas (CC4a — composite T18): deleting either deletes both ("Delete tab and
830
+ * its panel"), duplicating either duplicates both under one fresh key. The pairing is composite-core's (`itemKey`);
831
+ * this is the builder's tree surgery over it, shared by every path (toolbar, layer tree, the host's Cmd+D / Delete).
832
+ */
833
+ /** The Tabs a tab or a panel belongs to — a MANUAL one (From content, the rows are not nodes) — or null (a Segmented
834
+ * control's option has no pair: it is duplicated and deleted alone, CC4b). */
835
+ function tabsRootForPart(layout, globals, node) {
836
+ if (!isExclusiveItemPart(node) || !isTabsPart(node))
837
+ return null;
838
+ const path = nodeAncestorPathInEither(layout, globals, node) ?? [];
839
+ const root = exclusiveRootOf(node, path.filter(n => n !== node));
840
+ return root && !isTabsFromContent(root) ? root : null;
841
+ }
842
+ /** The words the pair is named by in the confirm: its tab's. */
843
+ function tabPairLabel(root, node) {
844
+ const tree = root;
845
+ const tab = node.role === TabsRole.Panel ? tabTriggerOf(tree, node) : node;
846
+ return tab ? tabLabel(tab) : '';
847
+ }
848
+ /** Delete a tab and its panel. True when the pair was removed. */
849
+ function deleteTabPair(root, node) {
850
+ return removeTabPair(root, node);
851
+ }
852
+ /**
853
+ * Duplicate a tab and its panel: copies (`clone` gives fresh ids) placed right after the originals, both under ONE new
854
+ * 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`.
855
+ */
856
+ function duplicateTabPair(root, node, clone) {
857
+ const tree = root;
858
+ const tab = (node.role === TabsRole.Panel ? tabTriggerOf(tree, node) : node);
859
+ const panel = tab ? tabPanelOf(tree, tab) : undefined;
860
+ const list = tabsListOf(tree);
861
+ const panels = tabsPanelsOf(tree);
862
+ if (!tab || !panel || !list?.children || !panels?.children)
863
+ return null;
864
+ const key = nextTabKey(tree);
865
+ const tabCopy = clone(tab);
866
+ const panelCopy = clone(panel);
867
+ tabCopy.itemKey = panelCopy.itemKey = key;
868
+ list.children = insertAfter(list.children, tab, tabCopy);
869
+ panels.children = insertAfter(panels.children, panel, panelCopy);
870
+ return tabCopy;
871
+ }
872
+ function insertAfter(list, anchor, node) {
873
+ const out = [...list];
874
+ out.splice(out.indexOf(anchor) + 1, 0, node);
875
+ return out;
876
+ }
877
+
878
+ var PanelEventTypes;
879
+ (function (PanelEventTypes) {
880
+ PanelEventTypes["ELEMENT_SELECT"] = "elementselect";
881
+ PanelEventTypes["ELEMENT_DESELECT"] = "elementdeselect";
882
+ PanelEventTypes["PANEL_SELECTED"] = "panelselect";
883
+ PanelEventTypes["PANEL_OPEN"] = "panelopen";
884
+ PanelEventTypes["PANEL_CLOSE"] = "panelclose";
885
+ PanelEventTypes["BINDING_CHANGED"] = "bindingchanged";
886
+ PanelEventTypes["NODE_RENDERED"] = "noderendered";
887
+ /** Open the Style panel in preset-edit mode for the given preset ID. */
888
+ PanelEventTypes["PRESET_EDIT"] = "presetedit";
889
+ /** Fired after a preset is created, updated, or deleted so panels refresh their list. */
890
+ PanelEventTypes["PRESETS_CHANGED"] = "presetschanged";
891
+ /** Fired when a linked symbol instance's styles change — propagates to all instances. */
892
+ PanelEventTypes["SYMBOLS_CHANGED"] = "symbolschanged";
893
+ /** Fired on every structural or style mutation — used by BuilderHistoryService to snapshot. */
894
+ PanelEventTypes["LAYOUT_CHANGED"] = "layoutchanged";
895
+ /** Open the Interactions panel in preset-edit mode for the given preset ID. */
896
+ PanelEventTypes["INTERACTION_PRESET_EDIT"] = "interactionpresetedit";
897
+ /** Fired after an interaction preset is created, updated, or deleted so panels refresh. */
898
+ PanelEventTypes["INTERACTION_PRESETS_CHANGED"] = "interactionpresetschanged";
899
+ /** Open the Interactions panel and EXPAND a specific interaction for editing (data = interaction
900
+ * id). Emitted when the overlay interaction badge is clicked so that badge → "edit THIS
901
+ * interaction", without firing the element's (live) animation. */
902
+ PanelEventTypes["INTERACTION_EDIT"] = "interactionedit";
903
+ /** A node's `interactions` were added/removed/edited. Carries `{ node }`.
904
+ * Repeater/Slider listen so their PREVIEW CLONES are re-cloned with the new interactions —
905
+ * clones are attached by ELEMENT at render time, so without this a new interaction only starts
906
+ * working after a save + reload. */
907
+ PanelEventTypes["INTERACTION_CHANGED"] = "interactionchanged";
908
+ /** CLICK-TO-INSERT (B2): a components-panel entry was clicked. Carries the same payload a drag
909
+ * would (`string` component type, or a block / symbol object). The builder resolves WHERE from
910
+ * the current selection — see `resolveInsertTarget`. */
911
+ PanelEventTypes["INSERT_COMPONENT"] = "insertcomponent";
912
+ /** Insert prebuilt node(s) as GLOBAL elements (data = ComponentNode[]) — the consent preset's click and
913
+ * the site header's click or drop (CC1.1). The HOST handles it (it owns the globals, their undo step and
914
+ * the one-header rule); the builder only routes a dropped global payload here (`globalInsert$`). */
915
+ PanelEventTypes["INSERT_GLOBAL"] = "insertglobal";
916
+ /** A panel makes `data.node` a GLOBAL element at `data.position` (default before) — the same move as the
917
+ * toolbar's Make Global (the Style panel's Global switch, Bindings ▸ Phone menu ▸ "Make it the site header"). */
918
+ PanelEventTypes["MAKE_GLOBAL"] = "make-global";
919
+ /** A panel returns the global `data.node` to the page — the toolbar's Remove Global. */
920
+ PanelEventTypes["REMOVE_GLOBAL"] = "remove-global";
921
+ /** A panel moved the global `data.node` to `data.position` (before / after the page content). */
922
+ PanelEventTypes["GLOBAL_POSITION"] = "update-global-position";
923
+ /** A click on a compound component's PART (Field label/control/help/…) — the style panel
924
+ * activates that part (data = { part }). Doron 2026-10-10: "both shall be selectable". */
925
+ PanelEventTypes["PART_SELECT"] = "partselect";
926
+ /** A panel wants to select a NODE it already has a reference to (e.g. the Form's nested field
927
+ * list). Carries `{ node }`. `OverlayService.setSelect()` alone only updates the overlay's own
928
+ * visual state — `selectedItem`/`selectedPath` (breadcrumb) and every other panel's idea of
929
+ * "what's selected" stay stale unless the SAME `_emitElementSelect` a real canvas click goes
930
+ * through also runs. This event is that path for code outside `BuilderComponent` itself,
931
+ * mirroring what `onTreeNodeSelect`/`onCrumbSelect` already do internally. */
932
+ PanelEventTypes["SELECT_NODE"] = "selectnode";
933
+ /** A panel rewrote a node's SUBTREE in place — its children (a Card's Parts), its layout styles (Card ▸
934
+ * Layout), a part's element style or scheme. Carries `{ node }`. The builder re-renders the subtree,
935
+ * re-registers every node's rules and design classes, refreshes a Repeater holding it, and records ONE
936
+ * undo step (LAYOUT_CHANGED). */
937
+ PanelEventTypes["NODE_STRUCTURE_CHANGED"] = "nodestructurechanged";
938
+ /** A Repeater's rows changed on the canvas (fetched, re-fetched, its source cleared). Carries `{ nodeId,
939
+ * count }`. Not a page edit — never an undo step: the layer tree redraws its "N more rows" line. */
940
+ PanelEventTypes["REPEATER_ROWS_CHANGED"] = "repeaterrowschanged";
941
+ })(PanelEventTypes || (PanelEventTypes = {}));
942
+ class PanelEventService {
943
+ constructor() {
944
+ this.panelEventSubject = new Subject();
945
+ }
946
+ subscribe(next) {
947
+ return this.panelEventSubject.asObservable().subscribe(next);
948
+ }
949
+ emit(type, data) {
950
+ this.panelEventSubject.next({ type, data });
951
+ }
952
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
953
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService }); }
954
+ }
955
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService, decorators: [{
956
+ type: Injectable
957
+ }] });
958
+
959
+ /**
960
+ * Deep-clones a ComponentNode tree and overrides trait defaults
961
+ * according to each node's bindings resolved against `item`.
962
+ *
963
+ * Each clone gets a new `id` (for Angular tracking / overlay registration)
964
+ * but shares the original node's CSS class by setting `styleId = originalId`.
965
+ * This means all repeated preview rows point to the same CSS rule block as the
966
+ * template node — the style engine only needs one rule per logical slot.
967
+ */
968
+ function cloneNodesForItem(nodes, item) {
969
+ return nodes.map(node => {
970
+ // Capture the CSS-owning id before clone
971
+ const cssId = node.styleId ?? node.id;
972
+ const clone = JSON.parse(JSON.stringify(node));
973
+ clone.id = Math.random().toString(36).substr(2, 9);
974
+ // Point this clone and all its descendants at the original CSS rule.
975
+ clone.styleId = cssId;
976
+ // Mark as a preview clone: display-only, not selectable/editable on the canvas.
977
+ // (Distinct from styleId, which duplication/paste also uses.) The render directive
978
+ // skips overlay registration for nodes carrying this flag.
979
+ clone.__previewClone = true;
980
+ if (clone.bindings?.length && item) {
981
+ for (const binding of clone.bindings) {
982
+ // Path lookup AND pipes (UI "Filters"). Resolving the path alone left a `date` or
983
+ // `uppercase` filter unapplied on every preview row, so the canvas showed the raw
984
+ // value while the SSR engine showed the formatted one — see binding-filters-plan.md.
985
+ const value = resolveBoundValue(item, binding);
986
+ if (value !== undefined) {
987
+ for (const trait of clone.traits || []) {
988
+ if (trait.name === binding.prop) {
989
+ trait.default = value;
990
+ }
991
+ }
992
+ }
993
+ }
994
+ }
995
+ if (clone.children?.length) {
996
+ clone.children = cloneNodesForItem(clone.children, item);
997
+ }
998
+ return clone;
999
+ });
1000
+ }
1001
+
1002
+ /** The content type a data source reads (`faqs`), or '' for a source that reads none (external, static). */
1003
+ function sourceContentType(ds) {
1004
+ return ds?.request?.cms?.contentType || ds?.request?.shop?.contentType || '';
1005
+ }
1006
+ /**
1007
+ * What the builder's chrome calls the list a source reads — the CONTENT TYPE's label ("From FAQ", board 3), as the
1008
+ * host knows it (`IOX_CONTENT_TYPE_LABELS`), else the source's alias. ONE answer for the layer tree's
1009
+ * "From FAQ · 3 rows", the Repeater's empty-source hint and the Items section's source options.
1010
+ */
1011
+ function sourceTypeLabel(alias, ds, resolve) {
1012
+ const type = sourceContentType(ds);
1013
+ return (type && resolve(type)) || alias;
1014
+ }
1015
+
1016
+ const freshId = () => Math.random().toString(36).substr(2, 9);
1017
+ /**
1018
+ * Assign fresh ids to a cloned ComponentNode subtree AND remap intra-subtree references to the
1019
+ * new ids. Used by every author-time clone path (duplicate, paste, block/external drop) so a copied
1020
+ * structure stays self-consistent.
1021
+ *
1022
+ * Absolute node-id references that break under duplication if left unmapped:
1023
+ * - `stateTriggers[state]` — the trigger-hover target (see style-engine.md → Targeted states)
1024
+ * - interaction `action.target` / `reverseActions.target`
1025
+ * A reference pointing OUTSIDE the cloned subtree (id not in the map) is left untouched — the copy
1026
+ * intentionally still points at the external node.
1027
+ *
1028
+ * TRAIT values are NOT ids and are copied verbatim — with one exception. A trait that carries an
1029
+ * IDENTITY rather than a setting must be reissued, or the copy silently shares the original's
1030
+ * identity. `formId` is the first: two forms with the same id share one submissions stream, and the
1031
+ * server resolves a submission against whichever page it reaches first. See REGENERATED_TRAITS.
1032
+ *
1033
+ * `styleId` strategy:
1034
+ * - `'clear'` (default) — drop styleId so the clone OWNS its own CSS rule (independent copy).
1035
+ * - `'regenerate'` — keep a styleId but give it a fresh value (legacy builder.component path).
1036
+ */
1037
+ /**
1038
+ * Trait names whose value is an IDENTITY, not a setting, and must therefore be reissued on every
1039
+ * clone. Add a name here the moment a trait starts identifying something rather than describing it.
1040
+ *
1041
+ * `formId` — a form's submissions stream. Duplicating a page, pasting a form or dropping a saved
1042
+ * block twice used to produce two forms claiming the same stream.
1043
+ */
1044
+ const REGENERATED_TRAITS = new Set(['formId']);
1045
+ /** Reissue identity-carrying traits on a node, preserving the original's prefix where it had one. */
1046
+ function reissueIdentityTraits(node) {
1047
+ for (const trait of (node.traits ?? [])) {
1048
+ if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
1049
+ continue;
1050
+ if (trait.default === undefined || trait.default === '')
1051
+ continue;
1052
+ // Keep the human-readable prefix a preset gave it (`contact-a1b2c3` → `contact-<new>`), so a
1053
+ // duplicated contact form is still recognisably a contact form in the inbox.
1054
+ const prefix = String(trait.default).split('-')[0];
1055
+ trait.default = `${prefix}-${freshId()}`;
1056
+ }
1057
+ }
1058
+ /**
1059
+ * A NEW node is born with its identity: an empty identity trait (a palette Form's `formId`) is issued,
1060
+ * one already set (a preset's `contact-…`) is kept. A Form inserted from the palette used to keep
1061
+ * `formId: ''` — its submissions posted to `forms//submit` and nothing was ever stored.
1062
+ */
1063
+ function issueIdentityTraits(root) {
1064
+ for (const trait of (root.traits ?? [])) {
1065
+ if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
1066
+ continue;
1067
+ if (trait.default === undefined || trait.default === '')
1068
+ trait.default = `${IDENTITY_PREFIX[trait.name] ?? trait.name}-${freshId()}`;
1069
+ }
1070
+ for (const child of root.children ?? [])
1071
+ if (child)
1072
+ issueIdentityTraits(child);
1073
+ }
1074
+ /** The readable prefix a freshly issued identity carries (`form-a1b2c3`). */
1075
+ const IDENTITY_PREFIX = { formId: 'form' };
1076
+ function regenerateNodeIds(root, opts = {}) {
1077
+ const idMap = new Map();
1078
+ // A composite item's identity (CC2 `itemKey`) is reissued per copy, consistently inside it: a duplicated
1079
+ // accordion item must not share its original's ARIA ids (composite plan §3).
1080
+ const keyMap = new Map();
1081
+ const assign = (node) => {
1082
+ const oldId = node.id;
1083
+ node.id = freshId();
1084
+ reissueIdentityTraits(node);
1085
+ if (node.itemKey) {
1086
+ if (!keyMap.has(node.itemKey))
1087
+ keyMap.set(node.itemKey, `k-${freshId()}`);
1088
+ node.itemKey = keyMap.get(node.itemKey);
1089
+ }
1090
+ if (opts.styleId === 'regenerate') {
1091
+ if (node.styleId)
1092
+ node.styleId = freshId();
1093
+ }
1094
+ else {
1095
+ node.styleId = undefined;
1096
+ }
1097
+ if (oldId)
1098
+ idMap.set(oldId, node.id);
1099
+ (node.children || []).forEach(assign);
1100
+ };
1101
+ assign(root);
1102
+ remapNodeReferences(root, idMap);
1103
+ }
1104
+ /** Remap absolute node-id references inside a subtree to their new ids (per `idMap`). Ids not in
1105
+ * the map (references to nodes outside the cloned subtree) are preserved. */
1106
+ function remapNodeReferences(node, idMap) {
1107
+ const triggers = node.stateTriggers;
1108
+ if (triggers) {
1109
+ for (const state of Object.keys(triggers)) {
1110
+ const mapped = idMap.get(triggers[state]);
1111
+ if (mapped)
1112
+ triggers[state] = mapped;
1113
+ }
1114
+ }
1115
+ for (const interaction of node.interactions || []) {
1116
+ const actions = [...(interaction.actions || []), ...(interaction.reverseActions || [])];
1117
+ for (const action of actions) {
1118
+ if (action?.target && action.target !== 'self') {
1119
+ const mapped = idMap.get(action.target);
1120
+ if (mapped)
1121
+ action.target = mapped;
1122
+ }
1123
+ }
1124
+ }
1125
+ (node.children || []).forEach(child => remapNodeReferences(child, idMap));
658
1126
  }
659
- function containsType(node, type) {
660
- if (node.type === type)
661
- return true;
662
- return (node.children ?? []).some(child => containsType(child, type));
1127
+
1128
+ /**
1129
+ * The text on the drop indicator's chip — "into Card grid · position 2 of 3" (B2).
1130
+ *
1131
+ * The most common drag failure is landing a node one level off and not noticing; naming the PARENT
1132
+ * and the slot makes the target unambiguous before the pointer is released.
1133
+ *
1134
+ * @param containerName The target container's label or type; blank / unknown → "the page".
1135
+ * @param index Insert index within the container (0-based).
1136
+ * @param childCount How many children it has now — the node being dropped makes `childCount + 1`.
1137
+ */
1138
+ function dropTargetLabel(containerName, index, childCount) {
1139
+ const name = (containerName ?? '').trim() || 'the page';
1140
+ const total = Math.max(0, childCount) + 1;
1141
+ const position = Math.min(Math.max(index, 0), total - 1) + 1;
1142
+ return `into ${name} · position ${position} of ${total}`;
663
1143
  }
1144
+
664
1145
  /**
665
- * Why `node` may not be inserted under `ancestors` (outermost first, the new parent last; empty = the page),
666
- * or null when it may. `node` is the whole subtree being placed — a Container holding a Form is refused
667
- * inside a Form just as a bare Form is, and a link that holds a form is refused wherever it goes. Reordering a node inside its CURRENT parent is not an insert and
668
- * never asks.
1146
+ * Per-frame exponential smoothing step — moves `current` a fraction of the way
1147
+ * toward `target`. Used to ease the drag ghost toward the pointer instead of
1148
+ * snapping to it 1:1 on every dragmove (see DragEngineService._ghostFollowStep).
669
1149
  */
670
- function insertRefusal(node, ancestors) {
671
- if (!node)
672
- return null;
673
- if (node.type && FORM_STATE_BLOCK_TYPES.has(node.type))
674
- return InsertRefusal.StateBlockAlone;
675
- if (ancestors.some(a => a?.type === FORM_TYPE) && containsType(node, FORM_TYPE))
676
- return InsertRefusal.NestedForm;
677
- if (formContentInHost(node, ancestors.some(isInteractiveHost)))
678
- return InsertRefusal.FormInInteractive;
679
- const structural = structureRefusal(node, StructureEdit.Move, ancestors);
680
- if (structural === StructureRefusal.NotAccepted)
681
- return InsertRefusal.TabsAccepts;
682
- if (structural)
683
- return lockedRefusal(node, StructureEdit.Move);
684
- return null;
1150
+ function easeToward(current, target, smoothing) {
1151
+ return current + (target - current) * smoothing;
685
1152
  }
686
- /** Why `node` may not be REMOVED from where it is (deleted, cut, made global), or null when it may: a
687
- * composite's locked part stays — its Parts switch or deleting the whole composite are the ways out — an
688
- * accordion keeps its last item, Tabs their last tab, and a tab or a panel never leaves without its pair (the host
689
- * offers to delete both). `siblings` = the list `node` sits in (where the caller has it). */
690
- function removalRefusal(node, siblings) {
691
- if (structureRefusal(node, StructureEdit.Delete))
692
- return lockedRefusal(node, StructureEdit.Delete);
693
- if (node?.role === AccordionRole.Item && siblings && siblings.filter(s => s?.role === AccordionRole.Item).length <= 1)
694
- return InsertRefusal.LastItem;
695
- if (isExclusiveItemPart(node)) {
696
- const role = node.role === TabsRole.Trigger ? TabsRole.Trigger : TabsRole.Panel;
697
- if (siblings && siblings.filter(s => s?.role === role).length <= 1)
698
- return InsertRefusal.LastTab;
699
- return InsertRefusal.PairedPart;
700
- }
701
- return null;
1153
+
1154
+ /**
1155
+ * Decides which element a drag gesture should actually operate on.
1156
+ *
1157
+ * A drag must never silently descend into a nested child just because the pointer happened to
1158
+ * be over it while its ANCESTOR is the currently selected node, and the overlay's drag-handle
1159
+ * (which carries no node of its own — it lives outside the canvas DOM entirely) must always
1160
+ * redirect to whatever is currently selected. In both cases the redirect target is the
1161
+ * currently-selected element; otherwise the gesture proceeds on its natural origin unchanged.
1162
+ */
1163
+ function resolveDragTarget(el, selectedElement, isDragHandle) {
1164
+ if (!selectedElement || el === selectedElement)
1165
+ return el;
1166
+ const isDescendantOfSelected = selectedElement.contains(el);
1167
+ return (isDragHandle || isDescendantOfSelected) ? selectedElement : el;
702
1168
  }
703
1169
 
704
1170
  /**
@@ -1281,6 +1747,9 @@ class TypographyGroupStyleConfig extends GroupStyleConfig {
1281
1747
  ], TextAlign.Start),
1282
1748
  new TraitConfig('fontStyle', '', TraitInputType.SelectButton, { items: [{ value: 'italic', icon: 'ph-thin ph-text-italic', label: 'Italic' }], allowEmpty: true, noneValue: 'normal' }, 'normal', true),
1283
1749
  new TraitConfig('textDecoration', '', TraitInputType.SelectButton, { items: [{ value: 'underline', icon: 'ph-thin ph-text-underline', label: 'Underline' }], allowEmpty: true, noneValue: 'none' }, 'none', true),
1750
+ // CC4b: tabular figures (the Price style's) — every digit one width, so a price that changes never shifts.
1751
+ // `normal` is the CSS initial and a compiler no-op, so it never leaks onto nodes.
1752
+ new TraitConfig('fontVariantNumeric', '', TraitInputType.SelectButton, { items: [{ value: 'tabular-nums', icon: 'ph-thin ph-text-columns', label: 'Tabular figures' }], allowEmpty: true, noneValue: 'normal' }, 'normal', true),
1284
1753
  new TraitConfig('_sep_stroke', 'Stroke', TraitInputType.SectionLabel),
1285
1754
  new TraitConfig('WebkitTextStrokeWidth', 'Width', TraitInputType.Scrub, { min: 0, max: 10, step: 0.5, units: ['px'] }, '0px', true),
1286
1755
  new TraitConfig('WebkitTextStrokeColor', 'Color', TraitInputType.Color, { allowGradient: false, allowTransparent: true, formats: ['hex', 'rgb', 'hsl'] }, 'transparent', true),
@@ -4359,7 +4828,7 @@ var CanvasShowState;
4359
4828
  */
4360
4829
  class CanvasDesignStateService {
4361
4830
  constructor() {
4362
- this._state$ = new BehaviorSubject({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), shown: new Map(), items: new Map() });
4831
+ this._state$ = new BehaviorSubject({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), triggeredBySelection: new Set(), shown: new Map(), items: new Map() });
4363
4832
  this.state$ = this._state$.asObservable();
4364
4833
  this._pagePath$ = new BehaviorSubject(null);
4365
4834
  /** The app path of the page on the canvas (`/work`), or null when unknown. */
@@ -4384,11 +4853,12 @@ class CanvasDesignStateService {
4384
4853
  const ids = (reveal) => path.filter(n => selectionRevealOf(n) === reveal).map(n => n.id).filter((id) => !!id);
4385
4854
  const revealed = new Set(ids(SelectionReveal.WhileSelected));
4386
4855
  const selectedTriggers = new Set(path.filter(n => isExclusiveTrigger(n)).map(n => n.id).filter((id) => !!id));
4387
- const { revealed: was, pinned: held, selectedTriggers: wasTriggers } = this._state$.value;
4856
+ const triggeredBySelection = new Set(path.map(displayTriggerOf).filter((id) => !!id));
4857
+ const { revealed: was, pinned: held, selectedTriggers: wasTriggers, triggeredBySelection: wasTriggered } = this._state$.value;
4388
4858
  const pinned = new Set([...held, ...ids(SelectionReveal.UntilClosed)]);
4389
- if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers))
4859
+ if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers) && sameSet(triggeredBySelection, wasTriggered))
4390
4860
  return;
4391
- this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers });
4861
+ this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers, triggeredBySelection });
4392
4862
  }
4393
4863
  /** Closed on the canvas (its X, its backdrop, the eye): no longer pinned or switched open. */
4394
4864
  dismiss(nodeId) {
@@ -4438,13 +4908,17 @@ class CanvasDesignStateService {
4438
4908
  const { revealed, pinned, shown } = this._state$.value;
4439
4909
  return revealed.has(nodeId) || pinned.has(nodeId) || shown.get(nodeId) === CanvasShowState.Open;
4440
4910
  }
4911
+ /** True when the canvas shows an item because the selection is styled "when it is on" (the switcher's hint). */
4912
+ isTriggeredBySelection(triggerId) {
4913
+ return !!triggerId && this._state$.value.triggeredBySelection.has(triggerId);
4914
+ }
4441
4915
  /** True when the part is shown only because the selection is inside it (the switcher's hint line). */
4442
4916
  isRevealedBySelection(nodeId) {
4443
4917
  return !!nodeId && this._state$.value.revealed.has(nodeId);
4444
4918
  }
4445
4919
  /** A new page on the canvas: nothing is revealed or switched open. */
4446
4920
  reset() {
4447
- this._state$.next({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), shown: new Map(), items: new Map() });
4921
+ this._state$.next({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), triggeredBySelection: new Set(), shown: new Map(), items: new Map() });
4448
4922
  }
4449
4923
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4450
4924
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService }); }
@@ -4497,10 +4971,13 @@ class CompositeCanvasService {
4497
4971
  const cssId = node.styleId ?? node.id;
4498
4972
  const rootId = () => compositeScopeId(cssId, compositeRowKeys(element));
4499
4973
  if (isExclusiveRoot(node)) {
4974
+ const tree = node;
4500
4975
  const runtime = this.zone.runOutsideAngular(() => new ExclusiveRuntime(element, {
4976
+ kind: exclusiveKindOf(node) ?? undefined,
4501
4977
  rootId,
4502
- behaviour: tabsBehaviour(node),
4503
- label: () => tabsLabel(node),
4978
+ behaviour: exclusiveBehaviourOf(tree),
4979
+ label: () => exclusiveLabelOf(tree),
4980
+ defaultKey: () => exclusiveDefaultOf(tree),
4504
4981
  observe: true,
4505
4982
  forced: () => this.forcedTab(node, element),
4506
4983
  // The switcher follows the TEMPLATE's clicks; a preview row's copy switches on its own.
@@ -4542,13 +5019,21 @@ class CompositeCanvasService {
4542
5019
  const entry = node.id ? this.attached.get(node.id) : undefined;
4543
5020
  entry?.runtime.setBehaviour(accordionBehaviour(node));
4544
5021
  const tabs = node.id ? this.exclusive.get(node.id) : undefined;
4545
- tabs?.runtime.setBehaviour(tabsBehaviour(node));
5022
+ tabs?.runtime.setBehaviour(exclusiveBehaviourOf(node));
5023
+ }
5024
+ /** A Segmented control's On by default changed (CC4b): the canvas shows the new default, like a click on it. */
5025
+ showDefault(node) {
5026
+ const runtime = node.id ? this.exclusive.get(node.id)?.runtime : undefined;
5027
+ const key = exclusiveDefaultOf(node);
5028
+ if (runtime && key)
5029
+ runtime.activate(key);
4546
5030
  }
4547
5031
  /** The runtime of an attached Accordion / Disclosure root (the e2e harness and the panels read its state). */
4548
5032
  runtimeOf(nodeId) {
4549
5033
  return (nodeId && this.attached.get(nodeId)?.runtime) || null;
4550
5034
  }
4551
- /** The runtime of an attached Tabs root (CC4a — "Showing on canvas" reads its tabs and the one on). */
5035
+ /** The runtime of an attached exclusive root — a Tabs (CC4a) or a Segmented control (CC4b): "Showing on canvas" reads
5036
+ * its items and the one on. */
4552
5037
  tabsRuntimeOf(nodeId) {
4553
5038
  return (nodeId && this.exclusive.get(nodeId)?.runtime) || null;
4554
5039
  }
@@ -4605,13 +5090,14 @@ class CompositeCanvasService {
4605
5090
  }
4606
5091
  return forced;
4607
5092
  }
4608
- /** The tab forced on: the one whose panel the selection sits in (the template's, in a preview row's copy). */
5093
+ /** The item forced on: the tab whose panel the selection sits in (the template's, in a preview row's copy) — or
5094
+ * the item the selection is styled "when it is on" by (CC4b: a price shown when Yearly is on turns Yearly on). */
4609
5095
  forcedTab(node, root) {
4610
5096
  const state = this.designState;
4611
5097
  const runtime = node.id ? this.exclusive.get(node.id)?.runtime : undefined;
4612
5098
  if (!state || !runtime)
4613
5099
  return null;
4614
- for (const id of state.snapshot.revealed) {
5100
+ for (const id of [...state.snapshot.revealed, ...state.snapshot.triggeredBySelection]) {
4615
5101
  const key = runtime.keyAt(root.querySelector(`.iox-node-${CSS.escape(id)}`));
4616
5102
  if (key)
4617
5103
  return key;
@@ -4675,6 +5161,18 @@ const DIRECTIVE_HOST_TYPES = new Set([
4675
5161
  // the engine never had it (DS2.1).
4676
5162
  'Form', 'Field', 'FieldGroup', 'SubmitButton', 'FormSuccess', 'FormError',
4677
5163
  ]);
5164
+ /**
5165
+ * Node types whose canvas host draws NO box (CC1.1): a Dialog is its fixed stage when open and nothing when
5166
+ * closed (the live site's closed `<dialog>` is display: none). A block host was a full-width flex item of its
5167
+ * parent — the CC1 drawer inside the header took a wrapped line plus a row gap, 24px the live header never had.
5168
+ */
5169
+ const BOXLESS_HOST_TYPES = new Set(['Dialog']);
5170
+ /** The inline `display` the canvas gives a node's custom-element host, or null to leave the host alone. */
5171
+ function canvasHostDisplay(type) {
5172
+ if (BOXLESS_HOST_TYPES.has(type))
5173
+ return 'contents';
5174
+ return DIRECTIVE_HOST_TYPES.has(type) ? null : 'block';
5175
+ }
4678
5176
  const CONTAINER_TAGS = ['div', 'section', 'article', 'header', 'footer', 'main', 'nav', 'ul', 'ol', 'li'];
4679
5177
  /** A Heading inside a button (an Accordion header, CC2) — phrasing content only. */
4680
5178
  const PHRASING_HEADING_TAG = 'span';
@@ -5045,9 +5543,9 @@ class RenderDirective {
5045
5543
  // properly in layout and CDK can capture its bounding rect. SKIP for directive-hosts: the
5046
5544
  // host is a real element with its own display, and an inline style here out-ranks both
5047
5545
  // `:host` and the styleProps `.iox-node` rule (it would e.g. force Link's <a> to block).
5048
- if (nativeEl && !DIRECTIVE_HOST_TYPES.has(node.type)) {
5049
- nativeEl.style.display = 'block';
5050
- }
5546
+ const hostDisplay = canvasHostDisplay(node.type);
5547
+ if (nativeEl && hostDisplay)
5548
+ nativeEl.style.display = hostDisplay;
5051
5549
  // Preview clones (repeater/slider-generated rows) are display-only — only the
5052
5550
  // template node is selectable/editable. Skip overlay registration + interaction
5053
5551
  // wiring for them so they can't be hovered, selected, or edited on the canvas.
@@ -5699,10 +6197,12 @@ var CompositePresetId;
5699
6197
  CompositePresetId["Accordion"] = "accordion";
5700
6198
  CompositePresetId["Disclosure"] = "disclosure";
5701
6199
  CompositePresetId["Tabs"] = "tabs";
6200
+ CompositePresetId["Segmented"] = "segmented";
5702
6201
  CompositePresetId["Lightbox"] = "lightbox";
5703
6202
  })(CompositePresetId || (CompositePresetId = {}));
5704
6203
  /** Where a composite sits in the Components panel (composite plan §6d): content blocks, or the interactive
5705
- * composites (the Accordion — CC2, the Disclosure — CC2.1, Tabs — CC4a; the floating family later). */
6204
+ * composites (the Accordion — CC2, the Disclosure — CC2.1, Tabs — CC4a, the Segmented control — CC4b; the floating
6205
+ * family later). */
5706
6206
  var CompositePaletteGroup;
5707
6207
  (function (CompositePaletteGroup) {
5708
6208
  CompositePaletteGroup["ContentBlocks"] = "content-blocks";
@@ -5739,6 +6239,11 @@ const COMPOSITE_PRESETS = Object.freeze([
5739
6239
  description: 'Tabs that switch what shows below them — three tabs, the first on. Every tab and panel is its own element.',
5740
6240
  build: create => buildTabsRecipe(create),
5741
6241
  },
6242
+ {
6243
+ id: CompositePresetId.Segmented, group: CompositePaletteGroup.Interactive, name: 'Segmented', icon: 'ph-thin ph-pill',
6244
+ description: 'One of a few, always one on — Monthly · Yearly. Style anything "when Yearly is on" to swap what shows.',
6245
+ build: create => buildSegmentedRecipe(create),
6246
+ },
5742
6247
  {
5743
6248
  id: CompositePresetId.Lightbox, group: CompositePaletteGroup.Interactive, name: 'Lightbox', icon: 'ph-thin ph-frame-corners',
5744
6249
  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.',
@@ -5882,339 +6387,65 @@ function overlayGlobals(globals) {
5882
6387
  else if (globalCanvasLayer(node) === GlobalCanvasLayer.Overlay)
5883
6388
  pinnedAfter.push(node);
5884
6389
  }
5885
- return { before, pinnedAfter };
5886
- }
5887
-
5888
- /** Counts for ONE node (the selection). Breakpoints with nothing overridden are omitted. */
5889
- function nodeBreakpointCounts(node) {
5890
- const counts = {};
5891
- if (!node?.responsive)
5892
- return counts;
5893
- for (const { id } of BREAKPOINTS) {
5894
- if (id === Breakpoint.Base)
5895
- continue;
5896
- const n = countOverrides(node.responsive, id);
5897
- if (n)
5898
- counts[id] = n;
5899
- }
5900
- return counts;
5901
- }
5902
- /** Counts summed over whole trees (the page, when nothing is selected). */
5903
- function treeBreakpointCounts(...roots) {
5904
- const counts = {};
5905
- const visit = (nodes) => {
5906
- for (const node of nodes ?? []) {
5907
- for (const [bp, n] of Object.entries(nodeBreakpointCounts(node))) {
5908
- counts[bp] = (counts[bp] ?? 0) + n;
5909
- }
5910
- visit(node.children);
5911
- }
5912
- };
5913
- roots.forEach(visit);
5914
- return counts;
5915
- }
5916
-
5917
- /**
5918
- * Resolve where "click to insert" puts a new element, from the current selection — the rule the
5919
- * components panel and the canvas "+" share (builder-usability-plan.md, B2):
5920
- *
5921
- * nothing selected → appended to the page root
5922
- * a CONTAINER selected → appended INSIDE it (its last child)
5923
- * a LEAF selected → inserted AFTER it, as its next sibling
5924
- *
5925
- * `isContainer` decides what can hold children — the builder passes "has a registered drop zone",
5926
- * the same test a drag uses, so a click can never insert somewhere a drop couldn't.
5927
- * A selection not found in `roots` (stale) falls back to appending to the root.
5928
- */
5929
- function resolveInsertTarget(roots, selected, isContainer) {
5930
- const append = (children, parent) => ({ children, index: children.length, parent });
5931
- if (!selected)
5932
- return append(roots, null);
5933
- const path = nodeAncestorPath(roots, selected);
5934
- if (!path)
5935
- return append(roots, null);
5936
- if (isContainer(selected)) {
5937
- if (!selected.children)
5938
- selected.children = [];
5939
- return append(selected.children, selected);
5940
- }
5941
- const parent = path.length >= 2 ? path[path.length - 2] : null;
5942
- const siblings = parent ? (parent.children ?? (parent.children = [])) : roots;
5943
- return { children: siblings, index: siblings.indexOf(selected) + 1, parent };
5944
- }
5945
-
5946
- /**
5947
- * STRUCTURE OPS (A4) — the one-click layout changes people actually make at a narrow width, as pure
5948
- * "what to write" functions. Each returns a flat style map the caller stores in the node's
5949
- * `responsive[bp]` layer (or in the base styles at Base) through the shared `setOverride`.
5950
- *
5951
- * All of it is ordinary CSS the SSR engine already renders — there is no second tree and no
5952
- * per-width HTML. See responsive-breakpoints-plan.md (D5).
5953
- */
5954
- /** CSS lengths that are a FIXED pixel size — the ones that stop a layout fitting a phone. */
5955
- const PX_LENGTH = /^-?\d+(\.\d+)?px$/i;
5956
- /**
5957
- * Hide / show at a width. Hiding is `display: none` INSIDE that breakpoint's rules — the node stays
5958
- * in the DOM and in the SSR output (one HTML document serves every width), so hidden content still
5959
- * ships bytes and is still indexed. Showing again clears the override rather than guessing a value.
5960
- *
5961
- * @returns the props to write, or `null` meaning "clear the display override at this breakpoint".
5962
- */
5963
- function visibilityProps(hidden) {
5964
- return hidden ? { display: 'none' } : null;
5965
- }
5966
- /** True when this node is hidden by the resolved styles at some width. */
5967
- function isHiddenAt(resolvedStyleProps) {
5968
- return String(resolvedStyleProps?.['display'] ?? '') === 'none';
5969
- }
5970
- /**
5971
- * Stack a container's children in one column — the most common mobile edit, and four separate
5972
- * controls today (display, direction, wrap, columns). A grid becomes a single-column grid; anything
5973
- * else becomes a column flex box.
5974
- */
5975
- function stackChildrenProps(resolvedStyleProps) {
5976
- const display = String(resolvedStyleProps?.['display'] ?? '');
5977
- if (display === 'grid' || display === 'inline-grid') {
5978
- return { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))' };
5979
- }
5980
- return { display: 'flex', flexDirection: 'column', flexWrap: 'nowrap' };
5981
- }
5982
- /**
5983
- * Collapse a grid to `count` columns (a flex row is stacked instead — it has no column track).
5984
- * `count` is clamped to at least 1.
5985
- */
5986
- function collapseColumnsProps(resolvedStyleProps, count) {
5987
- const columns = Math.max(1, Math.floor(count));
5988
- const display = String(resolvedStyleProps?.['display'] ?? '');
5989
- if (display === 'grid' || display === 'inline-grid') {
5990
- return { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` };
5991
- }
5992
- return columns === 1 ? stackChildrenProps(resolvedStyleProps) : { flexWrap: 'wrap' };
5993
- }
5994
- /** How many equal columns a `repeat(N, …)` track list describes, or null when it isn't one. */
5995
- function columnCountOf(resolvedStyleProps) {
5996
- const match = /^repeat\(\s*(\d+)\s*,/.exec(String(resolvedStyleProps?.['gridTemplateColumns'] ?? ''));
5997
- return match ? Number(match[1]) : null;
5998
- }
5999
- /**
6000
- * Make fixed pixel sizes fluid: `width: 520px` → `width: 100%` with the original kept as
6001
- * `max-width`, so the element still never exceeds its designed size on a wide screen. `minWidth` in
6002
- * px is dropped to 0 — it is what actually forces horizontal scrolling on a phone.
6003
- *
6004
- * @returns the props to write; EMPTY when there is nothing fixed to fluidise.
6005
- */
6006
- function fluidiseProps(resolvedStyleProps) {
6007
- const out = {};
6008
- const width = String(resolvedStyleProps?.['width'] ?? '');
6009
- const minWidth = String(resolvedStyleProps?.['minWidth'] ?? '');
6010
- if (PX_LENGTH.test(width)) {
6011
- out['width'] = '100%';
6012
- const maxWidth = String(resolvedStyleProps?.['maxWidth'] ?? '');
6013
- if (!maxWidth || maxWidth === 'none')
6014
- out['maxWidth'] = width;
6015
- }
6016
- if (PX_LENGTH.test(minWidth) && parseFloat(minWidth) > 0)
6017
- out['minWidth'] = '0px';
6018
- return out;
6390
+ return { before, pinnedAfter };
6019
6391
  }
6020
- /** True when `fluidiseProps` would change anything — drives the button's disabled state. */
6021
- function canFluidise(resolvedStyleProps) {
6022
- return Object.keys(fluidiseProps(resolvedStyleProps)).length > 0;
6392
+
6393
+ /** Counts for ONE node (the selection). Breakpoints with nothing overridden are omitted. */
6394
+ function nodeBreakpointCounts(node) {
6395
+ const counts = {};
6396
+ if (!node?.responsive)
6397
+ return counts;
6398
+ for (const { id } of BREAKPOINTS) {
6399
+ if (id === Breakpoint.Base)
6400
+ continue;
6401
+ const n = countOverrides(node.responsive, id);
6402
+ if (n)
6403
+ counts[id] = n;
6404
+ }
6405
+ return counts;
6023
6406
  }
6024
- /**
6025
- * Reorder children AT ONE WIDTH using CSS `order`, leaving the tree — and so the DOM and
6026
- * screen-reader order — untouched. Returns one value per child BY CHILD INDEX: the `order` it must
6027
- * take, or `null` for "no override needed".
6028
- *
6029
- * Either every child gets an explicit `order` or none does: CSS defaults `order` to 0, so a partial
6030
- * set would mix overridden children with unordered ones and read as an arbitrary interleave.
6031
- *
6032
- * @param count How many children there are.
6033
- * @param from Index of the child being moved.
6034
- * @param to Index it should occupy.
6035
- */
6036
- function reorderProps(count, from, to) {
6037
- const none = Array.from({ length: Math.max(0, count) }, () => null);
6038
- if (from < 0 || from >= count || to < 0 || to >= count || from === to)
6039
- return none;
6040
- // Visual order after the move, as a list of CHILD indices.
6041
- const sequence = Array.from({ length: count }, (_, i) => i);
6042
- sequence.splice(to, 0, ...sequence.splice(from, 1));
6043
- const orderByChild = none.slice();
6044
- sequence.forEach((childIndex, position) => { orderByChild[childIndex] = position; });
6045
- return orderByChild;
6407
+ /** Counts summed over whole trees (the page, when nothing is selected). */
6408
+ function treeBreakpointCounts(...roots) {
6409
+ const counts = {};
6410
+ const visit = (nodes) => {
6411
+ for (const node of nodes ?? []) {
6412
+ for (const [bp, n] of Object.entries(nodeBreakpointCounts(node))) {
6413
+ counts[bp] = (counts[bp] ?? 0) + n;
6414
+ }
6415
+ visit(node.children);
6416
+ }
6417
+ };
6418
+ roots.forEach(visit);
6419
+ return counts;
6046
6420
  }
6047
6421
 
6048
6422
  /**
6049
- * RESPONSIVE AUDIT (A5) — find what is broken at a width, from the node tree plus ONE measurement
6050
- * pass. A builder that lets you author five widths must also tell you which of them are broken.
6423
+ * Resolve where "click to insert" puts a new element, from the current selection — the rule the
6424
+ * components panel and the canvas "+" share (builder-usability-plan.md, B2):
6051
6425
  *
6052
- * Pure: the caller measures (one `getBoundingClientRect` per node, off the animation path) and
6053
- * passes the numbers in. Every finding that can be repaired carries the exact props to write, so a
6054
- * fix is one `setOverride` per key — one undo step, and it shows as an ordinary override.
6055
- */
6056
- var AuditCode;
6057
- (function (AuditCode) {
6058
- AuditCode["Overflow"] = "overflow";
6059
- AuditCode["FixedWidth"] = "fixed-width";
6060
- AuditCode["TapTarget"] = "tap-target";
6061
- AuditCode["TinyText"] = "tiny-text";
6062
- AuditCode["UnboundedImage"] = "unbounded-image";
6063
- /** A slider showing more slides than the width can carry — each one unreadably narrow. */
6064
- AuditCode["SlidesOverflow"] = "slides-overflow";
6065
- /** An out-of-flow node whose box lands outside the frame, so it is unreachable at this width. */
6066
- AuditCode["OffscreenAbsolute"] = "offscreen-absolute";
6067
- /** Form content inside a link or a button — a STRUCTURE finding, true at every width (form-nesting-audit.util). */
6068
- AuditCode["FormInLink"] = "form-in-link";
6069
- /** An icon-only button or link with no Accessible name — announced as an unnamed control (CC1 N6). */
6070
- AuditCode["UnnamedControl"] = "unnamed-control";
6071
- /** A composite whose items come from content, and its source returned no rows (CC2, board 9): the
6072
- * canvas shows the placeholder, the live site renders nothing. */
6073
- AuditCode["EmptySource"] = "empty-source";
6074
- })(AuditCode || (AuditCode = {}));
6075
- /** A fix that changes the tree, not a style or a trait — applied as one page-history step. */
6076
- var StructureFix;
6077
- (function (StructureFix) {
6078
- /** Move the form out of the link or button holding it, to right after it. */
6079
- StructureFix["MoveFormOut"] = "move-form-out";
6080
- })(StructureFix || (StructureFix = {}));
6081
- var AuditSeverity;
6082
- (function (AuditSeverity) {
6083
- AuditSeverity["Error"] = "error";
6084
- AuditSeverity["Warning"] = "warning";
6085
- AuditSeverity["Info"] = "info";
6086
- })(AuditSeverity || (AuditSeverity = {}));
6087
- /** WCAG 2.5.8 minimum target size. */
6088
- const MIN_TAP_PX = 44;
6089
- /** Below this, iOS Safari zooms the page when an adjacent input is focused. */
6090
- const MIN_TEXT_PX = 12;
6091
- /** Types a visitor taps. */
6092
- const TAPPABLE = new Set(['Button', 'ButtonBlock', 'Link', 'LinkedContainer']);
6093
- /** Narrower than this, a slide shows too little of its content to be worth showing at all. */
6094
- const MIN_SLIDE_PX = 200;
6095
- /** Out-of-flow positioning that can put a node outside the frame. */
6096
- const OUT_OF_FLOW = new Set(['absolute', 'fixed']);
6097
- const nameOf = (node) => node.label || node.type;
6098
- const pxOf = (value) => {
6099
- const match = /^(-?\d+(?:\.\d+)?)px$/i.exec(String(value ?? ''));
6100
- return match ? Number(match[1]) : null;
6101
- };
6102
- /**
6103
- * Audit nodes laid out at `viewportWidth`. Returns findings ordered by severity (errors first),
6104
- * then by the order the nodes were given — so the worst thing is at the top of the list.
6426
+ * nothing selected → appended to the page root
6427
+ * a CONTAINER selected → appended INSIDE it (its last child)
6428
+ * a LEAF selected → inserted AFTER it, as its next sibling
6429
+ *
6430
+ * `isContainer` decides what can hold children — the builder passes "has a registered drop zone",
6431
+ * the same test a drag uses, so a click can never insert somewhere a drop couldn't.
6432
+ * A selection not found in `roots` (stale) falls back to appending to the root.
6105
6433
  */
6106
- function auditNodes(nodes, viewportWidth) {
6107
- const findings = [];
6108
- for (const node of nodes) {
6109
- const name = nameOf(node);
6110
- const styles = node.styleProps ?? {};
6111
- if (String(styles['display'] ?? '') === 'none')
6112
- continue; // hidden here — nothing to report
6113
- // 1. Wider than the viewport: the page scrolls sideways.
6114
- if (node.width > viewportWidth + 0.5) {
6115
- findings.push({
6116
- nodeId: node.id, nodeName: name, code: AuditCode.Overflow, severity: AuditSeverity.Error,
6117
- message: `${name} is ${Math.round(node.width)}px wide in a ${viewportWidth}px viewport — the page scrolls sideways.`,
6118
- fix: canFluidise(styles) ? fluidiseProps(styles) : { maxWidth: '100%' },
6119
- });
6120
- }
6121
- else if (canFluidise(styles)) {
6122
- // 2. A fixed px size that still fits, but will not on a narrower phone.
6123
- const width = pxOf(styles['width']);
6124
- findings.push({
6125
- nodeId: node.id, nodeName: name, code: AuditCode.FixedWidth, severity: AuditSeverity.Warning,
6126
- message: width !== null
6127
- ? `${name} has a fixed width of ${width}px — it cannot shrink on a narrower screen.`
6128
- : `${name} has a fixed minimum width — it forces sideways scrolling below that size.`,
6129
- fix: fluidiseProps(styles),
6130
- });
6131
- }
6132
- // 3. Tap targets below the WCAG minimum.
6133
- if (TAPPABLE.has(node.type) && node.width > 0 && node.height > 0
6134
- && (node.width < MIN_TAP_PX || node.height < MIN_TAP_PX)) {
6135
- findings.push({
6136
- nodeId: node.id, nodeName: name, code: AuditCode.TapTarget, severity: AuditSeverity.Warning,
6137
- message: `${name} is ${Math.round(node.width)}×${Math.round(node.height)}px — below the ${MIN_TAP_PX}px minimum tap target.`,
6138
- fix: { minHeight: `${MIN_TAP_PX}px`, minWidth: `${MIN_TAP_PX}px` },
6139
- });
6140
- }
6141
- // 4. Text too small to read (and small enough to trigger iOS zoom).
6142
- const fontSize = pxOf(styles['fontSize']);
6143
- if (fontSize !== null && fontSize < MIN_TEXT_PX) {
6144
- findings.push({
6145
- nodeId: node.id, nodeName: name, code: AuditCode.TinyText, severity: AuditSeverity.Warning,
6146
- message: `${name} renders at ${fontSize}px — under the ${MIN_TEXT_PX}px readable minimum.`,
6147
- fix: { fontSize: `${MIN_TEXT_PX}px` },
6148
- });
6149
- }
6150
- // 5. An image with a fixed height and no ratio crops unpredictably as the width changes.
6151
- if (node.type === 'Image' && pxOf(styles['height']) !== null
6152
- && !String(styles['aspectRatio'] ?? '').trim().replace(/^auto$/, '')) {
6153
- findings.push({
6154
- nodeId: node.id, nodeName: name, code: AuditCode.UnboundedImage, severity: AuditSeverity.Info,
6155
- message: `${name} has a fixed height and no aspect ratio — it crops unpredictably as the width changes.`,
6156
- fix: { height: 'auto', aspectRatio: '16 / 9' },
6157
- });
6158
- }
6159
- // 6. A slider showing more slides than this width can carry. NOT a CSS problem: slide count
6160
- // is a component trait, so the fix is a per-breakpoint TRAIT override (P3-G1), not a style.
6161
- if (node.type === 'SliderContainer' && node.width > 0) {
6162
- const perView = Number(node.inputs?.['slidesPerView']);
6163
- if (Number.isFinite(perView) && perView > 1) {
6164
- const slideWidth = node.width / perView;
6165
- if (slideWidth < MIN_SLIDE_PX) {
6166
- const fits = Math.max(1, Math.floor(node.width / MIN_SLIDE_PX));
6167
- findings.push({
6168
- nodeId: node.id, nodeName: name, code: AuditCode.SlidesOverflow, severity: AuditSeverity.Warning,
6169
- message: `${name} shows ${perView} slides across ${Math.round(node.width)}px — ${Math.round(slideWidth)}px each. Show ${fits} at this width.`,
6170
- inputFix: { slidesPerView: fits },
6171
- });
6172
- }
6173
- }
6174
- }
6175
- // 7. An icon-only control with no name (WCAG 4.1.2): a screen reader says "button" and nothing
6176
- // else. No auto-fix — only the author knows what it does; the finding selects it.
6177
- if (node.unnamedControl) {
6178
- findings.push({
6179
- nodeId: node.id, nodeName: name, code: AuditCode.UnnamedControl, severity: AuditSeverity.Error,
6180
- message: `${name} shows only an icon and has no accessible name — screen readers announce it as an unnamed control. Name it in Bindings ▸ Accessibility.`,
6181
- });
6182
- }
6183
- // 7b. A composite from content whose source has no rows (CC2 R3): the visitor sees nothing there.
6184
- if (node.emptySource) {
6185
- findings.push({
6186
- nodeId: node.id, nodeName: name, code: AuditCode.EmptySource, severity: AuditSeverity.Warning,
6187
- message: `${name} in ${node.emptySource.section}: its source ${node.emptySource.alias} has no rows.`,
6188
- });
6189
- }
6190
- // 8. An out-of-flow node that has left the frame. Measured, not inferred: a negative offset
6191
- // is legitimate design until it actually puts the content beyond the edge.
6192
- if (OUT_OF_FLOW.has(String(styles['position'] ?? '')) && node.left !== undefined && node.width > 0) {
6193
- const right = node.left + node.width;
6194
- const offLeft = right <= 0.5;
6195
- const offRight = node.left >= viewportWidth - 0.5;
6196
- if (offLeft || offRight) {
6197
- findings.push({
6198
- nodeId: node.id, nodeName: name, code: AuditCode.OffscreenAbsolute, severity: AuditSeverity.Error,
6199
- message: `${name} is positioned ${offLeft ? 'left of' : 'beyond'} the ${viewportWidth}px viewport — it cannot be seen or tapped at this width.`,
6200
- // No blind auto-fix: the author's intent (a decorative overflow vs a mistake)
6201
- // is not knowable from geometry. Selecting the node is the useful action.
6202
- });
6203
- }
6204
- }
6434
+ function resolveInsertTarget(roots, selected, isContainer) {
6435
+ const append = (children, parent) => ({ children, index: children.length, parent });
6436
+ if (!selected)
6437
+ return append(roots, null);
6438
+ const path = nodeAncestorPath(roots, selected);
6439
+ if (!path)
6440
+ return append(roots, null);
6441
+ if (isContainer(selected)) {
6442
+ if (!selected.children)
6443
+ selected.children = [];
6444
+ return append(selected.children, selected);
6205
6445
  }
6206
- const rank = { [AuditSeverity.Error]: 0, [AuditSeverity.Warning]: 1, [AuditSeverity.Info]: 2 };
6207
- return findings
6208
- .map((finding, index) => ({ finding, index }))
6209
- .sort((a, b) => rank[a.finding.severity] - rank[b.finding.severity] || a.index - b.index)
6210
- .map(entry => entry.finding);
6211
- }
6212
- /** Counts per severity — the per-breakpoint summary strip. */
6213
- function summarise(findings) {
6214
- const out = { [AuditSeverity.Error]: 0, [AuditSeverity.Warning]: 0, [AuditSeverity.Info]: 0 };
6215
- for (const finding of findings)
6216
- out[finding.severity]++;
6217
- return out;
6446
+ const parent = path.length >= 2 ? path[path.length - 2] : null;
6447
+ const siblings = parent ? (parent.children ?? (parent.children = [])) : roots;
6448
+ return { children: siblings, index: siblings.indexOf(selected) + 1, parent };
6218
6449
  }
6219
6450
 
6220
6451
  const FINDING_KEY = 'builder.chrome.auditFormInLink';
@@ -7146,10 +7377,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
7146
7377
  * An accordion's and a disclosure's parts are named by the part alone (`trigger`, "Accordion · trigger", board 3);
7147
7378
  * a card keeps its full role (`card.title`, "Card · card.title", board 8).
7148
7379
  */
7149
- const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure, CompositeKind.Tabs]);
7380
+ const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure, CompositeKind.Tabs, CompositeKind.Segmented]);
7150
7381
  /** A composite kind's glyph (Phosphor, weight-less): the badge, the root's crumb and its tree row carry it
7151
7382
  * (board 3: "the composite root carrying its kind icon"). A card keeps the generic stack glyph. */
7152
- const KIND_GLYPHS = { [CompositeKind.Accordion]: 'ph-rows', [CompositeKind.Disclosure]: 'ph-caret-circle-down', [CompositeKind.Tabs]: 'ph-tabs', slider: 'ph-slideshow' };
7383
+ const KIND_GLYPHS = {
7384
+ [CompositeKind.Accordion]: 'ph-rows', [CompositeKind.Disclosure]: 'ph-caret-circle-down', [CompositeKind.Tabs]: 'ph-tabs', [CompositeKind.Segmented]: 'ph-pill',
7385
+ slider: 'ph-slideshow',
7386
+ };
7153
7387
  const DEFAULT_KIND_GLYPH = 'ph-stack';
7154
7388
  /** The tree icon of an accordion's structural parts (board 3): rows on the root, a stack on an item and a
7155
7389
  * panel, a cursor on the header, a sparkle on the icon. Other parts keep their type's icon. */
@@ -7171,6 +7405,10 @@ const PART_ICONS = {
7171
7405
  [TabsRole.Trigger]: 'ph-thin ph-cursor',
7172
7406
  [TabsRole.Panels]: 'ph-thin ph-stack',
7173
7407
  [TabsRole.Panel]: 'ph-thin ph-stack',
7408
+ // CC4b — the Segmented control (board 10): its pill glyph on the root, a cursor on an option, a seal on its badge.
7409
+ [SegmentedRole.Segmented]: 'ph-thin ph-pill',
7410
+ [SegmentedRole.Option]: 'ph-thin ph-cursor',
7411
+ [SegmentedRole.Badge]: 'ph-thin ph-seal',
7174
7412
  // CC3 — a Slider's controls (board 6): a cursor on the buttons, six dots on the pagination.
7175
7413
  [SliderRole.Pause]: 'ph-thin ph-cursor',
7176
7414
  [SliderRole.Prev]: 'ph-thin ph-cursor',
@@ -7206,6 +7444,8 @@ function compositePartSub(node, ancestors = []) {
7206
7444
  return accordionItemLabel(node) || null;
7207
7445
  if (node.role === TabsRole.Trigger)
7208
7446
  return tabLabel(node) || null;
7447
+ if (node.role === SegmentedRole.Option)
7448
+ return segmentedOptionLabel(node) || null;
7209
7449
  if (node.role === TabsRole.Panel) {
7210
7450
  const root = exclusiveRootOf(node, ancestors);
7211
7451
  const tab = root ? tabTriggerOf(root, node) : undefined;
@@ -7269,7 +7509,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
7269
7509
  const FROM_ROW_KEY = 'builder.chrome.fromRow';
7270
7510
  const ADD_ITEM_KEY = 'builder.chrome.addItem';
7271
7511
  /** The kind label a composite part's selection badge shows (Card v2: "Card · card.title"). */
7272
- const COMPOSITE_KIND_KEYS = { card: 'builder.chrome.kindCard', accordion: 'builder.chrome.kindAccordion', disclosure: 'builder.chrome.kindDisclosure', tabs: 'builder.chrome.kindTabs', slider: 'builder.chrome.kindSlider', nav: 'builder.chrome.kindNav' };
7512
+ const COMPOSITE_KIND_KEYS = { card: 'builder.chrome.kindCard', accordion: 'builder.chrome.kindAccordion', disclosure: 'builder.chrome.kindDisclosure', tabs: 'builder.chrome.kindTabs', segmented: 'builder.chrome.kindSegmented', slider: 'builder.chrome.kindSlider', nav: 'builder.chrome.kindNav' };
7273
7513
  const VIDEO_TYPE = 'Video';
7274
7514
  const VIDEO_KIND_KEY = 'builder.chrome.kindVideo';
7275
7515
  const VIDEO_POSTER_ONLY_KEY = 'builder.chrome.videoPosterOnly';
@@ -7278,9 +7518,12 @@ const VIDEO_FILE_KEY = 'builder.chrome.videoFile';
7278
7518
  const ADD_ITEM_ROLES = new Set([
7279
7519
  AccordionRole.Accordion, AccordionRole.Item, AccordionRole.Trigger, AccordionRole.Panel,
7280
7520
  TabsRole.Tabs, TabsRole.List, TabsRole.Trigger, TabsRole.Panels, TabsRole.Panel,
7521
+ SegmentedRole.Segmented, SegmentedRole.Option,
7281
7522
  ]);
7282
7523
  /** Tabs say "Add tab" (board 3) — the button adds a tab AND its panel. */
7283
7524
  const ADD_TAB_KEY = 'builder.chrome.addTab';
7525
+ /** A Segmented control says "Add option" (board 10). */
7526
+ const ADD_OPTION_KEY = 'builder.chrome.addOption';
7284
7527
  class OverlayComponent {
7285
7528
  constructor(elRef, instanceRegistry, overlayService, viewportService, presetRegistry, symbolRegistry, interactionPresetRegistry, interactionEngine, clipboard, styleRegistry, panelEventService, cdr, dragEngine, labels) {
7286
7529
  this.elRef = elRef;
@@ -7531,7 +7774,8 @@ class OverlayComponent {
7531
7774
  return ADD_ITEM_ROLES.has(this.selectEntry?.node?.role ?? '');
7532
7775
  }
7533
7776
  get addItemLabel() {
7534
- const key = isTabsPart(this.selectEntry?.node) ? ADD_TAB_KEY : ADD_ITEM_KEY;
7777
+ const node = this.selectEntry?.node;
7778
+ const key = isTabsPart(node) ? ADD_TAB_KEY : isSegmentedPart(node) ? ADD_OPTION_KEY : ADD_ITEM_KEY;
7535
7779
  return this.builderLabels(key, BUILDER_CHROME_EN[key]);
7536
7780
  }
7537
7781
  onAddItem(event) {
@@ -8138,7 +8382,7 @@ function walk(nodes, depth, out, ctx, inTemplate, templateOf, ancestors) {
8138
8382
  node,
8139
8383
  icon: bound ? BOUND_ICON : (compositePartIcon(node.role) || ctx.iconOf(node.type) || DEFAULT_ICON),
8140
8384
  label,
8141
- hiddenAtRest: isHiddenAtRest(node),
8385
+ hiddenAtRest: isHiddenAtRest(node) || (!!ctx.onAtRest && hiddenUntilItemOn(node, ctx.onAtRest)),
8142
8386
  depth,
8143
8387
  expanded,
8144
8388
  hasChildren,
@@ -8381,6 +8625,7 @@ class LayerTreeComponent {
8381
8625
  .replace('{source}', sourceTypeLabel(rows.alias, this.dataSources?.findByAlias(rows.alias), this.typeLabels))
8382
8626
  .replace('{count}', String(rows.count))
8383
8627
  : null,
8628
+ onAtRest: itemsOnAtRest([...(this.layout ?? []), ...(this.globalElements ?? [])]),
8384
8629
  };
8385
8630
  this.flatTree = flattenLayerTree(this.layout, ctx);
8386
8631
  this.flatGlobalTree = flattenLayerTree(this.globalElements, ctx);
@@ -8694,7 +8939,10 @@ class BuilderComponent {
8694
8939
  return out;
8695
8940
  }
8696
8941
  refreshStructureFindings() {
8697
- this.structureFindings = formNestingFindings(this.layout, this.globalElements, this.labels);
8942
+ this.structureFindings = [
8943
+ ...formNestingFindings(this.layout, this.globalElements, this.labels),
8944
+ ...compositeStructureFindings(this.layout, this.globalElements, this.labels),
8945
+ ];
8698
8946
  }
8699
8947
  /** A structure fix moves nodes on the page: ONE layout change, so ONE undo step. */
8700
8948
  applyStructureFix(finding) {
@@ -9684,6 +9932,9 @@ class BuilderComponent {
9684
9932
  else if (action === NodeAction.Duplicate) {
9685
9933
  if (this.duplicateTabPairOf(node))
9686
9934
  return;
9935
+ // The same acceptance rule as the toolbar's Duplicate (a fifth Segmented option is refused, CC4b).
9936
+ if (this.dragEngine.refuseInsert(node, this.siblingAncestors(node)))
9937
+ return;
9687
9938
  const clone = this.deepCloneWithNewIds(node);
9688
9939
  result.parent.splice(result.index + 1, 0, clone);
9689
9940
  this.cdr.detectChanges();
@@ -9944,6 +10195,8 @@ class BuilderComponent {
9944
10195
  const root = exclusiveRootOf(node, path);
9945
10196
  if (!root)
9946
10197
  return false;
10198
+ if (isSegmentedRoot(root))
10199
+ return this.addSegmentedOptionTo(root, path);
9947
10200
  if (isTabsFromContent(root))
9948
10201
  return true;
9949
10202
  const anchor = path.find(n => isExclusiveItemPart(n)) ?? null;
@@ -9955,6 +10208,23 @@ class BuilderComponent {
9955
10208
  this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: pair.tab });
9956
10209
  return true;
9957
10210
  }
10211
+ /** Toolbar ▸ Add option on a Segmented part (CC4b): a fresh option after the selection's own option (or at the end
10212
+ * for the root) — ONE undo step, then it is selected. A fifth is refused, said in a toast (Q40). */
10213
+ addSegmentedOptionTo(root, path) {
10214
+ const tree = root;
10215
+ if (segmentedAddRefusal(tree)) {
10216
+ this.dragEngine.insertRefused$.next({ reason: InsertRefusal.SegmentedMaxOptions, message: insertRefusalLabel(InsertRefusal.SegmentedMaxOptions, this.labels) });
10217
+ return true;
10218
+ }
10219
+ const anchor = path.find(n => n.role === SegmentedRole.Option) ?? null;
10220
+ const factory = builderCompositeFactory(t => this.registry.createConfig(t), t => this.registry.baselineStyleProps(t));
10221
+ const option = addSegmentedOption(tree, factory, anchor);
10222
+ if (!option)
10223
+ return true;
10224
+ this.panelEventService.emit(PanelEventTypes.NODE_STRUCTURE_CHANGED, { node: root });
10225
+ this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: option });
10226
+ return true;
10227
+ }
9958
10228
  /** Duplicate on a tab or a panel (toolbar, layer tree): the PAIR is duplicated under one new key. True when handled. */
9959
10229
  duplicateTabPairOf(node) {
9960
10230
  const root = tabsRootForPart(this.layout, this.globalElements, node);
@@ -13835,5 +14105,5 @@ class BuilderSpacerComponentConfig extends ComponentConfig {
13835
14105
  * Generated bundle index. Do not edit.
13836
14106
  */
13837
14107
 
13838
- 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 };
14108
+ 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, canvasHostDisplay, collapseColumnsProps, columnCountOf, compositeStructureFindings, defaultBuilderLabels, defaultPageSettings, deleteTabPair, displayTriggerOf, dropChildren, dropTargetLabel, duplicateTabPair, escapesRoot, essentialTraitsFor, findNodeLocation, findParentOf, flattenLayerTree, flattenSchemaFields, fluidiseProps, generateNodeId, globalCanvasLayer, hiddenAtBase, hiddenUntilItemOn, insertRefusal, insertRefusalLabel, interactionTargets, isHiddenAt, isHiddenAtRest, isPinnedGlobal, isSpecimen, itemsOnAtRest, markSpecimen, matchesControlSearch, nearestFormNode, neverShown, nodeAncestorPath, nodeAncestorPathInEither, nodeDescriptor, nodeStyleMap, overlayGlobals, pathToNode, refreshSubtree, regenerateNodeIds, removalRefusal, reorderProps, repeaterSourceAlias, repeaterSubLabel, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, retypeNode, rootEscapeBand, rootZone, selectionRevealOf, setStyleTrait, setTrait, shortDescriptor, shownBeforeControl, sourceContentType, sourceTypeLabel, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, syncNodeInputs, tabPairLabel, tabsRootForPart, textNode, upsertNodeStyles, visibilityProps, walkNodes, wrapNode };
13839
14109
  //# sourceMappingURL=vectoriox-iox-builder.mjs.map