@vectoriox/iox-builder 8.1.0 → 9.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.
@@ -16,10 +16,11 @@ import { SelectModule } from 'primeng/select';
16
16
  import { TooltipModule } from 'primeng/tooltip';
17
17
  import { DialogModule } from 'primeng/dialog';
18
18
  import * as i1 from '@vectoriox/iox-ui';
19
- import { VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, Breakpoint, BREAKPOINTS, countOverrides, setOverride, NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, applyElementStyle, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, buildTargetedStateSelector, statePseudoClass, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, breakpointDef, breakpointForWidth, FormRenderState, safeSetInput, ContextStore, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, resolveInputs, setInputOverride, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, resolveBoundValue, IS_PREVIEW, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
19
+ import { VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, Breakpoint, BREAKPOINTS, countOverrides, setOverride, NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, applyElementStyle, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, buildTargetedStateSelector, statePseudoClass, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, breakpointDef, breakpointForWidth, FormRenderState, safeSetInput, ContextStore, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, resolveInputs, setInputOverride, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, resolveBoundValue, IS_PREVIEW, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, LinkType, buttonBlockHref, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
20
20
  export { ComponentInstanceRegistryService, VIRTUAL_TRAIT_KEYS, composeVirtualTraits } from '@vectoriox/iox-ui';
21
21
  import Lenis from 'lenis';
22
22
  import { Subject, BehaviorSubject, of, forkJoin } from 'rxjs';
23
+ import { findFormContentInHosts, formContentInHost, isInteractiveHost } from '@vectoriox/iox-ui/forms-core';
23
24
  import interact from 'interactjs';
24
25
  import { BEHAVIOUR_ACTION_TYPES, SERVER_ELEMENT_ACTION_TYPES, CONSENT_ACTION_TYPES } from '@vectoriox/iox-ui/actions-core';
25
26
  import Moveable from 'moveable';
@@ -1339,7 +1340,15 @@ var AuditCode;
1339
1340
  AuditCode["SlidesOverflow"] = "slides-overflow";
1340
1341
  /** An out-of-flow node whose box lands outside the frame, so it is unreachable at this width. */
1341
1342
  AuditCode["OffscreenAbsolute"] = "offscreen-absolute";
1343
+ /** Form content inside a link or a button — a STRUCTURE finding, true at every width (form-nesting-audit.util). */
1344
+ AuditCode["FormInLink"] = "form-in-link";
1342
1345
  })(AuditCode || (AuditCode = {}));
1346
+ /** A fix that changes the tree, not a style or a trait — applied as one page-history step. */
1347
+ var StructureFix;
1348
+ (function (StructureFix) {
1349
+ /** Move the form out of the link or button holding it, to right after it. */
1350
+ StructureFix["MoveFormOut"] = "move-form-out";
1351
+ })(StructureFix || (StructureFix = {}));
1343
1352
  var AuditSeverity;
