@vectoriox/iox-ui 4.24.0 → 4.26.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-ui.mjs +1845 -1653
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +214 -140
- package/package.json +1 -1
|
@@ -1506,1858 +1506,2050 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
1506
1506
|
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
1507
1507
|
}] } });
|
|
1508
1508
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
if (this.enablePagination)
|
|
1572
|
-
modules.push(Pagination);
|
|
1573
|
-
if (this.enableScrollbar)
|
|
1574
|
-
modules.push(Scrollbar);
|
|
1575
|
-
if (this.enableAutoplay)
|
|
1576
|
-
modules.push(Autoplay);
|
|
1577
|
-
if (this.enableKeyboard)
|
|
1578
|
-
modules.push(Keyboard);
|
|
1579
|
-
if (this.enableMousewheel)
|
|
1580
|
-
modules.push(Mousewheel);
|
|
1581
|
-
if (this.enableZoom)
|
|
1582
|
-
modules.push(Zoom);
|
|
1583
|
-
if (this.enableGrid)
|
|
1584
|
-
modules.push(Grid);
|
|
1585
|
-
if (this.enableParallax)
|
|
1586
|
-
modules.push(Parallax);
|
|
1587
|
-
const effectMap = {
|
|
1588
|
-
fade: EffectFade, cube: EffectCube, flip: EffectFlip,
|
|
1589
|
-
coverflow: EffectCoverflow, cards: EffectCards, creative: EffectCreative,
|
|
1590
|
-
};
|
|
1591
|
-
if (this.effect !== 'slide' && effectMap[this.effect]) {
|
|
1592
|
-
modules.push(effectMap[this.effect]);
|
|
1593
|
-
}
|
|
1594
|
-
const config = {
|
|
1595
|
-
modules,
|
|
1596
|
-
// Swiper reads the container's `dir` attribute, but pass it explicitly so RTL is applied
|
|
1597
|
-
// on init regardless of attribute timing.
|
|
1598
|
-
dir: this.dir,
|
|
1599
|
-
direction: this.direction,
|
|
1600
|
-
loop: this.loop,
|
|
1601
|
-
slidesPerView: this.slidesPerView,
|
|
1602
|
-
spaceBetween: this.spaceBetween,
|
|
1603
|
-
speed: this.speed,
|
|
1604
|
-
effect: this.effect,
|
|
1605
|
-
grabCursor: this.grabCursor,
|
|
1606
|
-
centeredSlides: this.centeredSlides,
|
|
1607
|
-
initialSlide: this.initialSlide,
|
|
1608
|
-
};
|
|
1609
|
-
if (this.enableNavigation)
|
|
1610
|
-
config['navigation'] = { prevEl: '.swiper-button-prev', nextEl: '.swiper-button-next' };
|
|
1611
|
-
if (this.enablePagination)
|
|
1612
|
-
config['pagination'] = { el: '.swiper-pagination', type: this.paginationType, clickable: this.paginationClickable };
|
|
1613
|
-
if (this.enableScrollbar)
|
|
1614
|
-
config['scrollbar'] = { el: '.swiper-scrollbar', draggable: true };
|
|
1615
|
-
if (this.enableAutoplay)
|
|
1616
|
-
config['autoplay'] = { delay: this.autoplayDelay, pauseOnMouseEnter: this.autoplayPauseOnMouseEnter, disableOnInteraction: this.autoplayDisableOnInteraction };
|
|
1617
|
-
if (this.enableGrid && this.gridRows > 1)
|
|
1618
|
-
config['grid'] = { rows: this.gridRows };
|
|
1619
|
-
if (this.breakpoints) {
|
|
1620
|
-
try {
|
|
1621
|
-
config['breakpoints'] = JSON.parse(this.breakpoints);
|
|
1622
|
-
}
|
|
1623
|
-
catch { /* ignore */ }
|
|
1624
|
-
}
|
|
1625
|
-
this._swiper = new Swiper(el, config);
|
|
1626
|
-
this.instanceRegistry.register(this.nodeId, this._swiper);
|
|
1627
|
-
}
|
|
1628
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientSliderContainerComponent, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1629
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: ClientSliderContainerComponent, isStandalone: false, selector: "iox-slider-container", inputs: { nodeId: "nodeId", dir: "dir", direction: "direction", loop: "loop", slidesPerView: "slidesPerView", spaceBetween: "spaceBetween", speed: "speed", effect: "effect", grabCursor: "grabCursor", centeredSlides: "centeredSlides", initialSlide: "initialSlide", enableNavigation: "enableNavigation", enablePagination: "enablePagination", enableScrollbar: "enableScrollbar", enableAutoplay: "enableAutoplay", enableKeyboard: "enableKeyboard", enableMousewheel: "enableMousewheel", enableZoom: "enableZoom", enableGrid: "enableGrid", enableParallax: "enableParallax", autoplayDelay: "autoplayDelay", autoplayPauseOnMouseEnter: "autoplayPauseOnMouseEnter", autoplayDisableOnInteraction: "autoplayDisableOnInteraction", paginationType: "paginationType", paginationClickable: "paginationClickable", gridRows: "gridRows", breakpoints: "breakpoints" }, viewQueries: [{ propertyName: "childrenHost", first: true, predicate: ["childrenHost"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "swiperEl", first: true, predicate: ["swiperEl"], descendants: true, static: true }], ngImport: i0, template: `
|
|
1630
|
-
<div class="swiper" [class]="'iox-node-' + nodeId" [attr.dir]="dir" [style.direction]="dir" #swiperEl>
|
|
1631
|
-
<div class="swiper-wrapper">
|
|
1632
|
-
<ng-container #childrenHost></ng-container>
|
|
1633
|
-
</div>
|
|
1634
|
-
<div *ngIf="enableNavigation" class="swiper-button-prev"></div>
|
|
1635
|
-
<div *ngIf="enableNavigation" class="swiper-button-next"></div>
|
|
1636
|
-
<div *ngIf="enablePagination" class="swiper-pagination"></div>
|
|
1637
|
-
<div *ngIf="enableScrollbar" class="swiper-scrollbar"></div>
|
|
1638
|
-
</div>
|
|
1639
|
-
`, isInline: true, styles: [".swiper{width:100%;height:100%;box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); }
|
|
1509
|
+
// Virtual trait → composed CSS property helpers.
|
|
1510
|
+
//
|
|
1511
|
+
// Several style traits are "virtual" — they don't map 1:1 to a CSS property but
|
|
1512
|
+
// are components of a multi-function CSS value. composeVirtualTraits() strips
|
|
1513
|
+
// these virtual keys from the style map and replaces them with the correctly
|
|
1514
|
+
// composed CSS properties (filter, backdrop-filter, transform, transition).
|
|
1515
|
+
//
|
|
1516
|
+
// SHARED (iox-ui): consumed by BOTH the builder (render.directive + style panel)
|
|
1517
|
+
// and the SSR engine (LayoutRendererService). It MUST live here, not in iox-builder,
|
|
1518
|
+
// because the engine does not depend on iox-builder — without it the engine emitted
|
|
1519
|
+
// the raw virtual keys (`translate-x`, `filter-blur`, `scale-x`…) as invalid CSS.
|
|
1520
|
+
// Run this BEFORE compileDeclarations so the CSS output is identical on both sides.
|
|
1521
|
+
// ─── Filter ──────────────────────────────────────────────────────────────────
|
|
1522
|
+
const FILTER_FUNS = [
|
|
1523
|
+
['filterBlur', 'blur', '0px'],
|
|
1524
|
+
['filterBrightness', 'brightness', '1'],
|
|
1525
|
+
['filterContrast', 'contrast', '1'],
|
|
1526
|
+
['filterGrayscale', 'grayscale', '0'],
|
|
1527
|
+
['filterSaturate', 'saturate', '1'],
|
|
1528
|
+
['filterHueRotate', 'hue-rotate', '0deg'],
|
|
1529
|
+
['filterSepia', 'sepia', '0'],
|
|
1530
|
+
['filterInvert', 'invert', '0'],
|
|
1531
|
+
];
|
|
1532
|
+
// ─── Backdrop-filter ─────────────────────────────────────────────────────────
|
|
1533
|
+
const BACKDROP_FUNS = [
|
|
1534
|
+
['backdropBlur', 'blur', '0px'],
|
|
1535
|
+
['backdropBrightness', 'brightness', '1'],
|
|
1536
|
+
['backdropContrast', 'contrast', '1'],
|
|
1537
|
+
['backdropSaturate', 'saturate', '1'],
|
|
1538
|
+
];
|
|
1539
|
+
// ─── Transform ───────────────────────────────────────────────────────────────
|
|
1540
|
+
const TRANSFORM_FUNS = [
|
|
1541
|
+
['translateX', 'translateX', '0px'],
|
|
1542
|
+
['translateY', 'translateY', '0px'],
|
|
1543
|
+
['scaleX', 'scaleX', '1'],
|
|
1544
|
+
['scaleY', 'scaleY', '1'],
|
|
1545
|
+
['rotate', 'rotate', '0deg'],
|
|
1546
|
+
['skewX', 'skewX', '0deg'],
|
|
1547
|
+
['skewY', 'skewY', '0deg'],
|
|
1548
|
+
];
|
|
1549
|
+
// ─── ::marker pseudo-element ─────────────────────────────────────────────────
|
|
1550
|
+
const MARKER_FUNS = [
|
|
1551
|
+
['markerColor', 'color'],
|
|
1552
|
+
['markerFontSize', 'fontSize'],
|
|
1553
|
+
];
|
|
1554
|
+
const TRANSITION_KEYS = ['transitionDuration', 'transitionTimingFunction', 'transitionDelay'];
|
|
1555
|
+
const VIRTUAL_TRAIT_GROUPS = Object.freeze([
|
|
1556
|
+
{ output: 'filter', keys: FILTER_FUNS.map(([t]) => t), reset: 'none' },
|
|
1557
|
+
{ output: 'backdropFilter', keys: BACKDROP_FUNS.map(([t]) => t), reset: 'none' },
|
|
1558
|
+
{ output: 'transform', keys: TRANSFORM_FUNS.map(([t]) => t), reset: 'none' },
|
|
1559
|
+
{ output: 'transition', keys: [...TRANSITION_KEYS], reset: 'none' },
|
|
1560
|
+
{ output: '__markerStyles', keys: MARKER_FUNS.map(([t]) => t), reset: null },
|
|
1561
|
+
]);
|
|
1562
|
+
// ─── All virtual trait names ──────────────────────────────────────────────────
|
|
1563
|
+
/** Every virtual trait key — derived from the groups so there is one list to maintain. */
|
|
1564
|
+
const VIRTUAL_TRAIT_KEYS = new Set(VIRTUAL_TRAIT_GROUPS.flatMap(g => g.keys));
|
|
1565
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
1566
|
+
function buildFns(funs, src) {
|
|
1567
|
+
return funs
|
|
1568
|
+
.filter(([t, , d]) => src[t] != null && src[t] !== '' && src[t] !== d)
|
|
1569
|
+
.map(([t, fn]) => `${fn}(${src[t]})`)
|
|
1570
|
+
.join(' ');
|
|
1640
1571
|
}
|
|
1641
|
-
|
|
1642
|
-
type: Component,
|
|
1643
|
-
args: [{ selector: 'iox-slider-container', template: `
|
|
1644
|
-
<div class="swiper" [class]="'iox-node-' + nodeId" [attr.dir]="dir" [style.direction]="dir" #swiperEl>
|
|
1645
|
-
<div class="swiper-wrapper">
|
|
1646
|
-
<ng-container #childrenHost></ng-container>
|
|
1647
|
-
</div>
|
|
1648
|
-
<div *ngIf="enableNavigation" class="swiper-button-prev"></div>
|
|
1649
|
-
<div *ngIf="enableNavigation" class="swiper-button-next"></div>
|
|
1650
|
-
<div *ngIf="enablePagination" class="swiper-pagination"></div>
|
|
1651
|
-
<div *ngIf="enableScrollbar" class="swiper-scrollbar"></div>
|
|
1652
|
-
</div>
|
|
1653
|
-
`, standalone: false, styles: [".swiper{width:100%;height:100%;box-sizing:border-box}\n"] }]
|
|
1654
|
-
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
1655
|
-
type: Inject,
|
|
1656
|
-
args: [PLATFORM_ID]
|
|
1657
|
-
}] }], propDecorators: { nodeId: [{
|
|
1658
|
-
type: Input
|
|
1659
|
-
}], dir: [{
|
|
1660
|
-
type: Input
|
|
1661
|
-
}], direction: [{
|
|
1662
|
-
type: Input
|
|
1663
|
-
}], loop: [{
|
|
1664
|
-
type: Input
|
|
1665
|
-
}], slidesPerView: [{
|
|
1666
|
-
type: Input
|
|
1667
|
-
}], spaceBetween: [{
|
|
1668
|
-
type: Input
|
|
1669
|
-
}], speed: [{
|
|
1670
|
-
type: Input
|
|
1671
|
-
}], effect: [{
|
|
1672
|
-
type: Input
|
|
1673
|
-
}], grabCursor: [{
|
|
1674
|
-
type: Input
|
|
1675
|
-
}], centeredSlides: [{
|
|
1676
|
-
type: Input
|
|
1677
|
-
}], initialSlide: [{
|
|
1678
|
-
type: Input
|
|
1679
|
-
}], enableNavigation: [{
|
|
1680
|
-
type: Input
|
|
1681
|
-
}], enablePagination: [{
|
|
1682
|
-
type: Input
|
|
1683
|
-
}], enableScrollbar: [{
|
|
1684
|
-
type: Input
|
|
1685
|
-
}], enableAutoplay: [{
|
|
1686
|
-
type: Input
|
|
1687
|
-
}], enableKeyboard: [{
|
|
1688
|
-
type: Input
|
|
1689
|
-
}], enableMousewheel: [{
|
|
1690
|
-
type: Input
|
|
1691
|
-
}], enableZoom: [{
|
|
1692
|
-
type: Input
|
|
1693
|
-
}], enableGrid: [{
|
|
1694
|
-
type: Input
|
|
1695
|
-
}], enableParallax: [{
|
|
1696
|
-
type: Input
|
|
1697
|
-
}], autoplayDelay: [{
|
|
1698
|
-
type: Input
|
|
1699
|
-
}], autoplayPauseOnMouseEnter: [{
|
|
1700
|
-
type: Input
|
|
1701
|
-
}], autoplayDisableOnInteraction: [{
|
|
1702
|
-
type: Input
|
|
1703
|
-
}], paginationType: [{
|
|
1704
|
-
type: Input
|
|
1705
|
-
}], paginationClickable: [{
|
|
1706
|
-
type: Input
|
|
1707
|
-
}], gridRows: [{
|
|
1708
|
-
type: Input
|
|
1709
|
-
}], breakpoints: [{
|
|
1710
|
-
type: Input
|
|
1711
|
-
}], childrenHost: [{
|
|
1712
|
-
type: ViewChild,
|
|
1713
|
-
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
1714
|
-
}], swiperEl: [{
|
|
1715
|
-
type: ViewChild,
|
|
1716
|
-
args: ['swiperEl', { static: true }]
|
|
1717
|
-
}] } });
|
|
1718
|
-
|
|
1572
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
1719
1573
|
/**
|
|
1720
|
-
*
|
|
1721
|
-
* `swiper-slide` class so it is a DIRECT child of `.swiper-wrapper` — Swiper only
|
|
1722
|
-
* recognises slides that are direct children (the `.swiper-slide` direct-child
|
|
1723
|
-
* invariant; see iox-ai-guidance architecture/builder/slider.md). A wrapper div
|
|
1724
|
-
* would nest `.swiper-slide` one level too deep and Swiper would ignore the slides.
|
|
1574
|
+
* Strip virtual trait keys from `raw` and emit their composed CSS equivalents.
|
|
1725
1575
|
*
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1576
|
+
* When composing a partial state-override map (hover, active…), pass the full
|
|
1577
|
+
* base style map as `base` so non-overridden components (e.g. filterBrightness
|
|
1578
|
+
* when only filterBlur is overridden) are preserved in the composed output.
|
|
1579
|
+
* When operating on a complete style map, omit `base` — it defaults to `raw`.
|
|
1728
1580
|
*/
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1581
|
+
function composeVirtualTraits(raw, base = raw) {
|
|
1582
|
+
// Copy all non-virtual properties as-is
|
|
1583
|
+
const result = {};
|
|
1584
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
1585
|
+
if (!VIRTUAL_TRAIT_KEYS.has(k))
|
|
1586
|
+
result[k] = v;
|
|
1732
1587
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1588
|
+
// Merge base values for any virtual key absent from the partial override map
|
|
1589
|
+
const merged = (funs) => {
|
|
1590
|
+
const m = {};
|
|
1591
|
+
for (const [t] of funs)
|
|
1592
|
+
m[t] = raw[t] ?? base[t];
|
|
1593
|
+
return m;
|
|
1594
|
+
};
|
|
1595
|
+
// filter
|
|
1596
|
+
const filterStr = buildFns(FILTER_FUNS, merged(FILTER_FUNS));
|
|
1597
|
+
if (filterStr)
|
|
1598
|
+
result['filter'] = filterStr;
|
|
1599
|
+
// backdrop-filter
|
|
1600
|
+
const backdropStr = buildFns(BACKDROP_FUNS, merged(BACKDROP_FUNS));
|
|
1601
|
+
if (backdropStr)
|
|
1602
|
+
result['backdropFilter'] = backdropStr;
|
|
1603
|
+
// transform
|
|
1604
|
+
const transformStr = buildFns(TRANSFORM_FUNS, merged(TRANSFORM_FUNS));
|
|
1605
|
+
if (transformStr)
|
|
1606
|
+
result['transform'] = transformStr;
|
|
1607
|
+
// transition — only emit when duration is non-zero
|
|
1608
|
+
const dur = raw['transitionDuration'] ?? base['transitionDuration'];
|
|
1609
|
+
const ease = raw['transitionTimingFunction'] ?? base['transitionTimingFunction'];
|
|
1610
|
+
const del = raw['transitionDelay'] ?? base['transitionDelay'];
|
|
1611
|
+
if (dur && ease && dur !== '0ms' && dur !== '0s') {
|
|
1612
|
+
const delPart = del && del !== '0ms' && del !== '0s' ? ` ${del}` : '';
|
|
1613
|
+
result['transition'] = `all ${dur} ${ease}${delPart}`;
|
|
1614
|
+
}
|
|
1615
|
+
// ::marker pseudo-element — collected into __markerStyles so StyleRegistryService
|
|
1616
|
+
// can emit a separate `.iox-node-{id}::marker { … }` rule.
|
|
1617
|
+
const markerStyles = {};
|
|
1618
|
+
for (const [trait, cssProp] of MARKER_FUNS) {
|
|
1619
|
+
const val = raw[trait] ?? base[trait];
|
|
1620
|
+
if (val != null && val !== '')
|
|
1621
|
+
markerStyles[cssProp] = val;
|
|
1622
|
+
}
|
|
1623
|
+
if (Object.keys(markerStyles).length) {
|
|
1624
|
+
result['__markerStyles'] = markerStyles;
|
|
1625
|
+
}
|
|
1626
|
+
return result;
|
|
1735
1627
|
}
|
|
1736
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientSliderSlideComponent, decorators: [{
|
|
1737
|
-
type: Component,
|
|
1738
|
-
args: [{ selector: 'iox-slider-slide', template: `<ng-container #childrenHost></ng-container>`, host: { '[class]': `'swiper-slide iox-node-' + nodeId` }, standalone: false, styles: [":host{display:block;box-sizing:border-box}\n"] }]
|
|
1739
|
-
}], propDecorators: { nodeId: [{
|
|
1740
|
-
type: Input
|
|
1741
|
-
}], childrenHost: [{
|
|
1742
|
-
type: ViewChild,
|
|
1743
|
-
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
1744
|
-
}] } });
|
|
1745
1628
|
|
|
1629
|
+
// Responsive (per-breakpoint) styles — the shared model, cascade and emission plan.
|
|
1630
|
+
//
|
|
1631
|
+
// SHARED (iox-ui): the builder canvas AND the SSR engine both call this. The builder
|
|
1632
|
+
// FLATTENS the active breakpoint (its canvas is a transformed frame inside a desktop
|
|
1633
|
+
// browser, so a real `@media` query would read the editor's width); the engine WRAPS
|
|
1634
|
+
// each planned layer in `@media`. Same decisions, different transport — never fork them.
|
|
1635
|
+
//
|
|
1636
|
+
// Storage is a sparse layer beside the existing keys, shaped like `stateStyles`:
|
|
1637
|
+
//
|
|
1638
|
+
// styleProps — base, full resolved set (unchanged)
|
|
1639
|
+
// stateStyles — sparse per-state overrides (unchanged)
|
|
1640
|
+
// responsive — sparse per-breakpoint overrides (this file)
|
|
1641
|
+
// { mobile: { styleProps: {…}, stateStyles: { hover: {…} } } }
|
|
1642
|
+
//
|
|
1643
|
+
// A node with no `responsive` key renders exactly as before — no migration.
|
|
1644
|
+
// See iox-ai-guidance `architecture/builder/responsive-breakpoints-plan.md`.
|
|
1645
|
+
// ─── Breakpoints ─────────────────────────────────────────────────────────────
|
|
1646
|
+
/** The five fixed breakpoints (plan decision D2). Desktop-first: each narrower one
|
|
1647
|
+
* overrides the wider ones through `max-width`. */
|
|
1648
|
+
var Breakpoint;
|
|
1649
|
+
(function (Breakpoint) {
|
|
1650
|
+
Breakpoint["Base"] = "base";
|
|
1651
|
+
Breakpoint["Laptop"] = "laptop";
|
|
1652
|
+
Breakpoint["Tablet"] = "tablet";
|
|
1653
|
+
Breakpoint["Mobile"] = "mobile";
|
|
1654
|
+
Breakpoint["Small"] = "small";
|
|
1655
|
+
})(Breakpoint || (Breakpoint = {}));
|
|
1746
1656
|
/**
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1657
|
+
* Widest → narrowest. This order IS the cascade order and the `@media` emission order.
|
|
1658
|
+
*
|
|
1659
|
+
* ⚠️ Phones in portrait (360–430px) land in `Small`, NOT `Mobile` — `Mobile` is 480–767,
|
|
1660
|
+
* a phone held landscape. The labels say so (as Webflow's do). The ids stay short because
|
|
1661
|
+
* they are storage keys; the cascade still makes a `Mobile` override reach every phone.
|
|
1751
1662
|
*/
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1663
|
+
const BREAKPOINTS = Object.freeze([
|
|
1664
|
+
{ id: Breakpoint.Base, label: 'Base', maxWidth: null, previewWidth: 1440 },
|
|
1665
|
+
{ id: Breakpoint.Laptop, label: 'Laptop', maxWidth: 1200, previewWidth: 1200 },
|
|
1666
|
+
{ id: Breakpoint.Tablet, label: 'Tablet', maxWidth: 991, previewWidth: 768 },
|
|
1667
|
+
{ id: Breakpoint.Mobile, label: 'Mobile landscape', maxWidth: 767, previewWidth: 568 },
|
|
1668
|
+
{ id: Breakpoint.Small, label: 'Mobile portrait', maxWidth: 479, previewWidth: 390 },
|
|
1669
|
+
]);
|
|
1670
|
+
const BREAKPOINT_INDEX = new Map(BREAKPOINTS.map((d, i) => [d.id, i]));
|
|
1671
|
+
/** The definition for `bp`, or `undefined` for an unknown id (e.g. hand-edited JSON). */
|
|
1672
|
+
function breakpointDef(bp) {
|
|
1673
|
+
const i = BREAKPOINT_INDEX.get(bp);
|
|
1674
|
+
return i === undefined ? undefined : BREAKPOINTS[i];
|
|
1675
|
+
}
|
|
1676
|
+
/** Base → `bp`, inclusive — the layers whose values apply at `bp`. Unknown → `[Base]`. */
|
|
1677
|
+
function cascadeChain(bp) {
|
|
1678
|
+
const i = BREAKPOINT_INDEX.get(bp);
|
|
1679
|
+
return BREAKPOINTS.slice(0, (i ?? 0) + 1).map(d => d.id);
|
|
1761
1680
|
}
|
|
1762
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientListComponent, decorators: [{
|
|
1763
|
-
type: Component,
|
|
1764
|
-
args: [{ selector: 'ul[iox-list],ol[iox-list]', template: `<ng-container #childrenHost></ng-container>`, host: {
|
|
1765
|
-
'[class]': "'iox-node-' + nodeId",
|
|
1766
|
-
'[attr.start]': "htmlTag === 'ol' && start !== '' && start != null ? start : null",
|
|
1767
|
-
'[attr.reversed]': "htmlTag === 'ol' && reversed ? '' : null",
|
|
1768
|
-
}, standalone: false, styles: [":host{margin:0;box-sizing:border-box}\n"] }]
|
|
1769
|
-
}], propDecorators: { htmlTag: [{
|
|
1770
|
-
type: Input
|
|
1771
|
-
}], start: [{
|
|
1772
|
-
type: Input
|
|
1773
|
-
}], reversed: [{
|
|
1774
|
-
type: Input
|
|
1775
|
-
}], nodeId: [{
|
|
1776
|
-
type: Input
|
|
1777
|
-
}], childrenHost: [{
|
|
1778
|
-
type: ViewChild,
|
|
1779
|
-
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
1780
|
-
}] } });
|
|
1781
|
-
|
|
1782
1681
|
/**
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1682
|
+
* The breakpoint a viewport (or canvas) of `width` CSS px falls in — the narrowest one whose
|
|
1683
|
+
* `maxWidth` it fits under, else Base. Mirrors how the `max-width` media queries resolve, so the
|
|
1684
|
+
* builder's editing scope is always the breakpoint the canvas is actually showing.
|
|
1786
1685
|
*/
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1686
|
+
function breakpointForWidth(width) {
|
|
1687
|
+
for (let i = BREAKPOINTS.length - 1; i > 0; i--) {
|
|
1688
|
+
const max = BREAKPOINTS[i].maxWidth;
|
|
1689
|
+
if (max !== null && width <= max)
|
|
1690
|
+
return BREAKPOINTS[i].id;
|
|
1790
1691
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1692
|
+
return Breakpoint.Base;
|
|
1693
|
+
}
|
|
1694
|
+
/** The next-wider breakpoint (what `bp` inherits from), or `null` for Base / unknown. */
|
|
1695
|
+
function parentBreakpoint(bp) {
|
|
1696
|
+
const i = BREAKPOINT_INDEX.get(bp);
|
|
1697
|
+
return i ? BREAKPOINTS[i - 1].id : null;
|
|
1793
1698
|
}
|
|
1794
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientListItemComponent, decorators: [{
|
|
1795
|
-
type: Component,
|
|
1796
|
-
args: [{ selector: 'li[iox-list-item]', template: `<ng-container #childrenHost></ng-container>`, host: { '[class]': "'iox-node-' + nodeId" }, standalone: false, styles: [":host{box-sizing:border-box}\n"] }]
|
|
1797
|
-
}], propDecorators: { nodeId: [{
|
|
1798
|
-
type: Input
|
|
1799
|
-
}], childrenHost: [{
|
|
1800
|
-
type: ViewChild,
|
|
1801
|
-
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
1802
|
-
}] } });
|
|
1803
|
-
|
|
1804
1699
|
/**
|
|
1805
|
-
*
|
|
1806
|
-
* becomes a backend fetch, consumed by BOTH the page builder (iox-cms-client bindings panel) and
|
|
1807
|
-
* the SSR client engine (iox-client-engine DataSourceResolverService). There must be no second
|
|
1808
|
-
* copy of this decision logic; only the transport differs between the two:
|
|
1809
|
-
*
|
|
1810
|
-
* builder → authenticated org-scoped controllers (admin session):
|
|
1811
|
-
* single by id → GET admin.../resources/:type/:id (ContentService.getContentById)
|
|
1812
|
-
* collection → GET admin.../resources/:type (ContentService.getContent)
|
|
1813
|
-
* engine → @Public() controllers (no auth, isPublished filter applied server-side):
|
|
1814
|
-
* single by id → GET api/:type/:id (UserResourcesController)
|
|
1815
|
-
* collection → GET api/:type?filters (UserResourcesController)
|
|
1700
|
+
* What a breakpoint's width is measured against.
|
|
1816
1701
|
*
|
|
1817
|
-
*
|
|
1818
|
-
*
|
|
1819
|
-
*
|
|
1820
|
-
*
|
|
1702
|
+
* `Media` — the browser viewport. The SSR engine: a visitor's window IS the page.
|
|
1703
|
+
* `Container` — a named CSS size container. The builder: its canvas is a fixed-width frame inside a
|
|
1704
|
+
* desktop browser (and `transform: scale`d for zoom), so a media query would read the EDITOR's width.
|
|
1705
|
+
* A container query reads the frame's own layout width — which equals the simulated viewport and
|
|
1706
|
+
* ignores the zoom transform — so the builder emits the same structure as the engine.
|
|
1821
1707
|
*/
|
|
1708
|
+
var BreakpointQueryKind;
|
|
1709
|
+
(function (BreakpointQueryKind) {
|
|
1710
|
+
BreakpointQueryKind["Media"] = "media";
|
|
1711
|
+
BreakpointQueryKind["Container"] = "container";
|
|
1712
|
+
})(BreakpointQueryKind || (BreakpointQueryKind = {}));
|
|
1822
1713
|
/**
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1825
|
-
*
|
|
1714
|
+
* The at-rule prelude for `bp` — `@media (max-width: 767px)`, or with `Container`
|
|
1715
|
+
* `@container iox-canvas (max-width: 767px)`. `null` for Base or an unknown id.
|
|
1716
|
+
* `containerName` is required for `Container` (an unnamed container query would match
|
|
1717
|
+
* whichever ancestor container is nearest, which is not guaranteed to be the canvas).
|
|
1826
1718
|
*/
|
|
1827
|
-
function
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1719
|
+
function breakpointQueryFor(bp, kind = BreakpointQueryKind.Media, containerName) {
|
|
1720
|
+
const max = breakpointDef(bp)?.maxWidth;
|
|
1721
|
+
if (max == null)
|
|
1722
|
+
return null;
|
|
1723
|
+
if (kind === BreakpointQueryKind.Media)
|
|
1724
|
+
return `@media (max-width: ${max}px)`;
|
|
1725
|
+
if (!containerName)
|
|
1726
|
+
throw new Error('A container breakpoint query needs a container name');
|
|
1727
|
+
return `@container ${containerName} (max-width: ${max}px)`;
|
|
1728
|
+
}
|
|
1729
|
+
/** Wrap already-compiled rules for `bp`. Base (and blank css) passes through unwrapped. */
|
|
1730
|
+
function wrapInBreakpointQuery(bp, css, kind = BreakpointQueryKind.Media, containerName) {
|
|
1731
|
+
if (!css.trim())
|
|
1732
|
+
return '';
|
|
1733
|
+
const query = breakpointQueryFor(bp, kind, containerName);
|
|
1734
|
+
return query ? `${query} {\n${css}\n}` : css;
|
|
1735
|
+
}
|
|
1736
|
+
/** `@media (max-width: 767px)` for `bp`; `null` for Base or an unknown id. */
|
|
1737
|
+
function mediaQueryFor(bp) {
|
|
1738
|
+
return breakpointQueryFor(bp, BreakpointQueryKind.Media);
|
|
1739
|
+
}
|
|
1740
|
+
/** Wrap already-compiled rules for `bp` in its media query. Base (and blank css) passes through. */
|
|
1741
|
+
function wrapInMediaQuery(bp, css) {
|
|
1742
|
+
return wrapInBreakpointQuery(bp, css, BreakpointQueryKind.Media);
|
|
1743
|
+
}
|
|
1744
|
+
function layerAt(responsive, bp) {
|
|
1745
|
+
return bp === Breakpoint.Base ? undefined : responsive?.[bp];
|
|
1746
|
+
}
|
|
1747
|
+
/** Copy defined entries of `src` onto `target`. `undefined` never overwrites a value. */
|
|
1748
|
+
function assignDefined(target, src) {
|
|
1749
|
+
if (src)
|
|
1750
|
+
for (const [k, v] of Object.entries(src))
|
|
1751
|
+
if (v !== undefined)
|
|
1752
|
+
target[k] = v;
|
|
1753
|
+
return target;
|
|
1754
|
+
}
|
|
1755
|
+
function hasKeys(map) {
|
|
1756
|
+
return !!map && Object.keys(map).length > 0;
|
|
1757
|
+
}
|
|
1758
|
+
// ─── Cascade resolution ──────────────────────────────────────────────────────
|
|
1759
|
+
/** The full effective style map at `bp`: base, then each layer down the cascade. */
|
|
1760
|
+
function resolveStyleProps(styleProps, responsive, bp) {
|
|
1761
|
+
const out = assignDefined({}, styleProps ?? undefined);
|
|
1762
|
+
for (const id of cascadeChain(bp))
|
|
1763
|
+
assignDefined(out, layerAt(responsive, id)?.styleProps);
|
|
1764
|
+
return out;
|
|
1837
1765
|
}
|
|
1838
1766
|
/**
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1842
|
-
* `queryParam` is a constant on `defaultValue`. Empty values are omitted so the source isn't
|
|
1843
|
-
* accidentally filtered to nothing.
|
|
1767
|
+
* The full effective TRAIT map at `bp`: the node's own inputs, then each layer down the cascade.
|
|
1768
|
+
* Deliberately identical in shape and cascade order to `resolveStyleProps`, so "how does a value
|
|
1769
|
+
* at this width get decided" has ONE answer on both sides of the renderer split.
|
|
1844
1770
|
*/
|
|
1845
|
-
function
|
|
1846
|
-
const
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
if (!q?.field)
|
|
1855
|
-
continue;
|
|
1856
|
-
// No queryParam → a CONSTANT filter: defaultValue is the value, in the builder preview and
|
|
1857
|
-
// on the live site alike. With a queryParam the URL wins and defaultValue is the fallback.
|
|
1858
|
-
const value = q.queryParam ? (queryParams[q.queryParam] ?? q.defaultValue) : q.defaultValue;
|
|
1859
|
-
if (value !== undefined && value !== null && value !== '')
|
|
1860
|
-
params[q.field] = value;
|
|
1861
|
-
}
|
|
1862
|
-
return params;
|
|
1771
|
+
function resolveInputs(inputs, responsive, bp) {
|
|
1772
|
+
const out = assignDefined({}, inputs ?? undefined);
|
|
1773
|
+
for (const id of cascadeChain(bp))
|
|
1774
|
+
assignDefined(out, layerAt(responsive, id)?.inputs);
|
|
1775
|
+
return out;
|
|
1776
|
+
}
|
|
1777
|
+
/** True when `bp` itself sets trait `name` — the panel's override indicator. Never true for Base. */
|
|
1778
|
+
function isInputOverriddenAt(responsive, bp, name) {
|
|
1779
|
+
return layerAt(responsive, bp)?.inputs?.[name] !== undefined;
|
|
1863
1780
|
}
|
|
1864
1781
|
/**
|
|
1865
|
-
*
|
|
1866
|
-
*
|
|
1867
|
-
* Returns that id (route-param value, else `defaultId`), or `null` when the source isn't a
|
|
1868
|
-
* single-by-id fetch — in which case the caller uses the collection endpoint + filters. The
|
|
1869
|
-
* path-based form only applies to the id field; a single source filtered by any other field still
|
|
1870
|
-
* goes through the collection endpoint.
|
|
1782
|
+
* Every level from Base down to `bp` that sets trait `name`, in cascade order — the trait-row
|
|
1783
|
+
* equivalent of `resolveValueChain`, driving the same "Base 4 → Tablet 2 → Mobile 1" source chain.
|
|
1871
1784
|
*/
|
|
1872
|
-
function
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
return
|
|
1785
|
+
function resolveInputChain(name, inputs, responsive, bp) {
|
|
1786
|
+
const chain = [];
|
|
1787
|
+
for (const id of cascadeChain(bp)) {
|
|
1788
|
+
const value = id === Breakpoint.Base ? inputs?.[name] : layerAt(responsive, id)?.inputs?.[name];
|
|
1789
|
+
if (value !== undefined)
|
|
1790
|
+
chain.push({ breakpoint: id, value });
|
|
1791
|
+
}
|
|
1792
|
+
return chain;
|
|
1793
|
+
}
|
|
1794
|
+
/** The effective per-state maps at `bp`. States with no values anywhere are omitted. */
|
|
1795
|
+
function resolveStateStyles(stateStyles, responsive, bp) {
|
|
1796
|
+
const out = {};
|
|
1797
|
+
const merge = (src) => {
|
|
1798
|
+
for (const [state, map] of Object.entries(src ?? {})) {
|
|
1799
|
+
out[state] = assignDefined(out[state] ?? {}, map);
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
merge(stateStyles ?? undefined);
|
|
1803
|
+
for (const id of cascadeChain(bp))
|
|
1804
|
+
merge(layerAt(responsive, id)?.stateStyles);
|
|
1805
|
+
for (const state of Object.keys(out))
|
|
1806
|
+
if (!hasKeys(out[state]))
|
|
1807
|
+
delete out[state];
|
|
1808
|
+
return out;
|
|
1880
1809
|
}
|
|
1881
1810
|
/**
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1811
|
+
* Every state that has a value anywhere — at base or at any breakpoint — in a stable order
|
|
1812
|
+
* (base states first, then any that exist only at a breakpoint). A renderer iterates this, not
|
|
1813
|
+
* `Object.keys(stateStyles)`, so a state defined ONLY at a breakpoint (a Mobile-only hover) still
|
|
1814
|
+
* gets its selector scope resolved.
|
|
1886
1815
|
*/
|
|
1887
|
-
function
|
|
1888
|
-
const
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
const req = domain === 'cms' ? ds.request?.cms : ds.request?.shop;
|
|
1894
|
-
const resource = req?.contentType;
|
|
1895
|
-
if (!resource)
|
|
1896
|
-
return { kind: 'none' };
|
|
1897
|
-
const id = mode === 'single' ? resolveSingleItemId(req, routeParams) : null;
|
|
1898
|
-
if (id)
|
|
1899
|
-
return { kind: 'by-id', domain, resource, id, mode };
|
|
1900
|
-
const filter = buildDataSourceFilter(req, routeParams, queryParams);
|
|
1901
|
-
const plan = { kind: 'collection', domain, resource, filter, mode };
|
|
1902
|
-
if (req?.limit)
|
|
1903
|
-
plan.limit = req.limit;
|
|
1904
|
-
return plan;
|
|
1905
|
-
}
|
|
1906
|
-
case 'external-api': {
|
|
1907
|
-
const endpoint = ds.request?.endpoint;
|
|
1908
|
-
return endpoint ? { kind: 'external', endpoint, mode } : { kind: 'none' };
|
|
1909
|
-
}
|
|
1910
|
-
case 'static':
|
|
1911
|
-
return { kind: 'static', data: ds.request?.data ?? null, mode };
|
|
1912
|
-
case 'derived': {
|
|
1913
|
-
const d = ds.request?.derived;
|
|
1914
|
-
return d?.from ? { kind: 'derived', from: d.from, path: d.path ?? '', mode } : { kind: 'none' };
|
|
1816
|
+
function stateNamesIn(stateStyles, responsive) {
|
|
1817
|
+
const names = new Set();
|
|
1818
|
+
const add = (src) => {
|
|
1819
|
+
for (const [state, map] of Object.entries(src ?? {})) {
|
|
1820
|
+
if (Object.values(map ?? {}).some(v => v !== undefined))
|
|
1821
|
+
names.add(state);
|
|
1915
1822
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
return { kind: 'none' };
|
|
1922
|
-
default:
|
|
1923
|
-
return { kind: 'none' };
|
|
1924
|
-
}
|
|
1823
|
+
};
|
|
1824
|
+
add(stateStyles);
|
|
1825
|
+
for (const { id } of BREAKPOINTS)
|
|
1826
|
+
add(layerAt(responsive, id)?.stateStyles);
|
|
1827
|
+
return [...names];
|
|
1925
1828
|
}
|
|
1926
1829
|
/**
|
|
1927
|
-
*
|
|
1928
|
-
*
|
|
1929
|
-
*
|
|
1930
|
-
* interactions. See element-context-state-plan.md.
|
|
1830
|
+
* Every level from Base down to `bp` that sets `prop` (optionally within `state`), in
|
|
1831
|
+
* cascade order. The LAST entry is the effective value; an empty array means unset.
|
|
1832
|
+
* Drives the style panel's source chain ("Base 34 → Tablet 28 → Mobile 22").
|
|
1931
1833
|
*/
|
|
1932
|
-
function
|
|
1933
|
-
const
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
for (const ds of dataSources ?? []) {
|
|
1943
|
-
if (ds?.type === 'variable' && ds.alias === alias)
|
|
1944
|
-
return ds.request?.variable?.itemsFrom || undefined;
|
|
1834
|
+
function resolveValueChain(prop, styleProps, stateStyles, responsive, bp, state) {
|
|
1835
|
+
const read = (props, states) => (state ? states?.[state] : props)?.[prop];
|
|
1836
|
+
const chain = [];
|
|
1837
|
+
for (const id of cascadeChain(bp)) {
|
|
1838
|
+
const layer = layerAt(responsive, id);
|
|
1839
|
+
const value = id === Breakpoint.Base
|
|
1840
|
+
? read(styleProps ?? undefined, stateStyles ?? undefined)
|
|
1841
|
+
: read(layer?.styleProps, layer?.stateStyles);
|
|
1842
|
+
if (value !== undefined)
|
|
1843
|
+
chain.push({ breakpoint: id, value });
|
|
1945
1844
|
}
|
|
1946
|
-
return
|
|
1845
|
+
return chain;
|
|
1947
1846
|
}
|
|
1948
|
-
/**
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
function normalizeCollectionResult(mode, data) {
|
|
1954
|
-
const items = Array.isArray(data) ? data : (data?.items ?? data?.data ?? []);
|
|
1955
|
-
return mode === 'single' ? (items[0] ?? null) : items;
|
|
1847
|
+
/** True when `bp` itself sets `prop` (the style panel's orange edge). Never true for Base. */
|
|
1848
|
+
function isOverriddenAt(responsive, bp, prop, state) {
|
|
1849
|
+
const layer = layerAt(responsive, bp);
|
|
1850
|
+
const map = state ? layer?.stateStyles?.[state] : layer?.styleProps;
|
|
1851
|
+
return map?.[prop] !== undefined;
|
|
1956
1852
|
}
|
|
1957
|
-
/**
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
return undefined;
|
|
1969
|
-
cur = cur[seg];
|
|
1853
|
+
/** Number of overridden values — at `bp`, or across every breakpoint when omitted. */
|
|
1854
|
+
function countOverrides(responsive, bp) {
|
|
1855
|
+
const count = (map) => Object.values(map ?? {}).filter(v => v !== undefined).length;
|
|
1856
|
+
const ids = bp ? [bp] : BREAKPOINTS.map(d => d.id);
|
|
1857
|
+
let n = 0;
|
|
1858
|
+
for (const id of ids) {
|
|
1859
|
+
const layer = layerAt(responsive, id);
|
|
1860
|
+
n += count(layer?.styleProps);
|
|
1861
|
+
n += count(layer?.inputs);
|
|
1862
|
+
for (const map of Object.values(layer?.stateStyles ?? {}))
|
|
1863
|
+
n += count(map);
|
|
1970
1864
|
}
|
|
1971
|
-
return
|
|
1865
|
+
return n;
|
|
1972
1866
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
* to a single-row list (back-compat with the old `fetchItems`); null/undefined → `[]`.
|
|
1978
|
-
* - With `sourcePath`: the source resolved to a SINGLE resource (e.g. a product); dive into the
|
|
1979
|
-
* nested array property (e.g. `gallery`). Non-array / missing → `[]`.
|
|
1980
|
-
*/
|
|
1981
|
-
/**
|
|
1982
|
-
* Fill in `derived` aliases in an already-resolved map. Given the data sources and a map of resolved
|
|
1983
|
-
* NON-derived values (by alias), compute each `type:'derived'` alias as `getByPath(parent, path)` in
|
|
1984
|
-
* dependency order (chains resolved by iterating until stable). Mutates + returns the map. Used by the
|
|
1985
|
-
* SSR engine after it resolves the root sources; the builder resolves derived lazily per alias.
|
|
1986
|
-
*/
|
|
1987
|
-
function resolveDerivedInto(dataSources, resolved) {
|
|
1988
|
-
const derived = (dataSources ?? []).filter(d => d?.type === 'derived' && !!d.alias);
|
|
1989
|
-
let changed = true, guard = 0;
|
|
1990
|
-
while (changed && guard++ < 25) {
|
|
1991
|
-
changed = false;
|
|
1992
|
-
for (const d of derived) {
|
|
1993
|
-
if (d.alias in resolved)
|
|
1994
|
-
continue;
|
|
1995
|
-
const from = d.request?.derived?.from;
|
|
1996
|
-
if (from && from in resolved) {
|
|
1997
|
-
resolved[d.alias] = getByPath(resolved[from], d.request.derived.path ?? '');
|
|
1998
|
-
changed = true;
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
1867
|
+
// ─── Editing (immutable) ─────────────────────────────────────────────────────
|
|
1868
|
+
function assertOverrideBreakpoint(bp) {
|
|
1869
|
+
if (bp === Breakpoint.Base) {
|
|
1870
|
+
throw new Error('Base styles live in styleProps / stateStyles, not in responsive overrides');
|
|
2001
1871
|
}
|
|
2002
|
-
|
|
1872
|
+
if (!BREAKPOINT_INDEX.has(bp))
|
|
1873
|
+
throw new Error(`Unknown breakpoint "${bp}"`);
|
|
2003
1874
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
1875
|
+
/** Returns a new overrides object with `prop` set at `bp` (optionally within `state`). */
|
|
1876
|
+
function setOverride(responsive, bp, prop, value, state) {
|
|
1877
|
+
assertOverrideBreakpoint(bp);
|
|
1878
|
+
const layer = { ...(responsive?.[bp] ?? {}) };
|
|
1879
|
+
if (state) {
|
|
1880
|
+
layer.stateStyles = { ...(layer.stateStyles ?? {}), [state]: { ...(layer.stateStyles?.[state] ?? {}), [prop]: value } };
|
|
2008
1881
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
return resolved == null ? [] : [resolved];
|
|
2012
|
-
}
|
|
2013
|
-
// ── Referenced data sources (shared: builder-save + SSR resolve) ─────────────────────────────────
|
|
2014
|
-
// Which data-source aliases a node tree uses, so a consumer resolves ONLY what a page/global needs
|
|
2015
|
-
// (not the whole org library). Used by cms-client (persist globals' sources) and the SSR engine
|
|
2016
|
-
// (resolve the referenced subset per page). See org-data-sources-plan.md.
|
|
2017
|
-
/** Aliases referenced anywhere in a node tree: a repeater/slider `source` (a `bindings` entry with
|
|
2018
|
-
* `prop === 'source'`, or `props.source` for the slider) plus every binding's `source`. */
|
|
2019
|
-
function collectReferencedAliases(nodes, acc = new Set()) {
|
|
2020
|
-
for (const n of nodes ?? []) {
|
|
2021
|
-
for (const b of n?.bindings ?? []) {
|
|
2022
|
-
if (b?.source)
|
|
2023
|
-
acc.add(b.source);
|
|
2024
|
-
}
|
|
2025
|
-
if (n?.props?.source)
|
|
2026
|
-
acc.add(n.props.source);
|
|
2027
|
-
if (n?.children?.length)
|
|
2028
|
-
collectReferencedAliases(n.children, acc);
|
|
1882
|
+
else {
|
|
1883
|
+
layer.styleProps = { ...(layer.styleProps ?? {}), [prop]: value };
|
|
2029
1884
|
}
|
|
2030
|
-
return
|
|
2031
|
-
}
|
|
2032
|
-
/** The subset of `all` whose `alias` is referenced by `nodes` (shallow — direct references only). */
|
|
2033
|
-
function referencedDataSources(nodes, all) {
|
|
2034
|
-
const aliases = collectReferencedAliases(nodes);
|
|
2035
|
-
return (all ?? []).filter(ds => aliases.has(ds?.alias));
|
|
1885
|
+
return { ...(responsive ?? {}), [bp]: layer };
|
|
2036
1886
|
}
|
|
2037
|
-
/**
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
function referencedDataSourcesDeep(nodes, all) {
|
|
2044
|
-
const byAlias = new Map((all ?? []).map(d => [d?.alias, d]));
|
|
2045
|
-
const wanted = collectReferencedAliases(nodes);
|
|
2046
|
-
const queue = [...wanted];
|
|
2047
|
-
const enqueue = (alias) => {
|
|
2048
|
-
if (alias && !wanted.has(alias)) {
|
|
2049
|
-
wanted.add(alias);
|
|
2050
|
-
queue.push(alias);
|
|
2051
|
-
}
|
|
2052
|
-
};
|
|
2053
|
-
while (queue.length) {
|
|
2054
|
-
const ds = byAlias.get(queue.pop());
|
|
2055
|
-
enqueue(ds?.request?.derived?.from);
|
|
2056
|
-
enqueue(ds?.request?.relative?.subjectFrom);
|
|
2057
|
-
enqueue(ds?.request?.relative?.from);
|
|
2058
|
-
}
|
|
2059
|
-
return (all ?? []).filter(d => wanted.has(d?.alias));
|
|
1887
|
+
/** Returns a new overrides object with trait `name` set at `bp`. */
|
|
1888
|
+
function setInputOverride(responsive, bp, name, value) {
|
|
1889
|
+
assertOverrideBreakpoint(bp);
|
|
1890
|
+
const layer = { ...(responsive?.[bp] ?? {}) };
|
|
1891
|
+
layer.inputs = { ...(layer.inputs ?? {}), [name]: value };
|
|
1892
|
+
return { ...(responsive ?? {}), [bp]: layer };
|
|
2060
1893
|
}
|
|
2061
1894
|
/**
|
|
2062
|
-
*
|
|
2063
|
-
*
|
|
2064
|
-
* so both hit the SAME public endpoints (no admin, no duplication). Returns `null` for `static` /
|
|
2065
|
-
* `none` plans (no HTTP call). A single item is a PATH segment (`/:resource/:id`), never `?_id=`.
|
|
1895
|
+
* Remove trait overrides at `bp`, pruning empties exactly like `clearOverride` — omit `name` to
|
|
1896
|
+
* drop every trait override at that width. Returns `undefined` when nothing is left anywhere.
|
|
2066
1897
|
*/
|
|
2067
|
-
function
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
return { url: `${baseFor(plan.domain)}${plan.resource}`, params };
|
|
2077
|
-
}
|
|
2078
|
-
case 'external':
|
|
2079
|
-
return { url: plan.endpoint, params: {} };
|
|
2080
|
-
default:
|
|
2081
|
-
return null;
|
|
1898
|
+
function clearInputOverride(responsive, bp, name) {
|
|
1899
|
+
assertOverrideBreakpoint(bp);
|
|
1900
|
+
const next = { ...(responsive ?? {}) };
|
|
1901
|
+
const current = next[bp];
|
|
1902
|
+
if (!current?.inputs)
|
|
1903
|
+
return hasKeys(next) ? next : undefined;
|
|
1904
|
+
const layer = { ...current };
|
|
1905
|
+
if (name === undefined) {
|
|
1906
|
+
delete layer.inputs;
|
|
2082
1907
|
}
|
|
1908
|
+
else {
|
|
1909
|
+
const { [name]: _dropped, ...rest } = layer.inputs;
|
|
1910
|
+
layer.inputs = hasKeys(rest) ? rest : undefined;
|
|
1911
|
+
if (!layer.inputs)
|
|
1912
|
+
delete layer.inputs;
|
|
1913
|
+
}
|
|
1914
|
+
next[bp] = hasKeys(layer) ? layer : undefined;
|
|
1915
|
+
if (!next[bp])
|
|
1916
|
+
delete next[bp];
|
|
1917
|
+
return hasKeys(next) ? next : undefined;
|
|
2083
1918
|
}
|
|
2084
|
-
|
|
2085
1919
|
/**
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
* A Gallery is populated one of two ways (see gallery-component-plan.md):
|
|
2089
|
-
* - **Manual**: an ordered `GalleryImage[]` trait. Each item is a raw/filebox URL OR a per-item
|
|
2090
|
-
* binding path resolved against the item/route context.
|
|
2091
|
-
* - **Bound**: the whole gallery repeats over a data source (a collection, or an array property
|
|
2092
|
-
* inside one resource via `sourcePath`) — the items come from the shared `resolveRepeaterItems`,
|
|
2093
|
-
* and one field of each record (`imageField`) is the URL.
|
|
1920
|
+
* Remove overrides at `bp` and PRUNE whatever becomes empty, so "reset" and "never
|
|
1921
|
+
* overridden" are the same state on disk — no tombstones, no nulls.
|
|
2094
1922
|
*
|
|
2095
|
-
*
|
|
2096
|
-
*
|
|
2097
|
-
*
|
|
2098
|
-
*
|
|
2099
|
-
*/
|
|
2100
|
-
/**
|
|
2101
|
-
* Resolve a gallery's configured images to the flat, display-ready `string[]` the loop renders.
|
|
2102
|
-
* Pure. Empty/whitespace URLs are dropped so a half-filled manual list never yields blank frames.
|
|
1923
|
+
* clearOverride(r, bp) — the whole breakpoint
|
|
1924
|
+
* clearOverride(r, bp, undefined, st) — one state at that breakpoint
|
|
1925
|
+
* clearOverride(r, bp, prop) — one base-state value
|
|
1926
|
+
* clearOverride(r, bp, prop, st) — one value within a state
|
|
2103
1927
|
*
|
|
2104
|
-
*
|
|
2105
|
-
* @param bound bound-mode records + image field. When present, takes precedence over `images`.
|
|
2106
|
-
* @param ctx render context for per-item bindings (item/route data). Defaults to `{}`.
|
|
1928
|
+
* Returns `undefined` when nothing is left, so a serialiser omits the key entirely.
|
|
2107
1929
|
*/
|
|
2108
|
-
function
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
1930
|
+
function clearOverride(responsive, bp, prop, state) {
|
|
1931
|
+
assertOverrideBreakpoint(bp);
|
|
1932
|
+
const next = { ...(responsive ?? {}) };
|
|
1933
|
+
const current = next[bp];
|
|
1934
|
+
if (current && (prop !== undefined || state !== undefined)) {
|
|
1935
|
+
const layer = { ...current };
|
|
1936
|
+
if (state) {
|
|
1937
|
+
const states = { ...(layer.stateStyles ?? {}) };
|
|
1938
|
+
if (prop !== undefined && states[state]) {
|
|
1939
|
+
const { [prop]: _dropped, ...rest } = states[state];
|
|
1940
|
+
states[state] = rest;
|
|
1941
|
+
}
|
|
1942
|
+
if (prop === undefined || !hasKeys(states[state]))
|
|
1943
|
+
delete states[state];
|
|
1944
|
+
layer.stateStyles = hasKeys(states) ? states : undefined;
|
|
1945
|
+
}
|
|
1946
|
+
else if (prop !== undefined && layer.styleProps) {
|
|
1947
|
+
const { [prop]: _dropped, ...rest } = layer.styleProps;
|
|
1948
|
+
layer.styleProps = hasKeys(rest) ? rest : undefined;
|
|
1949
|
+
}
|
|
1950
|
+
if (!layer.stateStyles)
|
|
1951
|
+
delete layer.stateStyles;
|
|
1952
|
+
if (!layer.styleProps)
|
|
1953
|
+
delete layer.styleProps;
|
|
1954
|
+
next[bp] = hasKeys(layer) ? layer : undefined;
|
|
2115
1955
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
1956
|
+
else {
|
|
1957
|
+
next[bp] = undefined;
|
|
1958
|
+
}
|
|
1959
|
+
if (!next[bp])
|
|
1960
|
+
delete next[bp];
|
|
1961
|
+
return hasKeys(next) ? next : undefined;
|
|
2120
1962
|
}
|
|
2121
|
-
|
|
2122
|
-
function
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
1963
|
+
const ZERO_DURATIONS = new Set(['0ms', '0s']);
|
|
1964
|
+
function groupsTouchedBy(map) {
|
|
1965
|
+
const touched = new Set();
|
|
1966
|
+
if (!map)
|
|
1967
|
+
return touched;
|
|
1968
|
+
for (const group of VIRTUAL_TRAIT_GROUPS) {
|
|
1969
|
+
if (group.keys.some(k => map[k] !== undefined))
|
|
1970
|
+
touched.add(group);
|
|
1971
|
+
}
|
|
1972
|
+
return touched;
|
|
2127
1973
|
}
|
|
2128
1974
|
/**
|
|
2129
|
-
*
|
|
2130
|
-
*
|
|
1975
|
+
* Should a touched group that composed to nothing emit its cancelling value?
|
|
1976
|
+
* Transition is the exception: setting only the easing (no duration anywhere) composes to
|
|
1977
|
+
* nothing too, and cancelling there would also kill the renderer's default hover transition
|
|
1978
|
+
* — so transition cancels only when the merged duration is explicitly zero.
|
|
2131
1979
|
*/
|
|
2132
|
-
function
|
|
2133
|
-
if (
|
|
2134
|
-
return
|
|
2135
|
-
|
|
2136
|
-
.
|
|
2137
|
-
|
|
2138
|
-
.filter(Boolean)
|
|
2139
|
-
.map(url => ({ url }));
|
|
1980
|
+
function shouldReset(group, merged) {
|
|
1981
|
+
if (group.reset === null)
|
|
1982
|
+
return false;
|
|
1983
|
+
if (group.output === 'transition')
|
|
1984
|
+
return ZERO_DURATIONS.has(String(merged['transitionDuration']));
|
|
1985
|
+
return true;
|
|
2140
1986
|
}
|
|
2141
|
-
|
|
2142
1987
|
/**
|
|
2143
|
-
*
|
|
2144
|
-
*
|
|
2145
|
-
*
|
|
2146
|
-
* the
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
1988
|
+
* Compose a sparse override against everything wider than it, keeping ONLY what the
|
|
1989
|
+
* override changes: its own plain props, plus each virtual group it touches (or that
|
|
1990
|
+
* group's cancelling value when it now composes to nothing). Untouched groups are dropped
|
|
1991
|
+
* — re-emitting the wider rule's value inside a media block is noise at best.
|
|
1992
|
+
*/
|
|
1993
|
+
function composeOverride(own, wider, touched) {
|
|
1994
|
+
const composed = composeVirtualTraits(own, wider);
|
|
1995
|
+
const groupOutputs = new Set(VIRTUAL_TRAIT_GROUPS.map(g => g.output));
|
|
1996
|
+
const out = {};
|
|
1997
|
+
for (const [k, v] of Object.entries(composed)) {
|
|
1998
|
+
if (!groupOutputs.has(k) && v !== undefined)
|
|
1999
|
+
out[k] = v;
|
|
2000
|
+
}
|
|
2001
|
+
const merged = { ...wider, ...own };
|
|
2002
|
+
for (const group of touched) {
|
|
2003
|
+
const value = composed[group.output];
|
|
2004
|
+
if (value !== undefined)
|
|
2005
|
+
out[group.output] = value;
|
|
2006
|
+
else if (shouldReset(group, merged))
|
|
2007
|
+
out[group.output] = group.reset;
|
|
2008
|
+
}
|
|
2009
|
+
return out;
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* The per-breakpoint rule sets a node needs, widest → narrowest (the order they must be
|
|
2013
|
+
* emitted in, AFTER every base rule — equal specificity means source order decides).
|
|
2014
|
+
* Breakpoints with nothing to emit are omitted.
|
|
2151
2015
|
*
|
|
2152
|
-
*
|
|
2016
|
+
* Handles the cross-axis case: a breakpoint that changes a virtual group (say
|
|
2017
|
+
* `translateX`) also re-emits that group for every state the node has, because the wider
|
|
2018
|
+
* state rule was composed against the wider value and would otherwise snap back to it on
|
|
2019
|
+
* hover.
|
|
2153
2020
|
*/
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
const
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
const
|
|
2168
|
-
const
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2021
|
+
function planResponsiveRules(styleProps, stateStyles, responsive) {
|
|
2022
|
+
if (!hasKeys(responsive))
|
|
2023
|
+
return [];
|
|
2024
|
+
const plan = [];
|
|
2025
|
+
for (const def of BREAKPOINTS) {
|
|
2026
|
+
const layer = layerAt(responsive, def.id);
|
|
2027
|
+
if (!layer || def.maxWidth == null)
|
|
2028
|
+
continue;
|
|
2029
|
+
const bp = def.id;
|
|
2030
|
+
const wider = parentBreakpoint(bp);
|
|
2031
|
+
const own = layer.styleProps ?? {};
|
|
2032
|
+
const widerProps = resolveStyleProps(styleProps, responsive, wider);
|
|
2033
|
+
const touchedByProps = groupsTouchedBy(own);
|
|
2034
|
+
const composedProps = composeOverride(own, widerProps, touchedByProps);
|
|
2035
|
+
const propsHere = { ...widerProps, ...own };
|
|
2036
|
+
const statesHere = resolveStateStyles(stateStyles, responsive, bp);
|
|
2037
|
+
const statesWider = resolveStateStyles(stateStyles, responsive, wider);
|
|
2038
|
+
const composedStates = {};
|
|
2039
|
+
for (const state of Object.keys(statesHere)) {
|
|
2040
|
+
const ownState = layer.stateStyles?.[state] ?? {};
|
|
2041
|
+
const touched = groupsTouchedBy(ownState);
|
|
2042
|
+
for (const group of touchedByProps)
|
|
2043
|
+
touched.add(group);
|
|
2044
|
+
const composed = composeOverride(ownState, { ...propsHere, ...(statesWider[state] ?? {}) }, touched);
|
|
2045
|
+
if (hasKeys(composed))
|
|
2046
|
+
composedStates[state] = composed;
|
|
2047
|
+
}
|
|
2048
|
+
if (hasKeys(composedProps) || hasKeys(composedStates)) {
|
|
2049
|
+
plan.push({
|
|
2050
|
+
breakpoint: bp,
|
|
2051
|
+
mediaQuery: mediaQueryFor(bp),
|
|
2052
|
+
styleProps: composedProps,
|
|
2053
|
+
stateStyles: composedStates,
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
return plan;
|
|
2184
2058
|
}
|
|
2185
|
-
const GALLERY_EFFECT_DESCRIPTORS = Object.values(GALLERY_EFFECTS).map(e => ({ id: e.id, label: e.label }));
|
|
2186
2059
|
|
|
2060
|
+
// Swiper breakpoints, derived from a node's per-breakpoint TRAIT overrides.
|
|
2061
|
+
//
|
|
2062
|
+
// SHARED (iox-ui): the builder canvas and the SSR engine both hand Swiper the same config, so a
|
|
2063
|
+
// slider that goes 1-up on a phone in the editor does the same on the live site.
|
|
2064
|
+
//
|
|
2065
|
+
// ⚠️ THE INVERSION IS THE WHOLE POINT OF THIS FILE.
|
|
2066
|
+
// IOX is desktop-first: `max-width`, and a layer applies at its width AND NARROWER.
|
|
2067
|
+
// Swiper is mobile-first: `breakpoints` keys are MIN-widths, and the LARGEST matching key wins.
|
|
2068
|
+
// Converting by hand at each call site is how the two drift, so it happens once, here, tested.
|
|
2069
|
+
//
|
|
2070
|
+
// See iox-ai-guidance `architecture/builder/builder-quality-pass-plan.md` (P3-G2).
|
|
2071
|
+
/** Trait names whose value Swiper reads per breakpoint. Anything else is config-wide. */
|
|
2072
|
+
const SWIPER_RESPONSIVE_TRAITS = Object.freeze(['slidesPerView', 'slidesPerGroup', 'spaceBetween']);
|
|
2187
2073
|
/**
|
|
2188
|
-
*
|
|
2189
|
-
* visible over time. Ported from the reference fade-gallery's `scheduleCycle` / `tryStartLoop` /
|
|
2190
|
-
* first-cycle-activation logic, with Angular specifics removed so BOTH the builder twin and the
|
|
2191
|
-
* engine/runtime twin (`iox-gallery`) run ONE implementation — never a forked copy that drifts.
|
|
2074
|
+
* Build Swiper's mobile-first `breakpoints` map from IOX's desktop-first layers.
|
|
2192
2075
|
*
|
|
2193
|
-
*
|
|
2194
|
-
*
|
|
2195
|
-
*
|
|
2076
|
+
* Each IOX breakpoint covers `[nextNarrower.maxWidth + 1 … this.maxWidth]`, so the Swiper key for a
|
|
2077
|
+
* layer is **one px above the next-narrower breakpoint's max-width** — the low edge of the band the
|
|
2078
|
+
* layer governs. Base becomes the key above the widest bounded breakpoint.
|
|
2196
2079
|
*
|
|
2197
|
-
*
|
|
2080
|
+
* Values are the RESOLVED ones at each width (base + every wider layer), never the raw sparse layer:
|
|
2081
|
+
* Swiper does not cascade between its own breakpoints the way CSS does — the largest matching key
|
|
2082
|
+
* wins outright and is not merged with smaller ones — so each entry must be self-sufficient.
|
|
2083
|
+
*
|
|
2084
|
+
* Only `SWIPER_RESPONSIVE_TRAITS` are emitted, and a band is omitted when it matches the band below
|
|
2085
|
+
* it, keeping the config as small as the author's actual overrides.
|
|
2198
2086
|
*/
|
|
2199
|
-
|
|
2200
|
-
const
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
};
|
|
2211
|
-
class GalleryLoop {
|
|
2212
|
-
constructor(opts) {
|
|
2213
|
-
this.count = 0;
|
|
2214
|
-
this.currentIndex = 0;
|
|
2215
|
-
this.fadingOutIndex = null;
|
|
2216
|
-
this.firstCycleActive = false;
|
|
2217
|
-
this.cycleTimer = null;
|
|
2218
|
-
this.fadeTimer = null;
|
|
2219
|
-
this.frameHandle = null;
|
|
2220
|
-
/** Bumped on every (re)start so a stale timer callback from a previous run is ignored. */
|
|
2221
|
-
this.runToken = 0;
|
|
2222
|
-
this.running = false;
|
|
2223
|
-
this.scheduler = opts.scheduler ?? DEFAULT_GALLERY_SCHEDULER;
|
|
2224
|
-
this.showMs = opts.showMs;
|
|
2225
|
-
this.fadeMs = opts.fadeMs;
|
|
2226
|
-
this.onChange = opts.onChange;
|
|
2227
|
-
}
|
|
2228
|
-
get state() {
|
|
2229
|
-
return {
|
|
2230
|
-
currentIndex: this.currentIndex,
|
|
2231
|
-
fadingOutIndex: this.fadingOutIndex,
|
|
2232
|
-
firstCycleActive: this.firstCycleActive,
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2235
|
-
/** Start (or restart) the loop for `count` images. A count < 2 shows the single image without cycling. */
|
|
2236
|
-
start(count) {
|
|
2237
|
-
this.stop();
|
|
2238
|
-
this.count = Math.max(0, count | 0);
|
|
2239
|
-
this.currentIndex = 0;
|
|
2240
|
-
this.fadingOutIndex = null;
|
|
2241
|
-
this.firstCycleActive = false;
|
|
2242
|
-
if (this.count === 0) {
|
|
2243
|
-
this.emit();
|
|
2244
|
-
return;
|
|
2245
|
-
}
|
|
2246
|
-
this.running = true;
|
|
2247
|
-
const token = ++this.runToken;
|
|
2248
|
-
// Activate on the next frame so the first paint is flat (no entry-transition flash), then
|
|
2249
|
-
// begin cycling only when there is more than one image to advance through.
|
|
2250
|
-
this.frameHandle = this.scheduler.nextFrame(() => {
|
|
2251
|
-
if (token !== this.runToken || !this.running)
|
|
2252
|
-
return;
|
|
2253
|
-
this.firstCycleActive = true;
|
|
2254
|
-
this.emit();
|
|
2255
|
-
if (this.count > 1)
|
|
2256
|
-
this.scheduleCycle(token);
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
/** Update timing live. Takes effect on the next scheduled cycle; does not restart the loop. */
|
|
2260
|
-
setDurations(showMs, fadeMs) {
|
|
2261
|
-
this.showMs = showMs;
|
|
2262
|
-
this.fadeMs = fadeMs;
|
|
2087
|
+
function buildSwiperBreakpoints(inputs, responsive) {
|
|
2088
|
+
const bands = bandsWidestFirst();
|
|
2089
|
+
const map = {};
|
|
2090
|
+
let previous = null;
|
|
2091
|
+
// Narrowest → widest, so "same as the band below" is a comparison against what we just emitted.
|
|
2092
|
+
for (const band of [...bands].reverse()) {
|
|
2093
|
+
const resolved = pick(resolveInputs(inputs, responsive, band.id));
|
|
2094
|
+
if (previous && sameValues(previous, resolved))
|
|
2095
|
+
continue;
|
|
2096
|
+
map[band.minWidth] = resolved;
|
|
2097
|
+
previous = resolved;
|
|
2263
2098
|
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2099
|
+
return map;
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* The full `breakpoints` value to hand Swiper: the derived map, with any hand-written advanced JSON
|
|
2103
|
+
* merged OVER it so an author's explicit escape hatch always wins.
|
|
2104
|
+
*/
|
|
2105
|
+
function mergeSwiperBreakpoints(derived, advancedJson) {
|
|
2106
|
+
let advanced;
|
|
2107
|
+
if (advancedJson) {
|
|
2108
|
+
try {
|
|
2109
|
+
advanced = JSON.parse(advancedJson);
|
|
2275
2110
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
this.frameHandle = null;
|
|
2111
|
+
catch {
|
|
2112
|
+
advanced = undefined;
|
|
2279
2113
|
}
|
|
2280
2114
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
this.emit();
|
|
2290
|
-
this.fadeTimer = this.scheduler.setTimer(() => {
|
|
2291
|
-
if (token !== this.runToken || !this.running)
|
|
2292
|
-
return;
|
|
2293
|
-
this.fadingOutIndex = null;
|
|
2294
|
-
this.emit();
|
|
2295
|
-
this.scheduleCycle(token);
|
|
2296
|
-
}, this.fadeMs);
|
|
2297
|
-
}, this.showMs);
|
|
2115
|
+
if (!advanced)
|
|
2116
|
+
return Object.keys(derived).length ? derived : undefined;
|
|
2117
|
+
const out = { ...derived };
|
|
2118
|
+
for (const [key, value] of Object.entries(advanced)) {
|
|
2119
|
+
const min = Number(key);
|
|
2120
|
+
if (!Number.isFinite(min))
|
|
2121
|
+
continue;
|
|
2122
|
+
out[min] = { ...(out[min] ?? {}), ...(value ?? {}) };
|
|
2298
2123
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2124
|
+
return Object.keys(out).length ? out : undefined;
|
|
2125
|
+
}
|
|
2126
|
+
/** Each IOX breakpoint with the MIN width at which it starts applying. Widest first. */
|
|
2127
|
+
function bandsWidestFirst() {
|
|
2128
|
+
return BREAKPOINTS.map((def, i) => {
|
|
2129
|
+
const narrower = BREAKPOINTS[i + 1];
|
|
2130
|
+
// Base starts one px above the widest bounded breakpoint; every other band starts one px
|
|
2131
|
+
// above the breakpoint below it. The narrowest band starts at 0 — it has nothing below.
|
|
2132
|
+
const minWidth = narrower?.maxWidth != null ? narrower.maxWidth + 1 : 0;
|
|
2133
|
+
return { id: def.id, minWidth };
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
function pick(resolved) {
|
|
2137
|
+
const out = {};
|
|
2138
|
+
for (const name of SWIPER_RESPONSIVE_TRAITS) {
|
|
2139
|
+
if (resolved[name] !== undefined)
|
|
2140
|
+
out[name] = resolved[name];
|
|
2301
2141
|
}
|
|
2142
|
+
return out;
|
|
2143
|
+
}
|
|
2144
|
+
function sameValues(a, b) {
|
|
2145
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
2146
|
+
for (const key of keys)
|
|
2147
|
+
if (a[key] !== b[key])
|
|
2148
|
+
return false;
|
|
2149
|
+
return true;
|
|
2302
2150
|
}
|
|
2303
2151
|
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
class ClientGalleryComponent {
|
|
2319
|
-
/** Manual images — `GalleryImage[]` or a plain `string[]` of URLs (normalised on set). */
|
|
2320
|
-
set images(v) {
|
|
2321
|
-
this._images = v ?? [];
|
|
2322
|
-
this.urls = resolveGalleryUrls(this.normalizeImages(), null);
|
|
2323
|
-
// A live edit (image added/removed/reordered) while the loop runs → restart on the new set so
|
|
2324
|
-
// the canvas reflects it and the loop cycles through the new count.
|
|
2325
|
-
if (this.loopActive)
|
|
2326
|
-
this.startLoop();
|
|
2152
|
+
class ClientSliderContainerComponent {
|
|
2153
|
+
/** The base (Base-breakpoint) values of the traits Swiper reads per width. Read from the live
|
|
2154
|
+
* inputs rather than the node, so the builder's direct-assignment edits are picked up too. */
|
|
2155
|
+
collectSwiperInputs() {
|
|
2156
|
+
const all = {
|
|
2157
|
+
slidesPerView: this.slidesPerView,
|
|
2158
|
+
slidesPerGroup: this.slidesPerGroup,
|
|
2159
|
+
spaceBetween: this.spaceBetween,
|
|
2160
|
+
};
|
|
2161
|
+
const out = {};
|
|
2162
|
+
for (const name of SWIPER_RESPONSIVE_TRAITS)
|
|
2163
|
+
if (all[name] !== undefined)
|
|
2164
|
+
out[name] = all[name];
|
|
2165
|
+
return out;
|
|
2327
2166
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
get showDuration() { return this._showDuration; }
|
|
2331
|
-
set fadeDuration(v) { this._fadeDuration = v; this.loop?.setDurations(this._showDuration, v); }
|
|
2332
|
-
get fadeDuration() { return this._fadeDuration; }
|
|
2333
|
-
constructor(platformId, cdr) {
|
|
2334
|
-
this.cdr = cdr;
|
|
2167
|
+
constructor(platformId) {
|
|
2168
|
+
this.platformId = platformId;
|
|
2335
2169
|
this.nodeId = '';
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
this.
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
this.
|
|
2346
|
-
this.
|
|
2347
|
-
this.
|
|
2348
|
-
|
|
2349
|
-
this.
|
|
2350
|
-
this.
|
|
2351
|
-
this.
|
|
2352
|
-
this.
|
|
2353
|
-
|
|
2354
|
-
this.
|
|
2355
|
-
this.
|
|
2356
|
-
this.
|
|
2170
|
+
/** Language (writing) direction. Rendered as the `dir` attribute so it survives SSR and drives
|
|
2171
|
+
* Swiper's RTL layout — must match the builder's app-slider-container, which had `dir` while
|
|
2172
|
+
* this engine-runtime component did not (RTL showed in the editor but never on the live site). */
|
|
2173
|
+
this.dir = 'ltr';
|
|
2174
|
+
this.direction = 'horizontal';
|
|
2175
|
+
this.loop = false;
|
|
2176
|
+
this.slidesPerView = 1;
|
|
2177
|
+
/** Declared by the config since the slider shipped, but never declared HERE — so `safeSetInput`
|
|
2178
|
+
* dropped it and the panel control did nothing on either renderer. */
|
|
2179
|
+
this.slidesPerGroup = 1;
|
|
2180
|
+
this.spaceBetween = 0;
|
|
2181
|
+
this.speed = 300;
|
|
2182
|
+
this.effect = 'slide';
|
|
2183
|
+
this.grabCursor = true;
|
|
2184
|
+
this.centeredSlides = false;
|
|
2185
|
+
this.initialSlide = 0;
|
|
2186
|
+
this.enableNavigation = false;
|
|
2187
|
+
this.enablePagination = false;
|
|
2188
|
+
this.enableScrollbar = false;
|
|
2189
|
+
this.enableAutoplay = false;
|
|
2190
|
+
this.enableKeyboard = false;
|
|
2191
|
+
this.enableMousewheel = false;
|
|
2192
|
+
this.enableZoom = false;
|
|
2193
|
+
this.enableGrid = false;
|
|
2194
|
+
this.enableParallax = false;
|
|
2195
|
+
this.autoplayDelay = 3000;
|
|
2196
|
+
this.autoplayPauseOnMouseEnter = true;
|
|
2197
|
+
this.autoplayDisableOnInteraction = false;
|
|
2198
|
+
this.paginationType = 'bullets';
|
|
2199
|
+
this.paginationClickable = true;
|
|
2200
|
+
this.gridRows = 1;
|
|
2201
|
+
/** Advanced escape hatch: raw Swiper `breakpoints` JSON. Merged OVER the map derived from the
|
|
2202
|
+
* node's per-breakpoint traits, so an explicit hand-written value always wins. */
|
|
2203
|
+
this.breakpoints = '';
|
|
2204
|
+
/** The node's per-breakpoint overrides. Only its `inputs` layer is read here — the style layers
|
|
2205
|
+
* are compiled to CSS by the style engine, not handed to Swiper. */
|
|
2206
|
+
this.responsive = null;
|
|
2207
|
+
this._swiper = null;
|
|
2208
|
+
this.instanceRegistry = inject(ComponentInstanceRegistryService);
|
|
2357
2209
|
}
|
|
2210
|
+
ngOnInit() { }
|
|
2358
2211
|
ngAfterViewInit() {
|
|
2359
|
-
if (this.
|
|
2360
|
-
|
|
2212
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
2213
|
+
// Defer so all child slides are rendered first
|
|
2214
|
+
requestAnimationFrame(() => this._initSwiper());
|
|
2361
2215
|
}
|
|
2362
2216
|
}
|
|
2363
2217
|
ngOnDestroy() {
|
|
2364
|
-
this.
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
/** Per-slide inline style. Before the loop activates (server + no-JS + pre-first-frame) the first
|
|
2369
|
-
* image is the visible static frame; once running, the effect owns each slide's look. */
|
|
2370
|
-
slideStyle(i) {
|
|
2371
|
-
if (!this.started) {
|
|
2372
|
-
return i === 0
|
|
2373
|
-
? { opacity: '1', transform: 'scale(1)', transition: 'none' }
|
|
2374
|
-
: { opacity: '0', transform: 'scale(1)', transition: 'none' };
|
|
2218
|
+
if (this._swiper) {
|
|
2219
|
+
this._swiper.destroy(true, true);
|
|
2220
|
+
this._swiper = null;
|
|
2221
|
+
this.instanceRegistry.unregister(this.nodeId);
|
|
2375
2222
|
}
|
|
2376
|
-
return resolveGalleryEffect(this.effect).buildSlideStyle({
|
|
2377
|
-
active: i === this.currentIndex,
|
|
2378
|
-
fadingOut: i === this.fadingOutIndex,
|
|
2379
|
-
firstCycleActive: true,
|
|
2380
|
-
durations: { showMs: this.showDuration, fadeMs: this.fadeDuration },
|
|
2381
|
-
});
|
|
2382
2223
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2224
|
+
async _initSwiper() {
|
|
2225
|
+
const el = this.swiperEl?.nativeElement;
|
|
2226
|
+
if (!el)
|
|
2385
2227
|
return;
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2228
|
+
const [{ default: Swiper }, swiperModules] = await Promise.all([
|
|
2229
|
+
import('swiper'),
|
|
2230
|
+
import('swiper/modules'),
|
|
2231
|
+
]);
|
|
2232
|
+
const { Navigation, Pagination, Scrollbar, Autoplay, Keyboard, Mousewheel, Zoom, Grid, Parallax, EffectFade, EffectCube, EffectFlip, EffectCoverflow, EffectCards, EffectCreative, } = swiperModules;
|
|
2233
|
+
const modules = [];
|
|
2234
|
+
if (this.enableNavigation)
|
|
2235
|
+
modules.push(Navigation);
|
|
2236
|
+
if (this.enablePagination)
|
|
2237
|
+
modules.push(Pagination);
|
|
2238
|
+
if (this.enableScrollbar)
|
|
2239
|
+
modules.push(Scrollbar);
|
|
2240
|
+
if (this.enableAutoplay)
|
|
2241
|
+
modules.push(Autoplay);
|
|
2242
|
+
if (this.enableKeyboard)
|
|
2243
|
+
modules.push(Keyboard);
|
|
2244
|
+
if (this.enableMousewheel)
|
|
2245
|
+
modules.push(Mousewheel);
|
|
2246
|
+
if (this.enableZoom)
|
|
2247
|
+
modules.push(Zoom);
|
|
2248
|
+
if (this.enableGrid)
|
|
2249
|
+
modules.push(Grid);
|
|
2250
|
+
if (this.enableParallax)
|
|
2251
|
+
modules.push(Parallax);
|
|
2252
|
+
const effectMap = {
|
|
2253
|
+
fade: EffectFade, cube: EffectCube, flip: EffectFlip,
|
|
2254
|
+
coverflow: EffectCoverflow, cards: EffectCards, creative: EffectCreative,
|
|
2255
|
+
};
|
|
2256
|
+
if (this.effect !== 'slide' && effectMap[this.effect]) {
|
|
2257
|
+
modules.push(effectMap[this.effect]);
|
|
2258
|
+
}
|
|
2259
|
+
const config = {
|
|
2260
|
+
modules,
|
|
2261
|
+
// Swiper reads the container's `dir` attribute, but pass it explicitly so RTL is applied
|
|
2262
|
+
// on init regardless of attribute timing.
|
|
2263
|
+
dir: this.dir,
|
|
2264
|
+
direction: this.direction,
|
|
2265
|
+
loop: this.loop,
|
|
2266
|
+
slidesPerView: this.slidesPerView,
|
|
2267
|
+
slidesPerGroup: this.slidesPerGroup,
|
|
2268
|
+
spaceBetween: this.spaceBetween,
|
|
2269
|
+
speed: this.speed,
|
|
2270
|
+
effect: this.effect,
|
|
2271
|
+
grabCursor: this.grabCursor,
|
|
2272
|
+
centeredSlides: this.centeredSlides,
|
|
2273
|
+
initialSlide: this.initialSlide,
|
|
2274
|
+
};
|
|
2275
|
+
if (this.enableNavigation)
|
|
2276
|
+
config['navigation'] = { prevEl: '.swiper-button-prev', nextEl: '.swiper-button-next' };
|
|
2277
|
+
if (this.enablePagination)
|
|
2278
|
+
config['pagination'] = { el: '.swiper-pagination', type: this.paginationType, clickable: this.paginationClickable };
|
|
2279
|
+
if (this.enableScrollbar)
|
|
2280
|
+
config['scrollbar'] = { el: '.swiper-scrollbar', draggable: true };
|
|
2281
|
+
if (this.enableAutoplay)
|
|
2282
|
+
config['autoplay'] = { delay: this.autoplayDelay, pauseOnMouseEnter: this.autoplayPauseOnMouseEnter, disableOnInteraction: this.autoplayDisableOnInteraction };
|
|
2283
|
+
if (this.enableGrid && this.gridRows > 1)
|
|
2284
|
+
config['grid'] = { rows: this.gridRows };
|
|
2285
|
+
// Per-breakpoint traits (slidesPerView/slidesPerGroup/spaceBetween) become Swiper's own
|
|
2286
|
+
// mobile-first `breakpoints` map via the SHARED converter — the desktop-first → mobile-first
|
|
2287
|
+
// inversion lives there, tested, not at each call site. The hand-written `breakpoints` JSON
|
|
2288
|
+
// stays as an advanced escape hatch and is merged OVER the derived map.
|
|
2289
|
+
const derived = buildSwiperBreakpoints(this.collectSwiperInputs(), this.responsive);
|
|
2290
|
+
const breakpointMap = mergeSwiperBreakpoints(derived, this.breakpoints);
|
|
2291
|
+
if (breakpointMap)
|
|
2292
|
+
config['breakpoints'] = breakpointMap;
|
|
2293
|
+
this._swiper = new Swiper(el, config);
|
|
2294
|
+
this.instanceRegistry.register(this.nodeId, this._swiper);
|
|
2402
2295
|
}
|
|
2403
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type:
|
|
2404
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type:
|
|
2405
|
-
<div class="
|
|
2406
|
-
<div
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
<div class="
|
|
2296
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientSliderContainerComponent, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2297
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: ClientSliderContainerComponent, isStandalone: false, selector: "iox-slider-container", inputs: { nodeId: "nodeId", dir: "dir", direction: "direction", loop: "loop", slidesPerView: "slidesPerView", slidesPerGroup: "slidesPerGroup", spaceBetween: "spaceBetween", speed: "speed", effect: "effect", grabCursor: "grabCursor", centeredSlides: "centeredSlides", initialSlide: "initialSlide", enableNavigation: "enableNavigation", enablePagination: "enablePagination", enableScrollbar: "enableScrollbar", enableAutoplay: "enableAutoplay", enableKeyboard: "enableKeyboard", enableMousewheel: "enableMousewheel", enableZoom: "enableZoom", enableGrid: "enableGrid", enableParallax: "enableParallax", autoplayDelay: "autoplayDelay", autoplayPauseOnMouseEnter: "autoplayPauseOnMouseEnter", autoplayDisableOnInteraction: "autoplayDisableOnInteraction", paginationType: "paginationType", paginationClickable: "paginationClickable", gridRows: "gridRows", breakpoints: "breakpoints", responsive: "responsive" }, viewQueries: [{ propertyName: "childrenHost", first: true, predicate: ["childrenHost"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "swiperEl", first: true, predicate: ["swiperEl"], descendants: true, static: true }], ngImport: i0, template: `
|
|
2298
|
+
<div class="swiper" [class]="'iox-node-' + nodeId" [attr.dir]="dir" [style.direction]="dir" #swiperEl>
|
|
2299
|
+
<div class="swiper-wrapper">
|
|
2300
|
+
<ng-container #childrenHost></ng-container>
|
|
2301
|
+
</div>
|
|
2302
|
+
<div *ngIf="enableNavigation" class="swiper-button-prev"></div>
|
|
2303
|
+
<div *ngIf="enableNavigation" class="swiper-button-next"></div>
|
|
2304
|
+
<div *ngIf="enablePagination" class="swiper-pagination"></div>
|
|
2305
|
+
<div *ngIf="enableScrollbar" class="swiper-scrollbar"></div>
|
|
2411
2306
|
</div>
|
|
2412
|
-
`, isInline: true, styles: ["
|
|
2307
|
+
`, isInline: true, styles: [".swiper{width:100%;height:100%;box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); }
|
|
2413
2308
|
}
|
|
2414
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type:
|
|
2309
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientSliderContainerComponent, decorators: [{
|
|
2415
2310
|
type: Component,
|
|
2416
|
-
args: [{ selector: 'iox-
|
|
2417
|
-
<div class="
|
|
2418
|
-
<div
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
<div class="
|
|
2311
|
+
args: [{ selector: 'iox-slider-container', template: `
|
|
2312
|
+
<div class="swiper" [class]="'iox-node-' + nodeId" [attr.dir]="dir" [style.direction]="dir" #swiperEl>
|
|
2313
|
+
<div class="swiper-wrapper">
|
|
2314
|
+
<ng-container #childrenHost></ng-container>
|
|
2315
|
+
</div>
|
|
2316
|
+
<div *ngIf="enableNavigation" class="swiper-button-prev"></div>
|
|
2317
|
+
<div *ngIf="enableNavigation" class="swiper-button-next"></div>
|
|
2318
|
+
<div *ngIf="enablePagination" class="swiper-pagination"></div>
|
|
2319
|
+
<div *ngIf="enableScrollbar" class="swiper-scrollbar"></div>
|
|
2423
2320
|
</div>
|
|
2424
|
-
`, standalone: false, styles: ["
|
|
2321
|
+
`, standalone: false, styles: [".swiper{width:100%;height:100%;box-sizing:border-box}\n"] }]
|
|
2425
2322
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
2426
2323
|
type: Inject,
|
|
2427
2324
|
args: [PLATFORM_ID]
|
|
2428
|
-
}] }
|
|
2325
|
+
}] }], propDecorators: { nodeId: [{
|
|
2326
|
+
type: Input
|
|
2327
|
+
}], dir: [{
|
|
2328
|
+
type: Input
|
|
2329
|
+
}], direction: [{
|
|
2330
|
+
type: Input
|
|
2331
|
+
}], loop: [{
|
|
2332
|
+
type: Input
|
|
2333
|
+
}], slidesPerView: [{
|
|
2334
|
+
type: Input
|
|
2335
|
+
}], slidesPerGroup: [{
|
|
2336
|
+
type: Input
|
|
2337
|
+
}], spaceBetween: [{
|
|
2338
|
+
type: Input
|
|
2339
|
+
}], speed: [{
|
|
2429
2340
|
type: Input
|
|
2430
2341
|
}], effect: [{
|
|
2431
2342
|
type: Input
|
|
2432
|
-
}],
|
|
2343
|
+
}], grabCursor: [{
|
|
2433
2344
|
type: Input
|
|
2434
|
-
}],
|
|
2345
|
+
}], centeredSlides: [{
|
|
2435
2346
|
type: Input
|
|
2436
|
-
}],
|
|
2347
|
+
}], initialSlide: [{
|
|
2437
2348
|
type: Input
|
|
2438
|
-
}],
|
|
2349
|
+
}], enableNavigation: [{
|
|
2439
2350
|
type: Input
|
|
2440
|
-
}],
|
|
2351
|
+
}], enablePagination: [{
|
|
2352
|
+
type: Input
|
|
2353
|
+
}], enableScrollbar: [{
|
|
2354
|
+
type: Input
|
|
2355
|
+
}], enableAutoplay: [{
|
|
2356
|
+
type: Input
|
|
2357
|
+
}], enableKeyboard: [{
|
|
2358
|
+
type: Input
|
|
2359
|
+
}], enableMousewheel: [{
|
|
2360
|
+
type: Input
|
|
2361
|
+
}], enableZoom: [{
|
|
2362
|
+
type: Input
|
|
2363
|
+
}], enableGrid: [{
|
|
2364
|
+
type: Input
|
|
2365
|
+
}], enableParallax: [{
|
|
2366
|
+
type: Input
|
|
2367
|
+
}], autoplayDelay: [{
|
|
2368
|
+
type: Input
|
|
2369
|
+
}], autoplayPauseOnMouseEnter: [{
|
|
2370
|
+
type: Input
|
|
2371
|
+
}], autoplayDisableOnInteraction: [{
|
|
2372
|
+
type: Input
|
|
2373
|
+
}], paginationType: [{
|
|
2374
|
+
type: Input
|
|
2375
|
+
}], paginationClickable: [{
|
|
2376
|
+
type: Input
|
|
2377
|
+
}], gridRows: [{
|
|
2378
|
+
type: Input
|
|
2379
|
+
}], breakpoints: [{
|
|
2380
|
+
type: Input
|
|
2381
|
+
}], responsive: [{
|
|
2382
|
+
type: Input
|
|
2383
|
+
}], childrenHost: [{
|
|
2384
|
+
type: ViewChild,
|
|
2385
|
+
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
2386
|
+
}], swiperEl: [{
|
|
2387
|
+
type: ViewChild,
|
|
2388
|
+
args: ['swiperEl', { static: true }]
|
|
2389
|
+
}] } });
|
|
2390
|
+
|
|
2391
|
+
/**
|
|
2392
|
+
* Client-engine slide. The HOST element (`iox-slider-slide`) itself carries the
|
|
2393
|
+
* `swiper-slide` class so it is a DIRECT child of `.swiper-wrapper` — Swiper only
|
|
2394
|
+
* recognises slides that are direct children (the `.swiper-slide` direct-child
|
|
2395
|
+
* invariant; see iox-ai-guidance architecture/builder/slider.md). A wrapper div
|
|
2396
|
+
* would nest `.swiper-slide` one level too deep and Swiper would ignore the slides.
|
|
2397
|
+
*
|
|
2398
|
+
* The shared `iox-node-{id}` class is applied to the same host so the page-level
|
|
2399
|
+
* CSS rules match (parity with the string renderer / builder).
|
|
2400
|
+
*/
|
|
2401
|
+
class ClientSliderSlideComponent {
|
|
2402
|
+
constructor() {
|
|
2403
|
+
this.nodeId = '';
|
|
2404
|
+
}
|
|
2405
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientSliderSlideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2406
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: ClientSliderSlideComponent, isStandalone: false, selector: "iox-slider-slide", inputs: { nodeId: "nodeId" }, host: { properties: { "class": "'swiper-slide iox-node-' + nodeId" } }, viewQueries: [{ propertyName: "childrenHost", first: true, predicate: ["childrenHost"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: `<ng-container #childrenHost></ng-container>`, isInline: true, styles: [":host{display:block;box-sizing:border-box}\n"] }); }
|
|
2407
|
+
}
|
|
2408
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientSliderSlideComponent, decorators: [{
|
|
2409
|
+
type: Component,
|
|
2410
|
+
args: [{ selector: 'iox-slider-slide', template: `<ng-container #childrenHost></ng-container>`, host: { '[class]': `'swiper-slide iox-node-' + nodeId` }, standalone: false, styles: [":host{display:block;box-sizing:border-box}\n"] }]
|
|
2411
|
+
}], propDecorators: { nodeId: [{
|
|
2412
|
+
type: Input
|
|
2413
|
+
}], childrenHost: [{
|
|
2414
|
+
type: ViewChild,
|
|
2415
|
+
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
2416
|
+
}] } });
|
|
2417
|
+
|
|
2418
|
+
/**
|
|
2419
|
+
* Directive-host List: the real `<ul>`/`<ol>` IS the host — no `iox-list` wrapper, no
|
|
2420
|
+
* inner/outer split. The tag is chosen by whoever CREATES the component (engine passes
|
|
2421
|
+
* `hostElement: document.createElement(htmlTag)`), NOT an internal `*ngIf`. Children render
|
|
2422
|
+
* into the host via `childrenHost`. `start`/`reversed` are host attrs (only meaningful on `<ol>`).
|
|
2423
|
+
*/
|
|
2424
|
+
class ClientListComponent {
|
|
2425
|
+
constructor() {
|
|
2426
|
+
this.htmlTag = 'ul';
|
|
2427
|
+
this.start = '';
|
|
2428
|
+
this.reversed = false;
|
|
2429
|
+
this.nodeId = '';
|
|
2430
|
+
}
|
|
2431
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2432
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: ClientListComponent, isStandalone: false, selector: "ul[iox-list],ol[iox-list]", inputs: { htmlTag: "htmlTag", start: "start", reversed: "reversed", nodeId: "nodeId" }, host: { properties: { "class": "'iox-node-' + nodeId", "attr.start": "htmlTag === 'ol' && start !== '' && start != null ? start : null", "attr.reversed": "htmlTag === 'ol' && reversed ? '' : null" } }, viewQueries: [{ propertyName: "childrenHost", first: true, predicate: ["childrenHost"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: `<ng-container #childrenHost></ng-container>`, isInline: true, styles: [":host{margin:0;box-sizing:border-box}\n"] }); }
|
|
2433
|
+
}
|
|
2434
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientListComponent, decorators: [{
|
|
2435
|
+
type: Component,
|
|
2436
|
+
args: [{ selector: 'ul[iox-list],ol[iox-list]', template: `<ng-container #childrenHost></ng-container>`, host: {
|
|
2437
|
+
'[class]': "'iox-node-' + nodeId",
|
|
2438
|
+
'[attr.start]': "htmlTag === 'ol' && start !== '' && start != null ? start : null",
|
|
2439
|
+
'[attr.reversed]': "htmlTag === 'ol' && reversed ? '' : null",
|
|
2440
|
+
}, standalone: false, styles: [":host{margin:0;box-sizing:border-box}\n"] }]
|
|
2441
|
+
}], propDecorators: { htmlTag: [{
|
|
2442
|
+
type: Input
|
|
2443
|
+
}], start: [{
|
|
2444
|
+
type: Input
|
|
2445
|
+
}], reversed: [{
|
|
2446
|
+
type: Input
|
|
2447
|
+
}], nodeId: [{
|
|
2448
|
+
type: Input
|
|
2449
|
+
}], childrenHost: [{
|
|
2450
|
+
type: ViewChild,
|
|
2451
|
+
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
2452
|
+
}] } });
|
|
2453
|
+
|
|
2454
|
+
/**
|
|
2455
|
+
* Directive-host List item: the real `<li>` IS the host — no `iox-list-item` wrapper, no
|
|
2456
|
+
* inner/outer split. Children render into the `<li>` via `childrenHost`. Fixed tag (`<li>`),
|
|
2457
|
+
* so the engine creates it with `vcr.createComponent` (no `hostElement` needed).
|
|
2458
|
+
*/
|
|
2459
|
+
class ClientListItemComponent {
|
|
2460
|
+
constructor() {
|
|
2461
|
+
this.nodeId = '';
|
|
2462
|
+
}
|
|
2463
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2464
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: ClientListItemComponent, isStandalone: false, selector: "li[iox-list-item]", inputs: { nodeId: "nodeId" }, host: { properties: { "class": "'iox-node-' + nodeId" } }, viewQueries: [{ propertyName: "childrenHost", first: true, predicate: ["childrenHost"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: `<ng-container #childrenHost></ng-container>`, isInline: true, styles: [":host{box-sizing:border-box}\n"] }); }
|
|
2465
|
+
}
|
|
2466
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientListItemComponent, decorators: [{
|
|
2467
|
+
type: Component,
|
|
2468
|
+
args: [{ selector: 'li[iox-list-item]', template: `<ng-container #childrenHost></ng-container>`, host: { '[class]': "'iox-node-' + nodeId" }, standalone: false, styles: [":host{box-sizing:border-box}\n"] }]
|
|
2469
|
+
}], propDecorators: { nodeId: [{
|
|
2441
2470
|
type: Input
|
|
2471
|
+
}], childrenHost: [{
|
|
2472
|
+
type: ViewChild,
|
|
2473
|
+
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
2442
2474
|
}] } });
|
|
2443
2475
|
|
|
2444
|
-
const COMPONENTS = [
|
|
2445
|
-
BuilderImageComponent,
|
|
2446
|
-
BuilderSpacerComponent,
|
|
2447
|
-
CardComponent,
|
|
2448
|
-
BuilderDividerComponent,
|
|
2449
|
-
BuilderIconComponent,
|
|
2450
|
-
BuilderLinkComponent,
|
|
2451
|
-
BuilderButtonComponent,
|
|
2452
|
-
BuilderHeadingComponent,
|
|
2453
|
-
TextBlockComponent,
|
|
2454
|
-
SectionComponent,
|
|
2455
|
-
ContainerComponent,
|
|
2456
|
-
LinkedContainerComponent,
|
|
2457
|
-
ClientRepeaterComponent,
|
|
2458
|
-
ButtonBlockComponent,
|
|
2459
|
-
ClientSliderContainerComponent,
|
|
2460
|
-
ClientSliderSlideComponent,
|
|
2461
|
-
ClientListComponent,
|
|
2462
|
-
ClientListItemComponent,
|
|
2463
|
-
ClientGalleryComponent,
|
|
2464
|
-
];
|
|
2465
|
-
class IoxBuilderComponentsModule {
|
|
2466
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2467
|
-
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, declarations: [BuilderImageComponent,
|
|
2468
|
-
BuilderSpacerComponent,
|
|
2469
|
-
CardComponent,
|
|
2470
|
-
BuilderDividerComponent,
|
|
2471
|
-
BuilderIconComponent,
|
|
2472
|
-
BuilderLinkComponent,
|
|
2473
|
-
BuilderButtonComponent,
|
|
2474
|
-
BuilderHeadingComponent,
|
|
2475
|
-
TextBlockComponent,
|
|
2476
|
-
SectionComponent,
|
|
2477
|
-
ContainerComponent,
|
|
2478
|
-
LinkedContainerComponent,
|
|
2479
|
-
ClientRepeaterComponent,
|
|
2480
|
-
ButtonBlockComponent,
|
|
2481
|
-
ClientSliderContainerComponent,
|
|
2482
|
-
ClientSliderSlideComponent,
|
|
2483
|
-
ClientListComponent,
|
|
2484
|
-
ClientListItemComponent,
|
|
2485
|
-
ClientGalleryComponent], imports: [CommonModule], exports: [BuilderImageComponent,
|
|
2486
|
-
BuilderSpacerComponent,
|
|
2487
|
-
CardComponent,
|
|
2488
|
-
BuilderDividerComponent,
|
|
2489
|
-
BuilderIconComponent,
|
|
2490
|
-
BuilderLinkComponent,
|
|
2491
|
-
BuilderButtonComponent,
|
|
2492
|
-
BuilderHeadingComponent,
|
|
2493
|
-
TextBlockComponent,
|
|
2494
|
-
SectionComponent,
|
|
2495
|
-
ContainerComponent,
|
|
2496
|
-
LinkedContainerComponent,
|
|
2497
|
-
ClientRepeaterComponent,
|
|
2498
|
-
ButtonBlockComponent,
|
|
2499
|
-
ClientSliderContainerComponent,
|
|
2500
|
-
ClientSliderSlideComponent,
|
|
2501
|
-
ClientListComponent,
|
|
2502
|
-
ClientListItemComponent,
|
|
2503
|
-
ClientGalleryComponent] }); }
|
|
2504
|
-
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, imports: [CommonModule] }); }
|
|
2505
|
-
}
|
|
2506
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, decorators: [{
|
|
2507
|
-
type: NgModule,
|
|
2508
|
-
args: [{
|
|
2509
|
-
declarations: COMPONENTS,
|
|
2510
|
-
imports: [CommonModule],
|
|
2511
|
-
exports: COMPONENTS,
|
|
2512
|
-
}]
|
|
2513
|
-
}] });
|
|
2514
|
-
|
|
2515
2476
|
/**
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
2477
|
+
* Shared data-source resolving pattern — the SINGLE source of truth for how an IoxDataSource
|
|
2478
|
+
* becomes a backend fetch, consumed by BOTH the page builder (iox-cms-client bindings panel) and
|
|
2479
|
+
* the SSR client engine (iox-client-engine DataSourceResolverService). There must be no second
|
|
2480
|
+
* copy of this decision logic; only the transport differs between the two:
|
|
2519
2481
|
*
|
|
2520
|
-
*
|
|
2521
|
-
*
|
|
2522
|
-
*
|
|
2523
|
-
*
|
|
2524
|
-
*
|
|
2482
|
+
* builder → authenticated org-scoped controllers (admin session):
|
|
2483
|
+
* single by id → GET admin.../resources/:type/:id (ContentService.getContentById)
|
|
2484
|
+
* collection → GET admin.../resources/:type (ContentService.getContent)
|
|
2485
|
+
* engine → @Public() controllers (no auth, isPublished filter applied server-side):
|
|
2486
|
+
* single by id → GET api/:type/:id (UserResourcesController)
|
|
2487
|
+
* collection → GET api/:type?filters (UserResourcesController)
|
|
2525
2488
|
*
|
|
2526
|
-
*
|
|
2527
|
-
* for
|
|
2489
|
+
* The DECISION (by-id vs collection, which id, which filters, how to pick the single result) is
|
|
2490
|
+
* identical for both — that's what this module owns. Each app maps the returned {@link DataSourcePlan}
|
|
2491
|
+
* onto its own transport. A single item is ALWAYS fetched by its id as a path segment — never a
|
|
2492
|
+
* `?_id=` query — to match the builder's getContentById.
|
|
2528
2493
|
*/
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
['
|
|
2542
|
-
['Container', ContainerComponent],
|
|
2543
|
-
['LinkedContainer', LinkedContainerComponent],
|
|
2544
|
-
['Repeater', ClientRepeaterComponent],
|
|
2545
|
-
['ButtonBlock', ButtonBlockComponent],
|
|
2546
|
-
['SliderContainer', ClientSliderContainerComponent],
|
|
2547
|
-
['SliderSlide', ClientSliderSlideComponent],
|
|
2548
|
-
['List', ClientListComponent],
|
|
2549
|
-
['ListItem', ClientListItemComponent],
|
|
2550
|
-
['Gallery', ClientGalleryComponent],
|
|
2551
|
-
]);
|
|
2494
|
+
/**
|
|
2495
|
+
* Match a page route template against an actual path and extract named params.
|
|
2496
|
+
* matchRouteParams('/project/:projectId', 'project/68abc') → { projectId: '68abc' }
|
|
2497
|
+
* Both sides are normalised (leading slash stripped) and split on '/'; values are URL-decoded.
|
|
2498
|
+
*/
|
|
2499
|
+
function matchRouteParams(template, actual) {
|
|
2500
|
+
const t = (template ?? '').replace(/^\/+/, '').split('/');
|
|
2501
|
+
const a = (actual ?? '').replace(/^\/+/, '').split('/');
|
|
2502
|
+
const params = {};
|
|
2503
|
+
for (let i = 0; i < t.length; i++) {
|
|
2504
|
+
const seg = t[i];
|
|
2505
|
+
if (seg && seg.startsWith(':'))
|
|
2506
|
+
params[seg.slice(1)] = decodeURIComponent(a[i] ?? '');
|
|
2552
2507
|
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2508
|
+
return params;
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Build the query filter for a data-source request from its filterByRouteParam / filterByQueryParams
|
|
2512
|
+
* config. `field` is filtered by the route-param value (falling back to `defaultId`); query filters
|
|
2513
|
+
* map each `field` to a query-param value (falling back to `defaultValue`), and a filter with NO
|
|
2514
|
+
* `queryParam` is a constant on `defaultValue`. Empty values are omitted so the source isn't
|
|
2515
|
+
* accidentally filtered to nothing.
|
|
2516
|
+
*/
|
|
2517
|
+
function buildDataSourceFilter(req, routeParams = {}, queryParams = {}) {
|
|
2518
|
+
const params = {};
|
|
2519
|
+
const byRoute = req?.filterByRouteParam;
|
|
2520
|
+
if (byRoute?.field && byRoute?.routeParam) {
|
|
2521
|
+
const value = routeParams[byRoute.routeParam] ?? byRoute.defaultId;
|
|
2522
|
+
if (value !== undefined && value !== null && value !== '')
|
|
2523
|
+
params[byRoute.field] = value;
|
|
2555
2524
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2525
|
+
for (const q of req?.filterByQueryParams ?? []) {
|
|
2526
|
+
if (!q?.field)
|
|
2527
|
+
continue;
|
|
2528
|
+
// No queryParam → a CONSTANT filter: defaultValue is the value, in the builder preview and
|
|
2529
|
+
// on the live site alike. With a queryParam the URL wins and defaultValue is the fallback.
|
|
2530
|
+
const value = q.queryParam ? (queryParams[q.queryParam] ?? q.defaultValue) : q.defaultValue;
|
|
2531
|
+
if (value !== undefined && value !== null && value !== '')
|
|
2532
|
+
params[q.field] = value;
|
|
2533
|
+
}
|
|
2534
|
+
return params;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Mirrors the builder's getContentById decision. When a SINGLE source is filtered by the item id
|
|
2538
|
+
* (`_id`) via a route param, the item is fetched by PATH (`.../:type/:id`), not a `?_id=` query.
|
|
2539
|
+
* Returns that id (route-param value, else `defaultId`), or `null` when the source isn't a
|
|
2540
|
+
* single-by-id fetch — in which case the caller uses the collection endpoint + filters. The
|
|
2541
|
+
* path-based form only applies to the id field; a single source filtered by any other field still
|
|
2542
|
+
* goes through the collection endpoint.
|
|
2543
|
+
*/
|
|
2544
|
+
function resolveSingleItemId(req, routeParams = {}) {
|
|
2545
|
+
const byRoute = req?.filterByRouteParam;
|
|
2546
|
+
if (!byRoute?.routeParam)
|
|
2547
|
+
return null;
|
|
2548
|
+
if (byRoute.field && byRoute.field !== '_id')
|
|
2549
|
+
return null;
|
|
2550
|
+
const value = routeParams[byRoute.routeParam] ?? byRoute.defaultId;
|
|
2551
|
+
return value !== undefined && value !== null && value !== '' ? String(value) : null;
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Resolve an IoxDataSource into a transport-agnostic {@link DataSourcePlan}. This is the shared
|
|
2555
|
+
* decision both the builder and the SSR engine run; each then executes the plan with its own
|
|
2556
|
+
* HTTP layer. Single sources with a route-param id resolve to a `by-id` (path) fetch — exactly like
|
|
2557
|
+
* the builder's getContentById.
|
|
2558
|
+
*/
|
|
2559
|
+
function planDataSource(ds, routeParams = {}, queryParams = {}) {
|
|
2560
|
+
const mode = ds?.mode === 'single' ? 'single' : 'list';
|
|
2561
|
+
switch (ds?.type) {
|
|
2562
|
+
case 'cms':
|
|
2563
|
+
case 'shop': {
|
|
2564
|
+
const domain = ds.type;
|
|
2565
|
+
const req = domain === 'cms' ? ds.request?.cms : ds.request?.shop;
|
|
2566
|
+
const resource = req?.contentType;
|
|
2567
|
+
if (!resource)
|
|
2568
|
+
return { kind: 'none' };
|
|
2569
|
+
const id = mode === 'single' ? resolveSingleItemId(req, routeParams) : null;
|
|
2570
|
+
if (id)
|
|
2571
|
+
return { kind: 'by-id', domain, resource, id, mode };
|
|
2572
|
+
const filter = buildDataSourceFilter(req, routeParams, queryParams);
|
|
2573
|
+
const plan = { kind: 'collection', domain, resource, filter, mode };
|
|
2574
|
+
if (req?.limit)
|
|
2575
|
+
plan.limit = req.limit;
|
|
2576
|
+
return plan;
|
|
2577
|
+
}
|
|
2578
|
+
case 'external-api': {
|
|
2579
|
+
const endpoint = ds.request?.endpoint;
|
|
2580
|
+
return endpoint ? { kind: 'external', endpoint, mode } : { kind: 'none' };
|
|
2581
|
+
}
|
|
2582
|
+
case 'static':
|
|
2583
|
+
return { kind: 'static', data: ds.request?.data ?? null, mode };
|
|
2584
|
+
case 'derived': {
|
|
2585
|
+
const d = ds.request?.derived;
|
|
2586
|
+
return d?.from ? { kind: 'derived', from: d.from, path: d.path ?? '', mode } : { kind: 'none' };
|
|
2587
|
+
}
|
|
2588
|
+
case 'variable':
|
|
2589
|
+
// A VARIABLE is a page-local resource whose value is WRITTEN at runtime by an interaction
|
|
2590
|
+
// (setContext), never fetched. It resolves to nothing here on purpose: the page renders
|
|
2591
|
+
// its empty state until something writes it. `request.variable.itemsFrom` only names the
|
|
2592
|
+
// source whose items it holds, so the bind picker can suggest fields.
|
|
2593
|
+
return { kind: 'none' };
|
|
2594
|
+
default:
|
|
2595
|
+
return { kind: 'none' };
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
/**
|
|
2599
|
+
* The aliases on a page that are VARIABLES — runtime-written resources, not fetched data.
|
|
2600
|
+
*
|
|
2601
|
+
* Shared so the builder and the SSR engine agree on which aliases start empty and are filled by
|
|
2602
|
+
* interactions. See element-context-state-plan.md.
|
|
2603
|
+
*/
|
|
2604
|
+
function variableAliases(dataSources) {
|
|
2605
|
+
const out = new Set();
|
|
2606
|
+
for (const ds of dataSources ?? []) {
|
|
2607
|
+
if (ds?.type === 'variable' && ds.alias)
|
|
2608
|
+
out.add(ds.alias);
|
|
2609
|
+
}
|
|
2610
|
+
return out;
|
|
2611
|
+
}
|
|
2612
|
+
/** The source alias whose items a variable holds, if it declares one (for field suggestions). */
|
|
2613
|
+
function variableItemsFrom(dataSources, alias) {
|
|
2614
|
+
for (const ds of dataSources ?? []) {
|
|
2615
|
+
if (ds?.type === 'variable' && ds.alias === alias)
|
|
2616
|
+
return ds.request?.variable?.itemsFrom || undefined;
|
|
2617
|
+
}
|
|
2618
|
+
return undefined;
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Normalise a collection response into the shape a source expects: unwraps `{items}`/`{data}`
|
|
2622
|
+
* envelopes, then returns the first item for a `single` source or the whole array for a `list`.
|
|
2623
|
+
* A `by-id` response is already the item, so callers pass it through unchanged.
|
|
2624
|
+
*/
|
|
2625
|
+
function normalizeCollectionResult(mode, data) {
|
|
2626
|
+
const items = Array.isArray(data) ? data : (data?.items ?? data?.data ?? []);
|
|
2627
|
+
return mode === 'single' ? (items[0] ?? null) : items;
|
|
2628
|
+
}
|
|
2629
|
+
/** Navigate a dot/bracket path into an object, e.g. `gallery`, `details.images`, `variants[0].skus`.
|
|
2630
|
+
* Returns undefined if any segment is missing. Kept minimal + self-contained so it is shareable by
|
|
2631
|
+
* both the builder and the SSR engine (the two have divergent local `resolvePath` copies). */
|
|
2632
|
+
function getByPath(obj, path) {
|
|
2633
|
+
if (obj == null || !path)
|
|
2634
|
+
return obj;
|
|
2635
|
+
// Turn `a[0].b` into ['a','0','b'].
|
|
2636
|
+
const segments = path.replace(/\[(\w+)\]/g, '.$1').split('.').filter(Boolean);
|
|
2637
|
+
let cur = obj;
|
|
2638
|
+
for (const seg of segments) {
|
|
2639
|
+
if (cur == null)
|
|
2640
|
+
return undefined;
|
|
2641
|
+
cur = cur[seg];
|
|
2558
2642
|
}
|
|
2559
|
-
|
|
2560
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxComponentRegistryService, providedIn: 'root' }); }
|
|
2643
|
+
return cur;
|
|
2561
2644
|
}
|
|
2562
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxComponentRegistryService, decorators: [{
|
|
2563
|
-
type: Injectable,
|
|
2564
|
-
args: [{ providedIn: 'root' }]
|
|
2565
|
-
}] });
|
|
2566
|
-
|
|
2567
2645
|
/**
|
|
2568
|
-
*
|
|
2569
|
-
*
|
|
2570
|
-
*
|
|
2571
|
-
* -
|
|
2572
|
-
*
|
|
2573
|
-
*
|
|
2574
|
-
* Lives in @vectoriox/iox-ui because BOTH consumers depend on iox-ui (the engine does
|
|
2575
|
-
* NOT depend on iox-builder). Keeping it here — not duplicated per renderer — is what
|
|
2576
|
-
* prevents the builder and the live site from drifting.
|
|
2577
|
-
* See iox-ai-guidance `architecture/builder/render-parity.md`.
|
|
2646
|
+
* The array a Repeater (or a repeating Slider) iterates over — the SHARED decision so the builder
|
|
2647
|
+
* canvas and the SSR engine agree (see data-source-resolving.md).
|
|
2648
|
+
* - No `sourcePath`: the resolved source IS the list. An array passes through; a lone object degrades
|
|
2649
|
+
* to a single-row list (back-compat with the old `fetchItems`); null/undefined → `[]`.
|
|
2650
|
+
* - With `sourcePath`: the source resolved to a SINGLE resource (e.g. a product); dive into the
|
|
2651
|
+
* nested array property (e.g. `gallery`). Non-array / missing → `[]`.
|
|
2578
2652
|
*/
|
|
2579
2653
|
/**
|
|
2580
|
-
*
|
|
2581
|
-
*
|
|
2582
|
-
*
|
|
2583
|
-
*
|
|
2584
|
-
* viewport-state subscription). In production those variables are undefined, so the
|
|
2585
|
-
* `1vh` / `1vw` fallbacks restore native browser behaviour.
|
|
2654
|
+
* Fill in `derived` aliases in an already-resolved map. Given the data sources and a map of resolved
|
|
2655
|
+
* NON-derived values (by alias), compute each `type:'derived'` alias as `getByPath(parent, path)` in
|
|
2656
|
+
* dependency order (chains resolved by iterating until stable). Mutates + returns the map. Used by the
|
|
2657
|
+
* SSR engine after it resolves the root sources; the builder resolves derived lazily per alias.
|
|
2586
2658
|
*/
|
|
2587
|
-
function
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2659
|
+
function resolveDerivedInto(dataSources, resolved) {
|
|
2660
|
+
const derived = (dataSources ?? []).filter(d => d?.type === 'derived' && !!d.alias);
|
|
2661
|
+
let changed = true, guard = 0;
|
|
2662
|
+
while (changed && guard++ < 25) {
|
|
2663
|
+
changed = false;
|
|
2664
|
+
for (const d of derived) {
|
|
2665
|
+
if (d.alias in resolved)
|
|
2666
|
+
continue;
|
|
2667
|
+
const from = d.request?.derived?.from;
|
|
2668
|
+
if (from && from in resolved) {
|
|
2669
|
+
resolved[d.alias] = getByPath(resolved[from], d.request.derived.path ?? '');
|
|
2670
|
+
changed = true;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return resolved;
|
|
2591
2675
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2676
|
+
function resolveRepeaterItems(resolved, sourcePath) {
|
|
2677
|
+
if (sourcePath) {
|
|
2678
|
+
const nested = getByPath(resolved, sourcePath);
|
|
2679
|
+
return Array.isArray(nested) ? nested : [];
|
|
2680
|
+
}
|
|
2681
|
+
if (Array.isArray(resolved))
|
|
2682
|
+
return resolved;
|
|
2683
|
+
return resolved == null ? [] : [resolved];
|
|
2684
|
+
}
|
|
2685
|
+
// ── Referenced data sources (shared: builder-save + SSR resolve) ─────────────────────────────────
|
|
2686
|
+
// Which data-source aliases a node tree uses, so a consumer resolves ONLY what a page/global needs
|
|
2687
|
+
// (not the whole org library). Used by cms-client (persist globals' sources) and the SSR engine
|
|
2688
|
+
// (resolve the referenced subset per page). See org-data-sources-plan.md.
|
|
2689
|
+
/** Aliases referenced anywhere in a node tree: a repeater/slider `source` (a `bindings` entry with
|
|
2690
|
+
* `prop === 'source'`, or `props.source` for the slider) plus every binding's `source`. */
|
|
2691
|
+
function collectReferencedAliases(nodes, acc = new Set()) {
|
|
2692
|
+
for (const n of nodes ?? []) {
|
|
2693
|
+
for (const b of n?.bindings ?? []) {
|
|
2694
|
+
if (b?.source)
|
|
2695
|
+
acc.add(b.source);
|
|
2696
|
+
}
|
|
2697
|
+
if (n?.props?.source)
|
|
2698
|
+
acc.add(n.props.source);
|
|
2699
|
+
if (n?.children?.length)
|
|
2700
|
+
collectReferencedAliases(n.children, acc);
|
|
2701
|
+
}
|
|
2702
|
+
return acc;
|
|
2703
|
+
}
|
|
2704
|
+
/** The subset of `all` whose `alias` is referenced by `nodes` (shallow — direct references only). */
|
|
2705
|
+
function referencedDataSources(nodes, all) {
|
|
2706
|
+
const aliases = collectReferencedAliases(nodes);
|
|
2707
|
+
return (all ?? []).filter(ds => aliases.has(ds?.alias));
|
|
2708
|
+
}
|
|
2709
|
+
/** Like `referencedDataSources`, but also pulls in a referenced source's DEPENDENCIES (transitively),
|
|
2710
|
+
* none of which the layout names directly:
|
|
2711
|
+
* - a `derived` alias needs its `request.derived.from` parent;
|
|
2712
|
+
* - a `relative` (route-context) alias needs its `request.relative.subjectFrom` (the current item)
|
|
2713
|
+
* and `request.relative.from` (the collection it navigates).
|
|
2714
|
+
* Use this when RESOLVING (the engine) so derived views and siblings/related don't resolve to nothing. */
|
|
2715
|
+
function referencedDataSourcesDeep(nodes, all) {
|
|
2716
|
+
const byAlias = new Map((all ?? []).map(d => [d?.alias, d]));
|
|
2717
|
+
const wanted = collectReferencedAliases(nodes);
|
|
2718
|
+
const queue = [...wanted];
|
|
2719
|
+
const enqueue = (alias) => {
|
|
2720
|
+
if (alias && !wanted.has(alias)) {
|
|
2721
|
+
wanted.add(alias);
|
|
2722
|
+
queue.push(alias);
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
while (queue.length) {
|
|
2726
|
+
const ds = byAlias.get(queue.pop());
|
|
2727
|
+
enqueue(ds?.request?.derived?.from);
|
|
2728
|
+
enqueue(ds?.request?.relative?.subjectFrom);
|
|
2729
|
+
enqueue(ds?.request?.relative?.from);
|
|
2730
|
+
}
|
|
2731
|
+
return (all ?? []).filter(d => wanted.has(d?.alias));
|
|
2595
2732
|
}
|
|
2596
2733
|
/**
|
|
2597
|
-
*
|
|
2598
|
-
*
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2602
|
-
* Deliberately EXCLUDED, because a value equal to the initial can still be meaningful there:
|
|
2603
|
-
* - inherited properties (line-height, color, font-family/size/weight, text-align, letter-spacing…)
|
|
2604
|
-
* — the value may override an inherited ancestor value.
|
|
2605
|
-
* - margin / padding — UA stylesheets set them on h1–h6, p, ul, so `0` depends on a reset.
|
|
2606
|
-
* - border-* — the initial `medium` width + interaction with border-style makes it non-trivial.
|
|
2607
|
-
*/
|
|
2608
|
-
const NOOP_INITIAL_VALUES = {
|
|
2609
|
-
'width': ['auto'], 'height': ['auto'],
|
|
2610
|
-
'min-width': ['auto'], 'min-height': ['auto'],
|
|
2611
|
-
'max-width': ['none', 'auto'], 'max-height': ['none', 'auto'], // `auto` is invalid for max-* → inert
|
|
2612
|
-
'position': ['static'],
|
|
2613
|
-
'top': ['auto'], 'right': ['auto'], 'bottom': ['auto'], 'left': ['auto'],
|
|
2614
|
-
'opacity': ['1'], 'z-index': ['auto'],
|
|
2615
|
-
'flex-grow': ['0'], 'flex-shrink': ['1'], 'flex-basis': ['auto'],
|
|
2616
|
-
'flex-direction': ['row'], 'flex-wrap': ['nowrap'],
|
|
2617
|
-
'gap': ['0', '0px', 'normal'],
|
|
2618
|
-
'border-radius': ['0', '0px'],
|
|
2619
|
-
'aspect-ratio': ['auto'],
|
|
2620
|
-
'mix-blend-mode': ['normal'],
|
|
2621
|
-
'transform': ['none'], 'box-shadow': ['none'],
|
|
2622
|
-
// overflow initial is `visible`. A self-describing node serialises the Overflow trait's default,
|
|
2623
|
-
// so WITHOUT this the emitted `overflow: visible` would override component baselines that clip
|
|
2624
|
-
// (e.g. `.iox-card__img-wrap`, `.iox-slider`, `.iox-gallery` all set `overflow: hidden`), breaking
|
|
2625
|
-
// object-fit/aspect-ratio image clipping everywhere. A user-set hidden/scroll/auto/clip is not the
|
|
2626
|
-
// initial, so it still emits and wins.
|
|
2627
|
-
'overflow': ['visible'], 'overflow-x': ['visible'], 'overflow-y': ['visible'],
|
|
2628
|
-
'clip-path': ['none'],
|
|
2629
|
-
};
|
|
2630
|
-
/**
|
|
2631
|
-
* Style props a type's render-default fully REPLACES. Emitting them would produce a dead
|
|
2632
|
-
* declaration that is immediately overridden in the same rule (e.g. an Icon's serialised
|
|
2633
|
-
* `lineHeight: 1.5` followed by the forced `line-height: 1`), so callers strip them first via
|
|
2634
|
-
* {@link stripSuppressedProps} and let renderDefaultDeclarations supply the real value.
|
|
2734
|
+
* Turn a resolved {@link DataSourcePlan} into the concrete public HTTP request (url + params) — the
|
|
2735
|
+
* SINGLE place that decides the URL shape, shared by the builder binding preview and the SSR engine
|
|
2736
|
+
* so both hit the SAME public endpoints (no admin, no duplication). Returns `null` for `static` /
|
|
2737
|
+
* `none` plans (no HTTP call). A single item is a PATH segment (`/:resource/:id`), never `?_id=`.
|
|
2635
2738
|
*/
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2739
|
+
function dataSourcePlanToRequest(plan, endpoints) {
|
|
2740
|
+
const baseFor = (domain) => (domain === 'shop' ? endpoints.shopBase : endpoints.cmsBase);
|
|
2741
|
+
switch (plan.kind) {
|
|
2742
|
+
case 'by-id':
|
|
2743
|
+
return { url: `${baseFor(plan.domain)}${plan.resource}/${encodeURIComponent(plan.id)}`, params: {} };
|
|
2744
|
+
case 'collection': {
|
|
2745
|
+
const params = { ...plan.filter };
|
|
2746
|
+
if (plan.limit)
|
|
2747
|
+
params['limit'] = plan.limit;
|
|
2748
|
+
return { url: `${baseFor(plan.domain)}${plan.resource}`, params };
|
|
2749
|
+
}
|
|
2750
|
+
case 'external':
|
|
2751
|
+
return { url: plan.endpoint, params: {} };
|
|
2752
|
+
default:
|
|
2753
|
+
return null;
|
|
2754
|
+
}
|
|
2650
2755
|
}
|
|
2756
|
+
|
|
2651
2757
|
/**
|
|
2652
|
-
*
|
|
2758
|
+
* Gallery — data model + the SHARED "what are the image URLs" resolver.
|
|
2653
2759
|
*
|
|
2654
|
-
*
|
|
2655
|
-
*
|
|
2656
|
-
*
|
|
2657
|
-
*
|
|
2760
|
+
* A Gallery is populated one of two ways (see gallery-component-plan.md):
|
|
2761
|
+
* - **Manual**: an ordered `GalleryImage[]` trait. Each item is a raw/filebox URL OR a per-item
|
|
2762
|
+
* binding path resolved against the item/route context.
|
|
2763
|
+
* - **Bound**: the whole gallery repeats over a data source (a collection, or an array property
|
|
2764
|
+
* inside one resource via `sourcePath`) — the items come from the shared `resolveRepeaterItems`,
|
|
2765
|
+
* and one field of each record (`imageField`) is the URL.
|
|
2766
|
+
*
|
|
2767
|
+
* `resolveGalleryUrls` is the SINGLE place both the builder canvas and the SSR/runtime engine turn a
|
|
2768
|
+
* node's config into the final `string[]` the crossfade loop consumes — so both sides show the same
|
|
2769
|
+
* images. It is pure and transport-agnostic; never fork it. It reuses `getByPath` (the same path
|
|
2770
|
+
* reader used by data-source resolving) so binding paths behave identically everywhere.
|
|
2658
2771
|
*/
|
|
2659
|
-
var DeclarationContext;
|
|
2660
|
-
(function (DeclarationContext) {
|
|
2661
|
-
DeclarationContext["Base"] = "base";
|
|
2662
|
-
DeclarationContext["Override"] = "override";
|
|
2663
|
-
})(DeclarationContext || (DeclarationContext = {}));
|
|
2664
2772
|
/**
|
|
2665
|
-
*
|
|
2666
|
-
*
|
|
2667
|
-
* in a `Base` context only, optional `!important`). Callers wrap these in whatever selector
|
|
2668
|
-
* they emit.
|
|
2773
|
+
* Resolve a gallery's configured images to the flat, display-ready `string[]` the loop renders.
|
|
2774
|
+
* Pure. Empty/whitespace URLs are dropped so a half-filled manual list never yields blank frames.
|
|
2669
2775
|
*
|
|
2670
|
-
* @param
|
|
2671
|
-
* @param
|
|
2672
|
-
*
|
|
2673
|
-
* `!important` is never dropped as a no-op — the `!important` exists to win
|
|
2674
|
-
* the cascade, so its value must always be emitted.
|
|
2675
|
-
* @param context `Override` for a rule that overrides a wider one (a breakpoint layer):
|
|
2676
|
-
* initial values are kept, because there they reset rather than no-op.
|
|
2776
|
+
* @param images manual `GalleryImage[]` (manual mode). Ignored when `bound` is provided.
|
|
2777
|
+
* @param bound bound-mode records + image field. When present, takes precedence over `images`.
|
|
2778
|
+
* @param ctx render context for per-item bindings (item/route data). Defaults to `{}`.
|
|
2677
2779
|
*/
|
|
2678
|
-
function
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
const isImportant = importantProps?.has(k) ?? false;
|
|
2686
|
-
const dropsNoOps = context === DeclarationContext.Base && !isImportant;
|
|
2687
|
-
if (dropsNoOps && NOOP_INITIAL_VALUES[prop]?.includes(val.trim().toLowerCase()))
|
|
2688
|
-
continue;
|
|
2689
|
-
out.push(`${prop}: ${val}${isImportant ? ' !important' : ''}`);
|
|
2780
|
+
function resolveGalleryUrls(images, bound, ctx = {}) {
|
|
2781
|
+
if (bound) {
|
|
2782
|
+
const field = bound.imageField;
|
|
2783
|
+
return (bound.items ?? [])
|
|
2784
|
+
.map(item => (field ? getByPath(item, field) : item))
|
|
2785
|
+
.map(coerceUrl)
|
|
2786
|
+
.filter((u) => !!u);
|
|
2690
2787
|
}
|
|
2691
|
-
return
|
|
2788
|
+
return (images ?? [])
|
|
2789
|
+
.map(normalizeGalleryImage)
|
|
2790
|
+
.map(img => (img?.binding ? getByPath(ctx, img.binding) : img?.url))
|
|
2791
|
+
.map(coerceUrl)
|
|
2792
|
+
.filter((u) => !!u);
|
|
2692
2793
|
}
|
|
2693
2794
|
/**
|
|
2694
|
-
*
|
|
2695
|
-
*
|
|
2696
|
-
*
|
|
2697
|
-
*
|
|
2698
|
-
*
|
|
2795
|
+
* A plain string is a valid entry — a pasted URL list, an older saved node, or a bound field that
|
|
2796
|
+
* yielded a URL directly. Accepting it HERE (not in one consumer) is what keeps the two render paths
|
|
2797
|
+
* honest: `ClientGalleryComponent` used to normalise privately, so the engine's string-renderer
|
|
2798
|
+
* fallback — which calls this function directly — silently dropped every string-shaped image and
|
|
2799
|
+
* rendered an empty gallery. One resolver, one answer. See gallery-component-plan.md.
|
|
2800
|
+
*/
|
|
2801
|
+
function normalizeGalleryImage(image) {
|
|
2802
|
+
if (typeof image === 'string')
|
|
2803
|
+
return { url: image };
|
|
2804
|
+
return image ?? undefined;
|
|
2805
|
+
}
|
|
2806
|
+
/** A resolved value becomes a URL only if it's a non-empty string; everything else drops out. */
|
|
2807
|
+
function coerceUrl(value) {
|
|
2808
|
+
if (typeof value !== 'string')
|
|
2809
|
+
return null;
|
|
2810
|
+
const trimmed = value.trim();
|
|
2811
|
+
return trimmed ? trimmed : null;
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Parse a pasted block of URLs (newline- and/or comma-separated) into `GalleryImage[]`. Used by the
|
|
2815
|
+
* MediaList trait editor's "paste URL list" affordance. Pure so it can be unit-tested + reused.
|
|
2816
|
+
*/
|
|
2817
|
+
function parseUrlList(text) {
|
|
2818
|
+
if (!text)
|
|
2819
|
+
return [];
|
|
2820
|
+
return text
|
|
2821
|
+
.split(/[\n,]+/)
|
|
2822
|
+
.map(s => s.trim())
|
|
2823
|
+
.filter(Boolean)
|
|
2824
|
+
.map(url => ({ url }));
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
/**
|
|
2828
|
+
* Gallery effect registry — the SHARED, effect-agnostic description of how each gallery slide looks
|
|
2829
|
+
* and animates in a given crossfade state. Modeled on `binding-pipes.ts` / `PAGE_TRANSITION_PRESETS`:
|
|
2830
|
+
* a descriptor map + a resolve fn, all pure, consumed by BOTH the builder twin and the engine twin so
|
|
2831
|
+
* the fade looks identical on the canvas and the live site.
|
|
2699
2832
|
*
|
|
2700
|
-
*
|
|
2701
|
-
*
|
|
2702
|
-
*
|
|
2833
|
+
* v1 ships only `fade` (the look ported from `sites/interior-design` fade-gallery). Adding `slide`,
|
|
2834
|
+
* `zoom`, `kenburns`, … later is a new entry here + its CSS — NO component refactor, because the
|
|
2835
|
+
* component only ever asks `resolveGalleryEffect(id).buildSlideStyle(state)` for per-slide inline CSS.
|
|
2836
|
+
*
|
|
2837
|
+
* See iox-ai-guidance/architecture/builder/gallery-component-plan.md.
|
|
2703
2838
|
*/
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2839
|
+
/** The ken-burns zoom baked into the reference fade look (scale 1 → 1.05 over show+fade). */
|
|
2840
|
+
const FADE_ZOOM_EXTRA_MS = 1000;
|
|
2841
|
+
const fadeEffect = {
|
|
2842
|
+
id: 'fade',
|
|
2843
|
+
label: 'Fade',
|
|
2844
|
+
defaults: { showMs: 4000, fadeMs: 1000 },
|
|
2845
|
+
buildSlideStyle(state) {
|
|
2846
|
+
const { active, fadingOut, firstCycleActive, durations } = state;
|
|
2847
|
+
const zoomMs = durations.showMs + durations.fadeMs + FADE_ZOOM_EXTRA_MS;
|
|
2848
|
+
// Before the first cycle activates, everything is flat + instant so there is no entry flash.
|
|
2849
|
+
if (!firstCycleActive) {
|
|
2850
|
+
return { opacity: '0', transform: 'scale(1)', transition: 'opacity 0ms, transform 0ms' };
|
|
2851
|
+
}
|
|
2852
|
+
const visible = active && !fadingOut;
|
|
2853
|
+
const engaged = active || fadingOut; // current or outgoing slide is zoomed-in
|
|
2854
|
+
return {
|
|
2855
|
+
opacity: visible ? '1' : '0',
|
|
2856
|
+
transform: engaged ? 'scale(1.05)' : 'scale(1)',
|
|
2857
|
+
transition: `opacity ${durations.fadeMs}ms cubic-bezier(0.77, 0, 0.175, 1), ` +
|
|
2858
|
+
`transform ${zoomMs}ms linear`,
|
|
2859
|
+
};
|
|
2860
|
+
},
|
|
2861
|
+
};
|
|
2862
|
+
/** The registry — one entry per effect. Keyed by id; unknown ids fall back to `fade`. */
|
|
2863
|
+
const GALLERY_EFFECTS = {
|
|
2864
|
+
fade: fadeEffect,
|
|
2709
2865
|
};
|
|
2866
|
+
/** Resolve an effect id to its descriptor, defaulting to `fade` (forward-compatible with new ids). */
|
|
2867
|
+
function resolveGalleryEffect(id) {
|
|
2868
|
+
return (id && GALLERY_EFFECTS[id]) || GALLERY_EFFECTS['fade'];
|
|
2869
|
+
}
|
|
2870
|
+
const GALLERY_EFFECT_DESCRIPTORS = Object.values(GALLERY_EFFECTS).map(e => ({ id: e.id, label: e.label }));
|
|
2871
|
+
|
|
2710
2872
|
/**
|
|
2711
|
-
*
|
|
2712
|
-
*
|
|
2713
|
-
*
|
|
2873
|
+
* Gallery crossfade loop — the SHARED, framework-free state machine that drives which image is
|
|
2874
|
+
* visible over time. Ported from the reference fade-gallery's `scheduleCycle` / `tryStartLoop` /
|
|
2875
|
+
* first-cycle-activation logic, with Angular specifics removed so BOTH the builder twin and the
|
|
2876
|
+
* engine/runtime twin (`iox-gallery`) run ONE implementation — never a forked copy that drifts.
|
|
2714
2877
|
*
|
|
2715
|
-
*
|
|
2716
|
-
*
|
|
2717
|
-
*
|
|
2878
|
+
* The loop owns only *timing + index* state and emits it through `onChange`; the component maps that
|
|
2879
|
+
* state to CSS via the effect registry (`gallery-effects.ts`). Timers are injected so the loop is
|
|
2880
|
+
* deterministic under fake timers in tests and platform-guardable in the engine.
|
|
2881
|
+
*
|
|
2882
|
+
* See iox-ai-guidance/architecture/builder/gallery-component-plan.md.
|
|
2718
2883
|
*/
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2884
|
+
/** Default scheduler over the platform's `setTimeout` / `requestAnimationFrame` (with a timer fallback). */
|
|
2885
|
+
const DEFAULT_GALLERY_SCHEDULER = {
|
|
2886
|
+
setTimer: (fn, ms) => setTimeout(fn, ms),
|
|
2887
|
+
clearTimer: h => clearTimeout(h),
|
|
2888
|
+
nextFrame: fn => typeof requestAnimationFrame === 'function' ? requestAnimationFrame(fn) : setTimeout(fn, 16),
|
|
2889
|
+
cancelFrame: h => {
|
|
2890
|
+
if (typeof cancelAnimationFrame === 'function')
|
|
2891
|
+
cancelAnimationFrame(h);
|
|
2892
|
+
else
|
|
2893
|
+
clearTimeout(h);
|
|
2894
|
+
},
|
|
2895
|
+
};
|
|
2896
|
+
class GalleryLoop {
|
|
2897
|
+
constructor(opts) {
|
|
2898
|
+
this.count = 0;
|
|
2899
|
+
this.currentIndex = 0;
|
|
2900
|
+
this.fadingOutIndex = null;
|
|
2901
|
+
this.firstCycleActive = false;
|
|
2902
|
+
this.cycleTimer = null;
|
|
2903
|
+
this.fadeTimer = null;
|
|
2904
|
+
this.frameHandle = null;
|
|
2905
|
+
/** Bumped on every (re)start so a stale timer callback from a previous run is ignored. */
|
|
2906
|
+
this.runToken = 0;
|
|
2907
|
+
this.running = false;
|
|
2908
|
+
this.scheduler = opts.scheduler ?? DEFAULT_GALLERY_SCHEDULER;
|
|
2909
|
+
this.showMs = opts.showMs;
|
|
2910
|
+
this.fadeMs = opts.fadeMs;
|
|
2911
|
+
this.onChange = opts.onChange;
|
|
2912
|
+
}
|
|
2913
|
+
get state() {
|
|
2914
|
+
return {
|
|
2915
|
+
currentIndex: this.currentIndex,
|
|
2916
|
+
fadingOutIndex: this.fadingOutIndex,
|
|
2917
|
+
firstCycleActive: this.firstCycleActive,
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
/** Start (or restart) the loop for `count` images. A count < 2 shows the single image without cycling. */
|
|
2921
|
+
start(count) {
|
|
2922
|
+
this.stop();
|
|
2923
|
+
this.count = Math.max(0, count | 0);
|
|
2924
|
+
this.currentIndex = 0;
|
|
2925
|
+
this.fadingOutIndex = null;
|
|
2926
|
+
this.firstCycleActive = false;
|
|
2927
|
+
if (this.count === 0) {
|
|
2928
|
+
this.emit();
|
|
2929
|
+
return;
|
|
2729
2930
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2931
|
+
this.running = true;
|
|
2932
|
+
const token = ++this.runToken;
|
|
2933
|
+
// Activate on the next frame so the first paint is flat (no entry-transition flash), then
|
|
2934
|
+
// begin cycling only when there is more than one image to advance through.
|
|
2935
|
+
this.frameHandle = this.scheduler.nextFrame(() => {
|
|
2936
|
+
if (token !== this.runToken || !this.running)
|
|
2937
|
+
return;
|
|
2938
|
+
this.firstCycleActive = true;
|
|
2939
|
+
this.emit();
|
|
2940
|
+
if (this.count > 1)
|
|
2941
|
+
this.scheduleCycle(token);
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
/** Update timing live. Takes effect on the next scheduled cycle; does not restart the loop. */
|
|
2945
|
+
setDurations(showMs, fadeMs) {
|
|
2946
|
+
this.showMs = showMs;
|
|
2947
|
+
this.fadeMs = fadeMs;
|
|
2948
|
+
}
|
|
2949
|
+
/** Stop and clear all timers. Idempotent. State is retained (call `start` to reset). */
|
|
2950
|
+
stop() {
|
|
2951
|
+
this.running = false;
|
|
2952
|
+
this.runToken++;
|
|
2953
|
+
if (this.cycleTimer != null) {
|
|
2954
|
+
this.scheduler.clearTimer(this.cycleTimer);
|
|
2955
|
+
this.cycleTimer = null;
|
|
2956
|
+
}
|
|
2957
|
+
if (this.fadeTimer != null) {
|
|
2958
|
+
this.scheduler.clearTimer(this.fadeTimer);
|
|
2959
|
+
this.fadeTimer = null;
|
|
2960
|
+
}
|
|
2961
|
+
if (this.frameHandle != null) {
|
|
2962
|
+
this.scheduler.cancelFrame(this.frameHandle);
|
|
2963
|
+
this.frameHandle = null;
|
|
2732
2964
|
}
|
|
2733
2965
|
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2966
|
+
scheduleCycle(token) {
|
|
2967
|
+
if (token !== this.runToken || !this.running || this.count < 2)
|
|
2968
|
+
return;
|
|
2969
|
+
this.cycleTimer = this.scheduler.setTimer(() => {
|
|
2970
|
+
if (token !== this.runToken || !this.running)
|
|
2971
|
+
return;
|
|
2972
|
+
this.fadingOutIndex = this.currentIndex;
|
|
2973
|
+
this.currentIndex = (this.currentIndex + 1) % this.count;
|
|
2974
|
+
this.emit();
|
|
2975
|
+
this.fadeTimer = this.scheduler.setTimer(() => {
|
|
2976
|
+
if (token !== this.runToken || !this.running)
|
|
2977
|
+
return;
|
|
2978
|
+
this.fadingOutIndex = null;
|
|
2979
|
+
this.emit();
|
|
2980
|
+
this.scheduleCycle(token);
|
|
2981
|
+
}, this.fadeMs);
|
|
2982
|
+
}, this.showMs);
|
|
2740
2983
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
// browser's default `medium` width and show a phantom border. Absent borderWidth = no border.
|
|
2744
|
-
if (sp['borderWidth'] && sp['borderStyle'] === undefined) {
|
|
2745
|
-
out.push('border-style: solid');
|
|
2984
|
+
emit() {
|
|
2985
|
+
this.onChange(this.state);
|
|
2746
2986
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
/**
|
|
2990
|
+
* `iox-gallery` — the engine/runtime twin of the Gallery builder component (the builder twin
|
|
2991
|
+
* `app-builder-gallery` arrives in Phase 2). It runs the SHARED crossfade loop + effect registry from
|
|
2992
|
+
* `@vectoriox/iox-ui`, so the fade behaves identically on the canvas and the live site — no forked
|
|
2993
|
+
* animation logic. See iox-ai-guidance/architecture/builder/gallery-component-plan.md.
|
|
2994
|
+
*
|
|
2995
|
+
* Population: v1 drives the loop from the `images` input (manual mode). Data-source binding
|
|
2996
|
+
* (`source`/`sourcePath`) is Phase 3 — this component already resolves through `resolveGalleryUrls`,
|
|
2997
|
+
* so that only adds a bound-source input, not new render logic.
|
|
2998
|
+
*
|
|
2999
|
+
* SSR / no-JS: on the server the loop never starts, so the **first image renders visible** as a static
|
|
3000
|
+
* frame (SEO + no-JS baseline). In the browser `ngAfterViewInit` starts the loop, which begins on the
|
|
3001
|
+
* same index 0 — so the static frame and the first live frame match and there is no hydration flash.
|
|
3002
|
+
*/
|
|
3003
|
+
class ClientGalleryComponent {
|
|
3004
|
+
/** Manual images — `GalleryImage[]` or a plain `string[]` of URLs (normalised on set). */
|
|
3005
|
+
set images(v) {
|
|
3006
|
+
this._images = v ?? [];
|
|
3007
|
+
this.urls = resolveGalleryUrls(this._images, null);
|
|
3008
|
+
// A live edit (image added/removed/reordered) while the loop runs → restart on the new set so
|
|
3009
|
+
// the canvas reflects it and the loop cycles through the new count.
|
|
3010
|
+
if (this.loopActive)
|
|
3011
|
+
this.startLoop();
|
|
3012
|
+
}
|
|
3013
|
+
get images() { return this._images; }
|
|
3014
|
+
set showDuration(v) { this._showDuration = v; this.loop?.setDurations(v, this._fadeDuration); }
|
|
3015
|
+
get showDuration() { return this._showDuration; }
|
|
3016
|
+
set fadeDuration(v) { this._fadeDuration = v; this.loop?.setDurations(this._showDuration, v); }
|
|
3017
|
+
get fadeDuration() { return this._fadeDuration; }
|
|
3018
|
+
constructor(platformId, cdr) {
|
|
3019
|
+
this.cdr = cdr;
|
|
3020
|
+
this.nodeId = '';
|
|
3021
|
+
this.effect = 'fade';
|
|
3022
|
+
this.autoStart = true;
|
|
3023
|
+
/** Layout mode — only `'stack'` is rendered in v1 (`'grid'` scaffolded for later). */
|
|
3024
|
+
this.layout = 'stack';
|
|
3025
|
+
// ── Inputs that mutate live state use SETTERS, not ngOnChanges ──────────────────────────────
|
|
3026
|
+
// The builder trait panel pushes edits via DIRECT property assignment (`instance.images = v`),
|
|
3027
|
+
// which does NOT trigger ngOnChanges — so a gallery that recomputed in ngOnChanges never refreshed
|
|
3028
|
+
// when you added an image on the canvas. A setter fires on BOTH direct assignment (builder panel)
|
|
3029
|
+
// and setInput (SSR engine / render.directive), so it's the one path that works everywhere.
|
|
3030
|
+
this._images = [];
|
|
3031
|
+
this._showDuration = 4000;
|
|
3032
|
+
this._fadeDuration = 1000;
|
|
3033
|
+
/** The flat, display-ready URLs the loop cycles. Recomputed whenever `images` is set. */
|
|
3034
|
+
this.urls = [];
|
|
3035
|
+
this.currentIndex = 0;
|
|
3036
|
+
this.fadingOutIndex = null;
|
|
3037
|
+
this.started = false;
|
|
3038
|
+
/** True once the loop has been started (in ngAfterViewInit) — gates the setter-driven restart. */
|
|
3039
|
+
this.loopActive = false;
|
|
3040
|
+
this.loop = null;
|
|
3041
|
+
this.isBrowser = isPlatformBrowser(platformId);
|
|
2750
3042
|
}
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
if (type === 'Image') {
|
|
2756
|
-
if (sp['aspectRatio'] === undefined)
|
|
2757
|
-
out.push('aspect-ratio: 1/1');
|
|
2758
|
-
// object-fit is a Size style trait with config default 'cover'; that default isn't persisted,
|
|
2759
|
-
// so reapply it here (matching the builder) when the node has none.
|
|
2760
|
-
if (sp['objectFit'] === undefined)
|
|
2761
|
-
out.push('object-fit: cover');
|
|
2762
|
-
// NOTE: no `height: auto` — when height is unset, `auto` is already the CSS initial value, so
|
|
2763
|
-
// emitting it is a pure no-op. A configured height comes through compileDeclarations and wins.
|
|
3043
|
+
ngAfterViewInit() {
|
|
3044
|
+
if (this.isBrowser && this.autoStart) {
|
|
3045
|
+
this.startLoop();
|
|
3046
|
+
}
|
|
2764
3047
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
3048
|
+
ngOnDestroy() {
|
|
3049
|
+
this.loop?.stop();
|
|
3050
|
+
this.loop = null;
|
|
3051
|
+
this.loopActive = false;
|
|
2769
3052
|
}
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
3053
|
+
/** Per-slide inline style. Before the loop activates (server + no-JS + pre-first-frame) the first
|
|
3054
|
+
* image is the visible static frame; once running, the effect owns each slide's look. */
|
|
3055
|
+
slideStyle(i) {
|
|
3056
|
+
if (!this.started) {
|
|
3057
|
+
return i === 0
|
|
3058
|
+
? { opacity: '1', transform: 'scale(1)', transition: 'none' }
|
|
3059
|
+
: { opacity: '0', transform: 'scale(1)', transition: 'none' };
|
|
3060
|
+
}
|
|
3061
|
+
return resolveGalleryEffect(this.effect).buildSlideStyle({
|
|
3062
|
+
active: i === this.currentIndex,
|
|
3063
|
+
fadingOut: i === this.fadingOutIndex,
|
|
3064
|
+
firstCycleActive: true,
|
|
3065
|
+
durations: { showMs: this.showDuration, fadeMs: this.fadeDuration },
|
|
3066
|
+
});
|
|
2773
3067
|
}
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
if (type === 'Icon' && sp['display'] === undefined) {
|
|
2791
|
-
out.push('display: inline-flex', 'align-items: center', 'justify-content: center');
|
|
3068
|
+
startLoop() {
|
|
3069
|
+
if (!this.isBrowser)
|
|
3070
|
+
return;
|
|
3071
|
+
this.loopActive = true;
|
|
3072
|
+
this.loop?.stop();
|
|
3073
|
+
this.loop = new GalleryLoop({
|
|
3074
|
+
showMs: this.showDuration,
|
|
3075
|
+
fadeMs: this.fadeDuration,
|
|
3076
|
+
onChange: state => {
|
|
3077
|
+
this.currentIndex = state.currentIndex;
|
|
3078
|
+
this.fadingOutIndex = state.fadingOutIndex;
|
|
3079
|
+
this.started = state.firstCycleActive;
|
|
3080
|
+
this.cdr.markForCheck();
|
|
3081
|
+
},
|
|
3082
|
+
});
|
|
3083
|
+
this.loop.start(this.urls.length);
|
|
2792
3084
|
}
|
|
2793
|
-
|
|
3085
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientGalleryComponent, deps: [{ token: PLATFORM_ID }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3086
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: ClientGalleryComponent, isStandalone: false, selector: "iox-gallery", inputs: { nodeId: "nodeId", effect: "effect", autoStart: "autoStart", layout: "layout", images: "images", showDuration: "showDuration", fadeDuration: "fadeDuration" }, ngImport: i0, template: `
|
|
3087
|
+
<div class="iox-gallery" [class]="'iox-node-' + nodeId">
|
|
3088
|
+
<div *ngFor="let url of urls; index as i"
|
|
3089
|
+
class="iox-gallery__img"
|
|
3090
|
+
[style.background-image]="'url(' + url + ')'"
|
|
3091
|
+
[ngStyle]="slideStyle(i)"></div>
|
|
3092
|
+
<div class="iox-gallery__overlay"></div>
|
|
3093
|
+
</div>
|
|
3094
|
+
`, isInline: true, styles: [":host{display:block}.iox-gallery{position:relative;overflow:hidden;box-sizing:border-box}.iox-gallery__img{position:absolute;inset:0;background-size:cover;background-position:center;pointer-events:none;opacity:0;filter:brightness(1.35);-webkit-mask-image:radial-gradient(circle at 50% 50%,rgba(0,0,0,1) 80%,rgba(0,0,0,.1) 95%,rgba(0,0,0,0) 100%);mask-image:radial-gradient(circle at 50% 50%,#000 80%,#0000001a 95%,#0000)}.iox-gallery__overlay{position:absolute;inset:0;pointer-events:none;z-index:3;background:linear-gradient(to bottom,#00000080,#00000026 20% 80%,#00000080)}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
|
|
2794
3095
|
}
|
|
3096
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientGalleryComponent, decorators: [{
|
|
3097
|
+
type: Component,
|
|
3098
|
+
args: [{ selector: 'iox-gallery', template: `
|
|
3099
|
+
<div class="iox-gallery" [class]="'iox-node-' + nodeId">
|
|
3100
|
+
<div *ngFor="let url of urls; index as i"
|
|
3101
|
+
class="iox-gallery__img"
|
|
3102
|
+
[style.background-image]="'url(' + url + ')'"
|
|
3103
|
+
[ngStyle]="slideStyle(i)"></div>
|
|
3104
|
+
<div class="iox-gallery__overlay"></div>
|
|
3105
|
+
</div>
|
|
3106
|
+
`, standalone: false, styles: [":host{display:block}.iox-gallery{position:relative;overflow:hidden;box-sizing:border-box}.iox-gallery__img{position:absolute;inset:0;background-size:cover;background-position:center;pointer-events:none;opacity:0;filter:brightness(1.35);-webkit-mask-image:radial-gradient(circle at 50% 50%,rgba(0,0,0,1) 80%,rgba(0,0,0,.1) 95%,rgba(0,0,0,0) 100%);mask-image:radial-gradient(circle at 50% 50%,#000 80%,#0000001a 95%,#0000)}.iox-gallery__overlay{position:absolute;inset:0;pointer-events:none;z-index:3;background:linear-gradient(to bottom,#00000080,#00000026 20% 80%,#00000080)}\n"] }]
|
|
3107
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
3108
|
+
type: Inject,
|
|
3109
|
+
args: [PLATFORM_ID]
|
|
3110
|
+
}] }, { type: i0.ChangeDetectorRef }], propDecorators: { nodeId: [{
|
|
3111
|
+
type: Input
|
|
3112
|
+
}], effect: [{
|
|
3113
|
+
type: Input
|
|
3114
|
+
}], autoStart: [{
|
|
3115
|
+
type: Input
|
|
3116
|
+
}], layout: [{
|
|
3117
|
+
type: Input
|
|
3118
|
+
}], images: [{
|
|
3119
|
+
type: Input
|
|
3120
|
+
}], showDuration: [{
|
|
3121
|
+
type: Input
|
|
3122
|
+
}], fadeDuration: [{
|
|
3123
|
+
type: Input
|
|
3124
|
+
}] } });
|
|
2795
3125
|
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
['filterSepia', 'sepia', '0'],
|
|
2817
|
-
['filterInvert', 'invert', '0'],
|
|
2818
|
-
];
|
|
2819
|
-
// ─── Backdrop-filter ─────────────────────────────────────────────────────────
|
|
2820
|
-
const BACKDROP_FUNS = [
|
|
2821
|
-
['backdropBlur', 'blur', '0px'],
|
|
2822
|
-
['backdropBrightness', 'brightness', '1'],
|
|
2823
|
-
['backdropContrast', 'contrast', '1'],
|
|
2824
|
-
['backdropSaturate', 'saturate', '1'],
|
|
2825
|
-
];
|
|
2826
|
-
// ─── Transform ───────────────────────────────────────────────────────────────
|
|
2827
|
-
const TRANSFORM_FUNS = [
|
|
2828
|
-
['translateX', 'translateX', '0px'],
|
|
2829
|
-
['translateY', 'translateY', '0px'],
|
|
2830
|
-
['scaleX', 'scaleX', '1'],
|
|
2831
|
-
['scaleY', 'scaleY', '1'],
|
|
2832
|
-
['rotate', 'rotate', '0deg'],
|
|
2833
|
-
['skewX', 'skewX', '0deg'],
|
|
2834
|
-
['skewY', 'skewY', '0deg'],
|
|
2835
|
-
];
|
|
2836
|
-
// ─── ::marker pseudo-element ─────────────────────────────────────────────────
|
|
2837
|
-
const MARKER_FUNS = [
|
|
2838
|
-
['markerColor', 'color'],
|
|
2839
|
-
['markerFontSize', 'fontSize'],
|
|
3126
|
+
const COMPONENTS = [
|
|
3127
|
+
BuilderImageComponent,
|
|
3128
|
+
BuilderSpacerComponent,
|
|
3129
|
+
CardComponent,
|
|
3130
|
+
BuilderDividerComponent,
|
|
3131
|
+
BuilderIconComponent,
|
|
3132
|
+
BuilderLinkComponent,
|
|
3133
|
+
BuilderButtonComponent,
|
|
3134
|
+
BuilderHeadingComponent,
|
|
3135
|
+
TextBlockComponent,
|
|
3136
|
+
SectionComponent,
|
|
3137
|
+
ContainerComponent,
|
|
3138
|
+
LinkedContainerComponent,
|
|
3139
|
+
ClientRepeaterComponent,
|
|
3140
|
+
ButtonBlockComponent,
|
|
3141
|
+
ClientSliderContainerComponent,
|
|
3142
|
+
ClientSliderSlideComponent,
|
|
3143
|
+
ClientListComponent,
|
|
3144
|
+
ClientListItemComponent,
|
|
3145
|
+
ClientGalleryComponent,
|
|
2840
3146
|
];
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
{
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
3147
|
+
class IoxBuilderComponentsModule {
|
|
3148
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
3149
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, declarations: [BuilderImageComponent,
|
|
3150
|
+
BuilderSpacerComponent,
|
|
3151
|
+
CardComponent,
|
|
3152
|
+
BuilderDividerComponent,
|
|
3153
|
+
BuilderIconComponent,
|
|
3154
|
+
BuilderLinkComponent,
|
|
3155
|
+
BuilderButtonComponent,
|
|
3156
|
+
BuilderHeadingComponent,
|
|
3157
|
+
TextBlockComponent,
|
|
3158
|
+
SectionComponent,
|
|
3159
|
+
ContainerComponent,
|
|
3160
|
+
LinkedContainerComponent,
|
|
3161
|
+
ClientRepeaterComponent,
|
|
3162
|
+
ButtonBlockComponent,
|
|
3163
|
+
ClientSliderContainerComponent,
|
|
3164
|
+
ClientSliderSlideComponent,
|
|
3165
|
+
ClientListComponent,
|
|
3166
|
+
ClientListItemComponent,
|
|
3167
|
+
ClientGalleryComponent], imports: [CommonModule], exports: [BuilderImageComponent,
|
|
3168
|
+
BuilderSpacerComponent,
|
|
3169
|
+
CardComponent,
|
|
3170
|
+
BuilderDividerComponent,
|
|
3171
|
+
BuilderIconComponent,
|
|
3172
|
+
BuilderLinkComponent,
|
|
3173
|
+
BuilderButtonComponent,
|
|
3174
|
+
BuilderHeadingComponent,
|
|
3175
|
+
TextBlockComponent,
|
|
3176
|
+
SectionComponent,
|
|
3177
|
+
ContainerComponent,
|
|
3178
|
+
LinkedContainerComponent,
|
|
3179
|
+
ClientRepeaterComponent,
|
|
3180
|
+
ButtonBlockComponent,
|
|
3181
|
+
ClientSliderContainerComponent,
|
|
3182
|
+
ClientSliderSlideComponent,
|
|
3183
|
+
ClientListComponent,
|
|
3184
|
+
ClientListItemComponent,
|
|
3185
|
+
ClientGalleryComponent] }); }
|
|
3186
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, imports: [CommonModule] }); }
|
|
2858
3187
|
}
|
|
2859
|
-
|
|
3188
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, decorators: [{
|
|
3189
|
+
type: NgModule,
|
|
3190
|
+
args: [{
|
|
3191
|
+
declarations: COMPONENTS,
|
|
3192
|
+
imports: [CommonModule],
|
|
3193
|
+
exports: COMPONENTS,
|
|
3194
|
+
}]
|
|
3195
|
+
}] });
|
|
3196
|
+
|
|
2860
3197
|
/**
|
|
2861
|
-
*
|
|
3198
|
+
* Single source of truth for the IOX component type → Angular class mapping.
|
|
3199
|
+
* Used by the client engine (iox-client-engine) to resolve component classes
|
|
3200
|
+
* from IoxLayoutNode.type strings at render time.
|
|
2862
3201
|
*
|
|
2863
|
-
*
|
|
2864
|
-
*
|
|
2865
|
-
*
|
|
2866
|
-
*
|
|
3202
|
+
* All 13 builder components are registered here:
|
|
3203
|
+
* - Leaf (Phase 2a): Image, Spacer, Card, Divider, Icon
|
|
3204
|
+
* - Inline-editable (Phase 2b): Link, Button, Heading, Text
|
|
3205
|
+
* - Layout shells (Phase 2c): Section, Container, LinkedContainer
|
|
3206
|
+
* - Repeater (Phase 2d): ClientRepeaterComponent
|
|
3207
|
+
*
|
|
3208
|
+
* Section/Container/LinkedContainer/Repeater expose childrenHost: ViewContainerRef
|
|
3209
|
+
* for ClientRenderDirective to insert child components into.
|
|
2867
3210
|
*/
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
const transformStr = buildFns(TRANSFORM_FUNS, merged(TRANSFORM_FUNS));
|
|
2892
|
-
if (transformStr)
|
|
2893
|
-
result['transform'] = transformStr;
|
|
2894
|
-
// transition — only emit when duration is non-zero
|
|
2895
|
-
const dur = raw['transitionDuration'] ?? base['transitionDuration'];
|
|
2896
|
-
const ease = raw['transitionTimingFunction'] ?? base['transitionTimingFunction'];
|
|
2897
|
-
const del = raw['transitionDelay'] ?? base['transitionDelay'];
|
|
2898
|
-
if (dur && ease && dur !== '0ms' && dur !== '0s') {
|
|
2899
|
-
const delPart = del && del !== '0ms' && del !== '0s' ? ` ${del}` : '';
|
|
2900
|
-
result['transition'] = `all ${dur} ${ease}${delPart}`;
|
|
3211
|
+
class IoxComponentRegistryService {
|
|
3212
|
+
constructor() {
|
|
3213
|
+
this.map = new Map([
|
|
3214
|
+
['Image', BuilderImageComponent],
|
|
3215
|
+
['Spacer', BuilderSpacerComponent],
|
|
3216
|
+
['Card', CardComponent],
|
|
3217
|
+
['Divider', BuilderDividerComponent],
|
|
3218
|
+
['Icon', BuilderIconComponent],
|
|
3219
|
+
['Link', BuilderLinkComponent],
|
|
3220
|
+
['Button', BuilderButtonComponent],
|
|
3221
|
+
['Heading', BuilderHeadingComponent],
|
|
3222
|
+
['Text', TextBlockComponent],
|
|
3223
|
+
['Section', SectionComponent],
|
|
3224
|
+
['Container', ContainerComponent],
|
|
3225
|
+
['LinkedContainer', LinkedContainerComponent],
|
|
3226
|
+
['Repeater', ClientRepeaterComponent],
|
|
3227
|
+
['ButtonBlock', ButtonBlockComponent],
|
|
3228
|
+
['SliderContainer', ClientSliderContainerComponent],
|
|
3229
|
+
['SliderSlide', ClientSliderSlideComponent],
|
|
3230
|
+
['List', ClientListComponent],
|
|
3231
|
+
['ListItem', ClientListItemComponent],
|
|
3232
|
+
['Gallery', ClientGalleryComponent],
|
|
3233
|
+
]);
|
|
2901
3234
|
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
const markerStyles = {};
|
|
2905
|
-
for (const [trait, cssProp] of MARKER_FUNS) {
|
|
2906
|
-
const val = raw[trait] ?? base[trait];
|
|
2907
|
-
if (val != null && val !== '')
|
|
2908
|
-
markerStyles[cssProp] = val;
|
|
3235
|
+
getComponent(type) {
|
|
3236
|
+
return this.map.get(type);
|
|
2909
3237
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
3238
|
+
hasComponent(type) {
|
|
3239
|
+
return this.map.has(type);
|
|
2912
3240
|
}
|
|
2913
|
-
|
|
3241
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxComponentRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3242
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxComponentRegistryService, providedIn: 'root' }); }
|
|
2914
3243
|
}
|
|
3244
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxComponentRegistryService, decorators: [{
|
|
3245
|
+
type: Injectable,
|
|
3246
|
+
args: [{ providedIn: 'root' }]
|
|
3247
|
+
}] });
|
|
2915
3248
|
|
|
2916
3249
|
/**
|
|
2917
|
-
*
|
|
2918
|
-
*
|
|
2919
|
-
* depend on iox-ui (the engine does NOT depend on iox-builder), so the same logic
|
|
2920
|
-
* drives the builder canvas AND the SSR site — no fork.
|
|
2921
|
-
* See iox-ai-guidance `architecture/builder/targeted-hover-state-plan.md`.
|
|
2922
|
-
*/
|
|
2923
|
-
/** The css id a node contributes to selectors: its clone `styleId`, else its own `id`. */
|
|
2924
|
-
function nodeCssId(node) {
|
|
2925
|
-
return node.styleId ?? node.id;
|
|
2926
|
-
}
|
|
2927
|
-
/**
|
|
2928
|
-
* Path of css ids from the root down to (and including) the node whose css id === `cssId`.
|
|
2929
|
-
* Returns `null` when no node matches. First match wins (depth-first, pre-order).
|
|
2930
|
-
*/
|
|
2931
|
-
function pathToCssId(nodes, cssId) {
|
|
2932
|
-
for (const node of nodes) {
|
|
2933
|
-
const ownId = nodeCssId(node);
|
|
2934
|
-
if (ownId === cssId)
|
|
2935
|
-
return [ownId];
|
|
2936
|
-
if (node.children?.length) {
|
|
2937
|
-
const childPath = pathToCssId(node.children, cssId);
|
|
2938
|
-
if (childPath)
|
|
2939
|
-
return ownId ? [ownId, ...childPath] : childPath;
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
return null;
|
|
2943
|
-
}
|
|
2944
|
-
/**
|
|
2945
|
-
* The css id of the NEAREST COMMON ANCESTOR of two nodes (identified by their css ids),
|
|
2946
|
-
* used to scope a `:has()` targeted-state selector so it stays repetition-safe.
|
|
2947
|
-
*
|
|
2948
|
-
* The result may equal `styledCssId` (the trigger sits INSIDE the styled node) or
|
|
2949
|
-
* `triggerCssId` (the styled node sits inside the trigger) — both are valid scopes;
|
|
2950
|
-
* `buildTargetedStateSelector` picks the right selector shape for each. Returns
|
|
2951
|
-
* `undefined` only when a valid scope can't exist: an id is blank, the two ids are the
|
|
2952
|
-
* same node, or they share no ancestor in `root`. Callers MUST skip emitting the rule
|
|
2953
|
-
* when this is `undefined` — never emit an unscoped (page-global) `:has()`, which would
|
|
2954
|
-
* break under repetition.
|
|
2955
|
-
*/
|
|
2956
|
-
function nearestCommonAncestorId(root, styledCssId, triggerCssId) {
|
|
2957
|
-
if (!styledCssId || !triggerCssId || styledCssId === triggerCssId)
|
|
2958
|
-
return undefined;
|
|
2959
|
-
const styledPath = pathToCssId(root, styledCssId);
|
|
2960
|
-
const triggerPath = pathToCssId(root, triggerCssId);
|
|
2961
|
-
if (!styledPath || !triggerPath)
|
|
2962
|
-
return undefined;
|
|
2963
|
-
// Walk both paths together; the last shared prefix element is the common ancestor.
|
|
2964
|
-
let common;
|
|
2965
|
-
const shorter = Math.min(styledPath.length, triggerPath.length);
|
|
2966
|
-
for (let i = 0; i < shorter; i++) {
|
|
2967
|
-
if (styledPath[i] !== triggerPath[i])
|
|
2968
|
-
break;
|
|
2969
|
-
common = styledPath[i];
|
|
2970
|
-
}
|
|
2971
|
-
return common;
|
|
2972
|
-
}
|
|
2973
|
-
/**
|
|
2974
|
-
* Build the selector BODY (no leading `.`, matching the builder's `compile()` convention)
|
|
2975
|
-
* for a targeted state, given the resolved scope. Three topologies, one shared rule:
|
|
3250
|
+
* style-compiler — the SINGLE source of truth for turning a node's style map into
|
|
3251
|
+
* CSS declarations. Framework-agnostic (no Angular): consumed by both renderers.
|
|
2976
3252
|
*
|
|
2977
|
-
* -
|
|
2978
|
-
* -
|
|
2979
|
-
* - separate subtrees (scope is above both): `iox-node-{scope}:has(.iox-node-{trigger}:{base}) .iox-node-{styled}`
|
|
3253
|
+
* - Builder canvas: `StyleRegistryService` (@vectoriox/iox-builder) → live `<style>` tag.
|
|
3254
|
+
* - SSR site: `LayoutRendererService` (iox-client-engine) → HTML string CSS.
|
|
2980
3255
|
*
|
|
2981
|
-
*
|
|
3256
|
+
* Lives in @vectoriox/iox-ui because BOTH consumers depend on iox-ui (the engine does
|
|
3257
|
+
* NOT depend on iox-builder). Keeping it here — not duplicated per renderer — is what
|
|
3258
|
+
* prevents the builder and the live site from drifting.
|
|
3259
|
+
* See iox-ai-guidance `architecture/builder/render-parity.md`.
|
|
2982
3260
|
*/
|
|
2983
|
-
function buildTargetedStateSelector(scopeCssId, triggerCssId, styledCssId, base) {
|
|
2984
|
-
if (scopeCssId === styledCssId) {
|
|
2985
|
-
return `iox-node-${styledCssId}:has(.iox-node-${triggerCssId}:${base})`;
|
|
2986
|
-
}
|
|
2987
|
-
if (scopeCssId === triggerCssId) {
|
|
2988
|
-
return `iox-node-${triggerCssId}:${base} .iox-node-${styledCssId}`;
|
|
2989
|
-
}
|
|
2990
|
-
return `iox-node-${scopeCssId}:has(.iox-node-${triggerCssId}:${base}) .iox-node-${styledCssId}`;
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
// Responsive (per-breakpoint) styles — the shared model, cascade and emission plan.
|
|
2994
|
-
//
|
|
2995
|
-
// SHARED (iox-ui): the builder canvas AND the SSR engine both call this. The builder
|
|
2996
|
-
// FLATTENS the active breakpoint (its canvas is a transformed frame inside a desktop
|
|
2997
|
-
// browser, so a real `@media` query would read the editor's width); the engine WRAPS
|
|
2998
|
-
// each planned layer in `@media`. Same decisions, different transport — never fork them.
|
|
2999
|
-
//
|
|
3000
|
-
// Storage is a sparse layer beside the existing keys, shaped like `stateStyles`:
|
|
3001
|
-
//
|
|
3002
|
-
// styleProps — base, full resolved set (unchanged)
|
|
3003
|
-
// stateStyles — sparse per-state overrides (unchanged)
|
|
3004
|
-
// responsive — sparse per-breakpoint overrides (this file)
|
|
3005
|
-
// { mobile: { styleProps: {…}, stateStyles: { hover: {…} } } }
|
|
3006
|
-
//
|
|
3007
|
-
// A node with no `responsive` key renders exactly as before — no migration.
|
|
3008
|
-
// See iox-ai-guidance `architecture/builder/responsive-breakpoints-plan.md`.
|
|
3009
|
-
// ─── Breakpoints ─────────────────────────────────────────────────────────────
|
|
3010
|
-
/** The five fixed breakpoints (plan decision D2). Desktop-first: each narrower one
|
|
3011
|
-
* overrides the wider ones through `max-width`. */
|
|
3012
|
-
var Breakpoint;
|
|
3013
|
-
(function (Breakpoint) {
|
|
3014
|
-
Breakpoint["Base"] = "base";
|
|
3015
|
-
Breakpoint["Laptop"] = "laptop";
|
|
3016
|
-
Breakpoint["Tablet"] = "tablet";
|
|
3017
|
-
Breakpoint["Mobile"] = "mobile";
|
|
3018
|
-
Breakpoint["Small"] = "small";
|
|
3019
|
-
})(Breakpoint || (Breakpoint = {}));
|
|
3020
3261
|
/**
|
|
3021
|
-
*
|
|
3262
|
+
* Rewrite viewport-relative units so they resolve to the simulated canvas dimensions
|
|
3263
|
+
* in the builder, and to the real viewport in production.
|
|
3022
3264
|
*
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3025
|
-
*
|
|
3026
|
-
*/
|
|
3027
|
-
const BREAKPOINTS = Object.freeze([
|
|
3028
|
-
{ id: Breakpoint.Base, label: 'Base', maxWidth: null, previewWidth: 1440 },
|
|
3029
|
-
{ id: Breakpoint.Laptop, label: 'Laptop', maxWidth: 1200, previewWidth: 1200 },
|
|
3030
|
-
{ id: Breakpoint.Tablet, label: 'Tablet', maxWidth: 991, previewWidth: 768 },
|
|
3031
|
-
{ id: Breakpoint.Mobile, label: 'Mobile landscape', maxWidth: 767, previewWidth: 568 },
|
|
3032
|
-
{ id: Breakpoint.Small, label: 'Mobile portrait', maxWidth: 479, previewWidth: 390 },
|
|
3033
|
-
]);
|
|
3034
|
-
const BREAKPOINT_INDEX = new Map(BREAKPOINTS.map((d, i) => [d.id, i]));
|
|
3035
|
-
/** The definition for `bp`, or `undefined` for an unknown id (e.g. hand-edited JSON). */
|
|
3036
|
-
function breakpointDef(bp) {
|
|
3037
|
-
const i = BREAKPOINT_INDEX.get(bp);
|
|
3038
|
-
return i === undefined ? undefined : BREAKPOINTS[i];
|
|
3039
|
-
}
|
|
3040
|
-
/** Base → `bp`, inclusive — the layers whose values apply at `bp`. Unknown → `[Base]`. */
|
|
3041
|
-
function cascadeChain(bp) {
|
|
3042
|
-
const i = BREAKPOINT_INDEX.get(bp);
|
|
3043
|
-
return BREAKPOINTS.slice(0, (i ?? 0) + 1).map(d => d.id);
|
|
3044
|
-
}
|
|
3045
|
-
/**
|
|
3046
|
-
* The breakpoint a viewport (or canvas) of `width` CSS px falls in — the narrowest one whose
|
|
3047
|
-
* `maxWidth` it fits under, else Base. Mirrors how the `max-width` media queries resolve, so the
|
|
3048
|
-
* builder's editing scope is always the breakpoint the canvas is actually showing.
|
|
3265
|
+
* `--preview-vh` / `--preview-vw` are set by BuilderComponent (ResizeObserver +
|
|
3266
|
+
* viewport-state subscription). In production those variables are undefined, so the
|
|
3267
|
+
* `1vh` / `1vw` fallbacks restore native browser behaviour.
|
|
3049
3268
|
*/
|
|
3050
|
-
function
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
return BREAKPOINTS[i].id;
|
|
3055
|
-
}
|
|
3056
|
-
return Breakpoint.Base;
|
|
3269
|
+
function rewriteViewportUnits(value) {
|
|
3270
|
+
return value
|
|
3271
|
+
.replace(/(-?\d*\.?\d+)vh/g, 'calc($1 * var(--preview-vh, 1vh))')
|
|
3272
|
+
.replace(/(-?\d*\.?\d+)vw/g, 'calc($1 * var(--preview-vw, 1vw))');
|
|
3057
3273
|
}
|
|
3058
|
-
/**
|
|
3059
|
-
function
|
|
3060
|
-
|
|
3061
|
-
return i ? BREAKPOINTS[i - 1].id : null;
|
|
3274
|
+
/** camelCase style key → kebab-case CSS property. */
|
|
3275
|
+
function styleKeyToKebab(key) {
|
|
3276
|
+
return key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
|
|
3062
3277
|
}
|
|
3063
3278
|
/**
|
|
3064
|
-
*
|
|
3279
|
+
* kebab CSS property → the value that equals its CSS initial AND has no UA-stylesheet override for
|
|
3280
|
+
* any element the builder renders (div/section/span/i/a/img/h1–h6/p/ul). Emitting these is a pure
|
|
3281
|
+
* no-op, so they're dropped to keep the compiled CSS lean (self-describing nodes serialise every
|
|
3282
|
+
* trait, many at their default).
|
|
3065
3283
|
*
|
|
3066
|
-
*
|
|
3067
|
-
*
|
|
3068
|
-
*
|
|
3069
|
-
*
|
|
3070
|
-
*
|
|
3284
|
+
* Deliberately EXCLUDED, because a value equal to the initial can still be meaningful there:
|
|
3285
|
+
* - inherited properties (line-height, color, font-family/size/weight, text-align, letter-spacing…)
|
|
3286
|
+
* — the value may override an inherited ancestor value.
|
|
3287
|
+
* - margin / padding — UA stylesheets set them on h1–h6, p, ul, so `0` depends on a reset.
|
|
3288
|
+
* - border-* — the initial `medium` width + interaction with border-style makes it non-trivial.
|
|
3071
3289
|
*/
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3290
|
+
const NOOP_INITIAL_VALUES = {
|
|
3291
|
+
'width': ['auto'], 'height': ['auto'],
|
|
3292
|
+
'min-width': ['auto'], 'min-height': ['auto'],
|
|
3293
|
+
'max-width': ['none', 'auto'], 'max-height': ['none', 'auto'], // `auto` is invalid for max-* → inert
|
|
3294
|
+
'position': ['static'],
|
|
3295
|
+
'top': ['auto'], 'right': ['auto'], 'bottom': ['auto'], 'left': ['auto'],
|
|
3296
|
+
'opacity': ['1'], 'z-index': ['auto'],
|
|
3297
|
+
'flex-grow': ['0'], 'flex-shrink': ['1'], 'flex-basis': ['auto'],
|
|
3298
|
+
'flex-direction': ['row'], 'flex-wrap': ['nowrap'],
|
|
3299
|
+
'gap': ['0', '0px', 'normal'],
|
|
3300
|
+
'border-radius': ['0', '0px'],
|
|
3301
|
+
'aspect-ratio': ['auto'],
|
|
3302
|
+
'mix-blend-mode': ['normal'],
|
|
3303
|
+
'transform': ['none'], 'box-shadow': ['none'],
|
|
3304
|
+
// overflow initial is `visible`. A self-describing node serialises the Overflow trait's default,
|
|
3305
|
+
// so WITHOUT this the emitted `overflow: visible` would override component baselines that clip
|
|
3306
|
+
// (e.g. `.iox-card__img-wrap`, `.iox-slider`, `.iox-gallery` all set `overflow: hidden`), breaking
|
|
3307
|
+
// object-fit/aspect-ratio image clipping everywhere. A user-set hidden/scroll/auto/clip is not the
|
|
3308
|
+
// initial, so it still emits and wins.
|
|
3309
|
+
'overflow': ['visible'], 'overflow-x': ['visible'], 'overflow-y': ['visible'],
|
|
3310
|
+
'clip-path': ['none'],
|
|
3311
|
+
};
|
|
3077
3312
|
/**
|
|
3078
|
-
*
|
|
3079
|
-
*
|
|
3080
|
-
* `
|
|
3081
|
-
*
|
|
3313
|
+
* Style props a type's render-default fully REPLACES. Emitting them would produce a dead
|
|
3314
|
+
* declaration that is immediately overridden in the same rule (e.g. an Icon's serialised
|
|
3315
|
+
* `lineHeight: 1.5` followed by the forced `line-height: 1`), so callers strip them first via
|
|
3316
|
+
* {@link stripSuppressedProps} and let renderDefaultDeclarations supply the real value.
|
|
3082
3317
|
*/
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
return '';
|
|
3097
|
-
const query = breakpointQueryFor(bp, kind, containerName);
|
|
3098
|
-
return query ? `${query} {\n${css}\n}` : css;
|
|
3099
|
-
}
|
|
3100
|
-
/** `@media (max-width: 767px)` for `bp`; `null` for Base or an unknown id. */
|
|
3101
|
-
function mediaQueryFor(bp) {
|
|
3102
|
-
return breakpointQueryFor(bp, BreakpointQueryKind.Media);
|
|
3103
|
-
}
|
|
3104
|
-
/** Wrap already-compiled rules for `bp` in its media query. Base (and blank css) passes through. */
|
|
3105
|
-
function wrapInMediaQuery(bp, css) {
|
|
3106
|
-
return wrapInBreakpointQuery(bp, css, BreakpointQueryKind.Media);
|
|
3107
|
-
}
|
|
3108
|
-
function layerAt(responsive, bp) {
|
|
3109
|
-
return bp === Breakpoint.Base ? undefined : responsive?.[bp];
|
|
3110
|
-
}
|
|
3111
|
-
/** Copy defined entries of `src` onto `target`. `undefined` never overwrites a value. */
|
|
3112
|
-
function assignDefined(target, src) {
|
|
3113
|
-
if (src)
|
|
3114
|
-
for (const [k, v] of Object.entries(src))
|
|
3115
|
-
if (v !== undefined)
|
|
3116
|
-
target[k] = v;
|
|
3117
|
-
return target;
|
|
3118
|
-
}
|
|
3119
|
-
function hasKeys(map) {
|
|
3120
|
-
return !!map && Object.keys(map).length > 0;
|
|
3121
|
-
}
|
|
3122
|
-
// ─── Cascade resolution ──────────────────────────────────────────────────────
|
|
3123
|
-
/** The full effective style map at `bp`: base, then each layer down the cascade. */
|
|
3124
|
-
function resolveStyleProps(styleProps, responsive, bp) {
|
|
3125
|
-
const out = assignDefined({}, styleProps ?? undefined);
|
|
3126
|
-
for (const id of cascadeChain(bp))
|
|
3127
|
-
assignDefined(out, layerAt(responsive, id)?.styleProps);
|
|
3128
|
-
return out;
|
|
3129
|
-
}
|
|
3130
|
-
/** The effective per-state maps at `bp`. States with no values anywhere are omitted. */
|
|
3131
|
-
function resolveStateStyles(stateStyles, responsive, bp) {
|
|
3132
|
-
const out = {};
|
|
3133
|
-
const merge = (src) => {
|
|
3134
|
-
for (const [state, map] of Object.entries(src ?? {})) {
|
|
3135
|
-
out[state] = assignDefined(out[state] ?? {}, map);
|
|
3136
|
-
}
|
|
3137
|
-
};
|
|
3138
|
-
merge(stateStyles ?? undefined);
|
|
3139
|
-
for (const id of cascadeChain(bp))
|
|
3140
|
-
merge(layerAt(responsive, id)?.stateStyles);
|
|
3141
|
-
for (const state of Object.keys(out))
|
|
3142
|
-
if (!hasKeys(out[state]))
|
|
3143
|
-
delete out[state];
|
|
3318
|
+
const SUPPRESSED_STYLE_PROPS_BY_TYPE = {
|
|
3319
|
+
// An icon's line box is structurally forced to 1 — see the Icon branch of renderDefaultDeclarations.
|
|
3320
|
+
Icon: ['lineHeight'],
|
|
3321
|
+
};
|
|
3322
|
+
/** Remove the style props whose value a type's render-default always overrides. Returns the map
|
|
3323
|
+
* unchanged (same reference) when the type suppresses nothing. */
|
|
3324
|
+
function stripSuppressedProps(type, styleProps) {
|
|
3325
|
+
const suppressed = SUPPRESSED_STYLE_PROPS_BY_TYPE[type];
|
|
3326
|
+
if (!suppressed?.length)
|
|
3327
|
+
return styleProps;
|
|
3328
|
+
const out = { ...styleProps };
|
|
3329
|
+
for (const key of suppressed)
|
|
3330
|
+
delete out[key];
|
|
3144
3331
|
return out;
|
|
3145
3332
|
}
|
|
3146
3333
|
/**
|
|
3147
|
-
*
|
|
3148
|
-
*
|
|
3149
|
-
* `
|
|
3150
|
-
*
|
|
3334
|
+
* What a compiled rule is FOR — decides whether an initial value is a no-op.
|
|
3335
|
+
*
|
|
3336
|
+
* In a `Base` rule nothing is being overridden, so `width: auto` or `transform: none` changes
|
|
3337
|
+
* nothing and is dropped (see NOOP_INITIAL_VALUES). In an `Override` rule — a breakpoint layer —
|
|
3338
|
+
* the initial value is exactly how you UNDO a wider rule (`width: 520px` → `auto` on mobile), so
|
|
3339
|
+
* dropping it would silently keep the desktop value.
|
|
3151
3340
|
*/
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
names.add(state);
|
|
3158
|
-
}
|
|
3159
|
-
};
|
|
3160
|
-
add(stateStyles);
|
|
3161
|
-
for (const { id } of BREAKPOINTS)
|
|
3162
|
-
add(layerAt(responsive, id)?.stateStyles);
|
|
3163
|
-
return [...names];
|
|
3164
|
-
}
|
|
3341
|
+
var DeclarationContext;
|
|
3342
|
+
(function (DeclarationContext) {
|
|
3343
|
+
DeclarationContext["Base"] = "base";
|
|
3344
|
+
DeclarationContext["Override"] = "override";
|
|
3345
|
+
})(DeclarationContext || (DeclarationContext = {}));
|
|
3165
3346
|
/**
|
|
3166
|
-
*
|
|
3167
|
-
*
|
|
3168
|
-
*
|
|
3347
|
+
* Compile a flat, camelCase style map into an array of `"prop: value"` CSS
|
|
3348
|
+
* declarations (kebab-cased, viewport-units rewritten, empties dropped, no-op initials dropped
|
|
3349
|
+
* in a `Base` context only, optional `!important`). Callers wrap these in whatever selector
|
|
3350
|
+
* they emit.
|
|
3351
|
+
*
|
|
3352
|
+
* @param props Flat camelCase style map.
|
|
3353
|
+
* @param importantProps Keys that must carry `!important` (e.g. `position`, to beat
|
|
3354
|
+
* Angular's scoped-CSS specificity boost in the builder). A prop forced
|
|
3355
|
+
* `!important` is never dropped as a no-op — the `!important` exists to win
|
|
3356
|
+
* the cascade, so its value must always be emitted.
|
|
3357
|
+
* @param context `Override` for a rule that overrides a wider one (a breakpoint layer):
|
|
3358
|
+
* initial values are kept, because there they reset rather than no-op.
|
|
3169
3359
|
*/
|
|
3170
|
-
function
|
|
3171
|
-
const
|
|
3172
|
-
const
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
const
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
}
|
|
3183
|
-
/** True when `bp` itself sets `prop` (the style panel's orange edge). Never true for Base. */
|
|
3184
|
-
function isOverriddenAt(responsive, bp, prop, state) {
|
|
3185
|
-
const layer = layerAt(responsive, bp);
|
|
3186
|
-
const map = state ? layer?.stateStyles?.[state] : layer?.styleProps;
|
|
3187
|
-
return map?.[prop] !== undefined;
|
|
3188
|
-
}
|
|
3189
|
-
/** Number of overridden values — at `bp`, or across every breakpoint when omitted. */
|
|
3190
|
-
function countOverrides(responsive, bp) {
|
|
3191
|
-
const count = (map) => Object.values(map ?? {}).filter(v => v !== undefined).length;
|
|
3192
|
-
const ids = bp ? [bp] : BREAKPOINTS.map(d => d.id);
|
|
3193
|
-
let n = 0;
|
|
3194
|
-
for (const id of ids) {
|
|
3195
|
-
const layer = layerAt(responsive, id);
|
|
3196
|
-
n += count(layer?.styleProps);
|
|
3197
|
-
for (const map of Object.values(layer?.stateStyles ?? {}))
|
|
3198
|
-
n += count(map);
|
|
3199
|
-
}
|
|
3200
|
-
return n;
|
|
3201
|
-
}
|
|
3202
|
-
// ─── Editing (immutable) ─────────────────────────────────────────────────────
|
|
3203
|
-
function assertOverrideBreakpoint(bp) {
|
|
3204
|
-
if (bp === Breakpoint.Base) {
|
|
3205
|
-
throw new Error('Base styles live in styleProps / stateStyles, not in responsive overrides');
|
|
3206
|
-
}
|
|
3207
|
-
if (!BREAKPOINT_INDEX.has(bp))
|
|
3208
|
-
throw new Error(`Unknown breakpoint "${bp}"`);
|
|
3209
|
-
}
|
|
3210
|
-
/** Returns a new overrides object with `prop` set at `bp` (optionally within `state`). */
|
|
3211
|
-
function setOverride(responsive, bp, prop, value, state) {
|
|
3212
|
-
assertOverrideBreakpoint(bp);
|
|
3213
|
-
const layer = { ...(responsive?.[bp] ?? {}) };
|
|
3214
|
-
if (state) {
|
|
3215
|
-
layer.stateStyles = { ...(layer.stateStyles ?? {}), [state]: { ...(layer.stateStyles?.[state] ?? {}), [prop]: value } };
|
|
3216
|
-
}
|
|
3217
|
-
else {
|
|
3218
|
-
layer.styleProps = { ...(layer.styleProps ?? {}), [prop]: value };
|
|
3360
|
+
function compileDeclarations(props, importantProps, context = DeclarationContext.Base) {
|
|
3361
|
+
const out = [];
|
|
3362
|
+
for (const [k, v] of Object.entries(props)) {
|
|
3363
|
+
if (v === undefined || v === null || v === '')
|
|
3364
|
+
continue;
|
|
3365
|
+
const prop = styleKeyToKebab(k);
|
|
3366
|
+
const val = rewriteViewportUnits(String(v));
|
|
3367
|
+
const isImportant = importantProps?.has(k) ?? false;
|
|
3368
|
+
const dropsNoOps = context === DeclarationContext.Base && !isImportant;
|
|
3369
|
+
if (dropsNoOps && NOOP_INITIAL_VALUES[prop]?.includes(val.trim().toLowerCase()))
|
|
3370
|
+
continue;
|
|
3371
|
+
out.push(`${prop}: ${val}${isImportant ? ' !important' : ''}`);
|
|
3219
3372
|
}
|
|
3220
|
-
return
|
|
3373
|
+
return out;
|
|
3221
3374
|
}
|
|
3222
3375
|
/**
|
|
3223
|
-
*
|
|
3224
|
-
*
|
|
3376
|
+
* Per-type default `width` that the builder applies from each component's config
|
|
3377
|
+
* (NOT persisted in styleProps — see render-parity.md "saved node is not
|
|
3378
|
+
* self-describing"). The engine has no config classes, so it resolves the same
|
|
3379
|
+
* default from THIS map at render time. Keeping the map here (not re-typed in the
|
|
3380
|
+
* engine) is the "shared default map" decision.
|
|
3225
3381
|
*
|
|
3226
|
-
*
|
|
3227
|
-
*
|
|
3228
|
-
*
|
|
3229
|
-
|
|
3382
|
+
* ONLY the `100%` container-type defaults live here. Fixed-size config defaults
|
|
3383
|
+
* (Card 200px, Image 300px, SliderSlide 33%) are starter placeholders real content
|
|
3384
|
+
* almost always overrides — forcing them onto width-less nodes breaks layout.
|
|
3385
|
+
*/
|
|
3386
|
+
const DEFAULT_WIDTH_BY_TYPE = {
|
|
3387
|
+
Container: '100%', Repeater: '100%', SliderContainer: '100%',
|
|
3388
|
+
List: '100%', ListItem: '100%', Text: '100%', Divider: '100%', Spacer: '100%',
|
|
3389
|
+
// Section, Heading, LinkedContainer, Card, Image, SliderSlide, Button, ButtonBlock,
|
|
3390
|
+
// Icon → no forced default (their config defaults are content sizes set explicitly).
|
|
3391
|
+
};
|
|
3392
|
+
/**
|
|
3393
|
+
* Extra "render default" declarations the builder gets implicitly from live component
|
|
3394
|
+
* behaviour, which the engine must re-apply explicitly for parity. Returns `"prop: value"`
|
|
3395
|
+
* declarations to append AFTER the node's own compiled declarations.
|
|
3230
3396
|
*
|
|
3231
|
-
*
|
|
3397
|
+
* @param type Node type (registry key).
|
|
3398
|
+
* @param styleProps The node's persisted styleProps.
|
|
3399
|
+
* @param hasHoverState Whether the node has a non-empty hover / parent-hover state.
|
|
3232
3400
|
*/
|
|
3233
|
-
function
|
|
3234
|
-
|
|
3235
|
-
const
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
states[state] = rest;
|
|
3244
|
-
}
|
|
3245
|
-
if (prop === undefined || !hasKeys(states[state]))
|
|
3246
|
-
delete states[state];
|
|
3247
|
-
layer.stateStyles = hasKeys(states) ? states : undefined;
|
|
3401
|
+
function renderDefaultDeclarations(type, styleProps, hasHoverState) {
|
|
3402
|
+
const sp = styleProps ?? {};
|
|
3403
|
+
const out = [];
|
|
3404
|
+
const isOutOfFlow = sp['position'] === 'fixed' || sp['position'] === 'absolute';
|
|
3405
|
+
// Container-type default width so it fills its track. Only when width is unset — once a page
|
|
3406
|
+
// is saved with fully-resolved styleProps (self-describing nodes), width comes through
|
|
3407
|
+
// compileDeclarations and this is a no-op.
|
|
3408
|
+
if (sp['width'] === undefined) {
|
|
3409
|
+
if (isOutOfFlow) {
|
|
3410
|
+
out.push('width: 100%');
|
|
3248
3411
|
}
|
|
3249
|
-
else if (
|
|
3250
|
-
|
|
3251
|
-
layer.styleProps = hasKeys(rest) ? rest : undefined;
|
|
3412
|
+
else if (DEFAULT_WIDTH_BY_TYPE[type]) {
|
|
3413
|
+
out.push(`width: ${DEFAULT_WIDTH_BY_TYPE[type]}`);
|
|
3252
3414
|
}
|
|
3253
|
-
if (!layer.stateStyles)
|
|
3254
|
-
delete layer.stateStyles;
|
|
3255
|
-
if (!layer.styleProps)
|
|
3256
|
-
delete layer.styleProps;
|
|
3257
|
-
next[bp] = hasKeys(layer) ? layer : undefined;
|
|
3258
3415
|
}
|
|
3259
|
-
|
|
3260
|
-
|
|
3416
|
+
// min-width:0 lets container flex children shrink below content and equal-split. Emit it for
|
|
3417
|
+
// container types INDEPENDENT of where width came from (styleProps vs the default above) —
|
|
3418
|
+
// otherwise a saved page whose width is persisted would lose the equal-split. In the builder the
|
|
3419
|
+
// same rule comes from the `.container-child` wrapper; here it lands on `.iox-node` directly.
|
|
3420
|
+
if (DEFAULT_WIDTH_BY_TYPE[type] && sp['minWidth'] === undefined && !isOutOfFlow) {
|
|
3421
|
+
out.push('min-width: 0');
|
|
3261
3422
|
}
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
function groupsTouchedBy(map) {
|
|
3268
|
-
const touched = new Set();
|
|
3269
|
-
if (!map)
|
|
3270
|
-
return touched;
|
|
3271
|
-
for (const group of VIRTUAL_TRAIT_GROUPS) {
|
|
3272
|
-
if (group.keys.some(k => map[k] !== undefined))
|
|
3273
|
-
touched.add(group);
|
|
3423
|
+
// A border only renders when there is a WIDTH. Gate border-style on borderWidth — NOT on
|
|
3424
|
+
// borderColor: a node with only a border colour and no width would otherwise get the
|
|
3425
|
+
// browser's default `medium` width and show a phantom border. Absent borderWidth = no border.
|
|
3426
|
+
if (sp['borderWidth'] && sp['borderStyle'] === undefined) {
|
|
3427
|
+
out.push('border-style: solid');
|
|
3274
3428
|
}
|
|
3275
|
-
|
|
3429
|
+
// Default transition so own/parent hover animates smoothly (parity with the builder).
|
|
3430
|
+
if (hasHoverState && sp['transition'] === undefined) {
|
|
3431
|
+
out.push('transition: all 0.2s ease');
|
|
3432
|
+
}
|
|
3433
|
+
// Image defaults reapplied when unset (the config defaults aren't always persisted). height:auto
|
|
3434
|
+
// is only a DEFAULT — when the user sets an explicit height (e.g. 100%) it must WIN, so guard it
|
|
3435
|
+
// on `sp['height'] === undefined` (matching the builder, which respects a configured height). An
|
|
3436
|
+
// unset height + aspect-ratio lets the ratio drive the box.
|
|
3437
|
+
if (type === 'Image') {
|
|
3438
|
+
if (sp['aspectRatio'] === undefined)
|
|
3439
|
+
out.push('aspect-ratio: 1/1');
|
|
3440
|
+
// object-fit is a Size style trait with config default 'cover'; that default isn't persisted,
|
|
3441
|
+
// so reapply it here (matching the builder) when the node has none.
|
|
3442
|
+
if (sp['objectFit'] === undefined)
|
|
3443
|
+
out.push('object-fit: cover');
|
|
3444
|
+
// NOTE: no `height: auto` — when height is unset, `auto` is already the CSS initial value, so
|
|
3445
|
+
// emitting it is a pure no-op. A configured height comes through compileDeclarations and wins.
|
|
3446
|
+
}
|
|
3447
|
+
// Containers default to a positioning context (for absolutely-positioned children). These live
|
|
3448
|
+
// in `.iox-node` (not the component `:host`) so an explicit styleProps value always wins.
|
|
3449
|
+
if ((type === 'Container' || type === 'Section' || type === 'LinkedContainer') && sp['position'] === undefined) {
|
|
3450
|
+
out.push('position: relative');
|
|
3451
|
+
}
|
|
3452
|
+
// LinkedContainer is an <a> (inline by default) → default it to block so children lay out.
|
|
3453
|
+
if (type === 'LinkedContainer' && sp['display'] === undefined) {
|
|
3454
|
+
out.push('display: block');
|
|
3455
|
+
}
|
|
3456
|
+
// An Icon is a single FONT GLYPH, not a paragraph: it must contribute no line-box height of its
|
|
3457
|
+
// own (spacing belongs to padding/margin). But every node serialises the Typography group's
|
|
3458
|
+
// `lineHeight` default of 1.5, so icons carry `line-height: 1.5` — a 24px glyph then gets a 36px
|
|
3459
|
+
// line box and pushes the surrounding layout around.
|
|
3460
|
+
//
|
|
3461
|
+
// This is emitted UNCONDITIONALLY (not guarded on `undefined`) and callers append render-defaults
|
|
3462
|
+
// AFTER the node's own compiled declarations in the SAME rule, so it overrides a stale serialised
|
|
3463
|
+
// value for EXISTING icons too — no re-save needed. line-height is inherited, so simply omitting
|
|
3464
|
+
// it is not enough: the icon would inherit 1.5 from its container instead.
|
|
3465
|
+
if (type === 'Icon')
|
|
3466
|
+
out.push('line-height: 0');
|
|
3467
|
+
// An icon's `<i>` must shrink-wrap its glyph so it grows with font-size and doesn't overflow its
|
|
3468
|
+
// parent. Phosphor forces `display:inline-flex` on its own class; generated (fantasticon) fonts
|
|
3469
|
+
// don't, so the engine's plain `<i class="bi-x iox-node-…">` would fall back to inline and the
|
|
3470
|
+
// glyph would overflow. Emit it here (guarded so a user-set display still wins) — mirrors the
|
|
3471
|
+
// builder Icon component's `:host{display:inline-flex}`.
|
|
3472
|
+
if (type === 'Icon' && sp['display'] === undefined) {
|
|
3473
|
+
out.push('display: inline-flex', 'align-items: center', 'justify-content: center');
|
|
3474
|
+
}
|
|
3475
|
+
return out;
|
|
3276
3476
|
}
|
|
3477
|
+
|
|
3277
3478
|
/**
|
|
3278
|
-
*
|
|
3279
|
-
*
|
|
3280
|
-
*
|
|
3281
|
-
*
|
|
3479
|
+
* tree-relations — framework-agnostic helpers for reasoning about a rendered node
|
|
3480
|
+
* tree's structural relationships. Lives in @vectoriox/iox-ui because BOTH renderers
|
|
3481
|
+
* depend on iox-ui (the engine does NOT depend on iox-builder), so the same logic
|
|
3482
|
+
* drives the builder canvas AND the SSR site — no fork.
|
|
3483
|
+
* See iox-ai-guidance `architecture/builder/targeted-hover-state-plan.md`.
|
|
3282
3484
|
*/
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
if (group.output === 'transition')
|
|
3287
|
-
return ZERO_DURATIONS.has(String(merged['transitionDuration']));
|
|
3288
|
-
return true;
|
|
3485
|
+
/** The css id a node contributes to selectors: its clone `styleId`, else its own `id`. */
|
|
3486
|
+
function nodeCssId(node) {
|
|
3487
|
+
return node.styleId ?? node.id;
|
|
3289
3488
|
}
|
|
3290
3489
|
/**
|
|
3291
|
-
*
|
|
3292
|
-
*
|
|
3293
|
-
* group's cancelling value when it now composes to nothing). Untouched groups are dropped
|
|
3294
|
-
* — re-emitting the wider rule's value inside a media block is noise at best.
|
|
3490
|
+
* Path of css ids from the root down to (and including) the node whose css id === `cssId`.
|
|
3491
|
+
* Returns `null` when no node matches. First match wins (depth-first, pre-order).
|
|
3295
3492
|
*/
|
|
3296
|
-
function
|
|
3297
|
-
const
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
if (
|
|
3302
|
-
|
|
3493
|
+
function pathToCssId(nodes, cssId) {
|
|
3494
|
+
for (const node of nodes) {
|
|
3495
|
+
const ownId = nodeCssId(node);
|
|
3496
|
+
if (ownId === cssId)
|
|
3497
|
+
return [ownId];
|
|
3498
|
+
if (node.children?.length) {
|
|
3499
|
+
const childPath = pathToCssId(node.children, cssId);
|
|
3500
|
+
if (childPath)
|
|
3501
|
+
return ownId ? [ownId, ...childPath] : childPath;
|
|
3502
|
+
}
|
|
3303
3503
|
}
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3504
|
+
return null;
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* The css id of the NEAREST COMMON ANCESTOR of two nodes (identified by their css ids),
|
|
3508
|
+
* used to scope a `:has()` targeted-state selector so it stays repetition-safe.
|
|
3509
|
+
*
|
|
3510
|
+
* The result may equal `styledCssId` (the trigger sits INSIDE the styled node) or
|
|
3511
|
+
* `triggerCssId` (the styled node sits inside the trigger) — both are valid scopes;
|
|
3512
|
+
* `buildTargetedStateSelector` picks the right selector shape for each. Returns
|
|
3513
|
+
* `undefined` only when a valid scope can't exist: an id is blank, the two ids are the
|
|
3514
|
+
* same node, or they share no ancestor in `root`. Callers MUST skip emitting the rule
|
|
3515
|
+
* when this is `undefined` — never emit an unscoped (page-global) `:has()`, which would
|
|
3516
|
+
* break under repetition.
|
|
3517
|
+
*/
|
|
3518
|
+
function nearestCommonAncestorId(root, styledCssId, triggerCssId) {
|
|
3519
|
+
if (!styledCssId || !triggerCssId || styledCssId === triggerCssId)
|
|
3520
|
+
return undefined;
|
|
3521
|
+
const styledPath = pathToCssId(root, styledCssId);
|
|
3522
|
+
const triggerPath = pathToCssId(root, triggerCssId);
|
|
3523
|
+
if (!styledPath || !triggerPath)
|
|
3524
|
+
return undefined;
|
|
3525
|
+
// Walk both paths together; the last shared prefix element is the common ancestor.
|
|
3526
|
+
let common;
|
|
3527
|
+
const shorter = Math.min(styledPath.length, triggerPath.length);
|
|
3528
|
+
for (let i = 0; i < shorter; i++) {
|
|
3529
|
+
if (styledPath[i] !== triggerPath[i])
|
|
3530
|
+
break;
|
|
3531
|
+
common = styledPath[i];
|
|
3311
3532
|
}
|
|
3312
|
-
return
|
|
3533
|
+
return common;
|
|
3313
3534
|
}
|
|
3314
3535
|
/**
|
|
3315
|
-
*
|
|
3316
|
-
*
|
|
3317
|
-
* Breakpoints with nothing to emit are omitted.
|
|
3536
|
+
* Build the selector BODY (no leading `.`, matching the builder's `compile()` convention)
|
|
3537
|
+
* for a targeted state, given the resolved scope. Three topologies, one shared rule:
|
|
3318
3538
|
*
|
|
3319
|
-
*
|
|
3320
|
-
*
|
|
3321
|
-
*
|
|
3322
|
-
*
|
|
3539
|
+
* - trigger INSIDE styled (scope === styled): `iox-node-{styled}:has(.iox-node-{trigger}:{base})`
|
|
3540
|
+
* - styled INSIDE trigger (scope === trigger): `iox-node-{trigger}:{base} .iox-node-{styled}`
|
|
3541
|
+
* - separate subtrees (scope is above both): `iox-node-{scope}:has(.iox-node-{trigger}:{base}) .iox-node-{styled}`
|
|
3542
|
+
*
|
|
3543
|
+
* `scopeCssId` MUST be a value returned by `nearestCommonAncestorId` for the same pair.
|
|
3323
3544
|
*/
|
|
3324
|
-
function
|
|
3325
|
-
if (
|
|
3326
|
-
return
|
|
3327
|
-
const plan = [];
|
|
3328
|
-
for (const def of BREAKPOINTS) {
|
|
3329
|
-
const layer = layerAt(responsive, def.id);
|
|
3330
|
-
if (!layer || def.maxWidth == null)
|
|
3331
|
-
continue;
|
|
3332
|
-
const bp = def.id;
|
|
3333
|
-
const wider = parentBreakpoint(bp);
|
|
3334
|
-
const own = layer.styleProps ?? {};
|
|
3335
|
-
const widerProps = resolveStyleProps(styleProps, responsive, wider);
|
|
3336
|
-
const touchedByProps = groupsTouchedBy(own);
|
|
3337
|
-
const composedProps = composeOverride(own, widerProps, touchedByProps);
|
|
3338
|
-
const propsHere = { ...widerProps, ...own };
|
|
3339
|
-
const statesHere = resolveStateStyles(stateStyles, responsive, bp);
|
|
3340
|
-
const statesWider = resolveStateStyles(stateStyles, responsive, wider);
|
|
3341
|
-
const composedStates = {};
|
|
3342
|
-
for (const state of Object.keys(statesHere)) {
|
|
3343
|
-
const ownState = layer.stateStyles?.[state] ?? {};
|
|
3344
|
-
const touched = groupsTouchedBy(ownState);
|
|
3345
|
-
for (const group of touchedByProps)
|
|
3346
|
-
touched.add(group);
|
|
3347
|
-
const composed = composeOverride(ownState, { ...propsHere, ...(statesWider[state] ?? {}) }, touched);
|
|
3348
|
-
if (hasKeys(composed))
|
|
3349
|
-
composedStates[state] = composed;
|
|
3350
|
-
}
|
|
3351
|
-
if (hasKeys(composedProps) || hasKeys(composedStates)) {
|
|
3352
|
-
plan.push({
|
|
3353
|
-
breakpoint: bp,
|
|
3354
|
-
mediaQuery: mediaQueryFor(bp),
|
|
3355
|
-
styleProps: composedProps,
|
|
3356
|
-
stateStyles: composedStates,
|
|
3357
|
-
});
|
|
3358
|
-
}
|
|
3545
|
+
function buildTargetedStateSelector(scopeCssId, triggerCssId, styledCssId, base) {
|
|
3546
|
+
if (scopeCssId === styledCssId) {
|
|
3547
|
+
return `iox-node-${styledCssId}:has(.iox-node-${triggerCssId}:${base})`;
|
|
3359
3548
|
}
|
|
3360
|
-
|
|
3549
|
+
if (scopeCssId === triggerCssId) {
|
|
3550
|
+
return `iox-node-${triggerCssId}:${base} .iox-node-${styledCssId}`;
|
|
3551
|
+
}
|
|
3552
|
+
return `iox-node-${scopeCssId}:has(.iox-node-${triggerCssId}:${base}) .iox-node-${styledCssId}`;
|
|
3361
3553
|
}
|
|
3362
3554
|
|
|
3363
3555
|
/**
|
|
@@ -4394,5 +4586,5 @@ function effectiveTransitionId(anim) {
|
|
|
4394
4586
|
* Generated bundle index. Do not edit.
|
|
4395
4587
|
*/
|
|
4396
4588
|
|
|
4397
|
-
export { AOSService, AnalyticsService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CMSClientInfraModule, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ConsentService, ContainerComponent, ContentService, ContextStore, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, ENVIRONMENT, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, IOX_BUILDER_EVENTS, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxPageComponent, IoxPageModule, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PrivacyModalComponent, PrivacyModalService, PrivacyModelEvent, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, WebSettingsService, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildRelatedFilter, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, effectiveTransitionId, excludeSelf, feelFor, getByPath, initialSlotValues, isActionEnabled, isContextAction, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, parentBreakpoint, parseUrlList, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setOverride, stateNamesIn, stripSuppressedProps, styleKeyToKebab, variableAliases, variableItemsFrom, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
|
|
4589
|
+
export { AOSService, AnalyticsService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CMSClientInfraModule, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ConsentService, ContainerComponent, ContentService, ContextStore, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, ENVIRONMENT, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, IOX_BUILDER_EVENTS, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxPageComponent, IoxPageModule, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PrivacyModalComponent, PrivacyModalService, PrivacyModelEvent, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, WebSettingsService, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, effectiveTransitionId, excludeSelf, feelFor, getByPath, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, parentBreakpoint, parseUrlList, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, stateNamesIn, stripSuppressedProps, styleKeyToKebab, variableAliases, variableItemsFrom, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
|
|
4398
4590
|
//# sourceMappingURL=vectoriox-iox-ui.mjs.map
|