@vectoriox/iox-ui 4.25.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 +1864 -1682
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +205 -138
- package/package.json +1 -1
|
@@ -1506,1868 +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
|
-
|
|
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(' ');
|
|
1571
|
+
}
|
|
1572
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
1573
|
+
/**
|
|
1574
|
+
* Strip virtual trait keys from `raw` and emit their composed CSS equivalents.
|
|
1575
|
+
*
|
|
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`.
|
|
1580
|
+
*/
|
|
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;
|
|
1544
1587
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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}`;
|
|
1551
1614
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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;
|
|
1558
1622
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
if (!el)
|
|
1562
|
-
return;
|
|
1563
|
-
const [{ default: Swiper }, swiperModules] = await Promise.all([
|
|
1564
|
-
import('swiper'),
|
|
1565
|
-
import('swiper/modules'),
|
|
1566
|
-
]);
|
|
1567
|
-
const { Navigation, Pagination, Scrollbar, Autoplay, Keyboard, Mousewheel, Zoom, Grid, Parallax, EffectFade, EffectCube, EffectFlip, EffectCoverflow, EffectCards, EffectCreative, } = swiperModules;
|
|
1568
|
-
const modules = [];
|
|
1569
|
-
if (this.enableNavigation)
|
|
1570
|
-
modules.push(Navigation);
|
|
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);
|
|
1623
|
+
if (Object.keys(markerStyles).length) {
|
|
1624
|
+
result['__markerStyles'] = markerStyles;
|
|
1627
1625
|
}
|
|
1628
|
-
|
|
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"] }] }); }
|
|
1626
|
+
return result;
|
|
1640
1627
|
}
|
|
1641
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientSliderContainerComponent, decorators: [{
|
|
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
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 = {}));
|
|
1656
|
+
/**
|
|
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.
|
|
1662
|
+
*/
|
|
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);
|
|
1680
|
+
}
|
|
1719
1681
|
/**
|
|
1720
|
-
*
|
|
1721
|
-
* `
|
|
1722
|
-
*
|
|
1723
|
-
|
|
1724
|
-
|
|
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.
|
|
1685
|
+
*/
|
|
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;
|
|
1691
|
+
}
|
|
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;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* What a breakpoint's width is measured against.
|
|
1725
1701
|
*
|
|
1726
|
-
*
|
|
1727
|
-
* CSS
|
|
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.
|
|
1728
1707
|
*/
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1708
|
+
var BreakpointQueryKind;
|
|
1709
|
+
(function (BreakpointQueryKind) {
|
|
1710
|
+
BreakpointQueryKind["Media"] = "media";
|
|
1711
|
+
BreakpointQueryKind["Container"] = "container";
|
|
1712
|
+
})(BreakpointQueryKind || (BreakpointQueryKind = {}));
|
|
1713
|
+
/**
|
|
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).
|
|
1718
|
+
*/
|
|
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;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
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.
|
|
1770
|
+
*/
|
|
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;
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
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.
|
|
1784
|
+
*/
|
|
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 });
|
|
1732
1791
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
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;
|
|
1735
1809
|
}
|
|
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
|
-
|
|
1746
1810
|
/**
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
1749
|
-
* `
|
|
1750
|
-
*
|
|
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.
|
|
1751
1815
|
*/
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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);
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
add(stateStyles);
|
|
1825
|
+
for (const { id } of BREAKPOINTS)
|
|
1826
|
+
add(layerAt(responsive, id)?.stateStyles);
|
|
1827
|
+
return [...names];
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
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").
|
|
1833
|
+
*/
|
|
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 });
|
|
1758
1844
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1845
|
+
return chain;
|
|
1846
|
+
}
|
|
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;
|
|
1852
|
+
}
|
|
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);
|
|
1864
|
+
}
|
|
1865
|
+
return n;
|
|
1866
|
+
}
|
|
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');
|
|
1871
|
+
}
|
|
1872
|
+
if (!BREAKPOINT_INDEX.has(bp))
|
|
1873
|
+
throw new Error(`Unknown breakpoint "${bp}"`);
|
|
1874
|
+
}
|
|
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 } };
|
|
1881
|
+
}
|
|
1882
|
+
else {
|
|
1883
|
+
layer.styleProps = { ...(layer.styleProps ?? {}), [prop]: value };
|
|
1884
|
+
}
|
|
1885
|
+
return { ...(responsive ?? {}), [bp]: layer };
|
|
1886
|
+
}
|
|
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 };
|
|
1761
1893
|
}
|
|
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
1894
|
/**
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
* so the engine creates it with `vcr.createComponent` (no `hostElement` needed).
|
|
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.
|
|
1786
1897
|
*/
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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;
|
|
1790
1907
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
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;
|
|
1793
1918
|
}
|
|
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
1919
|
/**
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
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:
|
|
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.
|
|
1809
1922
|
*
|
|
1810
|
-
*
|
|
1811
|
-
*
|
|
1812
|
-
*
|
|
1813
|
-
*
|
|
1814
|
-
* single by id → GET api/:type/:id (UserResourcesController)
|
|
1815
|
-
* collection → GET api/:type?filters (UserResourcesController)
|
|
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
|
|
1816
1927
|
*
|
|
1817
|
-
*
|
|
1818
|
-
* identical for both — that's what this module owns. Each app maps the returned {@link DataSourcePlan}
|
|
1819
|
-
* onto its own transport. A single item is ALWAYS fetched by its id as a path segment — never a
|
|
1820
|
-
* `?_id=` query — to match the builder's getContentById.
|
|
1821
|
-
*/
|
|
1822
|
-
/**
|
|
1823
|
-
* Match a page route template against an actual path and extract named params.
|
|
1824
|
-
* matchRouteParams('/project/:projectId', 'project/68abc') → { projectId: '68abc' }
|
|
1825
|
-
* Both sides are normalised (leading slash stripped) and split on '/'; values are URL-decoded.
|
|
1928
|
+
* Returns `undefined` when nothing is left, so a serialiser omits the key entirely.
|
|
1826
1929
|
*/
|
|
1827
|
-
function
|
|
1828
|
-
|
|
1829
|
-
const
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
const
|
|
1833
|
-
if (
|
|
1834
|
-
|
|
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;
|
|
1835
1955
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
/**
|
|
1839
|
-
* Build the query filter for a data-source request from its filterByRouteParam / filterByQueryParams
|
|
1840
|
-
* config. `field` is filtered by the route-param value (falling back to `defaultId`); query filters
|
|
1841
|
-
* map each `field` to a query-param value (falling back to `defaultValue`), and a filter with NO
|
|
1842
|
-
* `queryParam` is a constant on `defaultValue`. Empty values are omitted so the source isn't
|
|
1843
|
-
* accidentally filtered to nothing.
|
|
1844
|
-
*/
|
|
1845
|
-
function buildDataSourceFilter(req, routeParams = {}, queryParams = {}) {
|
|
1846
|
-
const params = {};
|
|
1847
|
-
const byRoute = req?.filterByRouteParam;
|
|
1848
|
-
if (byRoute?.field && byRoute?.routeParam) {
|
|
1849
|
-
const value = routeParams[byRoute.routeParam] ?? byRoute.defaultId;
|
|
1850
|
-
if (value !== undefined && value !== null && value !== '')
|
|
1851
|
-
params[byRoute.field] = value;
|
|
1956
|
+
else {
|
|
1957
|
+
next[bp] = undefined;
|
|
1852
1958
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1959
|
+
if (!next[bp])
|
|
1960
|
+
delete next[bp];
|
|
1961
|
+
return hasKeys(next) ? next : undefined;
|
|
1962
|
+
}
|
|
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);
|
|
1861
1971
|
}
|
|
1862
|
-
return
|
|
1972
|
+
return touched;
|
|
1863
1973
|
}
|
|
1864
1974
|
/**
|
|
1865
|
-
*
|
|
1866
|
-
*
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
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.
|
|
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.
|
|
1871
1979
|
*/
|
|
1872
|
-
function
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
const value = routeParams[byRoute.routeParam] ?? byRoute.defaultId;
|
|
1879
|
-
return value !== undefined && value !== null && value !== '' ? String(value) : null;
|
|
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;
|
|
1880
1986
|
}
|
|
1881
1987
|
/**
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
* the
|
|
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.
|
|
1886
1992
|
*/
|
|
1887
|
-
function
|
|
1888
|
-
const
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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' };
|
|
1915
|
-
}
|
|
1916
|
-
case 'variable':
|
|
1917
|
-
// A VARIABLE is a page-local resource whose value is WRITTEN at runtime by an interaction
|
|
1918
|
-
// (setContext), never fetched. It resolves to nothing here on purpose: the page renders
|
|
1919
|
-
// its empty state until something writes it. `request.variable.itemsFrom` only names the
|
|
1920
|
-
// source whose items it holds, so the bind picker can suggest fields.
|
|
1921
|
-
return { kind: 'none' };
|
|
1922
|
-
default:
|
|
1923
|
-
return { kind: 'none' };
|
|
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;
|
|
1924
2008
|
}
|
|
2009
|
+
return out;
|
|
1925
2010
|
}
|
|
1926
2011
|
/**
|
|
1927
|
-
* The
|
|
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.
|
|
1928
2015
|
*
|
|
1929
|
-
*
|
|
1930
|
-
*
|
|
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.
|
|
1931
2020
|
*/
|
|
1932
|
-
function
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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
|
+
}
|
|
1945
2056
|
}
|
|
1946
|
-
return
|
|
2057
|
+
return plan;
|
|
1947
2058
|
}
|
|
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']);
|
|
1948
2073
|
/**
|
|
1949
|
-
*
|
|
1950
|
-
*
|
|
1951
|
-
*
|
|
2074
|
+
* Build Swiper's mobile-first `breakpoints` map from IOX's desktop-first layers.
|
|
2075
|
+
*
|
|
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.
|
|
2079
|
+
*
|
|
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.
|
|
1952
2086
|
*/
|
|
1953
|
-
function
|
|
1954
|
-
const
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
const segments = path.replace(/\[(\w+)\]/g, '.$1').split('.').filter(Boolean);
|
|
1965
|
-
let cur = obj;
|
|
1966
|
-
for (const seg of segments) {
|
|
1967
|
-
if (cur == null)
|
|
1968
|
-
return undefined;
|
|
1969
|
-
cur = cur[seg];
|
|
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;
|
|
1970
2098
|
}
|
|
1971
|
-
return
|
|
2099
|
+
return map;
|
|
1972
2100
|
}
|
|
1973
2101
|
/**
|
|
1974
|
-
* The
|
|
1975
|
-
*
|
|
1976
|
-
* - No `sourcePath`: the resolved source IS the list. An array passes through; a lone object degrades
|
|
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.
|
|
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.
|
|
1986
2104
|
*/
|
|
1987
|
-
function
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
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
|
-
}
|
|
2105
|
+
function mergeSwiperBreakpoints(derived, advancedJson) {
|
|
2106
|
+
let advanced;
|
|
2107
|
+
if (advancedJson) {
|
|
2108
|
+
try {
|
|
2109
|
+
advanced = JSON.parse(advancedJson);
|
|
2110
|
+
}
|
|
2111
|
+
catch {
|
|
2112
|
+
advanced = undefined;
|
|
2000
2113
|
}
|
|
2001
2114
|
}
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
const
|
|
2007
|
-
|
|
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 ?? {}) };
|
|
2008
2123
|
}
|
|
2009
|
-
|
|
2010
|
-
return resolved;
|
|
2011
|
-
return resolved == null ? [] : [resolved];
|
|
2124
|
+
return Object.keys(out).length ? out : undefined;
|
|
2012
2125
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
collectReferencedAliases(n.children, acc);
|
|
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];
|
|
2029
2141
|
}
|
|
2030
|
-
return
|
|
2142
|
+
return out;
|
|
2031
2143
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
const
|
|
2035
|
-
|
|
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;
|
|
2036
2150
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2151
|
+
|
|
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;
|
|
2166
|
+
}
|
|
2167
|
+
constructor(platformId) {
|
|
2168
|
+
this.platformId = platformId;
|
|
2169
|
+
this.nodeId = '';
|
|
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);
|
|
2209
|
+
}
|
|
2210
|
+
ngOnInit() { }
|
|
2211
|
+
ngAfterViewInit() {
|
|
2212
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
2213
|
+
// Defer so all child slides are rendered first
|
|
2214
|
+
requestAnimationFrame(() => this._initSwiper());
|
|
2051
2215
|
}
|
|
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
2216
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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=`.
|
|
2066
|
-
*/
|
|
2067
|
-
function dataSourcePlanToRequest(plan, endpoints) {
|
|
2068
|
-
const baseFor = (domain) => (domain === 'shop' ? endpoints.shopBase : endpoints.cmsBase);
|
|
2069
|
-
switch (plan.kind) {
|
|
2070
|
-
case 'by-id':
|
|
2071
|
-
return { url: `${baseFor(plan.domain)}${plan.resource}/${encodeURIComponent(plan.id)}`, params: {} };
|
|
2072
|
-
case 'collection': {
|
|
2073
|
-
const params = { ...plan.filter };
|
|
2074
|
-
if (plan.limit)
|
|
2075
|
-
params['limit'] = plan.limit;
|
|
2076
|
-
return { url: `${baseFor(plan.domain)}${plan.resource}`, params };
|
|
2217
|
+
ngOnDestroy() {
|
|
2218
|
+
if (this._swiper) {
|
|
2219
|
+
this._swiper.destroy(true, true);
|
|
2220
|
+
this._swiper = null;
|
|
2221
|
+
this.instanceRegistry.unregister(this.nodeId);
|
|
2077
2222
|
}
|
|
2078
|
-
case 'external':
|
|
2079
|
-
return { url: plan.endpoint, params: {} };
|
|
2080
|
-
default:
|
|
2081
|
-
return null;
|
|
2082
2223
|
}
|
|
2224
|
+
async _initSwiper() {
|
|
2225
|
+
const el = this.swiperEl?.nativeElement;
|
|
2226
|
+
if (!el)
|
|
2227
|
+
return;
|
|
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);
|
|
2295
|
+
}
|
|
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>
|
|
2306
|
+
</div>
|
|
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"] }] }); }
|
|
2083
2308
|
}
|
|
2309
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: ClientSliderContainerComponent, decorators: [{
|
|
2310
|
+
type: Component,
|
|
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>
|
|
2320
|
+
</div>
|
|
2321
|
+
`, standalone: false, styles: [".swiper{width:100%;height:100%;box-sizing:border-box}\n"] }]
|
|
2322
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
2323
|
+
type: Inject,
|
|
2324
|
+
args: [PLATFORM_ID]
|
|
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: [{
|
|
2340
|
+
type: Input
|
|
2341
|
+
}], effect: [{
|
|
2342
|
+
type: Input
|
|
2343
|
+
}], grabCursor: [{
|
|
2344
|
+
type: Input
|
|
2345
|
+
}], centeredSlides: [{
|
|
2346
|
+
type: Input
|
|
2347
|
+
}], initialSlide: [{
|
|
2348
|
+
type: Input
|
|
2349
|
+
}], enableNavigation: [{
|
|
2350
|
+
type: Input
|
|
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
|
+
}] } });
|
|
2084
2390
|
|
|
2085
2391
|
/**
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
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.
|
|
2094
|
-
*
|
|
2095
|
-
* `resolveGalleryUrls` is the SINGLE place both the builder canvas and the SSR/runtime engine turn a
|
|
2096
|
-
* node's config into the final `string[]` the crossfade loop consumes — so both sides show the same
|
|
2097
|
-
* images. It is pure and transport-agnostic; never fork it. It reuses `getByPath` (the same path
|
|
2098
|
-
* reader used by data-source resolving) so binding paths behave identically everywhere.
|
|
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.
|
|
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.
|
|
2103
2397
|
*
|
|
2104
|
-
*
|
|
2105
|
-
*
|
|
2106
|
-
* @param ctx render context for per-item bindings (item/route data). Defaults to `{}`.
|
|
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).
|
|
2107
2400
|
*/
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
return (bound.items ?? [])
|
|
2112
|
-
.map(item => (field ? getByPath(item, field) : item))
|
|
2113
|
-
.map(coerceUrl)
|
|
2114
|
-
.filter((u) => !!u);
|
|
2401
|
+
class ClientSliderSlideComponent {
|
|
2402
|
+
constructor() {
|
|
2403
|
+
this.nodeId = '';
|
|
2115
2404
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
.map(img => (img?.binding ? getByPath(ctx, img.binding) : img?.url))
|
|
2119
|
-
.map(coerceUrl)
|
|
2120
|
-
.filter((u) => !!u);
|
|
2121
|
-
}
|
|
2122
|
-
/**
|
|
2123
|
-
* A plain string is a valid entry — a pasted URL list, an older saved node, or a bound field that
|
|
2124
|
-
* yielded a URL directly. Accepting it HERE (not in one consumer) is what keeps the two render paths
|
|
2125
|
-
* honest: `ClientGalleryComponent` used to normalise privately, so the engine's string-renderer
|
|
2126
|
-
* fallback — which calls this function directly — silently dropped every string-shaped image and
|
|
2127
|
-
* rendered an empty gallery. One resolver, one answer. See gallery-component-plan.md.
|
|
2128
|
-
*/
|
|
2129
|
-
function normalizeGalleryImage(image) {
|
|
2130
|
-
if (typeof image === 'string')
|
|
2131
|
-
return { url: image };
|
|
2132
|
-
return image ?? undefined;
|
|
2133
|
-
}
|
|
2134
|
-
/** A resolved value becomes a URL only if it's a non-empty string; everything else drops out. */
|
|
2135
|
-
function coerceUrl(value) {
|
|
2136
|
-
if (typeof value !== 'string')
|
|
2137
|
-
return null;
|
|
2138
|
-
const trimmed = value.trim();
|
|
2139
|
-
return trimmed ? trimmed : null;
|
|
2140
|
-
}
|
|
2141
|
-
/**
|
|
2142
|
-
* Parse a pasted block of URLs (newline- and/or comma-separated) into `GalleryImage[]`. Used by the
|
|
2143
|
-
* MediaList trait editor's "paste URL list" affordance. Pure so it can be unit-tested + reused.
|
|
2144
|
-
*/
|
|
2145
|
-
function parseUrlList(text) {
|
|
2146
|
-
if (!text)
|
|
2147
|
-
return [];
|
|
2148
|
-
return text
|
|
2149
|
-
.split(/[\n,]+/)
|
|
2150
|
-
.map(s => s.trim())
|
|
2151
|
-
.filter(Boolean)
|
|
2152
|
-
.map(url => ({ url }));
|
|
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"] }); }
|
|
2153
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
|
+
}] } });
|
|
2154
2417
|
|
|
2155
2418
|
/**
|
|
2156
|
-
*
|
|
2157
|
-
*
|
|
2158
|
-
*
|
|
2159
|
-
* the
|
|
2160
|
-
*
|
|
2161
|
-
* v1 ships only `fade` (the look ported from `sites/interior-design` fade-gallery). Adding `slide`,
|
|
2162
|
-
* `zoom`, `kenburns`, … later is a new entry here + its CSS — NO component refactor, because the
|
|
2163
|
-
* component only ever asks `resolveGalleryEffect(id).buildSlideStyle(state)` for per-slide inline CSS.
|
|
2164
|
-
*
|
|
2165
|
-
* See iox-ai-guidance/architecture/builder/gallery-component-plan.md.
|
|
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>`).
|
|
2166
2423
|
*/
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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"] }); }
|
|
2197
2465
|
}
|
|
2198
|
-
|
|
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: [{
|
|
2470
|
+
type: Input
|
|
2471
|
+
}], childrenHost: [{
|
|
2472
|
+
type: ViewChild,
|
|
2473
|
+
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
2474
|
+
}] } });
|
|
2199
2475
|
|
|
2200
2476
|
/**
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
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:
|
|
2205
2481
|
*
|
|
2206
|
-
*
|
|
2207
|
-
*
|
|
2208
|
-
*
|
|
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)
|
|
2209
2488
|
*
|
|
2210
|
-
*
|
|
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.
|
|
2211
2493
|
*/
|
|
2212
|
-
/**
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
constructor(opts) {
|
|
2226
|
-
this.count = 0;
|
|
2227
|
-
this.currentIndex = 0;
|
|
2228
|
-
this.fadingOutIndex = null;
|
|
2229
|
-
this.firstCycleActive = false;
|
|
2230
|
-
this.cycleTimer = null;
|
|
2231
|
-
this.fadeTimer = null;
|
|
2232
|
-
this.frameHandle = null;
|
|
2233
|
-
/** Bumped on every (re)start so a stale timer callback from a previous run is ignored. */
|
|
2234
|
-
this.runToken = 0;
|
|
2235
|
-
this.running = false;
|
|
2236
|
-
this.scheduler = opts.scheduler ?? DEFAULT_GALLERY_SCHEDULER;
|
|
2237
|
-
this.showMs = opts.showMs;
|
|
2238
|
-
this.fadeMs = opts.fadeMs;
|
|
2239
|
-
this.onChange = opts.onChange;
|
|
2240
|
-
}
|
|
2241
|
-
get state() {
|
|
2242
|
-
return {
|
|
2243
|
-
currentIndex: this.currentIndex,
|
|
2244
|
-
fadingOutIndex: this.fadingOutIndex,
|
|
2245
|
-
firstCycleActive: this.firstCycleActive,
|
|
2246
|
-
};
|
|
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] ?? '');
|
|
2247
2507
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
if (token !== this.runToken || !this.running)
|
|
2265
|
-
return;
|
|
2266
|
-
this.firstCycleActive = true;
|
|
2267
|
-
this.emit();
|
|
2268
|
-
if (this.count > 1)
|
|
2269
|
-
this.scheduleCycle(token);
|
|
2270
|
-
});
|
|
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;
|
|
2271
2524
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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;
|
|
2276
2533
|
}
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
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;
|
|
2284
2577
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2578
|
+
case 'external-api': {
|
|
2579
|
+
const endpoint = ds.request?.endpoint;
|
|
2580
|
+
return endpoint ? { kind: 'external', endpoint, mode } : { kind: 'none' };
|
|
2288
2581
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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' };
|
|
2292
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' };
|
|
2293
2596
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
this.fadingOutIndex = null;
|
|
2307
|
-
this.emit();
|
|
2308
|
-
this.scheduleCycle(token);
|
|
2309
|
-
}, this.fadeMs);
|
|
2310
|
-
}, this.showMs);
|
|
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);
|
|
2311
2609
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
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;
|
|
2314
2617
|
}
|
|
2618
|
+
return undefined;
|
|
2315
2619
|
}
|
|
2316
|
-
|
|
2317
2620
|
/**
|
|
2318
|
-
*
|
|
2319
|
-
*
|
|
2320
|
-
*
|
|
2321
|
-
* animation logic. See iox-ai-guidance/architecture/builder/gallery-component-plan.md.
|
|
2322
|
-
*
|
|
2323
|
-
* Population: v1 drives the loop from the `images` input (manual mode). Data-source binding
|
|
2324
|
-
* (`source`/`sourcePath`) is Phase 3 — this component already resolves through `resolveGalleryUrls`,
|
|
2325
|
-
* so that only adds a bound-source input, not new render logic.
|
|
2326
|
-
*
|
|
2327
|
-
* SSR / no-JS: on the server the loop never starts, so the **first image renders visible** as a static
|
|
2328
|
-
* frame (SEO + no-JS baseline). In the browser `ngAfterViewInit` starts the loop, which begins on the
|
|
2329
|
-
* same index 0 — so the static frame and the first live frame match and there is no hydration flash.
|
|
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.
|
|
2330
2624
|
*/
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
this.nodeId = '';
|
|
2349
|
-
this.effect = 'fade';
|
|
2350
|
-
this.autoStart = true;
|
|
2351
|
-
/** Layout mode — only `'stack'` is rendered in v1 (`'grid'` scaffolded for later). */
|
|
2352
|
-
this.layout = 'stack';
|
|
2353
|
-
// ── Inputs that mutate live state use SETTERS, not ngOnChanges ──────────────────────────────
|
|
2354
|
-
// The builder trait panel pushes edits via DIRECT property assignment (`instance.images = v`),
|
|
2355
|
-
// which does NOT trigger ngOnChanges — so a gallery that recomputed in ngOnChanges never refreshed
|
|
2356
|
-
// when you added an image on the canvas. A setter fires on BOTH direct assignment (builder panel)
|
|
2357
|
-
// and setInput (SSR engine / render.directive), so it's the one path that works everywhere.
|
|
2358
|
-
this._images = [];
|
|
2359
|
-
this._showDuration = 4000;
|
|
2360
|
-
this._fadeDuration = 1000;
|
|
2361
|
-
/** The flat, display-ready URLs the loop cycles. Recomputed whenever `images` is set. */
|
|
2362
|
-
this.urls = [];
|
|
2363
|
-
this.currentIndex = 0;
|
|
2364
|
-
this.fadingOutIndex = null;
|
|
2365
|
-
this.started = false;
|
|
2366
|
-
/** True once the loop has been started (in ngAfterViewInit) — gates the setter-driven restart. */
|
|
2367
|
-
this.loopActive = false;
|
|
2368
|
-
this.loop = null;
|
|
2369
|
-
this.isBrowser = isPlatformBrowser(platformId);
|
|
2370
|
-
}
|
|
2371
|
-
ngAfterViewInit() {
|
|
2372
|
-
if (this.isBrowser && this.autoStart) {
|
|
2373
|
-
this.startLoop();
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
ngOnDestroy() {
|
|
2377
|
-
this.loop?.stop();
|
|
2378
|
-
this.loop = null;
|
|
2379
|
-
this.loopActive = false;
|
|
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];
|
|
2380
2642
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2643
|
+
return cur;
|
|
2644
|
+
}
|
|
2645
|
+
/**
|
|
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 → `[]`.
|
|
2652
|
+
*/
|
|
2653
|
+
/**
|
|
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.
|
|
2658
|
+
*/
|
|
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
|
+
}
|
|
2388
2672
|
}
|
|
2389
|
-
return resolveGalleryEffect(this.effect).buildSlideStyle({
|
|
2390
|
-
active: i === this.currentIndex,
|
|
2391
|
-
fadingOut: i === this.fadingOutIndex,
|
|
2392
|
-
firstCycleActive: true,
|
|
2393
|
-
durations: { showMs: this.showDuration, fadeMs: this.fadeDuration },
|
|
2394
|
-
});
|
|
2395
2673
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
showMs: this.showDuration,
|
|
2403
|
-
fadeMs: this.fadeDuration,
|
|
2404
|
-
onChange: state => {
|
|
2405
|
-
this.currentIndex = state.currentIndex;
|
|
2406
|
-
this.fadingOutIndex = state.fadingOutIndex;
|
|
2407
|
-
this.started = state.firstCycleActive;
|
|
2408
|
-
this.cdr.markForCheck();
|
|
2409
|
-
},
|
|
2410
|
-
});
|
|
2411
|
-
this.loop.start(this.urls.length);
|
|
2674
|
+
return resolved;
|
|
2675
|
+
}
|
|
2676
|
+
function resolveRepeaterItems(resolved, sourcePath) {
|
|
2677
|
+
if (sourcePath) {
|
|
2678
|
+
const nested = getByPath(resolved, sourcePath);
|
|
2679
|
+
return Array.isArray(nested) ? nested : [];
|
|
2412
2680
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
<div *ngFor="let url of urls; index as i"
|
|
2417
|
-
class="iox-gallery__img"
|
|
2418
|
-
[style.background-image]="'url(' + url + ')'"
|
|
2419
|
-
[ngStyle]="slideStyle(i)"></div>
|
|
2420
|
-
<div class="iox-gallery__overlay"></div>
|
|
2421
|
-
</div>
|
|
2422
|
-
`, 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"] }] }); }
|
|
2681
|
+
if (Array.isArray(resolved))
|
|
2682
|
+
return resolved;
|
|
2683
|
+
return resolved == null ? [] : [resolved];
|
|
2423
2684
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
ClientListComponent,
|
|
2472
|
-
ClientListItemComponent,
|
|
2473
|
-
ClientGalleryComponent,
|
|
2474
|
-
];
|
|
2475
|
-
class IoxBuilderComponentsModule {
|
|
2476
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2477
|
-
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, declarations: [BuilderImageComponent,
|
|
2478
|
-
BuilderSpacerComponent,
|
|
2479
|
-
CardComponent,
|
|
2480
|
-
BuilderDividerComponent,
|
|
2481
|
-
BuilderIconComponent,
|
|
2482
|
-
BuilderLinkComponent,
|
|
2483
|
-
BuilderButtonComponent,
|
|
2484
|
-
BuilderHeadingComponent,
|
|
2485
|
-
TextBlockComponent,
|
|
2486
|
-
SectionComponent,
|
|
2487
|
-
ContainerComponent,
|
|
2488
|
-
LinkedContainerComponent,
|
|
2489
|
-
ClientRepeaterComponent,
|
|
2490
|
-
ButtonBlockComponent,
|
|
2491
|
-
ClientSliderContainerComponent,
|
|
2492
|
-
ClientSliderSlideComponent,
|
|
2493
|
-
ClientListComponent,
|
|
2494
|
-
ClientListItemComponent,
|
|
2495
|
-
ClientGalleryComponent], imports: [CommonModule], exports: [BuilderImageComponent,
|
|
2496
|
-
BuilderSpacerComponent,
|
|
2497
|
-
CardComponent,
|
|
2498
|
-
BuilderDividerComponent,
|
|
2499
|
-
BuilderIconComponent,
|
|
2500
|
-
BuilderLinkComponent,
|
|
2501
|
-
BuilderButtonComponent,
|
|
2502
|
-
BuilderHeadingComponent,
|
|
2503
|
-
TextBlockComponent,
|
|
2504
|
-
SectionComponent,
|
|
2505
|
-
ContainerComponent,
|
|
2506
|
-
LinkedContainerComponent,
|
|
2507
|
-
ClientRepeaterComponent,
|
|
2508
|
-
ButtonBlockComponent,
|
|
2509
|
-
ClientSliderContainerComponent,
|
|
2510
|
-
ClientSliderSlideComponent,
|
|
2511
|
-
ClientListComponent,
|
|
2512
|
-
ClientListItemComponent,
|
|
2513
|
-
ClientGalleryComponent] }); }
|
|
2514
|
-
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, imports: [CommonModule] }); }
|
|
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));
|
|
2515
2732
|
}
|
|
2516
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxBuilderComponentsModule, decorators: [{
|
|
2517
|
-
type: NgModule,
|
|
2518
|
-
args: [{
|
|
2519
|
-
declarations: COMPONENTS,
|
|
2520
|
-
imports: [CommonModule],
|
|
2521
|
-
exports: COMPONENTS,
|
|
2522
|
-
}]
|
|
2523
|
-
}] });
|
|
2524
|
-
|
|
2525
2733
|
/**
|
|
2526
|
-
*
|
|
2527
|
-
*
|
|
2528
|
-
*
|
|
2529
|
-
*
|
|
2530
|
-
* All 13 builder components are registered here:
|
|
2531
|
-
* - Leaf (Phase 2a): Image, Spacer, Card, Divider, Icon
|
|
2532
|
-
* - Inline-editable (Phase 2b): Link, Button, Heading, Text
|
|
2533
|
-
* - Layout shells (Phase 2c): Section, Container, LinkedContainer
|
|
2534
|
-
* - Repeater (Phase 2d): ClientRepeaterComponent
|
|
2535
|
-
*
|
|
2536
|
-
* Section/Container/LinkedContainer/Repeater expose childrenHost: ViewContainerRef
|
|
2537
|
-
* for ClientRenderDirective to insert child components into.
|
|
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=`.
|
|
2538
2738
|
*/
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
['Repeater', ClientRepeaterComponent],
|
|
2555
|
-
['ButtonBlock', ButtonBlockComponent],
|
|
2556
|
-
['SliderContainer', ClientSliderContainerComponent],
|
|
2557
|
-
['SliderSlide', ClientSliderSlideComponent],
|
|
2558
|
-
['List', ClientListComponent],
|
|
2559
|
-
['ListItem', ClientListItemComponent],
|
|
2560
|
-
['Gallery', ClientGalleryComponent],
|
|
2561
|
-
]);
|
|
2562
|
-
}
|
|
2563
|
-
getComponent(type) {
|
|
2564
|
-
return this.map.get(type);
|
|
2565
|
-
}
|
|
2566
|
-
hasComponent(type) {
|
|
2567
|
-
return this.map.has(type);
|
|
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;
|
|
2568
2754
|
}
|
|
2569
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxComponentRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2570
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxComponentRegistryService, providedIn: 'root' }); }
|
|
2571
2755
|
}
|
|
2572
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxComponentRegistryService, decorators: [{
|
|
2573
|
-
type: Injectable,
|
|
2574
|
-
args: [{ providedIn: 'root' }]
|
|
2575
|
-
}] });
|
|
2576
2756
|
|
|
2577
2757
|
/**
|
|
2578
|
-
*
|
|
2579
|
-
* CSS declarations. Framework-agnostic (no Angular): consumed by both renderers.
|
|
2758
|
+
* Gallery — data model + the SHARED "what are the image URLs" resolver.
|
|
2580
2759
|
*
|
|
2581
|
-
*
|
|
2582
|
-
*
|
|
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.
|
|
2583
2766
|
*
|
|
2584
|
-
*
|
|
2585
|
-
*
|
|
2586
|
-
*
|
|
2587
|
-
*
|
|
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.
|
|
2588
2771
|
*/
|
|
2589
2772
|
/**
|
|
2590
|
-
*
|
|
2591
|
-
*
|
|
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.
|
|
2592
2775
|
*
|
|
2593
|
-
*
|
|
2594
|
-
*
|
|
2595
|
-
*
|
|
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 `{}`.
|
|
2596
2779
|
*/
|
|
2597
|
-
function
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
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);
|
|
2787
|
+
}
|
|
2788
|
+
return (images ?? [])
|
|
2789
|
+
.map(normalizeGalleryImage)
|
|
2790
|
+
.map(img => (img?.binding ? getByPath(ctx, img.binding) : img?.url))
|
|
2791
|
+
.map(coerceUrl)
|
|
2792
|
+
.filter((u) => !!u);
|
|
2605
2793
|
}
|
|
2606
2794
|
/**
|
|
2607
|
-
*
|
|
2608
|
-
*
|
|
2609
|
-
*
|
|
2610
|
-
*
|
|
2611
|
-
*
|
|
2612
|
-
* Deliberately EXCLUDED, because a value equal to the initial can still be meaningful there:
|
|
2613
|
-
* - inherited properties (line-height, color, font-family/size/weight, text-align, letter-spacing…)
|
|
2614
|
-
* — the value may override an inherited ancestor value.
|
|
2615
|
-
* - margin / padding — UA stylesheets set them on h1–h6, p, ul, so `0` depends on a reset.
|
|
2616
|
-
* - border-* — the initial `medium` width + interaction with border-style makes it non-trivial.
|
|
2617
|
-
*/
|
|
2618
|
-
const NOOP_INITIAL_VALUES = {
|
|
2619
|
-
'width': ['auto'], 'height': ['auto'],
|
|
2620
|
-
'min-width': ['auto'], 'min-height': ['auto'],
|
|
2621
|
-
'max-width': ['none', 'auto'], 'max-height': ['none', 'auto'], // `auto` is invalid for max-* → inert
|
|
2622
|
-
'position': ['static'],
|
|
2623
|
-
'top': ['auto'], 'right': ['auto'], 'bottom': ['auto'], 'left': ['auto'],
|
|
2624
|
-
'opacity': ['1'], 'z-index': ['auto'],
|
|
2625
|
-
'flex-grow': ['0'], 'flex-shrink': ['1'], 'flex-basis': ['auto'],
|
|
2626
|
-
'flex-direction': ['row'], 'flex-wrap': ['nowrap'],
|
|
2627
|
-
'gap': ['0', '0px', 'normal'],
|
|
2628
|
-
'border-radius': ['0', '0px'],
|
|
2629
|
-
'aspect-ratio': ['auto'],
|
|
2630
|
-
'mix-blend-mode': ['normal'],
|
|
2631
|
-
'transform': ['none'], 'box-shadow': ['none'],
|
|
2632
|
-
// overflow initial is `visible`. A self-describing node serialises the Overflow trait's default,
|
|
2633
|
-
// so WITHOUT this the emitted `overflow: visible` would override component baselines that clip
|
|
2634
|
-
// (e.g. `.iox-card__img-wrap`, `.iox-slider`, `.iox-gallery` all set `overflow: hidden`), breaking
|
|
2635
|
-
// object-fit/aspect-ratio image clipping everywhere. A user-set hidden/scroll/auto/clip is not the
|
|
2636
|
-
// initial, so it still emits and wins.
|
|
2637
|
-
'overflow': ['visible'], 'overflow-x': ['visible'], 'overflow-y': ['visible'],
|
|
2638
|
-
'clip-path': ['none'],
|
|
2639
|
-
};
|
|
2640
|
-
/**
|
|
2641
|
-
* Style props a type's render-default fully REPLACES. Emitting them would produce a dead
|
|
2642
|
-
* declaration that is immediately overridden in the same rule (e.g. an Icon's serialised
|
|
2643
|
-
* `lineHeight: 1.5` followed by the forced `line-height: 1`), so callers strip them first via
|
|
2644
|
-
* {@link stripSuppressedProps} and let renderDefaultDeclarations supply the real value.
|
|
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.
|
|
2645
2800
|
*/
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
/** Remove the style props whose value a type's render-default always overrides. Returns the map
|
|
2651
|
-
* unchanged (same reference) when the type suppresses nothing. */
|
|
2652
|
-
function stripSuppressedProps(type, styleProps) {
|
|
2653
|
-
const suppressed = SUPPRESSED_STYLE_PROPS_BY_TYPE[type];
|
|
2654
|
-
if (!suppressed?.length)
|
|
2655
|
-
return styleProps;
|
|
2656
|
-
const out = { ...styleProps };
|
|
2657
|
-
for (const key of suppressed)
|
|
2658
|
-
delete out[key];
|
|
2659
|
-
return out;
|
|
2801
|
+
function normalizeGalleryImage(image) {
|
|
2802
|
+
if (typeof image === 'string')
|
|
2803
|
+
return { url: image };
|
|
2804
|
+
return image ?? undefined;
|
|
2660
2805
|
}
|
|
2661
|
-
/**
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
DeclarationContext["Base"] = "base";
|
|
2672
|
-
DeclarationContext["Override"] = "override";
|
|
2673
|
-
})(DeclarationContext || (DeclarationContext = {}));
|
|
2674
|
-
/**
|
|
2675
|
-
* Compile a flat, camelCase style map into an array of `"prop: value"` CSS
|
|
2676
|
-
* declarations (kebab-cased, viewport-units rewritten, empties dropped, no-op initials dropped
|
|
2677
|
-
* in a `Base` context only, optional `!important`). Callers wrap these in whatever selector
|
|
2678
|
-
* they emit.
|
|
2679
|
-
*
|
|
2680
|
-
* @param props Flat camelCase style map.
|
|
2681
|
-
* @param importantProps Keys that must carry `!important` (e.g. `position`, to beat
|
|
2682
|
-
* Angular's scoped-CSS specificity boost in the builder). A prop forced
|
|
2683
|
-
* `!important` is never dropped as a no-op — the `!important` exists to win
|
|
2684
|
-
* the cascade, so its value must always be emitted.
|
|
2685
|
-
* @param context `Override` for a rule that overrides a wider one (a breakpoint layer):
|
|
2686
|
-
* initial values are kept, because there they reset rather than no-op.
|
|
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.
|
|
2687
2816
|
*/
|
|
2688
|
-
function
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
const dropsNoOps = context === DeclarationContext.Base && !isImportant;
|
|
2697
|
-
if (dropsNoOps && NOOP_INITIAL_VALUES[prop]?.includes(val.trim().toLowerCase()))
|
|
2698
|
-
continue;
|
|
2699
|
-
out.push(`${prop}: ${val}${isImportant ? ' !important' : ''}`);
|
|
2700
|
-
}
|
|
2701
|
-
return out;
|
|
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 }));
|
|
2702
2825
|
}
|
|
2826
|
+
|
|
2703
2827
|
/**
|
|
2704
|
-
*
|
|
2705
|
-
*
|
|
2706
|
-
*
|
|
2707
|
-
*
|
|
2708
|
-
* engine) is the "shared default map" decision.
|
|
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.
|
|
2709
2832
|
*
|
|
2710
|
-
*
|
|
2711
|
-
*
|
|
2712
|
-
*
|
|
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.
|
|
2713
2838
|
*/
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
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,
|
|
2719
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
|
+
|
|
2720
2872
|
/**
|
|
2721
|
-
*
|
|
2722
|
-
*
|
|
2723
|
-
*
|
|
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.
|
|
2724
2877
|
*
|
|
2725
|
-
*
|
|
2726
|
-
*
|
|
2727
|
-
*
|
|
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.
|
|
2728
2883
|
*/
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
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;
|
|
2739
2930
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
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;
|
|
2742
2964
|
}
|
|
2743
2965
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
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);
|
|
2750
2983
|
}
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
// browser's default `medium` width and show a phantom border. Absent borderWidth = no border.
|
|
2754
|
-
if (sp['borderWidth'] && sp['borderStyle'] === undefined) {
|
|
2755
|
-
out.push('border-style: solid');
|
|
2984
|
+
emit() {
|
|
2985
|
+
this.onChange(this.state);
|
|
2756
2986
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
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);
|
|
2760
3042
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
if (type === 'Image') {
|
|
2766
|
-
if (sp['aspectRatio'] === undefined)
|
|
2767
|
-
out.push('aspect-ratio: 1/1');
|
|
2768
|
-
// object-fit is a Size style trait with config default 'cover'; that default isn't persisted,
|
|
2769
|
-
// so reapply it here (matching the builder) when the node has none.
|
|
2770
|
-
if (sp['objectFit'] === undefined)
|
|
2771
|
-
out.push('object-fit: cover');
|
|
2772
|
-
// NOTE: no `height: auto` — when height is unset, `auto` is already the CSS initial value, so
|
|
2773
|
-
// 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
|
+
}
|
|
2774
3047
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
3048
|
+
ngOnDestroy() {
|
|
3049
|
+
this.loop?.stop();
|
|
3050
|
+
this.loop = null;
|
|
3051
|
+
this.loopActive = false;
|
|
2779
3052
|
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
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
|
+
});
|
|
2783
3067
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
if (type === 'Icon' && sp['display'] === undefined) {
|
|
2801
|
-
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);
|
|
2802
3084
|
}
|
|
2803
|
-
|
|
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"] }] }); }
|
|
2804
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
|
+
}] } });
|
|
2805
3125
|
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
['filterSepia', 'sepia', '0'],
|
|
2827
|
-
['filterInvert', 'invert', '0'],
|
|
2828
|
-
];
|
|
2829
|
-
// ─── Backdrop-filter ─────────────────────────────────────────────────────────
|
|
2830
|
-
const BACKDROP_FUNS = [
|
|
2831
|
-
['backdropBlur', 'blur', '0px'],
|
|
2832
|
-
['backdropBrightness', 'brightness', '1'],
|
|
2833
|
-
['backdropContrast', 'contrast', '1'],
|
|
2834
|
-
['backdropSaturate', 'saturate', '1'],
|
|
2835
|
-
];
|
|
2836
|
-
// ─── Transform ───────────────────────────────────────────────────────────────
|
|
2837
|
-
const TRANSFORM_FUNS = [
|
|
2838
|
-
['translateX', 'translateX', '0px'],
|
|
2839
|
-
['translateY', 'translateY', '0px'],
|
|
2840
|
-
['scaleX', 'scaleX', '1'],
|
|
2841
|
-
['scaleY', 'scaleY', '1'],
|
|
2842
|
-
['rotate', 'rotate', '0deg'],
|
|
2843
|
-
['skewX', 'skewX', '0deg'],
|
|
2844
|
-
['skewY', 'skewY', '0deg'],
|
|
2845
|
-
];
|
|
2846
|
-
// ─── ::marker pseudo-element ─────────────────────────────────────────────────
|
|
2847
|
-
const MARKER_FUNS = [
|
|
2848
|
-
['markerColor', 'color'],
|
|
2849
|
-
['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,
|
|
2850
3146
|
];
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
{
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
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] }); }
|
|
2868
3187
|
}
|
|
2869
|
-
|
|
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
|
+
|
|
2870
3197
|
/**
|
|
2871
|
-
*
|
|
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.
|
|
2872
3201
|
*
|
|
2873
|
-
*
|
|
2874
|
-
*
|
|
2875
|
-
*
|
|
2876
|
-
*
|
|
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.
|
|
2877
3210
|
*/
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
const transformStr = buildFns(TRANSFORM_FUNS, merged(TRANSFORM_FUNS));
|
|
2902
|
-
if (transformStr)
|
|
2903
|
-
result['transform'] = transformStr;
|
|
2904
|
-
// transition — only emit when duration is non-zero
|
|
2905
|
-
const dur = raw['transitionDuration'] ?? base['transitionDuration'];
|
|
2906
|
-
const ease = raw['transitionTimingFunction'] ?? base['transitionTimingFunction'];
|
|
2907
|
-
const del = raw['transitionDelay'] ?? base['transitionDelay'];
|
|
2908
|
-
if (dur && ease && dur !== '0ms' && dur !== '0s') {
|
|
2909
|
-
const delPart = del && del !== '0ms' && del !== '0s' ? ` ${del}` : '';
|
|
2910
|
-
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
|
+
]);
|
|
2911
3234
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
const markerStyles = {};
|
|
2915
|
-
for (const [trait, cssProp] of MARKER_FUNS) {
|
|
2916
|
-
const val = raw[trait] ?? base[trait];
|
|
2917
|
-
if (val != null && val !== '')
|
|
2918
|
-
markerStyles[cssProp] = val;
|
|
3235
|
+
getComponent(type) {
|
|
3236
|
+
return this.map.get(type);
|
|
2919
3237
|
}
|
|
2920
|
-
|
|
2921
|
-
|
|
3238
|
+
hasComponent(type) {
|
|
3239
|
+
return this.map.has(type);
|
|
2922
3240
|
}
|
|
2923
|
-
|
|
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' }); }
|
|
2924
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
|
+
}] });
|
|
2925
3248
|
|
|
2926
3249
|
/**
|
|
2927
|
-
*
|
|
2928
|
-
*
|
|
2929
|
-
* depend on iox-ui (the engine does NOT depend on iox-builder), so the same logic
|
|
2930
|
-
* drives the builder canvas AND the SSR site — no fork.
|
|
2931
|
-
* See iox-ai-guidance `architecture/builder/targeted-hover-state-plan.md`.
|
|
2932
|
-
*/
|
|
2933
|
-
/** The css id a node contributes to selectors: its clone `styleId`, else its own `id`. */
|
|
2934
|
-
function nodeCssId(node) {
|
|
2935
|
-
return node.styleId ?? node.id;
|
|
2936
|
-
}
|
|
2937
|
-
/**
|
|
2938
|
-
* Path of css ids from the root down to (and including) the node whose css id === `cssId`.
|
|
2939
|
-
* Returns `null` when no node matches. First match wins (depth-first, pre-order).
|
|
2940
|
-
*/
|
|
2941
|
-
function pathToCssId(nodes, cssId) {
|
|
2942
|
-
for (const node of nodes) {
|
|
2943
|
-
const ownId = nodeCssId(node);
|
|
2944
|
-
if (ownId === cssId)
|
|
2945
|
-
return [ownId];
|
|
2946
|
-
if (node.children?.length) {
|
|
2947
|
-
const childPath = pathToCssId(node.children, cssId);
|
|
2948
|
-
if (childPath)
|
|
2949
|
-
return ownId ? [ownId, ...childPath] : childPath;
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
return null;
|
|
2953
|
-
}
|
|
2954
|
-
/**
|
|
2955
|
-
* The css id of the NEAREST COMMON ANCESTOR of two nodes (identified by their css ids),
|
|
2956
|
-
* used to scope a `:has()` targeted-state selector so it stays repetition-safe.
|
|
2957
|
-
*
|
|
2958
|
-
* The result may equal `styledCssId` (the trigger sits INSIDE the styled node) or
|
|
2959
|
-
* `triggerCssId` (the styled node sits inside the trigger) — both are valid scopes;
|
|
2960
|
-
* `buildTargetedStateSelector` picks the right selector shape for each. Returns
|
|
2961
|
-
* `undefined` only when a valid scope can't exist: an id is blank, the two ids are the
|
|
2962
|
-
* same node, or they share no ancestor in `root`. Callers MUST skip emitting the rule
|
|
2963
|
-
* when this is `undefined` — never emit an unscoped (page-global) `:has()`, which would
|
|
2964
|
-
* break under repetition.
|
|
2965
|
-
*/
|
|
2966
|
-
function nearestCommonAncestorId(root, styledCssId, triggerCssId) {
|
|
2967
|
-
if (!styledCssId || !triggerCssId || styledCssId === triggerCssId)
|
|
2968
|
-
return undefined;
|
|
2969
|
-
const styledPath = pathToCssId(root, styledCssId);
|
|
2970
|
-
const triggerPath = pathToCssId(root, triggerCssId);
|
|
2971
|
-
if (!styledPath || !triggerPath)
|
|
2972
|
-
return undefined;
|
|
2973
|
-
// Walk both paths together; the last shared prefix element is the common ancestor.
|
|
2974
|
-
let common;
|
|
2975
|
-
const shorter = Math.min(styledPath.length, triggerPath.length);
|
|
2976
|
-
for (let i = 0; i < shorter; i++) {
|
|
2977
|
-
if (styledPath[i] !== triggerPath[i])
|
|
2978
|
-
break;
|
|
2979
|
-
common = styledPath[i];
|
|
2980
|
-
}
|
|
2981
|
-
return common;
|
|
2982
|
-
}
|
|
2983
|
-
/**
|
|
2984
|
-
* Build the selector BODY (no leading `.`, matching the builder's `compile()` convention)
|
|
2985
|
-
* 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.
|
|
2986
3252
|
*
|
|
2987
|
-
* -
|
|
2988
|
-
* -
|
|
2989
|
-
* - 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.
|
|
2990
3255
|
*
|
|
2991
|
-
*
|
|
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`.
|
|
2992
3260
|
*/
|
|
2993
|
-
function buildTargetedStateSelector(scopeCssId, triggerCssId, styledCssId, base) {
|
|
2994
|
-
if (scopeCssId === styledCssId) {
|
|
2995
|
-
return `iox-node-${styledCssId}:has(.iox-node-${triggerCssId}:${base})`;
|
|
2996
|
-
}
|
|
2997
|
-
if (scopeCssId === triggerCssId) {
|
|
2998
|
-
return `iox-node-${triggerCssId}:${base} .iox-node-${styledCssId}`;
|
|
2999
|
-
}
|
|
3000
|
-
return `iox-node-${scopeCssId}:has(.iox-node-${triggerCssId}:${base}) .iox-node-${styledCssId}`;
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
// Responsive (per-breakpoint) styles — the shared model, cascade and emission plan.
|
|
3004
|
-
//
|
|
3005
|
-
// SHARED (iox-ui): the builder canvas AND the SSR engine both call this. The builder
|
|
3006
|
-
// FLATTENS the active breakpoint (its canvas is a transformed frame inside a desktop
|
|
3007
|
-
// browser, so a real `@media` query would read the editor's width); the engine WRAPS
|
|
3008
|
-
// each planned layer in `@media`. Same decisions, different transport — never fork them.
|
|
3009
|
-
//
|
|
3010
|
-
// Storage is a sparse layer beside the existing keys, shaped like `stateStyles`:
|
|
3011
|
-
//
|
|
3012
|
-
// styleProps — base, full resolved set (unchanged)
|
|
3013
|
-
// stateStyles — sparse per-state overrides (unchanged)
|
|
3014
|
-
// responsive — sparse per-breakpoint overrides (this file)
|
|
3015
|
-
// { mobile: { styleProps: {…}, stateStyles: { hover: {…} } } }
|
|
3016
|
-
//
|
|
3017
|
-
// A node with no `responsive` key renders exactly as before — no migration.
|
|
3018
|
-
// See iox-ai-guidance `architecture/builder/responsive-breakpoints-plan.md`.
|
|
3019
|
-
// ─── Breakpoints ─────────────────────────────────────────────────────────────
|
|
3020
|
-
/** The five fixed breakpoints (plan decision D2). Desktop-first: each narrower one
|
|
3021
|
-
* overrides the wider ones through `max-width`. */
|
|
3022
|
-
var Breakpoint;
|
|
3023
|
-
(function (Breakpoint) {
|
|
3024
|
-
Breakpoint["Base"] = "base";
|
|
3025
|
-
Breakpoint["Laptop"] = "laptop";
|
|
3026
|
-
Breakpoint["Tablet"] = "tablet";
|
|
3027
|
-
Breakpoint["Mobile"] = "mobile";
|
|
3028
|
-
Breakpoint["Small"] = "small";
|
|
3029
|
-
})(Breakpoint || (Breakpoint = {}));
|
|
3030
3261
|
/**
|
|
3031
|
-
*
|
|
3262
|
+
* Rewrite viewport-relative units so they resolve to the simulated canvas dimensions
|
|
3263
|
+
* in the builder, and to the real viewport in production.
|
|
3032
3264
|
*
|
|
3033
|
-
*
|
|
3034
|
-
*
|
|
3035
|
-
*
|
|
3036
|
-
*/
|
|
3037
|
-
const BREAKPOINTS = Object.freeze([
|
|
3038
|
-
{ id: Breakpoint.Base, label: 'Base', maxWidth: null, previewWidth: 1440 },
|
|
3039
|
-
{ id: Breakpoint.Laptop, label: 'Laptop', maxWidth: 1200, previewWidth: 1200 },
|
|
3040
|
-
{ id: Breakpoint.Tablet, label: 'Tablet', maxWidth: 991, previewWidth: 768 },
|
|
3041
|
-
{ id: Breakpoint.Mobile, label: 'Mobile landscape', maxWidth: 767, previewWidth: 568 },
|
|
3042
|
-
{ id: Breakpoint.Small, label: 'Mobile portrait', maxWidth: 479, previewWidth: 390 },
|
|
3043
|
-
]);
|
|
3044
|
-
const BREAKPOINT_INDEX = new Map(BREAKPOINTS.map((d, i) => [d.id, i]));
|
|
3045
|
-
/** The definition for `bp`, or `undefined` for an unknown id (e.g. hand-edited JSON). */
|
|
3046
|
-
function breakpointDef(bp) {
|
|
3047
|
-
const i = BREAKPOINT_INDEX.get(bp);
|
|
3048
|
-
return i === undefined ? undefined : BREAKPOINTS[i];
|
|
3049
|
-
}
|
|
3050
|
-
/** Base → `bp`, inclusive — the layers whose values apply at `bp`. Unknown → `[Base]`. */
|
|
3051
|
-
function cascadeChain(bp) {
|
|
3052
|
-
const i = BREAKPOINT_INDEX.get(bp);
|
|
3053
|
-
return BREAKPOINTS.slice(0, (i ?? 0) + 1).map(d => d.id);
|
|
3054
|
-
}
|
|
3055
|
-
/**
|
|
3056
|
-
* The breakpoint a viewport (or canvas) of `width` CSS px falls in — the narrowest one whose
|
|
3057
|
-
* `maxWidth` it fits under, else Base. Mirrors how the `max-width` media queries resolve, so the
|
|
3058
|
-
* 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.
|
|
3059
3268
|
*/
|
|
3060
|
-
function
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
return BREAKPOINTS[i].id;
|
|
3065
|
-
}
|
|
3066
|
-
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))');
|
|
3067
3273
|
}
|
|
3068
|
-
/**
|
|
3069
|
-
function
|
|
3070
|
-
|
|
3071
|
-
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()}`);
|
|
3072
3277
|
}
|
|
3073
3278
|
/**
|
|
3074
|
-
*
|
|
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).
|
|
3075
3283
|
*
|
|
3076
|
-
*
|
|
3077
|
-
*
|
|
3078
|
-
*
|
|
3079
|
-
*
|
|
3080
|
-
*
|
|
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.
|
|
3081
3289
|
*/
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
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
|
+
};
|
|
3087
3312
|
/**
|
|
3088
|
-
*
|
|
3089
|
-
*
|
|
3090
|
-
* `
|
|
3091
|
-
*
|
|
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.
|
|
3092
3317
|
*/
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
return '';
|
|
3107
|
-
const query = breakpointQueryFor(bp, kind, containerName);
|
|
3108
|
-
return query ? `${query} {\n${css}\n}` : css;
|
|
3109
|
-
}
|
|
3110
|
-
/** `@media (max-width: 767px)` for `bp`; `null` for Base or an unknown id. */
|
|
3111
|
-
function mediaQueryFor(bp) {
|
|
3112
|
-
return breakpointQueryFor(bp, BreakpointQueryKind.Media);
|
|
3113
|
-
}
|
|
3114
|
-
/** Wrap already-compiled rules for `bp` in its media query. Base (and blank css) passes through. */
|
|
3115
|
-
function wrapInMediaQuery(bp, css) {
|
|
3116
|
-
return wrapInBreakpointQuery(bp, css, BreakpointQueryKind.Media);
|
|
3117
|
-
}
|
|
3118
|
-
function layerAt(responsive, bp) {
|
|
3119
|
-
return bp === Breakpoint.Base ? undefined : responsive?.[bp];
|
|
3120
|
-
}
|
|
3121
|
-
/** Copy defined entries of `src` onto `target`. `undefined` never overwrites a value. */
|
|
3122
|
-
function assignDefined(target, src) {
|
|
3123
|
-
if (src)
|
|
3124
|
-
for (const [k, v] of Object.entries(src))
|
|
3125
|
-
if (v !== undefined)
|
|
3126
|
-
target[k] = v;
|
|
3127
|
-
return target;
|
|
3128
|
-
}
|
|
3129
|
-
function hasKeys(map) {
|
|
3130
|
-
return !!map && Object.keys(map).length > 0;
|
|
3131
|
-
}
|
|
3132
|
-
// ─── Cascade resolution ──────────────────────────────────────────────────────
|
|
3133
|
-
/** The full effective style map at `bp`: base, then each layer down the cascade. */
|
|
3134
|
-
function resolveStyleProps(styleProps, responsive, bp) {
|
|
3135
|
-
const out = assignDefined({}, styleProps ?? undefined);
|
|
3136
|
-
for (const id of cascadeChain(bp))
|
|
3137
|
-
assignDefined(out, layerAt(responsive, id)?.styleProps);
|
|
3138
|
-
return out;
|
|
3139
|
-
}
|
|
3140
|
-
/** The effective per-state maps at `bp`. States with no values anywhere are omitted. */
|
|
3141
|
-
function resolveStateStyles(stateStyles, responsive, bp) {
|
|
3142
|
-
const out = {};
|
|
3143
|
-
const merge = (src) => {
|
|
3144
|
-
for (const [state, map] of Object.entries(src ?? {})) {
|
|
3145
|
-
out[state] = assignDefined(out[state] ?? {}, map);
|
|
3146
|
-
}
|
|
3147
|
-
};
|
|
3148
|
-
merge(stateStyles ?? undefined);
|
|
3149
|
-
for (const id of cascadeChain(bp))
|
|
3150
|
-
merge(layerAt(responsive, id)?.stateStyles);
|
|
3151
|
-
for (const state of Object.keys(out))
|
|
3152
|
-
if (!hasKeys(out[state]))
|
|
3153
|
-
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];
|
|
3154
3331
|
return out;
|
|
3155
3332
|
}
|
|
3156
3333
|
/**
|
|
3157
|
-
*
|
|
3158
|
-
*
|
|
3159
|
-
* `
|
|
3160
|
-
*
|
|
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.
|
|
3161
3340
|
*/
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
names.add(state);
|
|
3168
|
-
}
|
|
3169
|
-
};
|
|
3170
|
-
add(stateStyles);
|
|
3171
|
-
for (const { id } of BREAKPOINTS)
|
|
3172
|
-
add(layerAt(responsive, id)?.stateStyles);
|
|
3173
|
-
return [...names];
|
|
3174
|
-
}
|
|
3341
|
+
var DeclarationContext;
|
|
3342
|
+
(function (DeclarationContext) {
|
|
3343
|
+
DeclarationContext["Base"] = "base";
|
|
3344
|
+
DeclarationContext["Override"] = "override";
|
|
3345
|
+
})(DeclarationContext || (DeclarationContext = {}));
|
|
3175
3346
|
/**
|
|
3176
|
-
*
|
|
3177
|
-
*
|
|
3178
|
-
*
|
|
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.
|
|
3179
3359
|
*/
|
|
3180
|
-
function
|
|
3181
|
-
const
|
|
3182
|
-
const
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
const
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
}
|
|
3193
|
-
/** True when `bp` itself sets `prop` (the style panel's orange edge). Never true for Base. */
|
|
3194
|
-
function isOverriddenAt(responsive, bp, prop, state) {
|
|
3195
|
-
const layer = layerAt(responsive, bp);
|
|
3196
|
-
const map = state ? layer?.stateStyles?.[state] : layer?.styleProps;
|
|
3197
|
-
return map?.[prop] !== undefined;
|
|
3198
|
-
}
|
|
3199
|
-
/** Number of overridden values — at `bp`, or across every breakpoint when omitted. */
|
|
3200
|
-
function countOverrides(responsive, bp) {
|
|
3201
|
-
const count = (map) => Object.values(map ?? {}).filter(v => v !== undefined).length;
|
|
3202
|
-
const ids = bp ? [bp] : BREAKPOINTS.map(d => d.id);
|
|
3203
|
-
let n = 0;
|
|
3204
|
-
for (const id of ids) {
|
|
3205
|
-
const layer = layerAt(responsive, id);
|
|
3206
|
-
n += count(layer?.styleProps);
|
|
3207
|
-
for (const map of Object.values(layer?.stateStyles ?? {}))
|
|
3208
|
-
n += count(map);
|
|
3209
|
-
}
|
|
3210
|
-
return n;
|
|
3211
|
-
}
|
|
3212
|
-
// ─── Editing (immutable) ─────────────────────────────────────────────────────
|
|
3213
|
-
function assertOverrideBreakpoint(bp) {
|
|
3214
|
-
if (bp === Breakpoint.Base) {
|
|
3215
|
-
throw new Error('Base styles live in styleProps / stateStyles, not in responsive overrides');
|
|
3216
|
-
}
|
|
3217
|
-
if (!BREAKPOINT_INDEX.has(bp))
|
|
3218
|
-
throw new Error(`Unknown breakpoint "${bp}"`);
|
|
3219
|
-
}
|
|
3220
|
-
/** Returns a new overrides object with `prop` set at `bp` (optionally within `state`). */
|
|
3221
|
-
function setOverride(responsive, bp, prop, value, state) {
|
|
3222
|
-
assertOverrideBreakpoint(bp);
|
|
3223
|
-
const layer = { ...(responsive?.[bp] ?? {}) };
|
|
3224
|
-
if (state) {
|
|
3225
|
-
layer.stateStyles = { ...(layer.stateStyles ?? {}), [state]: { ...(layer.stateStyles?.[state] ?? {}), [prop]: value } };
|
|
3226
|
-
}
|
|
3227
|
-
else {
|
|
3228
|
-
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' : ''}`);
|
|
3229
3372
|
}
|
|
3230
|
-
return
|
|
3373
|
+
return out;
|
|
3231
3374
|
}
|
|
3232
3375
|
/**
|
|
3233
|
-
*
|
|
3234
|
-
*
|
|
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.
|
|
3235
3381
|
*
|
|
3236
|
-
*
|
|
3237
|
-
*
|
|
3238
|
-
*
|
|
3239
|
-
|
|
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.
|
|
3240
3396
|
*
|
|
3241
|
-
*
|
|
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.
|
|
3242
3400
|
*/
|
|
3243
|
-
function
|
|
3244
|
-
|
|
3245
|
-
const
|
|
3246
|
-
const
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
states[state] = rest;
|
|
3254
|
-
}
|
|
3255
|
-
if (prop === undefined || !hasKeys(states[state]))
|
|
3256
|
-
delete states[state];
|
|
3257
|
-
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%');
|
|
3258
3411
|
}
|
|
3259
|
-
else if (
|
|
3260
|
-
|
|
3261
|
-
layer.styleProps = hasKeys(rest) ? rest : undefined;
|
|
3412
|
+
else if (DEFAULT_WIDTH_BY_TYPE[type]) {
|
|
3413
|
+
out.push(`width: ${DEFAULT_WIDTH_BY_TYPE[type]}`);
|
|
3262
3414
|
}
|
|
3263
|
-
if (!layer.stateStyles)
|
|
3264
|
-
delete layer.stateStyles;
|
|
3265
|
-
if (!layer.styleProps)
|
|
3266
|
-
delete layer.styleProps;
|
|
3267
|
-
next[bp] = hasKeys(layer) ? layer : undefined;
|
|
3268
3415
|
}
|
|
3269
|
-
|
|
3270
|
-
|
|
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');
|
|
3271
3422
|
}
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
function groupsTouchedBy(map) {
|
|
3278
|
-
const touched = new Set();
|
|
3279
|
-
if (!map)
|
|
3280
|
-
return touched;
|
|
3281
|
-
for (const group of VIRTUAL_TRAIT_GROUPS) {
|
|
3282
|
-
if (group.keys.some(k => map[k] !== undefined))
|
|
3283
|
-
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');
|
|
3284
3428
|
}
|
|
3285
|
-
|
|
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;
|
|
3286
3476
|
}
|
|
3477
|
+
|
|
3287
3478
|
/**
|
|
3288
|
-
*
|
|
3289
|
-
*
|
|
3290
|
-
*
|
|
3291
|
-
*
|
|
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`.
|
|
3292
3484
|
*/
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
if (group.output === 'transition')
|
|
3297
|
-
return ZERO_DURATIONS.has(String(merged['transitionDuration']));
|
|
3298
|
-
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;
|
|
3299
3488
|
}
|
|
3300
3489
|
/**
|
|
3301
|
-
*
|
|
3302
|
-
*
|
|
3303
|
-
* group's cancelling value when it now composes to nothing). Untouched groups are dropped
|
|
3304
|
-
* — 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).
|
|
3305
3492
|
*/
|
|
3306
|
-
function
|
|
3307
|
-
const
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
if (
|
|
3312
|
-
|
|
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
|
+
}
|
|
3313
3503
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
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];
|
|
3321
3532
|
}
|
|
3322
|
-
return
|
|
3533
|
+
return common;
|
|
3323
3534
|
}
|
|
3324
3535
|
/**
|
|
3325
|
-
*
|
|
3326
|
-
*
|
|
3327
|
-
* 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:
|
|
3328
3538
|
*
|
|
3329
|
-
*
|
|
3330
|
-
*
|
|
3331
|
-
*
|
|
3332
|
-
*
|
|
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.
|
|
3333
3544
|
*/
|
|
3334
|
-
function
|
|
3335
|
-
if (
|
|
3336
|
-
return
|
|
3337
|
-
const plan = [];
|
|
3338
|
-
for (const def of BREAKPOINTS) {
|
|
3339
|
-
const layer = layerAt(responsive, def.id);
|
|
3340
|
-
if (!layer || def.maxWidth == null)
|
|
3341
|
-
continue;
|
|
3342
|
-
const bp = def.id;
|
|
3343
|
-
const wider = parentBreakpoint(bp);
|
|
3344
|
-
const own = layer.styleProps ?? {};
|
|
3345
|
-
const widerProps = resolveStyleProps(styleProps, responsive, wider);
|
|
3346
|
-
const touchedByProps = groupsTouchedBy(own);
|
|
3347
|
-
const composedProps = composeOverride(own, widerProps, touchedByProps);
|
|
3348
|
-
const propsHere = { ...widerProps, ...own };
|
|
3349
|
-
const statesHere = resolveStateStyles(stateStyles, responsive, bp);
|
|
3350
|
-
const statesWider = resolveStateStyles(stateStyles, responsive, wider);
|
|
3351
|
-
const composedStates = {};
|
|
3352
|
-
for (const state of Object.keys(statesHere)) {
|
|
3353
|
-
const ownState = layer.stateStyles?.[state] ?? {};
|
|
3354
|
-
const touched = groupsTouchedBy(ownState);
|
|
3355
|
-
for (const group of touchedByProps)
|
|
3356
|
-
touched.add(group);
|
|
3357
|
-
const composed = composeOverride(ownState, { ...propsHere, ...(statesWider[state] ?? {}) }, touched);
|
|
3358
|
-
if (hasKeys(composed))
|
|
3359
|
-
composedStates[state] = composed;
|
|
3360
|
-
}
|
|
3361
|
-
if (hasKeys(composedProps) || hasKeys(composedStates)) {
|
|
3362
|
-
plan.push({
|
|
3363
|
-
breakpoint: bp,
|
|
3364
|
-
mediaQuery: mediaQueryFor(bp),
|
|
3365
|
-
styleProps: composedProps,
|
|
3366
|
-
stateStyles: composedStates,
|
|
3367
|
-
});
|
|
3368
|
-
}
|
|
3545
|
+
function buildTargetedStateSelector(scopeCssId, triggerCssId, styledCssId, base) {
|
|
3546
|
+
if (scopeCssId === styledCssId) {
|
|
3547
|
+
return `iox-node-${styledCssId}:has(.iox-node-${triggerCssId}:${base})`;
|
|
3369
3548
|
}
|
|
3370
|
-
|
|
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}`;
|
|
3371
3553
|
}
|
|
3372
3554
|
|
|
3373
3555
|
/**
|
|
@@ -4404,5 +4586,5 @@ function effectiveTransitionId(anim) {
|
|
|
4404
4586
|
* Generated bundle index. Do not edit.
|
|
4405
4587
|
*/
|
|
4406
4588
|
|
|
4407
|
-
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, normalizeGalleryImage, 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 };
|
|
4408
4590
|
//# sourceMappingURL=vectoriox-iox-ui.mjs.map
|