@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.
- package/fesm2022/vectoriox-iox-builder.mjs +210 -147
- package/fesm2022/vectoriox-iox-builder.mjs.map +1 -1
- package/index.d.ts +40 -36
- package/package.json +2 -2
|
@@ -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:
|
|
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"
|
|
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
|
|
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
|
|
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.
|
|
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": "
|
|
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]':
|
|
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 (
|
|
10830
|
-
|
|
10831
|
-
|
|
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,
|
|
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('
|
|
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,
|
|
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
|