@vectoriox/iox-ui 4.15.0 → 4.16.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.
@@ -1788,6 +1788,608 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
1788
1788
  args: ['childrenHost', { read: ViewContainerRef, static: true }]
1789
1789
  }] } });
1790
1790
 
1791
+ /**
1792
+ * Shared data-source resolving pattern — the SINGLE source of truth for how an IoxDataSource
1793
+ * becomes a backend fetch, consumed by BOTH the page builder (iox-cms-client bindings panel) and
1794
+ * the SSR client engine (iox-client-engine DataSourceResolverService). There must be no second
1795
+ * copy of this decision logic; only the transport differs between the two:
1796
+ *
1797
+ * builder → authenticated org-scoped controllers (admin session):
1798
+ * single by id → GET admin.../resources/:type/:id (ContentService.getContentById)
1799
+ * collection → GET admin.../resources/:type (ContentService.getContent)
1800
+ * engine → @Public() controllers (no auth, isPublished filter applied server-side):
1801
+ * single by id → GET api/:type/:id (UserResourcesController)
1802
+ * collection → GET api/:type?filters (UserResourcesController)
1803
+ *
1804
+ * The DECISION (by-id vs collection, which id, which filters, how to pick the single result) is
1805
+ * identical for both — that's what this module owns. Each app maps the returned {@link DataSourcePlan}
1806
+ * onto its own transport. A single item is ALWAYS fetched by its id as a path segment — never a
1807
+ * `?_id=` query — to match the builder's getContentById.
1808
+ */
1809
+ /**
1810
+ * Match a page route template against an actual path and extract named params.
1811
+ * matchRouteParams('/project/:projectId', 'project/68abc') → { projectId: '68abc' }
1812
+ * Both sides are normalised (leading slash stripped) and split on '/'; values are URL-decoded.
1813
+ */
1814
+ function matchRouteParams(template, actual) {
1815
+ const t = (template ?? '').replace(/^\/+/, '').split('/');
1816
+ const a = (actual ?? '').replace(/^\/+/, '').split('/');
1817
+ const params = {};
1818
+ for (let i = 0; i < t.length; i++) {
1819
+ const seg = t[i];
1820
+ if (seg && seg.startsWith(':'))
1821
+ params[seg.slice(1)] = decodeURIComponent(a[i] ?? '');
1822
+ }
1823
+ return params;
1824
+ }
1825
+ /**
1826
+ * Build the query filter for a data-source request from its filterByRouteParam / filterByQueryParams
1827
+ * config. `field` is filtered by the route-param value (falling back to `defaultId`); query filters
1828
+ * map each `field` to a query-param value (falling back to `defaultValue`), and a filter with NO
1829
+ * `queryParam` is a constant on `defaultValue`. Empty values are omitted so the source isn't
1830
+ * accidentally filtered to nothing.
1831
+ */
1832
+ function buildDataSourceFilter(req, routeParams = {}, queryParams = {}) {
1833
+ const params = {};
1834
+ const byRoute = req?.filterByRouteParam;
1835
+ if (byRoute?.field && byRoute?.routeParam) {
1836
+ const value = routeParams[byRoute.routeParam] ?? byRoute.defaultId;
1837
+ if (value !== undefined && value !== null && value !== '')
1838
+ params[byRoute.field] = value;
1839
+ }
1840
+ for (const q of req?.filterByQueryParams ?? []) {
1841
+ if (!q?.field)
1842
+ continue;
1843
+ // No queryParam → a CONSTANT filter: defaultValue is the value, in the builder preview and
1844
+ // on the live site alike. With a queryParam the URL wins and defaultValue is the fallback.
1845
+ const value = q.queryParam ? (queryParams[q.queryParam] ?? q.defaultValue) : q.defaultValue;
1846
+ if (value !== undefined && value !== null && value !== '')
1847
+ params[q.field] = value;
1848
+ }
1849
+ return params;
1850
+ }
1851
+ /**
1852
+ * Mirrors the builder's getContentById decision. When a SINGLE source is filtered by the item id
1853
+ * (`_id`) via a route param, the item is fetched by PATH (`.../:type/:id`), not a `?_id=` query.
1854
+ * Returns that id (route-param value, else `defaultId`), or `null` when the source isn't a
1855
+ * single-by-id fetch — in which case the caller uses the collection endpoint + filters. The
1856
+ * path-based form only applies to the id field; a single source filtered by any other field still
1857
+ * goes through the collection endpoint.
1858
+ */
1859
+ function resolveSingleItemId(req, routeParams = {}) {
1860
+ const byRoute = req?.filterByRouteParam;
1861
+ if (!byRoute?.routeParam)
1862
+ return null;
1863
+ if (byRoute.field && byRoute.field !== '_id')
1864
+ return null;
1865
+ const value = routeParams[byRoute.routeParam] ?? byRoute.defaultId;
1866
+ return value !== undefined && value !== null && value !== '' ? String(value) : null;
1867
+ }
1868
+ /**
1869
+ * Resolve an IoxDataSource into a transport-agnostic {@link DataSourcePlan}. This is the shared
1870
+ * decision both the builder and the SSR engine run; each then executes the plan with its own
1871
+ * HTTP layer. Single sources with a route-param id resolve to a `by-id` (path) fetch — exactly like
1872
+ * the builder's getContentById.
1873
+ */
1874
+ function planDataSource(ds, routeParams = {}, queryParams = {}) {
1875
+ const mode = ds?.mode === 'single' ? 'single' : 'list';
1876
+ switch (ds?.type) {
1877
+ case 'cms':
1878
+ case 'shop': {
1879
+ const domain = ds.type;
1880
+ const req = domain === 'cms' ? ds.request?.cms : ds.request?.shop;
1881
+ const resource = req?.contentType;
1882
+ if (!resource)
1883
+ return { kind: 'none' };
1884
+ const id = mode === 'single' ? resolveSingleItemId(req, routeParams) : null;
1885
+ if (id)
1886
+ return { kind: 'by-id', domain, resource, id, mode };
1887
+ const filter = buildDataSourceFilter(req, routeParams, queryParams);
1888
+ const plan = { kind: 'collection', domain, resource, filter, mode };
1889
+ if (req?.limit)
1890
+ plan.limit = req.limit;
1891
+ return plan;
1892
+ }
1893
+ case 'external-api': {
1894
+ const endpoint = ds.request?.endpoint;
1895
+ return endpoint ? { kind: 'external', endpoint, mode } : { kind: 'none' };
1896
+ }
1897
+ case 'static':
1898
+ return { kind: 'static', data: ds.request?.data ?? null, mode };
1899
+ case 'derived': {
1900
+ const d = ds.request?.derived;
1901
+ return d?.from ? { kind: 'derived', from: d.from, path: d.path ?? '', mode } : { kind: 'none' };
1902
+ }
1903
+ default:
1904
+ return { kind: 'none' };
1905
+ }
1906
+ }
1907
+ /**
1908
+ * Normalise a collection response into the shape a source expects: unwraps `{items}`/`{data}`
1909
+ * envelopes, then returns the first item for a `single` source or the whole array for a `list`.
1910
+ * A `by-id` response is already the item, so callers pass it through unchanged.
1911
+ */
1912
+ function normalizeCollectionResult(mode, data) {
1913
+ const items = Array.isArray(data) ? data : (data?.items ?? data?.data ?? []);
1914
+ return mode === 'single' ? (items[0] ?? null) : items;
1915
+ }
1916
+ /** Navigate a dot/bracket path into an object, e.g. `gallery`, `details.images`, `variants[0].skus`.
1917
+ * Returns undefined if any segment is missing. Kept minimal + self-contained so it is shareable by
1918
+ * both the builder and the SSR engine (the two have divergent local `resolvePath` copies). */
1919
+ function getByPath(obj, path) {
1920
+ if (obj == null || !path)
1921
+ return obj;
1922
+ // Turn `a[0].b` into ['a','0','b'].
1923
+ const segments = path.replace(/\[(\w+)\]/g, '.$1').split('.').filter(Boolean);
1924
+ let cur = obj;
1925
+ for (const seg of segments) {
1926
+ if (cur == null)
1927
+ return undefined;
1928
+ cur = cur[seg];
1929
+ }
1930
+ return cur;
1931
+ }
1932
+ /**
1933
+ * The array a Repeater (or a repeating Slider) iterates over — the SHARED decision so the builder
1934
+ * canvas and the SSR engine agree (see data-source-resolving.md).
1935
+ * - No `sourcePath`: the resolved source IS the list. An array passes through; a lone object degrades
1936
+ * to a single-row list (back-compat with the old `fetchItems`); null/undefined → `[]`.
1937
+ * - With `sourcePath`: the source resolved to a SINGLE resource (e.g. a product); dive into the
1938
+ * nested array property (e.g. `gallery`). Non-array / missing → `[]`.
1939
+ */
1940
+ /**
1941
+ * Fill in `derived` aliases in an already-resolved map. Given the data sources and a map of resolved
1942
+ * NON-derived values (by alias), compute each `type:'derived'` alias as `getByPath(parent, path)` in
1943
+ * dependency order (chains resolved by iterating until stable). Mutates + returns the map. Used by the
1944
+ * SSR engine after it resolves the root sources; the builder resolves derived lazily per alias.
1945
+ */
1946
+ function resolveDerivedInto(dataSources, resolved) {
1947
+ const derived = (dataSources ?? []).filter(d => d?.type === 'derived' && !!d.alias);
1948
+ let changed = true, guard = 0;
1949
+ while (changed && guard++ < 25) {
1950
+ changed = false;
1951
+ for (const d of derived) {
1952
+ if (d.alias in resolved)
1953
+ continue;
1954
+ const from = d.request?.derived?.from;
1955
+ if (from && from in resolved) {
1956
+ resolved[d.alias] = getByPath(resolved[from], d.request.derived.path ?? '');
1957
+ changed = true;
1958
+ }
1959
+ }
1960
+ }
1961
+ return resolved;
1962
+ }
1963
+ function resolveRepeaterItems(resolved, sourcePath) {
1964
+ if (sourcePath) {
1965
+ const nested = getByPath(resolved, sourcePath);
1966
+ return Array.isArray(nested) ? nested : [];
1967
+ }
1968
+ if (Array.isArray(resolved))
1969
+ return resolved;
1970
+ return resolved == null ? [] : [resolved];
1971
+ }
1972
+ // ── Referenced data sources (shared: builder-save + SSR resolve) ─────────────────────────────────
1973
+ // Which data-source aliases a node tree uses, so a consumer resolves ONLY what a page/global needs
1974
+ // (not the whole org library). Used by cms-client (persist globals' sources) and the SSR engine
1975
+ // (resolve the referenced subset per page). See org-data-sources-plan.md.
1976
+ /** Aliases referenced anywhere in a node tree: a repeater/slider `source` (a `bindings` entry with
1977
+ * `prop === 'source'`, or `props.source` for the slider) plus every binding's `source`. */
1978
+ function collectReferencedAliases(nodes, acc = new Set()) {
1979
+ for (const n of nodes ?? []) {
1980
+ for (const b of n?.bindings ?? []) {
1981
+ if (b?.source)
1982
+ acc.add(b.source);
1983
+ }
1984
+ if (n?.props?.source)
1985
+ acc.add(n.props.source);
1986
+ if (n?.children?.length)
1987
+ collectReferencedAliases(n.children, acc);
1988
+ }
1989
+ return acc;
1990
+ }
1991
+ /** The subset of `all` whose `alias` is referenced by `nodes` (shallow — direct references only). */
1992
+ function referencedDataSources(nodes, all) {
1993
+ const aliases = collectReferencedAliases(nodes);
1994
+ return (all ?? []).filter(ds => aliases.has(ds?.alias));
1995
+ }
1996
+ /** Like `referencedDataSources`, but also pulls in a referenced source's DEPENDENCIES (transitively),
1997
+ * none of which the layout names directly:
1998
+ * - a `derived` alias needs its `request.derived.from` parent;
1999
+ * - a `relative` (route-context) alias needs its `request.relative.subjectFrom` (the current item)
2000
+ * and `request.relative.from` (the collection it navigates).
2001
+ * Use this when RESOLVING (the engine) so derived views and siblings/related don't resolve to nothing. */
2002
+ function referencedDataSourcesDeep(nodes, all) {
2003
+ const byAlias = new Map((all ?? []).map(d => [d?.alias, d]));
2004
+ const wanted = collectReferencedAliases(nodes);
2005
+ const queue = [...wanted];
2006
+ const enqueue = (alias) => {
2007
+ if (alias && !wanted.has(alias)) {
2008
+ wanted.add(alias);
2009
+ queue.push(alias);
2010
+ }
2011
+ };
2012
+ while (queue.length) {
2013
+ const ds = byAlias.get(queue.pop());
2014
+ enqueue(ds?.request?.derived?.from);
2015
+ enqueue(ds?.request?.relative?.subjectFrom);
2016
+ enqueue(ds?.request?.relative?.from);
2017
+ }
2018
+ return (all ?? []).filter(d => wanted.has(d?.alias));
2019
+ }
2020
+ /**
2021
+ * Turn a resolved {@link DataSourcePlan} into the concrete public HTTP request (url + params) — the
2022
+ * SINGLE place that decides the URL shape, shared by the builder binding preview and the SSR engine
2023
+ * so both hit the SAME public endpoints (no admin, no duplication). Returns `null` for `static` /
2024
+ * `none` plans (no HTTP call). A single item is a PATH segment (`/:resource/:id`), never `?_id=`.
2025
+ */
2026
+ function dataSourcePlanToRequest(plan, endpoints) {
2027
+ const baseFor = (domain) => (domain === 'shop' ? endpoints.shopBase : endpoints.cmsBase);
2028
+ switch (plan.kind) {
2029
+ case 'by-id':
2030
+ return { url: `${baseFor(plan.domain)}${plan.resource}/${encodeURIComponent(plan.id)}`, params: {} };
2031
+ case 'collection': {
2032
+ const params = { ...plan.filter };
2033
+ if (plan.limit)
2034
+ params['limit'] = plan.limit;
2035
+ return { url: `${baseFor(plan.domain)}${plan.resource}`, params };
2036
+ }
2037
+ case 'external':
2038
+ return { url: plan.endpoint, params: {} };
2039
+ default:
2040
+ return null;
2041
+ }
2042
+ }
2043
+
2044
+ /**
2045
+ * Gallery — data model + the SHARED "what are the image URLs" resolver.
2046
+ *
2047
+ * A Gallery is populated one of two ways (see gallery-component-plan.md):
2048
+ * - **Manual**: an ordered `GalleryImage[]` trait. Each item is a raw/filebox URL OR a per-item
2049
+ * binding path resolved against the item/route context.
2050
+ * - **Bound**: the whole gallery repeats over a data source (a collection, or an array property
2051
+ * inside one resource via `sourcePath`) — the items come from the shared `resolveRepeaterItems`,
2052
+ * and one field of each record (`imageField`) is the URL.
2053
+ *
2054
+ * `resolveGalleryUrls` is the SINGLE place both the builder canvas and the SSR/runtime engine turn a
2055
+ * node's config into the final `string[]` the crossfade loop consumes — so both sides show the same
2056
+ * images. It is pure and transport-agnostic; never fork it. It reuses `getByPath` (the same path
2057
+ * reader used by data-source resolving) so binding paths behave identically everywhere.
2058
+ */
2059
+ /**
2060
+ * Resolve a gallery's configured images to the flat, display-ready `string[]` the loop renders.
2061
+ * Pure. Empty/whitespace URLs are dropped so a half-filled manual list never yields blank frames.
2062
+ *
2063
+ * @param images manual `GalleryImage[]` (manual mode). Ignored when `bound` is provided.
2064
+ * @param bound bound-mode records + image field. When present, takes precedence over `images`.
2065
+ * @param ctx render context for per-item bindings (item/route data). Defaults to `{}`.
2066
+ */
2067
+ function resolveGalleryUrls(images, bound, ctx = {}) {
2068
+ if (bound) {
2069
+ const field = bound.imageField;
2070
+ return (bound.items ?? [])
2071
+ .map(item => (field ? getByPath(item, field) : item))
2072
+ .map(coerceUrl)
2073
+ .filter((u) => !!u);
2074
+ }
2075
+ return (images ?? [])
2076
+ .map(img => (img?.binding ? getByPath(ctx, img.binding) : img?.url))
2077
+ .map(coerceUrl)
2078
+ .filter((u) => !!u);
2079
+ }
2080
+ /** A resolved value becomes a URL only if it's a non-empty string; everything else drops out. */
2081
+ function coerceUrl(value) {
2082
+ if (typeof value !== 'string')
2083
+ return null;
2084
+ const trimmed = value.trim();
2085
+ return trimmed ? trimmed : null;
2086
+ }
2087
+ /**
2088
+ * Parse a pasted block of URLs (newline- and/or comma-separated) into `GalleryImage[]`. Used by the
2089
+ * MediaList trait editor's "paste URL list" affordance. Pure so it can be unit-tested + reused.
2090
+ */
2091
+ function parseUrlList(text) {
2092
+ if (!text)
2093
+ return [];
2094
+ return text
2095
+ .split(/[\n,]+/)
2096
+ .map(s => s.trim())
2097
+ .filter(Boolean)
2098
+ .map(url => ({ url }));
2099
+ }
2100
+
2101
+ /**
2102
+ * Gallery effect registry — the SHARED, effect-agnostic description of how each gallery slide looks
2103
+ * and animates in a given crossfade state. Modeled on `binding-pipes.ts` / `PAGE_TRANSITION_PRESETS`:
2104
+ * a descriptor map + a resolve fn, all pure, consumed by BOTH the builder twin and the engine twin so
2105
+ * the fade looks identical on the canvas and the live site.
2106
+ *
2107
+ * v1 ships only `fade` (the look ported from `sites/interior-design` fade-gallery). Adding `slide`,
2108
+ * `zoom`, `kenburns`, … later is a new entry here + its CSS — NO component refactor, because the
2109
+ * component only ever asks `resolveGalleryEffect(id).buildSlideStyle(state)` for per-slide inline CSS.
2110
+ *
2111
+ * See iox-ai-guidance/architecture/builder/gallery-component-plan.md.
2112
+ */
2113
+ /** The ken-burns zoom baked into the reference fade look (scale 1 → 1.05 over show+fade). */
2114
+ const FADE_ZOOM_EXTRA_MS = 1000;
2115
+ const fadeEffect = {
2116
+ id: 'fade',
2117
+ label: 'Fade',
2118
+ defaults: { showMs: 4000, fadeMs: 1000 },
2119
+ buildSlideStyle(state) {
2120
+ const { active, fadingOut, firstCycleActive, durations } = state;
2121
+ const zoomMs = durations.showMs + durations.fadeMs + FADE_ZOOM_EXTRA_MS;
2122
+ // Before the first cycle activates, everything is flat + instant so there is no entry flash.
2123
+ if (!firstCycleActive) {
2124
+ return { opacity: '0', transform: 'scale(1)', transition: 'opacity 0ms, transform 0ms' };
2125
+ }
2126
+ const visible = active && !fadingOut;
2127
+ const engaged = active || fadingOut; // current or outgoing slide is zoomed-in
2128
+ return {
2129
+ opacity: visible ? '1' : '0',
2130
+ transform: engaged ? 'scale(1.05)' : 'scale(1)',
2131
+ transition: `opacity ${durations.fadeMs}ms cubic-bezier(0.77, 0, 0.175, 1), ` +
2132
+ `transform ${zoomMs}ms linear`,
2133
+ };
2134
+ },
2135
+ };
2136
+ /** The registry — one entry per effect. Keyed by id; unknown ids fall back to `fade`. */
2137
+ const GALLERY_EFFECTS = {
2138
+ fade: fadeEffect,
2139
+ };
2140
+ /** Resolve an effect id to its descriptor, defaulting to `fade` (forward-compatible with new ids). */
2141
+ function resolveGalleryEffect(id) {
2142
+ return (id && GALLERY_EFFECTS[id]) || GALLERY_EFFECTS['fade'];
2143
+ }
2144
+ const GALLERY_EFFECT_DESCRIPTORS = Object.values(GALLERY_EFFECTS).map(e => ({ id: e.id, label: e.label }));
2145
+
2146
+ /**
2147
+ * Gallery crossfade loop — the SHARED, framework-free state machine that drives which image is
2148
+ * visible over time. Ported from the reference fade-gallery's `scheduleCycle` / `tryStartLoop` /
2149
+ * first-cycle-activation logic, with Angular specifics removed so BOTH the builder twin and the
2150
+ * engine/runtime twin (`iox-gallery`) run ONE implementation — never a forked copy that drifts.
2151
+ *
2152
+ * The loop owns only *timing + index* state and emits it through `onChange`; the component maps that
2153
+ * state to CSS via the effect registry (`gallery-effects.ts`). Timers are injected so the loop is
2154
+ * deterministic under fake timers in tests and platform-guardable in the engine.
2155
+ *
2156
+ * See iox-ai-guidance/architecture/builder/gallery-component-plan.md.
2157
+ */
2158
+ /** Default scheduler over the platform's `setTimeout` / `requestAnimationFrame` (with a timer fallback). */
2159
+ const DEFAULT_GALLERY_SCHEDULER = {
2160
+ setTimer: (fn, ms) => setTimeout(fn, ms),
2161
+ clearTimer: h => clearTimeout(h),
2162
+ nextFrame: fn => typeof requestAnimationFrame === 'function' ? requestAnimationFrame(fn) : setTimeout(fn, 16),
2163
+ cancelFrame: h => {
2164
+ if (typeof cancelAnimationFrame === 'function')
2165
+ cancelAnimationFrame(h);
2166
+ else
2167
+ clearTimeout(h);
2168
+ },
2169
+ };
2170
+ class GalleryLoop {
2171
+ constructor(opts) {
2172
+ this.count = 0;
2173
+ this.currentIndex = 0;
2174
+ this.fadingOutIndex = null;
2175
+ this.firstCycleActive = false;
2176
+ this.cycleTimer = null;
2177
+ this.fadeTimer = null;
2178
+ this.frameHandle = null;
2179
+ /** Bumped on every (re)start so a stale timer callback from a previous run is ignored. */
2180
+ this.runToken = 0;
2181
+ this.running = false;
2182
+ this.scheduler = opts.scheduler ?? DEFAULT_GALLERY_SCHEDULER;
2183
+ this.showMs = opts.showMs;
2184
+ this.fadeMs = opts.fadeMs;
2185
+ this.onChange = opts.onChange;
2186
+ }
2187
+ get state() {
2188
+ return {
2189
+ currentIndex: this.currentIndex,
2190
+ fadingOutIndex: this.fadingOutIndex,
2191
+ firstCycleActive: this.firstCycleActive,
2192
+ };
2193
+ }
2194
+ /** Start (or restart) the loop for `count` images. A count < 2 shows the single image without cycling. */
2195
+ start(count) {
2196
+ this.stop();
2197
+ this.count = Math.max(0, count | 0);
2198
+ this.currentIndex = 0;
2199
+ this.fadingOutIndex = null;
2200
+ this.firstCycleActive = false;
2201
+ if (this.count === 0) {
2202
+ this.emit();
2203
+ return;
2204
+ }
2205
+ this.running = true;
2206
+ const token = ++this.runToken;
2207
+ // Activate on the next frame so the first paint is flat (no entry-transition flash), then
2208
+ // begin cycling only when there is more than one image to advance through.
2209
+ this.frameHandle = this.scheduler.nextFrame(() => {
2210
+ if (token !== this.runToken || !this.running)
2211
+ return;
2212
+ this.firstCycleActive = true;
2213
+ this.emit();
2214
+ if (this.count > 1)
2215
+ this.scheduleCycle(token);
2216
+ });
2217
+ }
2218
+ /** Update timing live. Takes effect on the next scheduled cycle; does not restart the loop. */
2219
+ setDurations(showMs, fadeMs) {
2220
+ this.showMs = showMs;
2221
+ this.fadeMs = fadeMs;
2222
+ }
2223
+ /** Stop and clear all timers. Idempotent. State is retained (call `start` to reset). */
2224
+ stop() {
2225
+ this.running = false;
2226
+ this.runToken++;
2227
+ if (this.cycleTimer != null) {
2228
+ this.scheduler.clearTimer(this.cycleTimer);
2229
+ this.cycleTimer = null;
2230
+ }
2231
+ if (this.fadeTimer != null) {
2232
+ this.scheduler.clearTimer(this.fadeTimer);
2233
+ this.fadeTimer = null;
2234
+ }
2235
+ if (this.frameHandle != null) {
2236
+ this.scheduler.cancelFrame(this.frameHandle);
2237
+ this.frameHandle = null;
2238
+ }
2239
+ }
2240
+ scheduleCycle(token) {
2241
+ if (token !== this.runToken || !this.running || this.count < 2)
2242
+ return;
2243
+ this.cycleTimer = this.scheduler.setTimer(() => {
2244
+ if (token !== this.runToken || !this.running)
2245
+ return;
2246
+ this.fadingOutIndex = this.currentIndex;
2247
+ this.currentIndex = (this.currentIndex + 1) % this.count;
2248
+ this.emit();
2249
+ this.fadeTimer = this.scheduler.setTimer(() => {
2250
+ if (token !== this.runToken || !this.running)
2251
+ return;
2252
+ this.fadingOutIndex = null;
2253
+ this.emit();
2254
+ this.scheduleCycle(token);
2255
+ }, this.fadeMs);
2256
+ }, this.showMs);
2257
+ }
2258
+ emit() {
2259
+ this.onChange(this.state);
2260
+ }
2261
+ }
2262
+
2263
+ /**
2264
+ * `iox-gallery` — the engine/runtime twin of the Gallery builder component (the builder twin
2265
+ * `app-builder-gallery` arrives in Phase 2). It runs the SHARED crossfade loop + effect registry from
2266
+ * `@vectoriox/iox-ui`, so the fade behaves identically on the canvas and the live site — no forked
2267
+ * animation logic. See iox-ai-guidance/architecture/builder/gallery-component-plan.md.
2268
+ *
2269
+ * Population: v1 drives the loop from the `images` input (manual mode). Data-source binding
2270
+ * (`source`/`sourcePath`) is Phase 3 — this component already resolves through `resolveGalleryUrls`,
2271
+ * so that only adds a bound-source input, not new render logic.
2272
+ *
2273
+ * SSR / no-JS: on the server the loop never starts, so the **first image renders visible** as a static
2274
+ * frame (SEO + no-JS baseline). In the browser `ngAfterViewInit` starts the loop, which begins on the
2275
+ * same index 0 — so the static frame and the first live frame match and there is no hydration flash.
2276
+ */
2277
+ class ClientGalleryComponent {
2278
+ constructor(platformId, cdr) {
2279
+ this.cdr = cdr;
2280
+ this.nodeId = '';
2281
+ /** Manual images — `GalleryImage[]` or a plain `string[]` of URLs (normalised on set). */
2282
+ this.images = [];
2283
+ this.effect = 'fade';
2284
+ this.showDuration = 4000;
2285
+ this.fadeDuration = 1000;
2286
+ this.autoStart = true;
2287
+ /** Layout mode — only `'stack'` is rendered in v1 (`'grid'` scaffolded for later). */
2288
+ this.layout = 'stack';
2289
+ /** The flat, display-ready URLs the loop cycles. Recomputed when `images` changes. */
2290
+ this.urls = [];
2291
+ this.currentIndex = 0;
2292
+ this.fadingOutIndex = null;
2293
+ this.started = false;
2294
+ this.loop = null;
2295
+ this.isBrowser = isPlatformBrowser(platformId);
2296
+ }
2297
+ ngOnChanges(changes) {
2298
+ if (changes['images']) {
2299
+ this.urls = resolveGalleryUrls(this.normalizeImages(), null);
2300
+ }
2301
+ if (this.loop && (changes['showDuration'] || changes['fadeDuration'])) {
2302
+ this.loop.setDurations(this.showDuration, this.fadeDuration);
2303
+ }
2304
+ // If the image set changed after the loop was already running, restart it on the new count.
2305
+ if (this.loop && changes['images'] && !changes['images'].firstChange) {
2306
+ this.startLoop();
2307
+ }
2308
+ }
2309
+ ngAfterViewInit() {
2310
+ if (this.isBrowser && this.autoStart) {
2311
+ this.startLoop();
2312
+ }
2313
+ }
2314
+ ngOnDestroy() {
2315
+ this.loop?.stop();
2316
+ this.loop = null;
2317
+ }
2318
+ /** Per-slide inline style. Before the loop activates (server + no-JS + pre-first-frame) the first
2319
+ * image is the visible static frame; once running, the effect owns each slide's look. */
2320
+ slideStyle(i) {
2321
+ if (!this.started) {
2322
+ return i === 0
2323
+ ? { opacity: '1', transform: 'scale(1)', transition: 'none' }
2324
+ : { opacity: '0', transform: 'scale(1)', transition: 'none' };
2325
+ }
2326
+ return resolveGalleryEffect(this.effect).buildSlideStyle({
2327
+ active: i === this.currentIndex,
2328
+ fadingOut: i === this.fadingOutIndex,
2329
+ firstCycleActive: true,
2330
+ durations: { showMs: this.showDuration, fadeMs: this.fadeDuration },
2331
+ });
2332
+ }
2333
+ startLoop() {
2334
+ if (!this.isBrowser)
2335
+ return;
2336
+ this.loop?.stop();
2337
+ this.loop = new GalleryLoop({
2338
+ showMs: this.showDuration,
2339
+ fadeMs: this.fadeDuration,
2340
+ onChange: state => {
2341
+ this.currentIndex = state.currentIndex;
2342
+ this.fadingOutIndex = state.fadingOutIndex;
2343
+ this.started = state.firstCycleActive;
2344
+ this.cdr.markForCheck();
2345
+ },
2346
+ });
2347
+ this.loop.start(this.urls.length);
2348
+ }
2349
+ normalizeImages() {
2350
+ return (this.images ?? []).map(img => (typeof img === 'string' ? { url: img } : img));
2351
+ }
2352
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ClientGalleryComponent, deps: [{ token: PLATFORM_ID }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
2353
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: ClientGalleryComponent, isStandalone: false, selector: "iox-gallery", inputs: { nodeId: "nodeId", images: "images", effect: "effect", showDuration: "showDuration", fadeDuration: "fadeDuration", autoStart: "autoStart", layout: "layout" }, usesOnChanges: true, ngImport: i0, template: `
2354
+ <div class="iox-gallery" [class]="'iox-node-' + nodeId">
2355
+ <div *ngFor="let url of urls; index as i"
2356
+ class="iox-gallery__img"
2357
+ [style.background-image]="'url(' + url + ')'"
2358
+ [ngStyle]="slideStyle(i)"></div>
2359
+ <div class="iox-gallery__overlay"></div>
2360
+ </div>
2361
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.iox-gallery{position:relative;width:100%;height:100%;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"] }] }); }
2362
+ }
2363
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ClientGalleryComponent, decorators: [{
2364
+ type: Component,
2365
+ args: [{ selector: 'iox-gallery', template: `
2366
+ <div class="iox-gallery" [class]="'iox-node-' + nodeId">
2367
+ <div *ngFor="let url of urls; index as i"
2368
+ class="iox-gallery__img"
2369
+ [style.background-image]="'url(' + url + ')'"
2370
+ [ngStyle]="slideStyle(i)"></div>
2371
+ <div class="iox-gallery__overlay"></div>
2372
+ </div>
2373
+ `, standalone: false, styles: [":host{display:block;width:100%;height:100%}.iox-gallery{position:relative;width:100%;height:100%;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"] }]
2374
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
2375
+ type: Inject,
2376
+ args: [PLATFORM_ID]
2377
+ }] }, { type: i0.ChangeDetectorRef }], propDecorators: { nodeId: [{
2378
+ type: Input
2379
+ }], images: [{
2380
+ type: Input
2381
+ }], effect: [{
2382
+ type: Input
2383
+ }], showDuration: [{
2384
+ type: Input
2385
+ }], fadeDuration: [{
2386
+ type: Input
2387
+ }], autoStart: [{
2388
+ type: Input
2389
+ }], layout: [{
2390
+ type: Input
2391
+ }] } });
2392
+
1791
2393
  const COMPONENTS = [
1792
2394
  BuilderImageComponent,
1793
2395
  BuilderSpacerComponent,
@@ -1807,6 +2409,7 @@ const COMPONENTS = [
1807
2409
  ClientSliderSlideComponent,
1808
2410
  ClientListComponent,
1809
2411
  ClientListItemComponent,
2412
+ ClientGalleryComponent,
1810
2413
  ];
1811
2414
  class IoxBuilderComponentsModule {
1812
2415
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxBuilderComponentsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
@@ -1827,7 +2430,8 @@ class IoxBuilderComponentsModule {
1827
2430
  ClientSliderContainerComponent,
1828
2431
  ClientSliderSlideComponent,
1829
2432
  ClientListComponent,
1830
- ClientListItemComponent], imports: [CommonModule], exports: [BuilderImageComponent,
2433
+ ClientListItemComponent,
2434
+ ClientGalleryComponent], imports: [CommonModule], exports: [BuilderImageComponent,
1831
2435
  BuilderSpacerComponent,
1832
2436
  CardComponent,
1833
2437
  BuilderDividerComponent,
@@ -1844,7 +2448,8 @@ class IoxBuilderComponentsModule {
1844
2448
  ClientSliderContainerComponent,
1845
2449
  ClientSliderSlideComponent,
1846
2450
  ClientListComponent,
1847
- ClientListItemComponent] }); }
2451
+ ClientListItemComponent,
2452
+ ClientGalleryComponent] }); }
1848
2453
  static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxBuilderComponentsModule, imports: [CommonModule] }); }
1849
2454
  }
1850
2455
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IoxBuilderComponentsModule, decorators: [{
@@ -1891,6 +2496,7 @@ class IoxComponentRegistryService {
1891
2496
  ['SliderSlide', ClientSliderSlideComponent],
1892
2497
  ['List', ClientListComponent],
1893
2498
  ['ListItem', ClientListItemComponent],
2499
+ ['Gallery', ClientGalleryComponent],
1894
2500
  ]);
1895
2501
  }
1896
2502
  getComponent(type) {
@@ -2229,259 +2835,6 @@ function composeVirtualTraits(raw, base = raw) {
2229
2835
  return result;
2230
2836
  }
2231
2837
 
2232
- /**
2233
- * Shared data-source resolving pattern — the SINGLE source of truth for how an IoxDataSource
2234
- * becomes a backend fetch, consumed by BOTH the page builder (iox-cms-client bindings panel) and
2235
- * the SSR client engine (iox-client-engine DataSourceResolverService). There must be no second
2236
- * copy of this decision logic; only the transport differs between the two:
2237
- *
2238
- * builder → authenticated org-scoped controllers (admin session):
2239
- * single by id → GET admin.../resources/:type/:id (ContentService.getContentById)
2240
- * collection → GET admin.../resources/:type (ContentService.getContent)
2241
- * engine → @Public() controllers (no auth, isPublished filter applied server-side):
2242
- * single by id → GET api/:type/:id (UserResourcesController)
2243
- * collection → GET api/:type?filters (UserResourcesController)
2244
- *
2245
- * The DECISION (by-id vs collection, which id, which filters, how to pick the single result) is
2246
- * identical for both — that's what this module owns. Each app maps the returned {@link DataSourcePlan}
2247
- * onto its own transport. A single item is ALWAYS fetched by its id as a path segment — never a
2248
- * `?_id=` query — to match the builder's getContentById.
2249
- */
2250
- /**
2251
- * Match a page route template against an actual path and extract named params.
2252
- * matchRouteParams('/project/:projectId', 'project/68abc') → { projectId: '68abc' }
2253
- * Both sides are normalised (leading slash stripped) and split on '/'; values are URL-decoded.
2254
- */
2255
- function matchRouteParams(template, actual) {
2256
- const t = (template ?? '').replace(/^\/+/, '').split('/');
2257
- const a = (actual ?? '').replace(/^\/+/, '').split('/');
2258
- const params = {};
2259
- for (let i = 0; i < t.length; i++) {
2260
- const seg = t[i];
2261
- if (seg && seg.startsWith(':'))
2262
- params[seg.slice(1)] = decodeURIComponent(a[i] ?? '');
2263
- }
2264
- return params;
2265
- }
2266
- /**
2267
- * Build the query filter for a data-source request from its filterByRouteParam / filterByQueryParams
2268
- * config. `field` is filtered by the route-param value (falling back to `defaultId`); query filters
2269
- * map each `field` to a query-param value (falling back to `defaultValue`), and a filter with NO
2270
- * `queryParam` is a constant on `defaultValue`. Empty values are omitted so the source isn't
2271
- * accidentally filtered to nothing.
2272
- */
2273
- function buildDataSourceFilter(req, routeParams = {}, queryParams = {}) {
2274
- const params = {};
2275
- const byRoute = req?.filterByRouteParam;
2276
- if (byRoute?.field && byRoute?.routeParam) {
2277
- const value = routeParams[byRoute.routeParam] ?? byRoute.defaultId;
2278
- if (value !== undefined && value !== null && value !== '')
2279
- params[byRoute.field] = value;
2280
- }
2281
- for (const q of req?.filterByQueryParams ?? []) {
2282
- if (!q?.field)
2283
- continue;
2284
- // No queryParam → a CONSTANT filter: defaultValue is the value, in the builder preview and
2285
- // on the live site alike. With a queryParam the URL wins and defaultValue is the fallback.
2286
- const value = q.queryParam ? (queryParams[q.queryParam] ?? q.defaultValue) : q.defaultValue;
2287
- if (value !== undefined && value !== null && value !== '')
2288
- params[q.field] = value;
2289
- }
2290
- return params;
2291
- }
2292
- /**
2293
- * Mirrors the builder's getContentById decision. When a SINGLE source is filtered by the item id
2294
- * (`_id`) via a route param, the item is fetched by PATH (`.../:type/:id`), not a `?_id=` query.
2295
- * Returns that id (route-param value, else `defaultId`), or `null` when the source isn't a
2296
- * single-by-id fetch — in which case the caller uses the collection endpoint + filters. The
2297
- * path-based form only applies to the id field; a single source filtered by any other field still
2298
- * goes through the collection endpoint.
2299
- */
2300
- function resolveSingleItemId(req, routeParams = {}) {
2301
- const byRoute = req?.filterByRouteParam;
2302
- if (!byRoute?.routeParam)
2303
- return null;
2304
- if (byRoute.field && byRoute.field !== '_id')
2305
- return null;
2306
- const value = routeParams[byRoute.routeParam] ?? byRoute.defaultId;
2307
- return value !== undefined && value !== null && value !== '' ? String(value) : null;
2308
- }
2309
- /**
2310
- * Resolve an IoxDataSource into a transport-agnostic {@link DataSourcePlan}. This is the shared
2311
- * decision both the builder and the SSR engine run; each then executes the plan with its own
2312
- * HTTP layer. Single sources with a route-param id resolve to a `by-id` (path) fetch — exactly like
2313
- * the builder's getContentById.
2314
- */
2315
- function planDataSource(ds, routeParams = {}, queryParams = {}) {
2316
- const mode = ds?.mode === 'single' ? 'single' : 'list';
2317
- switch (ds?.type) {
2318
- case 'cms':
2319
- case 'shop': {
2320
- const domain = ds.type;
2321
- const req = domain === 'cms' ? ds.request?.cms : ds.request?.shop;
2322
- const resource = req?.contentType;
2323
- if (!resource)
2324
- return { kind: 'none' };
2325
- const id = mode === 'single' ? resolveSingleItemId(req, routeParams) : null;
2326
- if (id)
2327
- return { kind: 'by-id', domain, resource, id, mode };
2328
- const filter = buildDataSourceFilter(req, routeParams, queryParams);
2329
- const plan = { kind: 'collection', domain, resource, filter, mode };
2330
- if (req?.limit)
2331
- plan.limit = req.limit;
2332
- return plan;
2333
- }
2334
- case 'external-api': {
2335
- const endpoint = ds.request?.endpoint;
2336
- return endpoint ? { kind: 'external', endpoint, mode } : { kind: 'none' };
2337
- }
2338
- case 'static':
2339
- return { kind: 'static', data: ds.request?.data ?? null, mode };
2340
- case 'derived': {
2341
- const d = ds.request?.derived;
2342
- return d?.from ? { kind: 'derived', from: d.from, path: d.path ?? '', mode } : { kind: 'none' };
2343
- }
2344
- default:
2345
- return { kind: 'none' };
2346
- }
2347
- }
2348
- /**
2349
- * Normalise a collection response into the shape a source expects: unwraps `{items}`/`{data}`
2350
- * envelopes, then returns the first item for a `single` source or the whole array for a `list`.
2351
- * A `by-id` response is already the item, so callers pass it through unchanged.
2352
- */
2353
- function normalizeCollectionResult(mode, data) {
2354
- const items = Array.isArray(data) ? data : (data?.items ?? data?.data ?? []);
2355
- return mode === 'single' ? (items[0] ?? null) : items;
2356
- }
2357
- /** Navigate a dot/bracket path into an object, e.g. `gallery`, `details.images`, `variants[0].skus`.
2358
- * Returns undefined if any segment is missing. Kept minimal + self-contained so it is shareable by
2359
- * both the builder and the SSR engine (the two have divergent local `resolvePath` copies). */
2360
- function getByPath(obj, path) {
2361
- if (obj == null || !path)
2362
- return obj;
2363
- // Turn `a[0].b` into ['a','0','b'].
2364
- const segments = path.replace(/\[(\w+)\]/g, '.$1').split('.').filter(Boolean);
2365
- let cur = obj;
2366
- for (const seg of segments) {
2367
- if (cur == null)
2368
- return undefined;
2369
- cur = cur[seg];
2370
- }
2371
- return cur;
2372
- }
2373
- /**
2374
- * The array a Repeater (or a repeating Slider) iterates over — the SHARED decision so the builder
2375
- * canvas and the SSR engine agree (see data-source-resolving.md).
2376
- * - No `sourcePath`: the resolved source IS the list. An array passes through; a lone object degrades
2377
- * to a single-row list (back-compat with the old `fetchItems`); null/undefined → `[]`.
2378
- * - With `sourcePath`: the source resolved to a SINGLE resource (e.g. a product); dive into the
2379
- * nested array property (e.g. `gallery`). Non-array / missing → `[]`.
2380
- */
2381
- /**
2382
- * Fill in `derived` aliases in an already-resolved map. Given the data sources and a map of resolved
2383
- * NON-derived values (by alias), compute each `type:'derived'` alias as `getByPath(parent, path)` in
2384
- * dependency order (chains resolved by iterating until stable). Mutates + returns the map. Used by the
2385
- * SSR engine after it resolves the root sources; the builder resolves derived lazily per alias.
2386
- */
2387
- function resolveDerivedInto(dataSources, resolved) {
2388
- const derived = (dataSources ?? []).filter(d => d?.type === 'derived' && !!d.alias);
2389
- let changed = true, guard = 0;
2390
- while (changed && guard++ < 25) {
2391
- changed = false;
2392
- for (const d of derived) {
2393
- if (d.alias in resolved)
2394
- continue;
2395
- const from = d.request?.derived?.from;
2396
- if (from && from in resolved) {
2397
- resolved[d.alias] = getByPath(resolved[from], d.request.derived.path ?? '');
2398
- changed = true;
2399
- }
2400
- }
2401
- }
2402
- return resolved;
2403
- }
2404
- function resolveRepeaterItems(resolved, sourcePath) {
2405
- if (sourcePath) {
2406
- const nested = getByPath(resolved, sourcePath);
2407
- return Array.isArray(nested) ? nested : [];
2408
- }
2409
- if (Array.isArray(resolved))
2410
- return resolved;
2411
- return resolved == null ? [] : [resolved];
2412
- }
2413
- // ── Referenced data sources (shared: builder-save + SSR resolve) ─────────────────────────────────
2414
- // Which data-source aliases a node tree uses, so a consumer resolves ONLY what a page/global needs
2415
- // (not the whole org library). Used by cms-client (persist globals' sources) and the SSR engine
2416
- // (resolve the referenced subset per page). See org-data-sources-plan.md.
2417
- /** Aliases referenced anywhere in a node tree: a repeater/slider `source` (a `bindings` entry with
2418
- * `prop === 'source'`, or `props.source` for the slider) plus every binding's `source`. */
2419
- function collectReferencedAliases(nodes, acc = new Set()) {
2420
- for (const n of nodes ?? []) {
2421
- for (const b of n?.bindings ?? []) {
2422
- if (b?.source)
2423
- acc.add(b.source);
2424
- }
2425
- if (n?.props?.source)
2426
- acc.add(n.props.source);
2427
- if (n?.children?.length)
2428
- collectReferencedAliases(n.children, acc);
2429
- }
2430
- return acc;
2431
- }
2432
- /** The subset of `all` whose `alias` is referenced by `nodes` (shallow — direct references only). */
2433
- function referencedDataSources(nodes, all) {
2434
- const aliases = collectReferencedAliases(nodes);
2435
- return (all ?? []).filter(ds => aliases.has(ds?.alias));
2436
- }
2437
- /** Like `referencedDataSources`, but also pulls in a referenced source's DEPENDENCIES (transitively),
2438
- * none of which the layout names directly:
2439
- * - a `derived` alias needs its `request.derived.from` parent;
2440
- * - a `relative` (route-context) alias needs its `request.relative.subjectFrom` (the current item)
2441
- * and `request.relative.from` (the collection it navigates).
2442
- * Use this when RESOLVING (the engine) so derived views and siblings/related don't resolve to nothing. */
2443
- function referencedDataSourcesDeep(nodes, all) {
2444
- const byAlias = new Map((all ?? []).map(d => [d?.alias, d]));
2445
- const wanted = collectReferencedAliases(nodes);
2446
- const queue = [...wanted];
2447
- const enqueue = (alias) => {
2448
- if (alias && !wanted.has(alias)) {
2449
- wanted.add(alias);
2450
- queue.push(alias);
2451
- }
2452
- };
2453
- while (queue.length) {
2454
- const ds = byAlias.get(queue.pop());
2455
- enqueue(ds?.request?.derived?.from);
2456
- enqueue(ds?.request?.relative?.subjectFrom);
2457
- enqueue(ds?.request?.relative?.from);
2458
- }
2459
- return (all ?? []).filter(d => wanted.has(d?.alias));
2460
- }
2461
- /**
2462
- * Turn a resolved {@link DataSourcePlan} into the concrete public HTTP request (url + params) — the
2463
- * SINGLE place that decides the URL shape, shared by the builder binding preview and the SSR engine
2464
- * so both hit the SAME public endpoints (no admin, no duplication). Returns `null` for `static` /
2465
- * `none` plans (no HTTP call). A single item is a PATH segment (`/:resource/:id`), never `?_id=`.
2466
- */
2467
- function dataSourcePlanToRequest(plan, endpoints) {
2468
- const baseFor = (domain) => (domain === 'shop' ? endpoints.shopBase : endpoints.cmsBase);
2469
- switch (plan.kind) {
2470
- case 'by-id':
2471
- return { url: `${baseFor(plan.domain)}${plan.resource}/${encodeURIComponent(plan.id)}`, params: {} };
2472
- case 'collection': {
2473
- const params = { ...plan.filter };
2474
- if (plan.limit)
2475
- params['limit'] = plan.limit;
2476
- return { url: `${baseFor(plan.domain)}${plan.resource}`, params };
2477
- }
2478
- case 'external':
2479
- return { url: plan.endpoint, params: {} };
2480
- default:
2481
- return null;
2482
- }
2483
- }
2484
-
2485
2838
  /**
2486
2839
  * Shared RELATIVE (route-context) data resolving — the SINGLE source of truth for how a detail page
2487
2840
  * derives values FROM its current route item, consumed by BOTH the page builder (iox-cms-client) and
@@ -3187,5 +3540,5 @@ function effectiveTransitionId(anim) {
3187
3540
  * Generated bundle index. Do not edit.
3188
3541
  */
3189
3542
 
3190
- export { AOSService, AnalyticsService, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CMSClientInfraModule, CardComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ConsentService, ContainerComponent, ContentService, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_WIDTH_BY_TYPE, ENVIRONMENT, 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_KEYS, ViewPositionDirective, ViewPositionModule, WebSettingsService, applyBindingPipes, applyFeel, buildDataSourceFilter, buildRelatedFilter, buildTransitionTimeline, collectReferencedAliases, compileDeclarations, composeVirtualTraits, computeRelative, dataSourcePlanToRequest, effectiveTransitionId, excludeSelf, feelFor, getByPath, isReservedAlias, legacyEnterToTransition, matchRouteParams, normalizeCollectionResult, planDataSource, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resolveDerivedInto, resolvePageTransition, resolveRelativeInto, resolveRepeaterItems, resolveSiblings, resolveSingleItemId, rewriteViewportUnits, stripSuppressedProps, styleKeyToKebab };
3543
+ export { AOSService, AnalyticsService, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CMSClientInfraModule, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ConsentService, ContainerComponent, ContentService, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_WIDTH_BY_TYPE, 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_KEYS, ViewPositionDirective, ViewPositionModule, WebSettingsService, applyBindingPipes, applyFeel, buildDataSourceFilter, buildRelatedFilter, buildTransitionTimeline, collectReferencedAliases, compileDeclarations, composeVirtualTraits, computeRelative, dataSourcePlanToRequest, effectiveTransitionId, excludeSelf, feelFor, getByPath, isReservedAlias, legacyEnterToTransition, matchRouteParams, normalizeCollectionResult, parseUrlList, planDataSource, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolvePageTransition, resolveRelativeInto, resolveRepeaterItems, resolveSiblings, resolveSingleItemId, rewriteViewportUnits, stripSuppressedProps, styleKeyToKebab };
3191
3544
  //# sourceMappingURL=vectoriox-iox-ui.mjs.map