1344
1353
  (function (AuditSeverity) {
1345
1354
  AuditSeverity["Error"] = "error";
@@ -1464,6 +1473,125 @@ function summarise(findings) {
1464
1473
  return out;
1465
1474
  }
1466
1475
 
1476
+ /**
1477
+ * Canvas data preview used to come from host-provided content services (IOX_CONTENT_SERVICE /
1478
+ * IOX_SHOP_SERVICE), whose `getContent(contentType, orgId)` hit the ADMIN resources endpoint and
1479
+ * accepted no filters — so the Repeater/Slider ignored every data-source filter. Data now flows
1480
+ * through BuilderDataSourceService + the shared planDataSource, configured with
1481
+ * IOX_DATA_SOURCE_ENDPOINTS. Those tokens are gone; do not reintroduce a fetch that bypasses it.
1482
+ */
1483
+ /**
1484
+ * True for nodes rendered on the EDITING canvas — provided by `BuilderComponent` at component level,
1485
+ * so it reaches every `[ioxRender]` under `<app-builder>` and nothing else (Preview renders the same
1486
+ * directive outside it). It answers "is this the editor?" — the canvas-only "Add image" label hangs off
1487
+ * it. `IS_PREVIEW` answers the other question, "is this a preview/live render?" (links navigate,
1488
+ * sliders run live), and is provided ONLY by Preview (component level) and the engine; neither token
1489
+ * implies the other — a design sampler, for one, is neither.
1490
+ */
1491
+ const IOX_BUILDER_CANVAS = new InjectionToken('IOX_BUILDER_CANVAS');
1492
+ /**
1493
+ * Translates the builder library's OWN chrome.
1494
+ *
1495
+ * The library cannot depend on ngx-translate — that is the host app's choice — but its toolbar was
1496
+ * hardcoded English, which shows through as untranslated controls in a Hebrew/RTL admin. This token
1497
+ * lets the host route those strings through its own i18n while the library keeps working standalone:
1498
+ * unprovided, `defaultBuilderLabels` returns the English text, so nothing ever renders a raw key.
1499
+ *
1500
+ * Keys are namespaced `builder.chrome.*` and listed in `BUILDER_CHROME_EN` below — that object is
1501
+ * the contract. A host providing this token should add those keys to its own bundles.
1502
+ */
1503
+ const IOX_BUILDER_LABELS = new InjectionToken('IOX_BUILDER_LABELS', {
1504
+ providedIn: 'root',
1505
+ factory: () => defaultBuilderLabels,
1506
+ });
1507
+ /** Standalone behaviour: the English text the library ships with. */
1508
+ const defaultBuilderLabels = (_key, fallback) => fallback;
1509
+ /**
1510
+ * Every user-visible string in the builder's own chrome, so a host can copy this into its bundles
1511
+ * in one go rather than discovering keys one missing label at a time.
1512
+ */
1513
+ const BUILDER_CHROME_EN = Object.freeze({
1514
+ 'builder.chrome.select': 'Select',
1515
+ 'builder.chrome.pan': 'Pan',
1516
+ 'builder.chrome.style': 'Style',
1517
+ 'builder.chrome.publish': 'Publish',
1518
+ 'builder.chrome.unpublish': 'Unpublish',
1519
+ 'builder.chrome.breakpoint': 'Breakpoint',
1520
+ 'builder.chrome.screenWidth': 'Screen width',
1521
+ 'builder.chrome.zoom': 'Zoom',
1522
+ 'builder.chrome.audit': 'Responsive audit',
1523
+ 'builder.chrome.auditHint': 'Responsive audit — what is broken at this width',
1524
+ 'builder.chrome.undo': 'Undo',
1525
+ 'builder.chrome.redo': 'Redo',
1526
+ 'builder.chrome.save': 'Save',
1527
+ 'builder.chrome.preview': 'Preview',
1528
+ 'builder.chrome.addImage': 'Add image',
1529
+ 'builder.chrome.refuseNestedForm': 'A form can’t go inside another form. Place it outside the form.',
1530
+ 'builder.chrome.refuseStateBlock': 'Success and error messages belong to their form. They can’t be placed on their own.',
1531
+ 'builder.chrome.refuseFormInLink': 'A form can’t go inside a link or a button — a click in a field would follow the link.',
1532
+ 'builder.chrome.auditFormInLink': 'Is inside {host} — a click in a field would follow the link.',
1533
+ 'builder.chrome.auditMoveFormOut': 'Move it out',
1534
+ });
1535
+ const CANVAS_EMPTY_IMAGE_LABEL_KEY = 'builder.chrome.addImage';
1536
+ const CANVAS_EMPTY_IMAGE_LABEL_EN = BUILDER_CHROME_EN[CANVAS_EMPTY_IMAGE_LABEL_KEY];
1537
+ /**
1538
+ * Marks an element the builder draws on the canvas that the live site never has — drop placeholders,
1539
+ * repeat badges, the empty slider hint. A page thumbnail is a picture of the SITE, so its capture
1540
+ * leaves every marked element out.
1541
+ */
1542
+ const EDITOR_CHROME_ATTRIBUTE = 'data-iox-chrome';
1543
+
1544
+ const FINDING_KEY = 'builder.chrome.auditFormInLink';
1545
+ const FIX_KEY = 'builder.chrome.auditMoveFormOut';
1546
+ const HOST_PLACEHOLDER = '{host}';
1547
+ /**
1548
+ * A page saved before the insert rule (Doron, 2026-10-20) may hold a form inside a link or a button. Nothing
1549
+ * moves by itself: each one is an audit finding naming the form and the link, with a one-click fix on the page
1550
+ * (a global element's is reported only — a global edit is not a page-history step, so it could not be undone).
1551
+ */
1552
+ function formNestingFindings(page, globals, labels = defaultBuilderLabels) {
1553
+ const say = (key) => labels(key, BUILDER_CHROME_EN[key]);
1554
+ const findings = [];
1555
+ for (const [roots, fixable] of [[page, true], [globals, false]]) {
1556
+ for (const { host, content } of findFormContentInHosts(roots)) {
1557
+ if (!content.id)
1558
+ continue;
1559
+ findings.push({
1560
+ nodeId: content.id,
1561
+ nodeName: content.label || content.type,
1562
+ code: AuditCode.FormInLink,
1563
+ severity: AuditSeverity.Error,
1564
+ message: say(FINDING_KEY).replace(HOST_PLACEHOLDER, host.label || host.type),
1565
+ ...(fixable ? { structureFix: StructureFix.MoveFormOut, fixLabel: say(FIX_KEY) } : {}),
1566
+ });
1567
+ }
1568
+ }
1569
+ return findings;
1570
+ }
1571
+ /**
1572
+ * The one-click fix: move the form content `contentId` out of the OUTERMOST link or button holding it, to
1573
+ * right after that host in the host's own parent — every element kept, the link kept, nothing restyled.
1574
+ * Returns the moved node, or null when `contentId` is not form content inside a host in `roots`. `afterRemove`
1575
+ * runs between taking it out and putting it back, so a renderer can let the old instance go first (as a
1576
+ * drag move does) — it owns the node's CSS rule, and its destroy would otherwise remove the new one's.
1577
+ */
1578
+ function moveFormContentOut(roots, contentId, afterRemove) {
1579
+ const hit = findFormContentInHosts(roots).find(f => f.content.id === contentId);
1580
+ if (!hit)
1581
+ return null;
1582
+ const parent = findParentOf(roots, hit.content);
1583
+ const hostPath = nodeAncestorPath(roots, hit.host);
1584
+ if (!parent?.children || !hostPath)
1585
+ return null;
1586
+ const hostSiblings = hostPath.length > 1 ? hostPath[hostPath.length - 2].children : roots;
1587
+ if (!hostSiblings)
1588
+ return null;
1589
+ parent.children.splice(parent.children.indexOf(hit.content), 1);
1590
+ afterRemove?.();
1591
+ hostSiblings.splice(hostSiblings.indexOf(hit.host) + 1, 0, hit.content);
1592
+ return hit.content;
1593
+ }
1594
+
1467
1595
  const GRID = (columns) => ({ display: 'grid', gridTemplateColumns: columns, gap: '24px', width: '100%' });
1468
1596
  const ONE_COLUMN = { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))' };
1469
1597
  const LAYOUT_PRESETS = Object.freeze([
@@ -1566,71 +1694,6 @@ function buildLayoutPreset(def, createNode) {
1566
1694
  return container;
1567
1695
  }
1568
1696
 
1569
- /**
1570
- * Canvas data preview used to come from host-provided content services (IOX_CONTENT_SERVICE /
1571
- * IOX_SHOP_SERVICE), whose `getContent(contentType, orgId)` hit the ADMIN resources endpoint and
1572
- * accepted no filters — so the Repeater/Slider ignored every data-source filter. Data now flows
1573
- * through BuilderDataSourceService + the shared planDataSource, configured with
1574
- * IOX_DATA_SOURCE_ENDPOINTS. Those tokens are gone; do not reintroduce a fetch that bypasses it.
1575
- */
1576
- /**
1577
- * True for nodes rendered on the EDITING canvas — provided by `BuilderComponent` at component level,
1578
- * so it reaches every `[ioxRender]` under `<app-builder>` and nothing else (Preview renders the same
1579
- * directive outside it). It answers "is this the editor?" — the canvas-only "Add image" label hangs off
1580
- * it. `IS_PREVIEW` answers the other question, "is this a preview/live render?" (links navigate,
1581
- * sliders run live), and is provided ONLY by Preview (component level) and the engine; neither token
1582
- * implies the other — a design sampler, for one, is neither.
1583
- */
1584
- const IOX_BUILDER_CANVAS = new InjectionToken('IOX_BUILDER_CANVAS');
1585
- /**
1586
- * Translates the builder library's OWN chrome.
1587
- *
1588
- * The library cannot depend on ngx-translate — that is the host app's choice — but its toolbar was
1589
- * hardcoded English, which shows through as untranslated controls in a Hebrew/RTL admin. This token
1590
- * lets the host route those strings through its own i18n while the library keeps working standalone:
1591
- * unprovided, `defaultBuilderLabels` returns the English text, so nothing ever renders a raw key.
1592
- *
1593
- * Keys are namespaced `builder.chrome.*` and listed in `BUILDER_CHROME_EN` below — that object is
1594
- * the contract. A host providing this token should add those keys to its own bundles.
1595
- */
1596
- const IOX_BUILDER_LABELS = new InjectionToken('IOX_BUILDER_LABELS', {
1597
- providedIn: 'root',
1598
- factory: () => defaultBuilderLabels,
1599
- });
1600
- /** Standalone behaviour: the English text the library ships with. */
1601
- const defaultBuilderLabels = (_key, fallback) => fallback;
1602
- /**
1603
- * Every user-visible string in the builder's own chrome, so a host can copy this into its bundles
1604
- * in one go rather than discovering keys one missing label at a time.
1605
- */
1606
- const BUILDER_CHROME_EN = Object.freeze({
1607
- 'builder.chrome.select': 'Select',
1608
- 'builder.chrome.pan': 'Pan',
1609
- 'builder.chrome.style': 'Style',
1610
- 'builder.chrome.publish': 'Publish',
1611
- 'builder.chrome.unpublish': 'Unpublish',
1612
- 'builder.chrome.breakpoint': 'Breakpoint',
1613
- 'builder.chrome.screenWidth': 'Screen width',
1614
- 'builder.chrome.zoom': 'Zoom',
1615
- 'builder.chrome.audit': 'Responsive audit',
1616
- 'builder.chrome.auditHint': 'Responsive audit — what is broken at this width',
1617
- 'builder.chrome.undo': 'Undo',
1618
- 'builder.chrome.redo': 'Redo',
1619
- 'builder.chrome.save': 'Save',
1620
- 'builder.chrome.preview': 'Preview',
1621
- 'builder.chrome.addImage': 'Add image',
1622
- 'builder.chrome.refuseNestedForm': 'A form can’t go inside another form. Place it outside the form.',
1623
- 'builder.chrome.refuseStateBlock': 'Success and error messages belong to their form. They can’t be placed on their own.',
1624
- });
1625
- const CANVAS_EMPTY_IMAGE_LABEL_KEY = 'builder.chrome.addImage';
1626
- const CANVAS_EMPTY_IMAGE_LABEL_EN = BUILDER_CHROME_EN[CANVAS_EMPTY_IMAGE_LABEL_KEY];
1627
- /**
1628
- * Marks an element the builder draws on the canvas that the live site never has — drop placeholders,
1629
- * repeat badges, the empty slider hint. A page thumbnail is a picture of the SITE, so its capture
1630
- * leaves every marked element out.
1631
- */
1632
- const EDITOR_CHROME_ATTRIBUTE = 'data-iox-chrome';
1633
-
1634
1697
  /**
1635
1698
  * The canvas owns every drag: interact.js moves nodes from pointer events, and a NATIVE drag steals
1636
1699
  * that pointer stream (the browser fires pointercancel) while carrying a payload of its own. On an
@@ -1944,8 +2007,8 @@ function resolveDragTarget(el, selectedElement, isDragHandle) {
1944
2007
 
1945
2008
  /**
1946
2009
  * What may NOT be placed where — the ONE rule every insert path asks (drag, click-to-insert, quick-add,
1947
- * the Form presets, paste, duplicate). Everything not refused here is accepted: any container-capable
1948
- * node takes any child, a Form included.
2010
+ * the Form presets, paste, duplicate, layer-tree moves, Wrap in ▸). Everything not refused here is accepted:
2011
+ * any container-capable node takes any child, a Form included.
1949
2012
  */
1950
2013
  var InsertRefusal;
1951
2014
  (function (InsertRefusal) {
@@ -1953,12 +2016,16 @@ var InsertRefusal;
1953
2016
  InsertRefusal["NestedForm"] = "nestedForm";
1954
2017
  /** A Success / Error block is part of its form's state machine; it is never placed on its own. */
1955
2018
  InsertRefusal["StateBlockAlone"] = "stateBlockAlone";
2019
+ /** Form content inside a link or a button (Doron, 2026-10-20) — HTML forbids interactive content in
2020
+ * `<a>`/`<button>`, and a click in a field would follow the link. The table is forms-core's. */
2021
+ InsertRefusal["FormInInteractive"] = "formInInteractive";
1956
2022
  })(InsertRefusal || (InsertRefusal = {}));
1957
2023
  const FORM_STATE_BLOCK_TYPES = new Set(['FormSuccess', 'FormError']);
1958
2024
  /** The builder-chrome label that says each refusal on screen (English in `BUILDER_CHROME_EN`). */
1959
2025
  const INSERT_REFUSAL_LABEL_KEYS = Object.freeze({
1960
2026
  [InsertRefusal.NestedForm]: 'builder.chrome.refuseNestedForm',
1961
2027
  [InsertRefusal.StateBlockAlone]: 'builder.chrome.refuseStateBlock',
2028
+ [InsertRefusal.FormInInteractive]: 'builder.chrome.refuseFormInLink',
1962
2029
  });
1963
2030
  /** The refusal in the host's language (the library's English when no host resolver is provided). */
1964
2031
  function insertRefusalLabel(reason, labels = defaultBuilderLabels) {
@@ -1973,7 +2040,7 @@ function containsType(node, type) {
1973
2040
  /**
1974
2041
  * Why `node` may not be inserted under `ancestors` (outermost first, the new parent last; empty = the page),
1975
2042
  * or null when it may. `node` is the whole subtree being placed — a Container holding a Form is refused
1976
- * inside a Form just as a bare Form is. Reordering a node inside its CURRENT parent is not an insert and
2043
+ * 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
1977
2044
  * never asks.
1978
2045
  */
1979
2046
  function insertRefusal(node, ancestors) {
@@ -1983,6 +2050,8 @@ function insertRefusal(node, ancestors) {
1983
2050
  return InsertRefusal.StateBlockAlone;
1984
2051
  if (ancestors.some(a => a?.type === FORM_TYPE) && containsType(node, FORM_TYPE))
1985
2052
  return InsertRefusal.NestedForm;
2053
+ if (formContentInHost(node, ancestors.some(isInteractiveHost)))
2054
+ return InsertRefusal.FormInInteractive;
1986
2055
  return null;
1987
2056
  }
1988
2057
 
@@ -6467,19 +6536,22 @@ class AuditPanelComponent {
6467
6536
  trackFinding(_index, finding) {
6468
6537
  return `${finding.nodeId}:${finding.code}`;
6469
6538
  }
6470
- /** Findings that carry a safe auto-fix — what "Fix all" would apply. */
6539
+ /** Findings that carry a safe auto-fix — what "Fix all" applies (a structure fix is the author's call). */
6471
6540
  get fixableCount() {
6472
- return this.findings.filter(f => !!f.fix).length;
6541
+ return this.findings.filter(f => !!f.fix || !!f.inputFix).length;
6542
+ }
6543
+ hasFix(finding) {
6544
+ return !!(finding.fix || finding.inputFix || finding.structureFix);
6473
6545
  }
6474
6546
  iconFor(severity) {
6475
6547
  return severity === AuditSeverity.Info ? 'ph-thin ph-info' : 'ph-thin ph-warning';
6476
6548
  }
6477
6549
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: AuditPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6478
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: AuditPanelComponent, isStandalone: false, selector: "app-audit-panel", inputs: { findings: "findings", breakpointLabel: "breakpointLabel", running: "running" }, outputs: { select: "select", fix: "fix", fixAll: "fixAll", rerun: "rerun", closePanel: "closePanel" }, ngImport: i0, template: "<div class=\"audit-panel\" role=\"dialog\" aria-label=\"Responsive audit\">\n <header class=\"audit-head\">\n <span class=\"audit-title\">Responsive audit</span>\n <span class=\"audit-scope\">{{ breakpointLabel }}</span>\n <span class=\"audit-spacer\"></span>\n <button type=\"button\" class=\"audit-link\" [disabled]=\"running\" (click)=\"rerun.emit()\">Re-run</button>\n <button type=\"button\" class=\"audit-close\" aria-label=\"Close audit\" (click)=\"closePanel.emit()\">\n <i class=\"ph-thin ph-x\" aria-hidden=\"true\"></i>\n </button>\n </header>\n\n @if (running) {\n <p class=\"audit-empty\">Measuring\u2026</p>\n } @else if (!findings.length) {\n <p class=\"audit-empty audit-empty--clean\">\n <i class=\"ph-thin ph-check-circle\" aria-hidden=\"true\"></i>\n Nothing to fix at this width.\n </p>\n } @else {\n <div class=\"audit-actions\">\n <span>{{ findings.length }} to look at</span>\n @if (fixableCount) {\n <button type=\"button\" class=\"audit-link\" (click)=\"fixAll.emit()\">Fix {{ fixableCount }} automatically</button>\n }\n </div>\n <ul class=\"audit-list\">\n @for (finding of findings; track trackFinding($index, finding)) {\n <li class=\"audit-item\" [class.is-error]=\"finding.severity === severities.Error\">\n <i class=\"audit-icon\" [class]=\"iconFor(finding.severity)\" aria-hidden=\"true\"></i>\n <div class=\"audit-body\">\n <span class=\"audit-node\">{{ finding.nodeName }}</span>\n <span class=\"audit-message\">{{ finding.message }}</span>\n </div>\n <div class=\"audit-buttons\">\n @if (finding.fix) {\n <button type=\"button\" class=\"audit-btn audit-btn--fix\" (click)=\"fix.emit(finding)\">Fix</button>\n }\n <button type=\"button\" class=\"audit-btn\" (click)=\"select.emit(finding)\">Select</button>\n </div>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [".audit-panel{display:flex;flex-direction:column;max-block-size:60vh;inline-size:340px;border:1px solid var(--p-content-border-color);border-radius:10px;background:var(--p-content-background);box-shadow:0 8px 28px #00000029;overflow:hidden}.audit-head{display:flex;align-items:center;gap:6px;padding:9px 11px;border-block-end:1px solid var(--p-content-border-color)}.audit-title{font-size:12px;font-weight:600}.audit-scope{font-size:10px;color:var(--p-text-muted-color)}.audit-spacer{flex:1}.audit-link{padding:0;border:none;background:none;font-size:10.5px;color:#a05c5c;cursor:pointer}.audit-link:hover:not(:disabled){text-decoration:underline}.audit-link:disabled{opacity:.5;pointer-events:none}.audit-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.audit-close{display:flex;padding:2px;border:none;background:none;color:var(--p-text-muted-color);cursor:pointer}.audit-close:hover{color:var(--p-text-color)}.audit-empty{margin:0;padding:18px 12px;text-align:center;font-size:11px;color:var(--p-text-muted-color)}.audit-empty--clean{display:flex;align-items:center;justify-content:center;gap:6px;color:#4f8f6d}.audit-empty--clean i{font-size:15px}.audit-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 11px;background:var(--p-surface-50);font-size:10.5px;color:var(--p-text-muted-color)}.audit-list{margin:0;padding:0;list-style:none;overflow-y:auto}.audit-item{display:flex;align-items:flex-start;gap:8px;padding:9px 11px;border-block-end:1px solid var(--p-content-border-color)}.audit-item:last-child{border-block-end:none}.audit-icon{flex:none;margin-block-start:1px;font-size:14px;color:#d9924e}.is-error .audit-icon{color:#c0554f}.audit-body{flex:1;min-inline-size:0}.audit-node{display:block;font-size:11px;font-weight:600;color:var(--p-text-color)}.audit-message{display:block;margin-block-start:2px;font-size:10px;line-height:1.45;color:var(--p-text-muted-color)}.audit-buttons{display:flex;flex:none;flex-direction:column;gap:4px}.audit-btn{padding:3px 9px;border:1px solid var(--p-content-border-color);border-radius:6px;background:transparent;color:var(--p-text-muted-color);font-size:10px;cursor:pointer;white-space:nowrap}.audit-btn:hover{border-color:#cb9090;color:#a05c5c}.audit-btn:focus-visible{outline:2px solid #cb9090;outline-offset:1px}.audit-btn--fix{background:#a05c5c;border-color:#a05c5c;color:#fff}.audit-btn--fix:hover{background:#8d4f4f;color:#fff}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6550
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: AuditPanelComponent, isStandalone: false, selector: "app-audit-panel", inputs: { findings: "findings", breakpointLabel: "breakpointLabel", running: "running" }, outputs: { select: "select", fix: "fix", fixAll: "fixAll", rerun: "rerun", closePanel: "closePanel" }, ngImport: i0, template: "<div class=\"audit-panel\" role=\"dialog\" aria-label=\"Responsive audit\">\n <header class=\"audit-head\">\n <span class=\"audit-title\">Responsive audit</span>\n <span class=\"audit-scope\">{{ breakpointLabel }}</span>\n <span class=\"audit-spacer\"></span>\n <button type=\"button\" class=\"audit-link\" [disabled]=\"running\" (click)=\"rerun.emit()\">Re-run</button>\n <button type=\"button\" class=\"audit-close\" aria-label=\"Close audit\" (click)=\"closePanel.emit()\">\n <i class=\"ph-thin ph-x\" aria-hidden=\"true\"></i>\n </button>\n </header>\n\n @if (running) {\n <p class=\"audit-empty\">Measuring\u2026</p>\n } @else if (!findings.length) {\n <p class=\"audit-empty audit-empty--clean\">\n <i class=\"ph-thin ph-check-circle\" aria-hidden=\"true\"></i>\n Nothing to fix at this width.\n </p>\n } @else {\n <div class=\"audit-actions\">\n <span>{{ findings.length }} to look at</span>\n @if (fixableCount) {\n <button type=\"button\" class=\"audit-link\" (click)=\"fixAll.emit()\">Fix {{ fixableCount }} automatically</button>\n }\n </div>\n <ul class=\"audit-list\">\n @for (finding of findings; track trackFinding($index, finding)) {\n <li class=\"audit-item\" [class.is-error]=\"finding.severity === severities.Error\">\n <i class=\"audit-icon\" [class]=\"iconFor(finding.severity)\" aria-hidden=\"true\"></i>\n <div class=\"audit-body\">\n <span class=\"audit-node\">{{ finding.nodeName }}</span>\n <span class=\"audit-message\">{{ finding.message }}</span>\n </div>\n <div class=\"audit-buttons\">\n @if (hasFix(finding)) {\n <button type=\"button\" class=\"audit-btn audit-btn--fix\" (click)=\"fix.emit(finding)\">{{ finding.fixLabel || 'Fix' }}</button>\n }\n <button type=\"button\" class=\"audit-btn\" (click)=\"select.emit(finding)\">Select</button>\n </div>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [".audit-panel{display:flex;flex-direction:column;max-block-size:60vh;inline-size:340px;border:1px solid var(--p-content-border-color);border-radius:10px;background:var(--p-content-background);box-shadow:0 8px 28px #00000029;overflow:hidden}.audit-head{display:flex;align-items:center;gap:6px;padding:9px 11px;border-block-end:1px solid var(--p-content-border-color)}.audit-title{font-size:12px;font-weight:600}.audit-scope{font-size:10px;color:var(--p-text-muted-color)}.audit-spacer{flex:1}.audit-link{padding:0;border:none;background:none;font-size:10.5px;color:#a05c5c;cursor:pointer}.audit-link:hover:not(:disabled){text-decoration:underline}.audit-link:disabled{opacity:.5;pointer-events:none}.audit-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.audit-close{display:flex;padding:2px;border:none;background:none;color:var(--p-text-muted-color);cursor:pointer}.audit-close:hover{color:var(--p-text-color)}.audit-empty{margin:0;padding:18px 12px;text-align:center;font-size:11px;color:var(--p-text-muted-color)}.audit-empty--clean{display:flex;align-items:center;justify-content:center;gap:6px;color:#4f8f6d}.audit-empty--clean i{font-size:15px}.audit-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 11px;background:var(--p-surface-50);font-size:10.5px;color:var(--p-text-muted-color)}.audit-list{margin:0;padding:0;list-style:none;overflow-y:auto}.audit-item{display:flex;align-items:flex-start;gap:8px;padding:9px 11px;border-block-end:1px solid var(--p-content-border-color)}.audit-item:last-child{border-block-end:none}.audit-icon{flex:none;margin-block-start:1px;font-size:14px;color:#d9924e}.is-error .audit-icon{color:#c0554f}.audit-body{flex:1;min-inline-size:0}.audit-node{display:block;font-size:11px;font-weight:600;color:var(--p-text-color)}.audit-message{display:block;margin-block-start:2px;font-size:10px;line-height:1.45;color:var(--p-text-muted-color)}.audit-buttons{display:flex;flex:none;flex-direction:column;gap:4px}.audit-btn{padding:3px 9px;border:1px solid var(--p-content-border-color);border-radius:6px;background:transparent;color:var(--p-text-muted-color);font-size:10px;cursor:pointer;white-space:nowrap}.audit-btn:hover{border-color:#cb9090;color:#a05c5c}.audit-btn:focus-visible{outline:2px solid #cb9090;outline-offset:1px}.audit-btn--fix{background:#a05c5c;border-color:#a05c5c;color:#fff}.audit-btn--fix:hover{background:#8d4f4f;color:#fff}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6479
6551
  }
6480
6552
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: AuditPanelComponent, decorators: [{
6481
6553
  type: Component,
6482
- args: [{ selector: 'app-audit-panel', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"audit-panel\" role=\"dialog\" aria-label=\"Responsive audit\">\n <header class=\"audit-head\">\n <span class=\"audit-title\">Responsive audit</span>\n <span class=\"audit-scope\">{{ breakpointLabel }}</span>\n <span class=\"audit-spacer\"></span>\n <button type=\"button\" class=\"audit-link\" [disabled]=\"running\" (click)=\"rerun.emit()\">Re-run</button>\n <button type=\"button\" class=\"audit-close\" aria-label=\"Close audit\" (click)=\"closePanel.emit()\">\n <i class=\"ph-thin ph-x\" aria-hidden=\"true\"></i>\n </button>\n </header>\n\n @if (running) {\n <p class=\"audit-empty\">Measuring\u2026</p>\n } @else if (!findings.length) {\n <p class=\"audit-empty audit-empty--clean\">\n <i class=\"ph-thin ph-check-circle\" aria-hidden=\"true\"></i>\n Nothing to fix at this width.\n </p>\n } @else {\n <div class=\"audit-actions\">\n <span>{{ findings.length }} to look at</span>\n @if (fixableCount) {\n <button type=\"button\" class=\"audit-link\" (click)=\"fixAll.emit()\">Fix {{ fixableCount }} automatically</button>\n }\n </div>\n <ul class=\"audit-list\">\n @for (finding of findings; track trackFinding($index, finding)) {\n <li class=\"audit-item\" [class.is-error]=\"finding.severity === severities.Error\">\n <i class=\"audit-icon\" [class]=\"iconFor(finding.severity)\" aria-hidden=\"true\"></i>\n <div class=\"audit-body\">\n <span class=\"audit-node\">{{ finding.nodeName }}</span>\n <span class=\"audit-message\">{{ finding.message }}</span>\n </div>\n <div class=\"audit-buttons\">\n @if (finding.fix) {\n <button type=\"button\" class=\"audit-btn audit-btn--fix\" (click)=\"fix.emit(finding)\">Fix</button>\n }\n <button type=\"button\" class=\"audit-btn\" (click)=\"select.emit(finding)\">Select</button>\n </div>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [".audit-panel{display:flex;flex-direction:column;max-block-size:60vh;inline-size:340px;border:1px solid var(--p-content-border-color);border-radius:10px;background:var(--p-content-background);box-shadow:0 8px 28px #00000029;overflow:hidden}.audit-head{display:flex;align-items:center;gap:6px;padding:9px 11px;border-block-end:1px solid var(--p-content-border-color)}.audit-title{font-size:12px;font-weight:600}.audit-scope{font-size:10px;color:var(--p-text-muted-color)}.audit-spacer{flex:1}.audit-link{padding:0;border:none;background:none;font-size:10.5px;color:#a05c5c;cursor:pointer}.audit-link:hover:not(:disabled){text-decoration:underline}.audit-link:disabled{opacity:.5;pointer-events:none}.audit-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.audit-close{display:flex;padding:2px;border:none;background:none;color:var(--p-text-muted-color);cursor:pointer}.audit-close:hover{color:var(--p-text-color)}.audit-empty{margin:0;padding:18px 12px;text-align:center;font-size:11px;color:var(--p-text-muted-color)}.audit-empty--clean{display:flex;align-items:center;justify-content:center;gap:6px;color:#4f8f6d}.audit-empty--clean i{font-size:15px}.audit-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 11px;background:var(--p-surface-50);font-size:10.5px;color:var(--p-text-muted-color)}.audit-list{margin:0;padding:0;list-style:none;overflow-y:auto}.audit-item{display:flex;align-items:flex-start;gap:8px;padding:9px 11px;border-block-end:1px solid var(--p-content-border-color)}.audit-item:last-child{border-block-end:none}.audit-icon{flex:none;margin-block-start:1px;font-size:14px;color:#d9924e}.is-error .audit-icon{color:#c0554f}.audit-body{flex:1;min-inline-size:0}.audit-node{display:block;font-size:11px;font-weight:600;color:var(--p-text-color)}.audit-message{display:block;margin-block-start:2px;font-size:10px;line-height:1.45;color:var(--p-text-muted-color)}.audit-buttons{display:flex;flex:none;flex-direction:column;gap:4px}.audit-btn{padding:3px 9px;border:1px solid var(--p-content-border-color);border-radius:6px;background:transparent;color:var(--p-text-muted-color);font-size:10px;cursor:pointer;white-space:nowrap}.audit-btn:hover{border-color:#cb9090;color:#a05c5c}.audit-btn:focus-visible{outline:2px solid #cb9090;outline-offset:1px}.audit-btn--fix{background:#a05c5c;border-color:#a05c5c;color:#fff}.audit-btn--fix:hover{background:#8d4f4f;color:#fff}\n"] }]
6554
+ args: [{ selector: 'app-audit-panel', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"audit-panel\" role=\"dialog\" aria-label=\"Responsive audit\">\n <header class=\"audit-head\">\n <span class=\"audit-title\">Responsive audit</span>\n <span class=\"audit-scope\">{{ breakpointLabel }}</span>\n <span class=\"audit-spacer\"></span>\n <button type=\"button\" class=\"audit-link\" [disabled]=\"running\" (click)=\"rerun.emit()\">Re-run</button>\n <button type=\"button\" class=\"audit-close\" aria-label=\"Close audit\" (click)=\"closePanel.emit()\">\n <i class=\"ph-thin ph-x\" aria-hidden=\"true\"></i>\n </button>\n </header>\n\n @if (running) {\n <p class=\"audit-empty\">Measuring\u2026</p>\n } @else if (!findings.length) {\n <p class=\"audit-empty audit-empty--clean\">\n <i class=\"ph-thin ph-check-circle\" aria-hidden=\"true\"></i>\n Nothing to fix at this width.\n </p>\n } @else {\n <div class=\"audit-actions\">\n <span>{{ findings.length }} to look at</span>\n @if (fixableCount) {\n <button type=\"button\" class=\"audit-link\" (click)=\"fixAll.emit()\">Fix {{ fixableCount }} automatically</button>\n }\n </div>\n <ul class=\"audit-list\">\n @for (finding of findings; track trackFinding($index, finding)) {\n <li class=\"audit-item\" [class.is-error]=\"finding.severity === severities.Error\">\n <i class=\"audit-icon\" [class]=\"iconFor(finding.severity)\" aria-hidden=\"true\"></i>\n <div class=\"audit-body\">\n <span class=\"audit-node\">{{ finding.nodeName }}</span>\n <span class=\"audit-message\">{{ finding.message }}</span>\n </div>\n <div class=\"audit-buttons\">\n @if (hasFix(finding)) {\n <button type=\"button\" class=\"audit-btn audit-btn--fix\" (click)=\"fix.emit(finding)\">{{ finding.fixLabel || 'Fix' }}</button>\n }\n <button type=\"button\" class=\"audit-btn\" (click)=\"select.emit(finding)\">Select</button>\n </div>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [".audit-panel{display:flex;flex-direction:column;max-block-size:60vh;inline-size:340px;border:1px solid var(--p-content-border-color);border-radius:10px;background:var(--p-content-background);box-shadow:0 8px 28px #00000029;overflow:hidden}.audit-head{display:flex;align-items:center;gap:6px;padding:9px 11px;border-block-end:1px solid var(--p-content-border-color)}.audit-title{font-size:12px;font-weight:600}.audit-scope{font-size:10px;color:var(--p-text-muted-color)}.audit-spacer{flex:1}.audit-link{padding:0;border:none;background:none;font-size:10.5px;color:#a05c5c;cursor:pointer}.audit-link:hover:not(:disabled){text-decoration:underline}.audit-link:disabled{opacity:.5;pointer-events:none}.audit-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.audit-close{display:flex;padding:2px;border:none;background:none;color:var(--p-text-muted-color);cursor:pointer}.audit-close:hover{color:var(--p-text-color)}.audit-empty{margin:0;padding:18px 12px;text-align:center;font-size:11px;color:var(--p-text-muted-color)}.audit-empty--clean{display:flex;align-items:center;justify-content:center;gap:6px;color:#4f8f6d}.audit-empty--clean i{font-size:15px}.audit-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 11px;background:var(--p-surface-50);font-size:10.5px;color:var(--p-text-muted-color)}.audit-list{margin:0;padding:0;list-style:none;overflow-y:auto}.audit-item{display:flex;align-items:flex-start;gap:8px;padding:9px 11px;border-block-end:1px solid var(--p-content-border-color)}.audit-item:last-child{border-block-end:none}.audit-icon{flex:none;margin-block-start:1px;font-size:14px;color:#d9924e}.is-error .audit-icon{color:#c0554f}.audit-body{flex:1;min-inline-size:0}.audit-node{display:block;font-size:11px;font-weight:600;color:var(--p-text-color)}.audit-message{display:block;margin-block-start:2px;font-size:10px;line-height:1.45;color:var(--p-text-muted-color)}.audit-buttons{display:flex;flex:none;flex-direction:column;gap:4px}.audit-btn{padding:3px 9px;border:1px solid var(--p-content-border-color);border-radius:6px;background:transparent;color:var(--p-text-muted-color);font-size:10px;cursor:pointer;white-space:nowrap}.audit-btn:hover{border-color:#cb9090;color:#a05c5c}.audit-btn:focus-visible{outline:2px solid #cb9090;outline-offset:1px}.audit-btn--fix{background:#a05c5c;border-color:#a05c5c;color:#fff}.audit-btn--fix:hover{background:#8d4f4f;color:#fff}\n"] }]
6483
6555
  }], propDecorators: { findings: [{
6484
6556
  type: Input
6485
6557
  }], breakpointLabel: [{
@@ -6577,7 +6649,8 @@ class BuilderComponent {
6577
6649
  };
6578
6650
  visit(this.layout);
6579
6651
  visit(this.globalElements);
6580
- this.auditFindings = auditNodes(nodes, width);
6652
+ this.refreshStructureFindings();
6653
+ this.auditFindings = [...this.structureFindings, ...auditNodes(nodes, width)];
6581
6654
  this.auditRunning = false;
6582
6655
  this.cdr.detectChanges();
6583
6656
  }
@@ -6590,8 +6663,27 @@ class BuilderComponent {
6590
6663
  }
6591
6664
  return out;
6592
6665
  }
6666
+ refreshStructureFindings() {
6667
+ this.structureFindings = formNestingFindings(this.layout, this.globalElements, this.labels);
6668
+ }
6669
+ /** A structure fix moves nodes on the page: ONE layout change, so ONE undo step. */
6670
+ applyStructureFix(finding) {
6671
+ if (finding.structureFix !== StructureFix.MoveFormOut)
6672
+ return;
6673
+ const moved = moveFormContentOut(this.layout, finding.nodeId, () => this.cdr.detectChanges());
6674
+ if (moved) {
6675
+ this.cdr.detectChanges();
6676
+ this._refreshRepeaterFor(moved);
6677
+ this.panelEventService.emit(PanelEventTypes.LAYOUT_CHANGED, this.layout);
6678
+ }
6679
+ this.runAudit();
6680
+ }
6593
6681
  /** Apply one finding's fix into the current breakpoint's layer (or the base styles at Base). */
6594
6682
  applyAuditFix(finding) {
6683
+ if (finding.structureFix) {
6684
+ this.applyStructureFix(finding);
6685
+ return;
6686
+ }
6595
6687
  const node = this.findNodeById(finding.nodeId, this.layout) ?? this.findNodeById(finding.nodeId, this.globalElements);
6596
6688
  if (!node)
6597
6689
  return;
@@ -6769,6 +6861,7 @@ class BuilderComponent {
6769
6861
  this.selectoCanvas = selectoCanvas;
6770
6862
  this.styleRegistry = styleRegistry;
6771
6863
  this.contextStore = inject(ContextStore);
6864
+ this.labels = inject(IOX_BUILDER_LABELS);
6772
6865
  /**
6773
6866
  * Re-apply context-bound inputs on the CANVAS when a slot changes — the builder twin of the
6774
6867
  * engine's effect. The canvas is a live runtime, so hovering a repeater row while editing must
@@ -6829,6 +6922,9 @@ class BuilderComponent {
6829
6922
  // ── Responsive audit (A5) ──────────────────────────────────────────────────
6830
6923
  /** Findings for the width being edited; the panel is open while `auditOpen`. */
6831
6924
  this.auditFindings = [];
6925
+ /** Width-independent findings (a form inside a link), kept current on every load and edit so the
6926
+ * audit button shows them before the audit is ever opened. */
6927
+ this.structureFindings = [];
6832
6928
  this.auditOpen = false;
6833
6929
  this.auditRunning = false;
6834
6930
  this.activeZoom = 100;
@@ -6925,8 +7021,10 @@ class BuilderComponent {
6925
7021
  setTimeout(() => this.updatePreviewVh());
6926
7022
  }
6927
7023
  // A newly loaded page: the bar's dots must reflect its overrides before anything is selected.
6928
- if (changes['layout'] || changes['globalElements'])
7024
+ if (changes['layout'] || changes['globalElements']) {
6929
7025
  this.refreshBreakpointCounts();
7026
+ this.refreshStructureFindings();
7027
+ }
6930
7028
  }
6931
7029
  ngAfterViewInit() {
6932
7030
  this.updatePreviewVh();
@@ -7477,6 +7575,10 @@ class BuilderComponent {
7477
7575
  const wrapper = this.registry.createConfig(wrapType);
7478
7576
  if (!wrapper)
7479
7577
  break;
7578
+ // Wrapping places the node under the wrapper: the same acceptance rule as any insert
7579
+ // (a form never goes inside a Linked Container).
7580
+ if (this.dragEngine.refuseInsert(node, [...this.siblingAncestors(node), wrapper]))
7581
+ break;
7480
7582
  // Wrap in place within whichever tree the node lives in — page
7481
7583
  // layout first, then global elements. Context is never crossed.
7482
7584
  let wrapped = wrapNode(this.layout, node, wrapper);
@@ -7862,6 +7964,7 @@ class BuilderComponent {
7862
7964
  // Every style or structural edit — including a breakpoint override — emits LAYOUT_CHANGED.
7863
7965
  if (panelEvent.type === PanelEventTypes.LAYOUT_CHANGED) {
7864
7966
  this.refreshBreakpointCounts();
7967
+ this.refreshStructureFindings();
7865
7968
  return;
7866
7969
  }
7867
7970
  if (panelEvent.type === PanelEventTypes.PANEL_CLOSE) {
@@ -7873,11 +7976,11 @@ class BuilderComponent {
7873
7976
  }
7874
7977
  }
7875
7978
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderComponent, deps: [{ token: ComponentRegistryService }, { token: OverlayService }, { token: PanelEventService }, { token: DragEngineService }, { token: DataSourceRegistryService }, { token: ViewportService }, { token: FormStateService }, { token: InteractionEngineService }, { token: InteractionPresetRegistryService }, { token: BuilderClipboardService }, { token: SymbolRegistryService }, { token: i0.ChangeDetectorRef }, { token: i0.ApplicationRef }, { token: MoveableCanvasService }, { token: SelectoCanvasService }, { token: StyleRegistryService }], target: i0.ɵɵFactoryTarget.Component }); }
7876
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: BuilderComponent, isStandalone: false, selector: "app-builder", inputs: { layout: "layout", dataSources: "dataSources", orgId: "orgId", routeParams: "routeParams", pageName: "pageName", isSaving: "isSaving", pageStatus: "pageStatus", pageSettings: "pageSettings", globalElements: "globalElements", canUndo: "canUndo", canRedo: "canRedo", moveableMode: "moveableMode" }, outputs: { save: "save", undo: "undo", redo: "redo", back: "back", preview: "preview", publishToggle: "publishToggle", pageSettingsChange: "pageSettingsChange", applyAnimationToAll: "applyAnimationToAll", saveBlock: "saveBlock", globalElementsChange: "globalElementsChange" }, providers: [{ provide: IOX_BUILDER_CANVAS, useValue: true }], viewQueries: [{ propertyName: "previewCanvas", first: true, predicate: ["previewCanvas"], descendants: true }, { propertyName: "previewScrollRef", first: true, predicate: ["previewScroll"], descendants: true }, { propertyName: "lenisContentRef", first: true, predicate: ["lenisContent"], descendants: true }, { propertyName: "globalOverlayRef", first: true, predicate: ["globalOverlay"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"builder\">\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <!-- The switcher is the panel's OWN tab bar. It used to live in the header, at the\n maximum possible distance from the thing it controls; attached, the column and the\n panel are one card with one shadow. See board D2. -->\n <aside class=\"sidebar\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'pages'\" [class.active]=\"activeSidebar === 'pages'\" title=\"Pages\" aria-label=\"Pages\">\n <i class=\"ph-thin ph-browsers\"></i>\n </button>\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\" title=\"Components\" aria-label=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\" aria-label=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\" aria-label=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n <div class=\"sidebar-body\">\n <div *ngIf=\"activeSidebar === 'pages'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderPages]\"></ng-content>\n </div>\n <div *ngIf=\"activeSidebar === 'components'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n <div *ngIf=\"activeSidebar === 'tokens'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\"\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n </div>\n </aside>\n\n <!-- \u26A0\uFE0F The strips are SIBLINGS of .preview, not children of it. Board D2 floated them\n over the canvas and that is what they did \u2014 over the page being edited, which is the\n one thing in the builder that must stay clear. They keep their pill shape and their\n centre alignment; they just take their own row.\n\n Siblings, specifically, rather than a wrapper INSIDE .preview: `previewCanvas` is the\n overlay container and the coordinate origin for the selection box, the drag clone,\n Lenis and the zoom maths (nine call sites). Nesting anything between it and the canvas\n would shift every one of them by the strip's height. -->\n <div class=\"canvas-column\">\n <!-- \u2460 THE ACTION STRIP \u2014 floating, top-centre (board D2). It was a solid\n full-width header bar costing 56px of height on every screen; the canvas now\n starts at the top of the viewport and the strip hovers over it.\n stopPropagation for the same reason the breadcrumb does: .preview's (click)\n is \"you clicked empty canvas, drop the selection\". -->\n <div class=\"toolbar-strip\" (click)=\"$event.stopPropagation()\">\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeBreakpoint]=\"activeBreakpoint\"\n [breakpointCounts]=\"breakpointCounts\"\n [auditCount]=\"auditFindings.length\"\n [auditOpen]=\"auditOpen\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (breakpointChange)=\"handleToolbarBreakpointChange($event)\"\n (auditToggle)=\"toggleAudit()\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n <ng-content select=\"[builderToolbarActions]\"></ng-content>\n </app-toolbar>\n </div>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\"\n (dragstart)=\"onNativeDragStart($event)\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <!-- Responsive audit (A5) \u2014 floats over the canvas, right gutter. -->\n @if (auditOpen) {\n <div class=\"audit-dock\" (click)=\"$event.stopPropagation()\">\n <app-audit-panel\n [findings]=\"auditFindings\"\n [breakpointLabel]=\"auditBreakpointLabel\"\n [running]=\"auditRunning\"\n (select)=\"selectAuditNode($event)\"\n (fix)=\"applyAuditFix($event)\"\n (fixAll)=\"applyAllAuditFixes()\"\n (rerun)=\"runAudit()\"\n (closePanel)=\"toggleAudit()\"></app-audit-panel>\n </div>\n }\n\n <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height matches viewportHeight so position:fixed + inset:0 fills the\n simulated viewport correctly (CSS calc(100%) would give the wrong\n containing-block height and break full-screen fixed overlays). -->\n <div *ngIf=\"globalElements.length\"\n #globalOverlay\n class=\"global-elements-overlay iox-token-scope\"\n [ngStyle]=\"globalLayerCss\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n <ng-container *ngFor=\"let item of globalElementsBefore\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n <ng-container *ngFor=\"let item of globalElementsAfter\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n </div>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <!-- iox-token-scope: the ORG's design tokens are declared on this class, not :root\n (StyleRegistryService.upsertTokens), so they never repaint the admin chrome. -->\n <iox-page-component class=\"iox-token-scope\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n\n <!-- ngStyle carries the page's own background/typography (pageSettings.style),\n compiled by the SAME function the engine applies to <body>. Inline wins over\n the #canvas-preview white default, which stays as the unset fallback. -->\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [ngStyle]=\"pageStyleCss\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <!-- Empty page (B4): a starting point beats a blank canvas. Each starter is\n a responsive layout preset \u2014 ordinary nodes, editable afterwards. -->\n @if (isPageEmpty) {\n <div class=\"empty-canvas\" (click)=\"$event.stopPropagation()\">\n <h2 class=\"empty-title\">Start with a layout</h2>\n <p class=\"empty-sub\">Each one already adapts to tablet and phone. You can edit or take it apart afterwards.</p>\n <div class=\"empty-grid\">\n @for (starter of starterLayouts; track starter.id) {\n <button type=\"button\" class=\"empty-card\" (click)=\"startWithLayout(starter)\">\n <i [class]=\"starter.icon\" aria-hidden=\"true\"></i>\n <span class=\"empty-card-name\">{{ starter.name }}</span>\n <span class=\"empty-card-desc\">{{ starter.description }}</span>\n </button>\n }\n </div>\n <button type=\"button\" class=\"empty-link\" (click)=\"browseComponents()\">Or start from a single element</button>\n </div>\n }\n\n <!-- WRAPPER COLLAPSE (task 9c): top-level nodes render DIRECTLY into the\n canvas viewport \u2014 no .preview-node wrapper; iox-outer + drag move to the\n host (RenderDirective, via ioxDragSourceId). -->\n <ng-container *ngFor=\"let item of layout\"\n [ioxRender]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n <!-- \u2462 THE ELEMENT PATH \u2014 a thin floating strip, BOTTOM-CENTRE (board D2). It is\n pinned to the PREVIEW VIEWPORT, not to the canvas frame: the frame pans and\n scales, so a breadcrumb tracking its edge would drift off-screen and shrink with\n zoom. It used to sit in the top gutter, which is where the eye is least often \u2014\n and where the floating action strip now lives.\n\n \u26A0\uFE0F stopPropagation is REQUIRED, not defensive. This element lives inside .preview,\n whose (click) is handleCanvasClick() \u2014 \"you clicked empty canvas, drop the\n selection\". Without it, picking an ancestor crumb selects that node and then\n immediately bubbles into the canvas handler, which clears it: the selection and the\n layer-tree highlight both fall back to Body. Same reason .audit-dock stops its own. -->\n <app-breadcrumb-bar\n (click)=\"$event.stopPropagation()\"\n [path]=\"selectedPath\"\n (nodeSelect)=\"onCrumbSelect($event)\"\n (rootSelect)=\"onCrumbRootSelect()\"\n (nodePeek)=\"onCrumbPeek($event)\">\n </app-breadcrumb-bar>\n\n </div>\n\n <!-- Same rule as the sidebar: the switcher belongs to the panel it drives, as a\n tab ROW across its top. -->\n <div class=\"panel-dock\">\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\" aria-label=\"Page settings\">\n <i class=\"ph-thin ph-gear-six\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></i>\n </button>\n </div>\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n }\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .toolbar-strip{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:6px 8px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 40px);overflow-x:auto;scrollbar-width:none}.builder .toolbar-strip::-webkit-scrollbar{display:none}.builder .sidebar-switcher,.builder .panel-switcher{flex:none;display:flex;flex-direction:row;align-items:center;gap:.375rem;padding:7px 10px;background:var(--p-surface-0);border-block-end:1px solid var(--iox-neutral-100, #EDEFF3)}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;border:none;background:var(--iox-surface-3, #F6F7F9);color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--iox-rose-100, #F8EAEA);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:var(--iox-grad-brand, linear-gradient(135deg, #9B4458 0%, #8A3A72 50%, #63347F 100%));color:#fff}.builder .panel-dock{display:grid;grid-template-rows:auto minmax(0,1fr);flex:none;min-height:0;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden;gap:12px;padding:14px;background:var(--iox-surface-2, #EDEFF3)}.builder .sidebar{display:flex;flex-direction:column;inline-size:260px;min-inline-size:260px;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden;flex-shrink:0;min-block-size:0}.builder .sidebar .sidebar-body{flex:1;min-inline-size:0;overflow-y:auto;overflow-x:hidden}.builder .canvas-column{flex:1 1 0;min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden;background:transparent}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;container:iox-canvas/inline-size;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:visible!important}.builder #canvas-preview{background:#fff}.builder .empty-canvas{max-inline-size:560px;margin:0 auto;padding:48px 24px;text-align:center;color:#4b5563}.builder .empty-title{margin:0;font-size:18px;font-weight:600;color:#111827}.builder .empty-sub{margin:6px 0 20px;font-size:12px;line-height:1.5;color:#6b7280}.builder .empty-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.builder .empty-card{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 10px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.builder .empty-card:hover{border-color:#cb9090;box-shadow:0 2px 10px #0000000f}.builder .empty-card:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder .empty-card i{font-size:20px;color:#a05c5c}.builder .empty-card-name{font-size:11.5px;font-weight:600;color:#111827}.builder .empty-card-desc{font-size:9.5px;line-height:1.35;color:#6b7280}.builder .empty-link{margin-block-start:16px;padding:0;border:none;background:none;font-size:11px;color:#a05c5c;cursor:pointer}.builder .empty-link:hover{text-decoration:underline}.builder .empty-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder app-breadcrumb-bar{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:5px 13px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 112px);pointer-events:none}.builder .audit-dock{position:absolute;inset-block-start:20px;inset-inline-end:16px;z-index:20}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden;container:iox-canvas/inline-size}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-inline-start:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i16.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i16.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i16.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i17.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i1.IoxPageComponent, selector: "iox-page-component", inputs: ["class"], outputs: ["scrollEvent"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "component", type: OverlayComponent, selector: "app-overlay", outputs: ["toolbarAction"] }, { kind: "component", type: ToolbarComponent, selector: "app-toolbar", inputs: ["activeMode", "activeBreakpoint", "breakpointCounts", "auditCount", "auditOpen", "activeZoom", "activeScreenWidth", "canUndo", "canRedo", "isSaving", "pageStatus"], outputs: ["modeChange", "breakpointChange", "auditToggle", "zoomChange", "screenWidthChange", "undo", "redo", "save", "preview", "publishToggle"] }, { kind: "component", type: LayerTreeComponent, selector: "app-layer-tree", inputs: ["layout", "globalElements"], outputs: ["nodeSelect", "nodeAction", "nodeMove"] }, { kind: "component", type: BreadcrumbBarComponent, selector: "app-breadcrumb-bar", inputs: ["path"], outputs: ["nodeSelect", "rootSelect", "nodePeek"] }, { kind: "component", type: AuditPanelComponent, selector: "app-audit-panel", inputs: ["findings", "breakpointLabel", "running"], outputs: ["select", "fix", "fixAll", "rerun", "closePanel"] }] }); }
7979
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: BuilderComponent, isStandalone: false, selector: "app-builder", inputs: { layout: "layout", dataSources: "dataSources", orgId: "orgId", routeParams: "routeParams", pageName: "pageName", isSaving: "isSaving", pageStatus: "pageStatus", pageSettings: "pageSettings", globalElements: "globalElements", canUndo: "canUndo", canRedo: "canRedo", moveableMode: "moveableMode" }, outputs: { save: "save", undo: "undo", redo: "redo", back: "back", preview: "preview", publishToggle: "publishToggle", pageSettingsChange: "pageSettingsChange", applyAnimationToAll: "applyAnimationToAll", saveBlock: "saveBlock", globalElementsChange: "globalElementsChange" }, providers: [{ provide: IOX_BUILDER_CANVAS, useValue: true }], viewQueries: [{ propertyName: "previewCanvas", first: true, predicate: ["previewCanvas"], descendants: true }, { propertyName: "previewScrollRef", first: true, predicate: ["previewScroll"], descendants: true }, { propertyName: "lenisContentRef", first: true, predicate: ["lenisContent"], descendants: true }, { propertyName: "globalOverlayRef", first: true, predicate: ["globalOverlay"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"builder\">\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <!-- The switcher is the panel's OWN tab bar. It used to live in the header, at the\n maximum possible distance from the thing it controls; attached, the column and the\n panel are one card with one shadow. See board D2. -->\n <aside class=\"sidebar\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'pages'\" [class.active]=\"activeSidebar === 'pages'\" title=\"Pages\" aria-label=\"Pages\">\n <i class=\"ph-thin ph-browsers\"></i>\n </button>\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\" title=\"Components\" aria-label=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\" aria-label=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\" aria-label=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n <div class=\"sidebar-body\">\n <div *ngIf=\"activeSidebar === 'pages'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderPages]\"></ng-content>\n </div>\n <div *ngIf=\"activeSidebar === 'components'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n <div *ngIf=\"activeSidebar === 'tokens'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\"\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n </div>\n </aside>\n\n <!-- \u26A0\uFE0F The strips are SIBLINGS of .preview, not children of it. Board D2 floated them\n over the canvas and that is what they did \u2014 over the page being edited, which is the\n one thing in the builder that must stay clear. They keep their pill shape and their\n centre alignment; they just take their own row.\n\n Siblings, specifically, rather than a wrapper INSIDE .preview: `previewCanvas` is the\n overlay container and the coordinate origin for the selection box, the drag clone,\n Lenis and the zoom maths (nine call sites). Nesting anything between it and the canvas\n would shift every one of them by the strip's height. -->\n <div class=\"canvas-column\">\n <!-- \u2460 THE ACTION STRIP \u2014 floating, top-centre (board D2). It was a solid\n full-width header bar costing 56px of height on every screen; the canvas now\n starts at the top of the viewport and the strip hovers over it.\n stopPropagation for the same reason the breadcrumb does: .preview's (click)\n is \"you clicked empty canvas, drop the selection\". -->\n <div class=\"toolbar-strip\" (click)=\"$event.stopPropagation()\">\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeBreakpoint]=\"activeBreakpoint\"\n [breakpointCounts]=\"breakpointCounts\"\n [auditCount]=\"auditFindings.length || structureFindings.length\"\n [auditOpen]=\"auditOpen\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (breakpointChange)=\"handleToolbarBreakpointChange($event)\"\n (auditToggle)=\"toggleAudit()\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n <ng-content select=\"[builderToolbarActions]\"></ng-content>\n </app-toolbar>\n </div>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\"\n (dragstart)=\"onNativeDragStart($event)\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <!-- Responsive audit (A5) \u2014 floats over the canvas, right gutter. -->\n @if (auditOpen) {\n <div class=\"audit-dock\" (click)=\"$event.stopPropagation()\">\n <app-audit-panel\n [findings]=\"auditFindings\"\n [breakpointLabel]=\"auditBreakpointLabel\"\n [running]=\"auditRunning\"\n (select)=\"selectAuditNode($event)\"\n (fix)=\"applyAuditFix($event)\"\n (fixAll)=\"applyAllAuditFixes()\"\n (rerun)=\"runAudit()\"\n (closePanel)=\"toggleAudit()\"></app-audit-panel>\n </div>\n }\n\n <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height matches viewportHeight so position:fixed + inset:0 fills the\n simulated viewport correctly (CSS calc(100%) would give the wrong\n containing-block height and break full-screen fixed overlays). -->\n <div *ngIf=\"globalElements.length\"\n #globalOverlay\n class=\"global-elements-overlay iox-token-scope\"\n [ngStyle]=\"globalLayerCss\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n <ng-container *ngFor=\"let item of globalElementsBefore\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n <ng-container *ngFor=\"let item of globalElementsAfter\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n </div>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <!-- iox-token-scope: the ORG's design tokens are declared on this class, not :root\n (StyleRegistryService.upsertTokens), so they never repaint the admin chrome. -->\n <iox-page-component class=\"iox-token-scope\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n\n <!-- ngStyle carries the page's own background/typography (pageSettings.style),\n compiled by the SAME function the engine applies to <body>. Inline wins over\n the #canvas-preview white default, which stays as the unset fallback. -->\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [ngStyle]=\"pageStyleCss\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <!-- Empty page (B4): a starting point beats a blank canvas. Each starter is\n a responsive layout preset \u2014 ordinary nodes, editable afterwards. -->\n @if (isPageEmpty) {\n <div class=\"empty-canvas\" (click)=\"$event.stopPropagation()\">\n <h2 class=\"empty-title\">Start with a layout</h2>\n <p class=\"empty-sub\">Each one already adapts to tablet and phone. You can edit or take it apart afterwards.</p>\n <div class=\"empty-grid\">\n @for (starter of starterLayouts; track starter.id) {\n <button type=\"button\" class=\"empty-card\" (click)=\"startWithLayout(starter)\">\n <i [class]=\"starter.icon\" aria-hidden=\"true\"></i>\n <span class=\"empty-card-name\">{{ starter.name }}</span>\n <span class=\"empty-card-desc\">{{ starter.description }}</span>\n </button>\n }\n </div>\n <button type=\"button\" class=\"empty-link\" (click)=\"browseComponents()\">Or start from a single element</button>\n </div>\n }\n\n <!-- WRAPPER COLLAPSE (task 9c): top-level nodes render DIRECTLY into the\n canvas viewport \u2014 no .preview-node wrapper; iox-outer + drag move to the\n host (RenderDirective, via ioxDragSourceId). -->\n <ng-container *ngFor=\"let item of layout\"\n [ioxRender]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n <!-- \u2462 THE ELEMENT PATH \u2014 a thin floating strip, BOTTOM-CENTRE (board D2). It is\n pinned to the PREVIEW VIEWPORT, not to the canvas frame: the frame pans and\n scales, so a breadcrumb tracking its edge would drift off-screen and shrink with\n zoom. It used to sit in the top gutter, which is where the eye is least often \u2014\n and where the floating action strip now lives.\n\n \u26A0\uFE0F stopPropagation is REQUIRED, not defensive. This element lives inside .preview,\n whose (click) is handleCanvasClick() \u2014 \"you clicked empty canvas, drop the\n selection\". Without it, picking an ancestor crumb selects that node and then\n immediately bubbles into the canvas handler, which clears it: the selection and the\n layer-tree highlight both fall back to Body. Same reason .audit-dock stops its own. -->\n <app-breadcrumb-bar\n (click)=\"$event.stopPropagation()\"\n [path]=\"selectedPath\"\n (nodeSelect)=\"onCrumbSelect($event)\"\n (rootSelect)=\"onCrumbRootSelect()\"\n (nodePeek)=\"onCrumbPeek($event)\">\n </app-breadcrumb-bar>\n\n </div>\n\n <!-- Same rule as the sidebar: the switcher belongs to the panel it drives, as a\n tab ROW across its top. -->\n <div class=\"panel-dock\">\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\" aria-label=\"Page settings\">\n <i class=\"ph-thin ph-gear-six\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></i>\n </button>\n </div>\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n }\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .toolbar-strip{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:6px 8px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 40px);overflow-x:auto;scrollbar-width:none}.builder .toolbar-strip::-webkit-scrollbar{display:none}.builder .sidebar-switcher,.builder .panel-switcher{flex:none;display:flex;flex-direction:row;align-items:center;gap:.375rem;padding:7px 10px;background:var(--p-surface-0);border-block-end:1px solid var(--iox-neutral-100, #EDEFF3)}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;border:none;background:var(--iox-surface-3, #F6F7F9);color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--iox-rose-100, #F8EAEA);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:var(--iox-grad-brand, linear-gradient(135deg, #9B4458 0%, #8A3A72 50%, #63347F 100%));color:#fff}.builder .panel-dock{display:grid;grid-template-rows:auto minmax(0,1fr);flex:none;min-height:0;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden;gap:12px;padding:14px;background:var(--iox-surface-2, #EDEFF3)}.builder .sidebar{display:flex;flex-direction:column;inline-size:260px;min-inline-size:260px;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden;flex-shrink:0;min-block-size:0}.builder .sidebar .sidebar-body{flex:1;min-inline-size:0;overflow-y:auto;overflow-x:hidden}.builder .canvas-column{flex:1 1 0;min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden;background:transparent}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;container:iox-canvas/inline-size;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:visible!important}.builder #canvas-preview{background:#fff}.builder .empty-canvas{max-inline-size:560px;margin:0 auto;padding:48px 24px;text-align:center;color:#4b5563}.builder .empty-title{margin:0;font-size:18px;font-weight:600;color:#111827}.builder .empty-sub{margin:6px 0 20px;font-size:12px;line-height:1.5;color:#6b7280}.builder .empty-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.builder .empty-card{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 10px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.builder .empty-card:hover{border-color:#cb9090;box-shadow:0 2px 10px #0000000f}.builder .empty-card:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder .empty-card i{font-size:20px;color:#a05c5c}.builder .empty-card-name{font-size:11.5px;font-weight:600;color:#111827}.builder .empty-card-desc{font-size:9.5px;line-height:1.35;color:#6b7280}.builder .empty-link{margin-block-start:16px;padding:0;border:none;background:none;font-size:11px;color:#a05c5c;cursor:pointer}.builder .empty-link:hover{text-decoration:underline}.builder .empty-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder app-breadcrumb-bar{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:5px 13px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 112px);pointer-events:none}.builder .audit-dock{position:absolute;inset-block-start:20px;inset-inline-end:16px;z-index:20}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden;container:iox-canvas/inline-size}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-inline-start:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i16.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i16.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i16.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i17.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i1.IoxPageComponent, selector: "iox-page-component", inputs: ["class"], outputs: ["scrollEvent"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender", "ioxDragSourceId"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "component", type: OverlayComponent, selector: "app-overlay", outputs: ["toolbarAction"] }, { kind: "component", type: ToolbarComponent, selector: "app-toolbar", inputs: ["activeMode", "activeBreakpoint", "breakpointCounts", "auditCount", "auditOpen", "activeZoom", "activeScreenWidth", "canUndo", "canRedo", "isSaving", "pageStatus"], outputs: ["modeChange", "breakpointChange", "auditToggle", "zoomChange", "screenWidthChange", "undo", "redo", "save", "preview", "publishToggle"] }, { kind: "component", type: LayerTreeComponent, selector: "app-layer-tree", inputs: ["layout", "globalElements"], outputs: ["nodeSelect", "nodeAction", "nodeMove"] }, { kind: "component", type: BreadcrumbBarComponent, selector: "app-breadcrumb-bar", inputs: ["path"], outputs: ["nodeSelect", "rootSelect", "nodePeek"] }, { kind: "component", type: AuditPanelComponent, selector: "app-audit-panel", inputs: ["findings", "breakpointLabel", "running"], outputs: ["select", "fix", "fixAll", "rerun", "closePanel"] }] }); }
7877
7980
  }
7878
7981
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderComponent, decorators: [{
7879
7982
  type: Component,
7880
- args: [{ selector: 'app-builder', standalone: false, providers: [{ provide: IOX_BUILDER_CANVAS, useValue: true }], template: "<div class=\"builder\">\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <!-- The switcher is the panel's OWN tab bar. It used to live in the header, at the\n maximum possible distance from the thing it controls; attached, the column and the\n panel are one card with one shadow. See board D2. -->\n <aside class=\"sidebar\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'pages'\" [class.active]=\"activeSidebar === 'pages'\" title=\"Pages\" aria-label=\"Pages\">\n <i class=\"ph-thin ph-browsers\"></i>\n </button>\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\" title=\"Components\" aria-label=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\" aria-label=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\" aria-label=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n <div class=\"sidebar-body\">\n <div *ngIf=\"activeSidebar === 'pages'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderPages]\"></ng-content>\n </div>\n <div *ngIf=\"activeSidebar === 'components'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n <div *ngIf=\"activeSidebar === 'tokens'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\"\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n </div>\n </aside>\n\n <!-- \u26A0\uFE0F The strips are SIBLINGS of .preview, not children of it. Board D2 floated them\n over the canvas and that is what they did \u2014 over the page being edited, which is the\n one thing in the builder that must stay clear. They keep their pill shape and their\n centre alignment; they just take their own row.\n\n Siblings, specifically, rather than a wrapper INSIDE .preview: `previewCanvas` is the\n overlay container and the coordinate origin for the selection box, the drag clone,\n Lenis and the zoom maths (nine call sites). Nesting anything between it and the canvas\n would shift every one of them by the strip's height. -->\n <div class=\"canvas-column\">\n <!-- \u2460 THE ACTION STRIP \u2014 floating, top-centre (board D2). It was a solid\n full-width header bar costing 56px of height on every screen; the canvas now\n starts at the top of the viewport and the strip hovers over it.\n stopPropagation for the same reason the breadcrumb does: .preview's (click)\n is \"you clicked empty canvas, drop the selection\". -->\n <div class=\"toolbar-strip\" (click)=\"$event.stopPropagation()\">\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeBreakpoint]=\"activeBreakpoint\"\n [breakpointCounts]=\"breakpointCounts\"\n [auditCount]=\"auditFindings.length\"\n [auditOpen]=\"auditOpen\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (breakpointChange)=\"handleToolbarBreakpointChange($event)\"\n (auditToggle)=\"toggleAudit()\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n <ng-content select=\"[builderToolbarActions]\"></ng-content>\n </app-toolbar>\n </div>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\"\n (dragstart)=\"onNativeDragStart($event)\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <!-- Responsive audit (A5) \u2014 floats over the canvas, right gutter. -->\n @if (auditOpen) {\n <div class=\"audit-dock\" (click)=\"$event.stopPropagation()\">\n <app-audit-panel\n [findings]=\"auditFindings\"\n [breakpointLabel]=\"auditBreakpointLabel\"\n [running]=\"auditRunning\"\n (select)=\"selectAuditNode($event)\"\n (fix)=\"applyAuditFix($event)\"\n (fixAll)=\"applyAllAuditFixes()\"\n (rerun)=\"runAudit()\"\n (closePanel)=\"toggleAudit()\"></app-audit-panel>\n </div>\n }\n\n <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height matches viewportHeight so position:fixed + inset:0 fills the\n simulated viewport correctly (CSS calc(100%) would give the wrong\n containing-block height and break full-screen fixed overlays). -->\n <div *ngIf=\"globalElements.length\"\n #globalOverlay\n class=\"global-elements-overlay iox-token-scope\"\n [ngStyle]=\"globalLayerCss\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n <ng-container *ngFor=\"let item of globalElementsBefore\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n <ng-container *ngFor=\"let item of globalElementsAfter\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n </div>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <!-- iox-token-scope: the ORG's design tokens are declared on this class, not :root\n (StyleRegistryService.upsertTokens), so they never repaint the admin chrome. -->\n <iox-page-component class=\"iox-token-scope\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n\n <!-- ngStyle carries the page's own background/typography (pageSettings.style),\n compiled by the SAME function the engine applies to <body>. Inline wins over\n the #canvas-preview white default, which stays as the unset fallback. -->\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [ngStyle]=\"pageStyleCss\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <!-- Empty page (B4): a starting point beats a blank canvas. Each starter is\n a responsive layout preset \u2014 ordinary nodes, editable afterwards. -->\n @if (isPageEmpty) {\n <div class=\"empty-canvas\" (click)=\"$event.stopPropagation()\">\n <h2 class=\"empty-title\">Start with a layout</h2>\n <p class=\"empty-sub\">Each one already adapts to tablet and phone. You can edit or take it apart afterwards.</p>\n <div class=\"empty-grid\">\n @for (starter of starterLayouts; track starter.id) {\n <button type=\"button\" class=\"empty-card\" (click)=\"startWithLayout(starter)\">\n <i [class]=\"starter.icon\" aria-hidden=\"true\"></i>\n <span class=\"empty-card-name\">{{ starter.name }}</span>\n <span class=\"empty-card-desc\">{{ starter.description }}</span>\n </button>\n }\n </div>\n <button type=\"button\" class=\"empty-link\" (click)=\"browseComponents()\">Or start from a single element</button>\n </div>\n }\n\n <!-- WRAPPER COLLAPSE (task 9c): top-level nodes render DIRECTLY into the\n canvas viewport \u2014 no .preview-node wrapper; iox-outer + drag move to the\n host (RenderDirective, via ioxDragSourceId). -->\n <ng-container *ngFor=\"let item of layout\"\n [ioxRender]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n <!-- \u2462 THE ELEMENT PATH \u2014 a thin floating strip, BOTTOM-CENTRE (board D2). It is\n pinned to the PREVIEW VIEWPORT, not to the canvas frame: the frame pans and\n scales, so a breadcrumb tracking its edge would drift off-screen and shrink with\n zoom. It used to sit in the top gutter, which is where the eye is least often \u2014\n and where the floating action strip now lives.\n\n \u26A0\uFE0F stopPropagation is REQUIRED, not defensive. This element lives inside .preview,\n whose (click) is handleCanvasClick() \u2014 \"you clicked empty canvas, drop the\n selection\". Without it, picking an ancestor crumb selects that node and then\n immediately bubbles into the canvas handler, which clears it: the selection and the\n layer-tree highlight both fall back to Body. Same reason .audit-dock stops its own. -->\n <app-breadcrumb-bar\n (click)=\"$event.stopPropagation()\"\n [path]=\"selectedPath\"\n (nodeSelect)=\"onCrumbSelect($event)\"\n (rootSelect)=\"onCrumbRootSelect()\"\n (nodePeek)=\"onCrumbPeek($event)\">\n </app-breadcrumb-bar>\n\n </div>\n\n <!-- Same rule as the sidebar: the switcher belongs to the panel it drives, as a\n tab ROW across its top. -->\n <div class=\"panel-dock\">\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\" aria-label=\"Page settings\">\n <i class=\"ph-thin ph-gear-six\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></i>\n </button>\n </div>\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n }\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .toolbar-strip{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:6px 8px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 40px);overflow-x:auto;scrollbar-width:none}.builder .toolbar-strip::-webkit-scrollbar{display:none}.builder .sidebar-switcher,.builder .panel-switcher{flex:none;display:flex;flex-direction:row;align-items:center;gap:.375rem;padding:7px 10px;background:var(--p-surface-0);border-block-end:1px solid var(--iox-neutral-100, #EDEFF3)}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;border:none;background:var(--iox-surface-3, #F6F7F9);color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--iox-rose-100, #F8EAEA);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:var(--iox-grad-brand, linear-gradient(135deg, #9B4458 0%, #8A3A72 50%, #63347F 100%));color:#fff}.builder .panel-dock{display:grid;grid-template-rows:auto minmax(0,1fr);flex:none;min-height:0;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden;gap:12px;padding:14px;background:var(--iox-surface-2, #EDEFF3)}.builder .sidebar{display:flex;flex-direction:column;inline-size:260px;min-inline-size:260px;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden;flex-shrink:0;min-block-size:0}.builder .sidebar .sidebar-body{flex:1;min-inline-size:0;overflow-y:auto;overflow-x:hidden}.builder .canvas-column{flex:1 1 0;min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden;background:transparent}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;container:iox-canvas/inline-size;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:visible!important}.builder #canvas-preview{background:#fff}.builder .empty-canvas{max-inline-size:560px;margin:0 auto;padding:48px 24px;text-align:center;color:#4b5563}.builder .empty-title{margin:0;font-size:18px;font-weight:600;color:#111827}.builder .empty-sub{margin:6px 0 20px;font-size:12px;line-height:1.5;color:#6b7280}.builder .empty-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.builder .empty-card{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 10px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.builder .empty-card:hover{border-color:#cb9090;box-shadow:0 2px 10px #0000000f}.builder .empty-card:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder .empty-card i{font-size:20px;color:#a05c5c}.builder .empty-card-name{font-size:11.5px;font-weight:600;color:#111827}.builder .empty-card-desc{font-size:9.5px;line-height:1.35;color:#6b7280}.builder .empty-link{margin-block-start:16px;padding:0;border:none;background:none;font-size:11px;color:#a05c5c;cursor:pointer}.builder .empty-link:hover{text-decoration:underline}.builder .empty-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder app-breadcrumb-bar{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:5px 13px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 112px);pointer-events:none}.builder .audit-dock{position:absolute;inset-block-start:20px;inset-inline-end:16px;z-index:20}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden;container:iox-canvas/inline-size}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-inline-start:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\n"] }]
7983
+ args: [{ selector: 'app-builder', standalone: false, providers: [{ provide: IOX_BUILDER_CANVAS, useValue: true }], template: "<div class=\"builder\">\n\n <div class=\"builder-main\" [class.panel-open]=\"isPanelOpen\">\n <!-- The switcher is the panel's OWN tab bar. It used to live in the header, at the\n maximum possible distance from the thing it controls; attached, the column and the\n panel are one card with one shadow. See board D2. -->\n <aside class=\"sidebar\">\n <div class=\"sidebar-switcher\">\n <button (click)=\"activeSidebar = 'pages'\" [class.active]=\"activeSidebar === 'pages'\" title=\"Pages\" aria-label=\"Pages\">\n <i class=\"ph-thin ph-browsers\"></i>\n </button>\n <button (click)=\"activeSidebar = 'components'\" [class.active]=\"activeSidebar === 'components'\" title=\"Components\" aria-label=\"Components\">\n <i class=\"ph-thin ph-cube\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tree'\" [class.active]=\"activeSidebar === 'tree'\" title=\"Layers\" aria-label=\"Layers\">\n <i class=\"ph-thin ph-tree-structure\"></i>\n </button>\n <button (click)=\"activeSidebar = 'tokens'\" [class.active]=\"activeSidebar === 'tokens'\" title=\"Design Tokens\" aria-label=\"Design Tokens\">\n <i class=\"ph-thin ph-circles-three\"></i>\n </button>\n </div>\n <div class=\"sidebar-body\">\n <div *ngIf=\"activeSidebar === 'pages'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderPages]\"></ng-content>\n </div>\n <div *ngIf=\"activeSidebar === 'components'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderComponents]\"></ng-content>\n </div>\n <div *ngIf=\"activeSidebar === 'tokens'\" class=\"sidebar-slot\">\n <ng-content select=\"[builderTokens]\"></ng-content>\n </div>\n <app-layer-tree *ngIf=\"activeSidebar === 'tree'\"\n [layout]=\"layout\"\n [globalElements]=\"globalElements\"\n (nodeSelect)=\"onTreeNodeSelect($event)\"\n (nodeAction)=\"onTreeNodeAction($event)\"\n (nodeMove)=\"onTreeNodeMove()\"></app-layer-tree>\n </div>\n </aside>\n\n <!-- \u26A0\uFE0F The strips are SIBLINGS of .preview, not children of it. Board D2 floated them\n over the canvas and that is what they did \u2014 over the page being edited, which is the\n one thing in the builder that must stay clear. They keep their pill shape and their\n centre alignment; they just take their own row.\n\n Siblings, specifically, rather than a wrapper INSIDE .preview: `previewCanvas` is the\n overlay container and the coordinate origin for the selection box, the drag clone,\n Lenis and the zoom maths (nine call sites). Nesting anything between it and the canvas\n would shift every one of them by the strip's height. -->\n <div class=\"canvas-column\">\n <!-- \u2460 THE ACTION STRIP \u2014 floating, top-centre (board D2). It was a solid\n full-width header bar costing 56px of height on every screen; the canvas now\n starts at the top of the viewport and the strip hovers over it.\n stopPropagation for the same reason the breadcrumb does: .preview's (click)\n is \"you clicked empty canvas, drop the selection\". -->\n <div class=\"toolbar-strip\" (click)=\"$event.stopPropagation()\">\n <app-toolbar\n [activeMode]=\"activeMode\"\n [activeBreakpoint]=\"activeBreakpoint\"\n [breakpointCounts]=\"breakpointCounts\"\n [auditCount]=\"auditFindings.length || structureFindings.length\"\n [auditOpen]=\"auditOpen\"\n [activeZoom]=\"activeZoom\"\n [activeScreenWidth]=\"activeScreenWidth\"\n [isSaving]=\"isSaving\"\n [pageStatus]=\"pageStatus\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (modeChange)=\"handleToolbarModeChange($event)\"\n (breakpointChange)=\"handleToolbarBreakpointChange($event)\"\n (auditToggle)=\"toggleAudit()\"\n (zoomChange)=\"handleToolbarZoomChange($event)\"\n (screenWidthChange)=\"handleToolbarScreenWidthChange($event)\"\n (save)=\"save.emit()\"\n (preview)=\"preview.emit()\"\n (publishToggle)=\"publishToggle.emit()\"\n (undo)=\"undo.emit()\"\n (redo)=\"redo.emit()\">\n <ng-content select=\"[builderToolbarActions]\"></ng-content>\n </app-toolbar>\n </div>\n\n <main #previewCanvas class=\"preview\"\n [class.grab-mode]=\"activeMode === builderModes.Pan\"\n [class.grabbing]=\"isPanning\"\n (click)=\"handleCanvasClick()\"\n (mousedown)=\"onBoardMouseDown($event)\"\n (mousemove)=\"onBoardMouseMove($event)\"\n (mouseup)=\"onBoardMouseUp()\"\n (mouseleave)=\"onBoardMouseUp()\"\n (dragstart)=\"onNativeDragStart($event)\">\n <app-overlay (toolbarAction)=\"onToolbarAction($event)\"></app-overlay>\n\n <!-- Responsive audit (A5) \u2014 floats over the canvas, right gutter. -->\n @if (auditOpen) {\n <div class=\"audit-dock\" (click)=\"$event.stopPropagation()\">\n <app-audit-panel\n [findings]=\"auditFindings\"\n [breakpointLabel]=\"auditBreakpointLabel\"\n [running]=\"auditRunning\"\n (select)=\"selectAuditNode($event)\"\n (fix)=\"applyAuditFix($event)\"\n (fixAll)=\"applyAllAuditFixes()\"\n (rerun)=\"runAudit()\"\n (closePanel)=\"toggleAudit()\"></app-audit-panel>\n </div>\n }\n\n <!-- Custom Lenis scrollbar \u2014 lives in .preview (not .preview-scroll) so it\n stays in the right gutter and never overlaps the canvas at large widths.\n No translateY counteract needed since it isn't inside the Lenis wrapper. -->\n <div class=\"canvas-scrollbar\"\n [class.visible]=\"isScrollbarVisible\">\n <div class=\"canvas-scrollbar-thumb\"\n [style.height.px]=\"scrollThumbHeight\"\n [style.top.px]=\"scrollThumbTop\">\n </div>\n </div>\n\n <!-- Global elements overlay \u2014 outside Lenis so position:fixed children\n stay pinned to the canvas viewport rather than scrolling with content.\n Shares viewportTransform so elements scale correctly at any zoom level.\n Height matches viewportHeight so position:fixed + inset:0 fills the\n simulated viewport correctly (CSS calc(100%) would give the wrong\n containing-block height and break full-screen fixed overlays). -->\n <div *ngIf=\"globalElements.length\"\n #globalOverlay\n class=\"global-elements-overlay iox-token-scope\"\n [ngStyle]=\"globalLayerCss\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n <ng-container *ngFor=\"let item of globalElementsBefore\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n <ng-container *ngFor=\"let item of globalElementsAfter\">\n <ng-container [ioxRender]=\"item\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </ng-container>\n </div>\n\n <div class=\"preview-scroll\"\n #previewScroll\n [style.width.px]=\"viewportScaledWidth\"\n [class.pan-mode]=\"activeMode === builderModes.Pan\">\n\n <!-- Lenis content proxy: height = realContentHeight \u00D7 scale.\n Lenis scrolls against this value so scroll range is always in\n visual px \u2014 correct at any zoom level. -->\n <div class=\"preview-lenis-content\" #lenisContent\n [style.height.px]=\"scaledContentHeight\">\n\n <!-- iox-token-scope: the ORG's design tokens are declared on this class, not :root\n (StyleRegistryService.upsertTokens), so they never repaint the admin chrome. -->\n <iox-page-component class=\"iox-token-scope\"\n [style.width.px]=\"viewportWidth\"\n [style.height.px]=\"canvasFrameHeight || viewportHeight\"\n [style.transform]=\"viewportTransform\">\n\n <!-- ngStyle carries the page's own background/typography (pageSettings.style),\n compiled by the SAME function the engine applies to <body>. Inline wins over\n the #canvas-preview white default, which stays as the unset fallback. -->\n <div class=\"canvas-viewport\"\n id=\"canvas-preview\"\n [ngStyle]=\"pageStyleCss\"\n [style.min-height.px]=\"canvasMinHeight\">\n\n <!-- Empty page (B4): a starting point beats a blank canvas. Each starter is\n a responsive layout preset \u2014 ordinary nodes, editable afterwards. -->\n @if (isPageEmpty) {\n <div class=\"empty-canvas\" (click)=\"$event.stopPropagation()\">\n <h2 class=\"empty-title\">Start with a layout</h2>\n <p class=\"empty-sub\">Each one already adapts to tablet and phone. You can edit or take it apart afterwards.</p>\n <div class=\"empty-grid\">\n @for (starter of starterLayouts; track starter.id) {\n <button type=\"button\" class=\"empty-card\" (click)=\"startWithLayout(starter)\">\n <i [class]=\"starter.icon\" aria-hidden=\"true\"></i>\n <span class=\"empty-card-name\">{{ starter.name }}</span>\n <span class=\"empty-card-desc\">{{ starter.description }}</span>\n </button>\n }\n </div>\n <button type=\"button\" class=\"empty-link\" (click)=\"browseComponents()\">Or start from a single element</button>\n </div>\n }\n\n <!-- WRAPPER COLLAPSE (task 9c): top-level nodes render DIRECTLY into the\n canvas viewport \u2014 no .preview-node wrapper; iox-outer + drag move to the\n host (RenderDirective, via ioxDragSourceId). -->\n <ng-container *ngFor=\"let item of layout\"\n [ioxRender]=\"item\"\n [ioxDragSourceId]=\"'canvas-preview'\"\n (onClick)=\"handleClick($event)\"\n (onMouseEnter)=\"handleMouseEnter($event)\"\n (onMouseLeave)=\"handleMouseLeave()\">\n </ng-container>\n </div>\n </iox-page-component>\n </div>\n </div>\n </main>\n <!-- \u2462 THE ELEMENT PATH \u2014 a thin floating strip, BOTTOM-CENTRE (board D2). It is\n pinned to the PREVIEW VIEWPORT, not to the canvas frame: the frame pans and\n scales, so a breadcrumb tracking its edge would drift off-screen and shrink with\n zoom. It used to sit in the top gutter, which is where the eye is least often \u2014\n and where the floating action strip now lives.\n\n \u26A0\uFE0F stopPropagation is REQUIRED, not defensive. This element lives inside .preview,\n whose (click) is handleCanvasClick() \u2014 \"you clicked empty canvas, drop the\n selection\". Without it, picking an ancestor crumb selects that node and then\n immediately bubbles into the canvas handler, which clears it: the selection and the\n layer-tree highlight both fall back to Body. Same reason .audit-dock stops its own. -->\n <app-breadcrumb-bar\n (click)=\"$event.stopPropagation()\"\n [path]=\"selectedPath\"\n (nodeSelect)=\"onCrumbSelect($event)\"\n (rootSelect)=\"onCrumbRootSelect()\"\n (nodePeek)=\"onCrumbPeek($event)\">\n </app-breadcrumb-bar>\n\n </div>\n\n <!-- Same rule as the sidebar: the switcher belongs to the panel it drives, as a\n tab ROW across its top. -->\n <div class=\"panel-dock\">\n <div class=\"panel-switcher\">\n <button (click)=\"selectPanel(panelTypes.BINDINGS)\" [class.active]=\"activePanel === panelTypes.BINDINGS\" title=\"Bindings\">\n <i class=\"ph-thin ph-plugs-connected\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.STYLES)\" [class.active]=\"activePanel === panelTypes.STYLES\" title=\"Styles\">\n <i class=\"ph-thin ph-paint-brush\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.PAGE)\" [class.active]=\"activePanel === panelTypes.PAGE\" title=\"Page settings\" aria-label=\"Page settings\">\n <i class=\"ph-thin ph-gear-six\"></i>\n </button>\n <button (click)=\"selectPanel(panelTypes.INTERACTIONS)\" [class.active]=\"activePanel === panelTypes.INTERACTIONS\" title=\"Interactions\">\n <i class=\"ph-thin ph-lightning\"></i>\n </button>\n </div>\n <ng-content select=\"[builderPanel]\"></ng-content>\n </div>\n </div>\n\n <!-- Save as Reusable Block dialog -->\n @if (showSaveBlockDialog) {\n <div class=\"save-block-backdrop\" (click)=\"cancelSaveBlock()\"></div>\n <div class=\"save-block-dialog\" (click)=\"$event.stopPropagation()\">\n <h4 class=\"save-block-title\">Save as Reusable Block</h4>\n <input class=\"save-block-input\"\n type=\"text\"\n placeholder=\"Block name...\"\n [(ngModel)]=\"saveBlockName\"\n (keydown.enter)=\"confirmSaveBlock()\"\n (keydown.escape)=\"cancelSaveBlock()\"\n autofocus>\n <div class=\"save-block-actions\">\n <p-button size=\"small\" severity=\"secondary\" label=\"Cancel\" (click)=\"cancelSaveBlock()\"></p-button>\n <p-button size=\"small\" label=\"Save\" [disabled]=\"!saveBlockName.trim()\" (click)=\"confirmSaveBlock()\"></p-button>\n </div>\n </div>\n }\n</div>", styles: ["@charset \"UTF-8\";:host{display:flex;flex:1;min-height:0;overflow:hidden}.builder{display:flex;flex-direction:column;direction:ltr;width:100%;height:100%;min-height:0;border-radius:0;overflow:hidden;position:relative}.builder .toolbar-strip{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:6px 8px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 40px);overflow-x:auto;scrollbar-width:none}.builder .toolbar-strip::-webkit-scrollbar{display:none}.builder .sidebar-switcher,.builder .panel-switcher{flex:none;display:flex;flex-direction:row;align-items:center;gap:.375rem;padding:7px 10px;background:var(--p-surface-0);border-block-end:1px solid var(--iox-neutral-100, #EDEFF3)}.builder .sidebar-switcher button,.builder .panel-switcher button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;border:none;background:var(--iox-surface-3, #F6F7F9);color:var(--p-text-muted-color);cursor:pointer;font-size:1rem;transition:background .15s ease-out,color .15s ease-out}.builder .sidebar-switcher button i,.builder .panel-switcher button i{pointer-events:none}.builder .sidebar-switcher button:hover,.builder .panel-switcher button:hover{background:var(--iox-rose-100, #F8EAEA);color:var(--p-text-color)}.builder .sidebar-switcher button.active,.builder .panel-switcher button.active{background:var(--iox-grad-brand, linear-gradient(135deg, #9B4458 0%, #8A3A72 50%, #63347F 100%));color:#fff}.builder .panel-dock{display:grid;grid-template-rows:auto minmax(0,1fr);flex:none;min-height:0;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden}.builder .builder-main{display:flex;flex:1 1 0;min-height:0;position:relative;overflow:hidden;gap:12px;padding:14px;background:var(--iox-surface-2, #EDEFF3)}.builder .sidebar{display:flex;flex-direction:column;inline-size:260px;min-inline-size:260px;background:var(--p-surface-0);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:var(--iox-radius-card, 20px);box-shadow:var(--iox-elev-1, 0 0 12px rgba(16, 27, 66, .05));overflow:hidden;flex-shrink:0;min-block-size:0}.builder .sidebar .sidebar-body{flex:1;min-inline-size:0;overflow-y:auto;overflow-x:hidden}.builder .canvas-column{flex:1 1 0;min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px}.builder .preview{flex:1 1 0;min-width:0;min-height:0;position:relative;overflow:hidden;background:transparent}.builder .preview.grab-mode{cursor:grab}.builder .preview.grab-mode iox-page-component{pointer-events:none}.builder .preview.grabbing{cursor:grabbing}.builder .canvas-scrollbar{position:absolute;right:4px;top:20px;bottom:20px;width:6px;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none}.builder .canvas-scrollbar.visible{opacity:1}.builder .canvas-scrollbar-thumb{position:absolute;left:0;width:100%;background:#00000059;border-radius:3px;transition:top .05s linear}.builder .preview-scroll{position:absolute;inset:20px auto;inset-inline-start:50%;transform:translate(-50%);max-width:calc(100% - 40px);height:calc(100% - 40px);overflow:hidden}.builder .preview-lenis-content{position:relative;width:100%;overflow:hidden;contain:paint}.builder iox-page-component{position:absolute!important;top:0;bottom:auto!important;right:auto!important;left:50%;transform-origin:top center;overflow:visible!important;container:iox-canvas/inline-size;box-shadow:0 1px 4px #00000014,0 4px 16px #0000000a;transition:width .25s ease,transform .25s ease}.builder iox-page-component ::ng-deep .iox-page-wrapper{height:auto!important;overflow:visible!important}.builder #canvas-preview{background:#fff}.builder .empty-canvas{max-inline-size:560px;margin:0 auto;padding:48px 24px;text-align:center;color:#4b5563}.builder .empty-title{margin:0;font-size:18px;font-weight:600;color:#111827}.builder .empty-sub{margin:6px 0 20px;font-size:12px;line-height:1.5;color:#6b7280}.builder .empty-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.builder .empty-card{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 10px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.builder .empty-card:hover{border-color:#cb9090;box-shadow:0 2px 10px #0000000f}.builder .empty-card:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder .empty-card i{font-size:20px;color:#a05c5c}.builder .empty-card-name{font-size:11.5px;font-weight:600;color:#111827}.builder .empty-card-desc{font-size:9.5px;line-height:1.35;color:#6b7280}.builder .empty-link{margin-block-start:16px;padding:0;border:none;background:none;font-size:11px;color:#a05c5c;cursor:pointer}.builder .empty-link:hover{text-decoration:underline}.builder .empty-link:focus-visible{outline:2px solid #cb9090;outline-offset:2px}.builder app-breadcrumb-bar{flex:none;align-self:center;max-inline-size:100%;display:flex;align-items:center;padding:5px 13px;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid var(--iox-border-1, #DDE1E8);border-radius:999px;box-shadow:0 6px 20px #101b421a;max-inline-size:calc(100% - 112px);pointer-events:none}.builder .audit-dock{position:absolute;inset-block-start:20px;inset-inline-end:16px;z-index:20}.builder .global-elements-overlay{position:absolute;top:20px;left:50%;transform-origin:top center;pointer-events:none;z-index:9;overflow:hidden;container:iox-canvas/inline-size}.builder .global-elements-overlay ::ng-deep>*{pointer-events:auto}.builder .preview-node.is-selected{cursor:grab}.builder .empty-state{min-height:240px;border:1px dashed var(--p-surface-200);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--p-text-muted-color);text-align:center;padding:1rem}.save-block-backdrop{position:fixed;inset:0;z-index:1000;background:#00000040}.save-block-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1001;background:#fff;border-radius:8px;box-shadow:0 8px 32px #0000002e;padding:24px;width:320px}.save-block-dialog .save-block-title{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--p-text-color, #333)}.save-block-dialog .save-block-input{width:100%;padding:8px 10px;border:1px solid var(--p-surface-300, #d1d5db);border-radius:6px;font-size:13px;outline:none;box-sizing:border-box;margin-bottom:16px}.save-block-dialog .save-block-input:focus{border-color:#cb9090;box-shadow:0 0 0 2px #cb909033}.save-block-dialog .save-block-actions{display:flex;justify-content:flex-end;gap:8px}::ng-deep .moveable-control-box.iox-moveable-silent{z-index:1000!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-line,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation,::ng-deep .moveable-control-box.iox-moveable-silent .moveable-rotation-line{display:none!important}::ng-deep .moveable-control-box.iox-moveable-silent .moveable-control{display:block!important;width:10px!important;height:10px!important;margin-top:-5px!important;margin-inline-start:-5px!important;background:#fff!important;border:1.5px solid #cb9090!important;border-radius:2px!important;box-shadow:0 1px 3px #0003!important}\n"] }]
7881
7984
  }], ctorParameters: () => [{ type: ComponentRegistryService }, { type: OverlayService }, { type: PanelEventService }, { type: DragEngineService }, { type: DataSourceRegistryService }, { type: ViewportService }, { type: FormStateService }, { type: InteractionEngineService }, { type: InteractionPresetRegistryService }, { type: BuilderClipboardService }, { type: SymbolRegistryService }, { type: i0.ChangeDetectorRef }, { type: i0.ApplicationRef }, { type: MoveableCanvasService }, { type: SelectoCanvasService }, { type: StyleRegistryService }], propDecorators: { layout: [{
7882
7985
  type: Input
7883
7986
  }], dataSources: [{
@@ -8864,7 +8967,10 @@ class BuilderButtonBlockComponent {
8864
8967
  this.children = [];
8865
8968
  this.style = {};
8866
8969
  this.tag = 'button';
8970
+ this.linkType = LinkType.External;
8867
8971
  this.href = '';
8972
+ /** A page link shows its route as authored, as the canvas Link does (the engine resolves it). */
8973
+ this.pageRoute = '';
8868
8974
  this.target = '_self';
8869
8975
  this.type = 'button';
8870
8976
  this.nodeId = '';
@@ -8873,6 +8979,9 @@ class BuilderButtonBlockComponent {
8873
8979
  this.childMouseEnter = new EventEmitter();
8874
8980
  this.childMouseLeave = new EventEmitter();
8875
8981
  }
8982
+ get resolvedHref() {
8983
+ return buttonBlockHref(this);
8984
+ }
8876
8985
  ngOnInit() {
8877
8986
  // Register the HOST as the dropzone (was the ioxDropzone directive on the inner button).
8878
8987
  this.dragEngine.registerDropzone(this.dropListId, this.elRef.nativeElement, this.children, this.cdr, (evt) => this.onDrop(evt));
@@ -8884,7 +8993,7 @@ class BuilderButtonBlockComponent {
8884
8993
  this.dragEngine.handleDrop(this.children, event, this.dropListId, this.cdr);
8885
8994
  }
8886
8995
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: BuilderButtonBlockComponent, deps: [{ token: i0.ElementRef }, { token: DragEngineService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
8887
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: BuilderButtonBlockComponent, isStandalone: false, selector: "button[iox-button-block],a[iox-button-block]", inputs: { children: "children", style: "style", tag: "tag", href: "href", target: "target", type: "type", nodeId: "nodeId", dropListId: "dropListId" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, host: { properties: { "class": "'button-block-root iox-node-' + nodeId", "attr.href": "tag === 'a' ? (href || null) : null", "attr.target": "tag === 'a' ? (target || null) : null", "attr.type": "tag === 'a' ? null : type" } }, ngImport: i0, template: `
8996
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: BuilderButtonBlockComponent, isStandalone: false, selector: "button[iox-button-block],a[iox-button-block]", inputs: { children: "children", style: "style", tag: "tag", linkType: "linkType", href: "href", pageRoute: "pageRoute", target: "target", type: "type", nodeId: "nodeId", dropListId: "dropListId" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, host: { properties: { "class": "'button-block-root iox-node-' + nodeId", "attr.href": "resolvedHref", "attr.target": "tag === 'a' ? (target || null) : null", "attr.type": "tag === 'a' ? null : type" } }, ngImport: i0, template: `
8888
8997
  <span *ngIf="!children.length" class="button-block-placeholder" data-iox-chrome>Button</span>
8889
8998
 
8890
8999
  <!-- WRAPPER COLLAPSE (task 9c): render children directly into the host; iox-outer + drag land
@@ -8914,7 +9023,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
8914
9023
  </ng-container>
8915
9024
  `, host: {
8916
9025
  '[class]': "'button-block-root iox-node-' + nodeId",
8917
- '[attr.href]': "tag === 'a' ? (href || null) : null",
9026
+ '[attr.href]': 'resolvedHref',
8918
9027
  '[attr.target]': "tag === 'a' ? (target || null) : null",
8919
9028
  '[attr.type]': "tag === 'a' ? null : type",
8920
9029
  }, standalone: false, styles: [":host{box-sizing:border-box}.button-block-placeholder{flex:1;display:flex;align-items:center;justify-content:center;color:#64748b66;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}:host ::ng-deep>[class*=iox-outer-]{min-width:0}\n"] }]
@@ -8924,8 +9033,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
8924
9033
  type: Input
8925
9034
  }], tag: [{
8926
9035
  type: Input
9036
+ }], linkType: [{
9037
+ type: Input
8927
9038
  }], href: [{
8928
9039
  type: Input
9040
+ }], pageRoute: [{
9041
+ type: Input
8929
9042
  }], target: [{
8930
9043
  type: Input
8931
9044
  }], type: [{
@@ -10374,66 +10487,6 @@ function matchesControlSearch(name, label, term) {
10374
10487
  return name.toLowerCase().includes(needle) || (label ?? '').toLowerCase().includes(needle);
10375
10488
  }
10376
10489
 
10377
- const FORM_PRESETS = Object.freeze([
10378
- {
10379
- id: 'contact', name: 'Contact', icon: 'ph-thin ph-envelope-simple',
10380
- description: 'Name, email and a message. Stored, and emailed to you.',
10381
- formName: 'Contact form',
10382
- fields: [
10383
- { label: 'Full name', control: 'text', required: true },
10384
- { label: 'Email', control: 'email', required: true },
10385
- { label: 'Message', control: 'textarea', required: true, rows: 5 },
10386
- ],
10387
- submitLabel: 'Send message',
10388
- successText: 'Thanks — we’ll be in touch shortly.',
10389
- errorText: 'We couldn’t send your message. Please try again — your answers are still here.',
10390
- },
10391
- {
10392
- id: 'newsletter', name: 'Newsletter', icon: 'ph-thin ph-paper-plane-tilt',
10393
- description: 'One email field plus consent. Stored.',
10394
- formName: 'Newsletter signup',
10395
- fields: [
10396
- { label: 'Email', control: 'email', required: true, placeholder: 'you@company.com' },
10397
- // Consent is a checkbox that must be TICKED — `required` on a checkbox becomes the
10398
- // `accepted` rule, with its own message, rather than a generic "this is required".
10399
- { label: 'Email me occasional updates', control: 'checkbox', required: true },
10400
- ],
10401
- submitLabel: 'Subscribe',
10402
- successText: 'You’re on the list.',
10403
- errorText: 'We couldn’t sign you up. Please try again.',
10404
- },
10405
- {
10406
- id: 'application', name: 'Application', icon: 'ph-thin ph-read-cv-logo',
10407
- description: 'Details plus a CV upload. Stored, with an auto-reply.',
10408
- formName: 'Application',
10409
- fields: [
10410
- { label: 'Full name', control: 'text', required: true },
10411
- { label: 'Email', control: 'email', required: true },
10412
- { label: 'Phone', control: 'tel' },
10413
- { label: 'Role', control: 'select', required: true, optionsText: 'Design\nEngineering\nOther' },
10414
- { label: 'CV', control: 'file', required: true, help: 'PDF or Word, up to 5MB' },
10415
- { label: 'Anything else', control: 'textarea', rows: 4 },
10416
- ],
10417
- submitLabel: 'Apply',
10418
- successText: 'Application received — we’ll read it properly and reply.',
10419
- errorText: 'We couldn’t submit your application. Please try again.',
10420
- },
10421
- {
10422
- id: 'booking', name: 'Booking request', icon: 'ph-thin ph-calendar-check',
10423
- description: 'Date and party size. Stored, and it notifies you.',
10424
- formName: 'Booking request',
10425
- fields: [
10426
- { label: 'Name', control: 'text', required: true },
10427
- { label: 'Email', control: 'email', required: true },
10428
- { label: 'Date', control: 'date', required: true },
10429
- { label: 'People', control: 'number', required: true },
10430
- { label: 'Notes', control: 'textarea', rows: 3, help: 'Allergies, access needs, anything useful' },
10431
- ],
10432
- submitLabel: 'Request a booking',
10433
- successText: 'Request received — we’ll confirm by email.',
10434
- errorText: 'We couldn’t send your request. Please try again.',
10435
- },
10436
- ]);
10437
10490
  /**
10438
10491
  * Build a preset's node tree.
10439
10492
  *
@@ -10826,11 +10879,9 @@ function text(content, createConfig, styleId, styles = {}) {
10826
10879
  wearing(node, styleId);
10827
10880
  return styled(node, { padding: '0', margin: '0', ...styles });
10828
10881
  }
10829
- /** A button's label takes the BUTTON's type (the style's size, weight, case, tracking) — not body's. */
10830
- const BUTTON_LABEL = {
10831
- color: 'inherit', fontFamily: 'inherit', fontSize: 'inherit', fontWeight: 'inherit', lineHeight: 'inherit',
10832
- letterSpacing: 'inherit', textTransform: 'inherit',
10833
- };
10882
+ /** A button's label takes the BUTTON's type (size, weight, case, tracking) and ink — the contract's
10883
+ * `button-label` element style, the one the site composer gives a composed button's label too (W1.1). */
10884
+ const BUTTON_LABEL_STYLE = 'button-label';
10834
10885
  function withChildren(node, children) {
10835
10886
  if (!node || children.some(c => !c))
10836
10887
  return null;
@@ -10856,7 +10907,7 @@ function button(styleId, label, createConfig, styles = {}) {
10856
10907
  return null;
10857
10908
  setTrait(btn, 'tag', 'button');
10858
10909
  styled(btn, styles);
10859
- return withChildren(btn, [text(label, createConfig, undefined, BUTTON_LABEL)]);
10910
+ return withChildren(btn, [text(label, createConfig, BUTTON_LABEL_STYLE)]);
10860
10911
  }
10861
10912
  /** A Container sized by its content: the palette Container's 200px drop-zone min-height is canvas
10862
10913
  * furniture, and a specimen has no drop zone. */
@@ -11030,6 +11081,10 @@ class BuilderButtonComponentConfig extends ComponentConfig {
11030
11081
  }
11031
11082
  }
11032
11083
 
11084
+ const BUTTON_LINK_TYPES = [
11085
+ { label: 'External URL', value: LinkType.External },
11086
+ { label: 'Website Page', value: LinkType.Page },
11087
+ ];
11033
11088
  class ButtonBlockComponentConfig extends ComponentConfig {
11034
11089
  constructor() {
11035
11090
  super('ButtonBlock', 'app-builder-button-block', 'ph-thin ph-cursor-click', 'Basic');
@@ -11038,13 +11093,21 @@ class ButtonBlockComponentConfig extends ComponentConfig {
11038
11093
  this.children = [];
11039
11094
  this.inputs = {
11040
11095
  tag: 'select',
11096
+ linkType: 'select',
11041
11097
  href: 'text',
11098
+ pageRoute: 'text',
11042
11099
  target: 'select',
11043
11100
  type: 'select',
11044
11101
  };
11102
+ // A link button points at a URL or at a website PAGE by route — the Link's two link types
11103
+ // (W1.1): a page route is shown as the route here and resolved base-relative by the engine.
11104
+ // `href` / `pageRoute` depend on `linkType`, which depends on `tag`: a trait whose controlling
11105
+ // trait is hidden is hidden too, so a <button> shows neither.
11045
11106
  this.traits = [
11046
11107
  new TraitConfig('tag', 'Tag', TraitInputType.Select, ['button', 'a'], 'button'),
11047
- new TraitConfig('href', 'URL', TraitInputType.Text, undefined, '', false, { trait: 'tag', values: 'a' }),
11108
+ new TraitConfig('linkType', 'Link Type', TraitInputType.Select, BUTTON_LINK_TYPES, LinkType.External, false, { trait: 'tag', values: 'a' }),
11109
+ new TraitConfig('href', 'URL', TraitInputType.Text, undefined, '', false, { trait: 'linkType', values: LinkType.External }),
11110
+ new TraitConfig('pageRoute', 'Page Route', TraitInputType.Text, undefined, '', false, { trait: 'linkType', values: LinkType.Page }),
11048
11111
  new TraitConfig('target', 'Target', TraitInputType.Select, ['_self', '_blank'], '_self', false, { trait: 'tag', values: 'a' }),
11049
11112
  new TraitConfig('type', 'Type', TraitInputType.Select, ['button', 'submit', 'reset'], 'button', false, { trait: 'tag', values: 'button' }),
11050
11113
  ];
@@ -11843,5 +11906,5 @@ class BuilderSpacerComponentConfig extends ComponentConfig {
11843
11906
  * Generated bundle index. Do not edit.
11844
11907
  */
11845
11908
 
11846
- export { ACTION_TYPE_OPTIONS, AuditCode, AuditPanelComponent, AuditSeverity, BODY_CRUMB_LABEL, 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, CardComponentConfig, ChildLayoutGroupStyleConfig, ComponentConfig, ComponentRegistryService, DEFAULT_ESSENTIAL_TRAITS, 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_PRESETS, FORM_STATE_BLOCK_TYPES, FORM_TYPE, FieldComponentConfig, FieldGroupComponentConfig, FormComponentConfig, FormErrorComponentConfig, FormStateService, FormSuccessComponentConfig, GalleryConfig, GroupStyleConfig, INSERT_REFUSAL_LABEL_KEYS, INTERACTION_STATES, INVERSE_ACTION, IOX_BUILDER_CANVAS, IOX_BUILDER_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, ROOT_ESCAPE_PX, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, RootZone, SCREEN_WIDTH_OPTIONS, STRUCTURAL_STATES, SUPPORTED_STATES, SectionComponent, SectionComponentConfig, SizeGroupStyleConfig, SliderContainerComponent, SliderContainerConfig, SliderSlideComponent, SliderSlideConfig, SpacingGroupStyleConfig, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, SubmitButtonComponentConfig, SymbolRegistryService, TARGETED_STATES, TARGETED_STATE_BASE, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, TypographyGroupStyleConfig, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, VALIDATION_STATES, VALIDATION_STATE_CLASS, ViewportService, ZOOM_OPTIONS, arrayItemSchemaFields, arraySchemaFields, auditNodes, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, canFluidise, canvasEmptyImageLabel, collapseColumnsProps, columnCountOf, defaultBuilderLabels, defaultPageSettings, dropTargetLabel, escapesRoot, essentialTraitsFor, findNodeLocation, findParentOf, flattenSchemaFields, fluidiseProps, generateNodeId, insertRefusal, insertRefusalLabel, isHiddenAt, isSpecimen, markSpecimen, matchesControlSearch, nearestFormNode, nodeAncestorPath, nodeAncestorPathInEither, reorderProps, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, rootEscapeBand, rootZone, setStyleTrait, setTrait, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, textNode, visibilityProps, wrapNode };
11909
+ export { ACTION_TYPE_OPTIONS, AuditCode, AuditPanelComponent, AuditSeverity, BODY_CRUMB_LABEL, 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, CardComponentConfig, ChildLayoutGroupStyleConfig, ComponentConfig, ComponentRegistryService, DEFAULT_ESSENTIAL_TRAITS, 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, GalleryConfig, GroupStyleConfig, INSERT_REFUSAL_LABEL_KEYS, INTERACTION_STATES, INVERSE_ACTION, IOX_BUILDER_CANVAS, IOX_BUILDER_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, ROOT_ESCAPE_PX, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, RootZone, SCREEN_WIDTH_OPTIONS, STRUCTURAL_STATES, SUPPORTED_STATES, SectionComponent, SectionComponentConfig, SizeGroupStyleConfig, SliderContainerComponent, SliderContainerConfig, SliderSlideComponent, SliderSlideConfig, SpacingGroupStyleConfig, StructureFix, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, SubmitButtonComponentConfig, SymbolRegistryService, TARGETED_STATES, TARGETED_STATE_BASE, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, TypographyGroupStyleConfig, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, VALIDATION_STATES, VALIDATION_STATE_CLASS, ViewportService, ZOOM_OPTIONS, arrayItemSchemaFields, arraySchemaFields, auditNodes, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, canFluidise, canvasEmptyImageLabel, collapseColumnsProps, columnCountOf, defaultBuilderLabels, defaultPageSettings, dropTargetLabel, escapesRoot, essentialTraitsFor, findNodeLocation, findParentOf, flattenSchemaFields, fluidiseProps, generateNodeId, insertRefusal, insertRefusalLabel, isHiddenAt, isSpecimen, markSpecimen, matchesControlSearch, nearestFormNode, nodeAncestorPath, nodeAncestorPathInEither, reorderProps, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, rootEscapeBand, rootZone, setStyleTrait, setTrait, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, textNode, visibilityProps, wrapNode };
11847
11910
  //# sourceMappingURL=vectoriox-iox-builder.mjs.map