@contentful/experiences-svelte 0.5.0 → 0.5.2
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/CHANGELOG.md +18 -0
- package/README.md +45 -11
- package/dist/ClientExperienceRenderer.svelte +2 -0
- package/dist/ClientExperienceRenderer.svelte.d.ts.map +1 -1
- package/dist/DebugExperience.test.js +14 -5
- package/dist/NodeRenderer.svelte +10 -9
- package/dist/NodeRenderer.svelte.d.ts +1 -1
- package/dist/NodeRenderer.svelte.d.ts.map +1 -1
- package/dist/NodesRenderer.svelte +29 -21
- package/dist/NodesRenderer.svelte.d.ts +0 -1
- package/dist/NodesRenderer.svelte.d.ts.map +1 -1
- package/dist/ServerExperienceRenderer.svelte +1 -0
- package/dist/ServerExperienceRenderer.svelte.d.ts.map +1 -1
- package/dist/WrapWithExperienceTemplate.svelte +10 -7
- package/dist/WrapWithExperienceTemplate.svelte.d.ts.map +1 -1
- package/dist/design-utils.d.ts +8 -0
- package/dist/design-utils.d.ts.map +1 -1
- package/dist/design-utils.js +10 -1
- package/dist/server-renderer.test.js +145 -19
- package/dist/test-fixtures/ContainerFixture.svelte +4 -2
- package/dist/test-fixtures/ContainerFixture.svelte.d.ts +1 -1
- package/dist/test-fixtures/ContainerFixture.svelte.d.ts.map +1 -1
- package/dist/test-fixtures/WrappingContainerFixture.svelte +27 -0
- package/dist/test-fixtures/WrappingContainerFixture.svelte.d.ts +8 -0
- package/dist/test-fixtures/WrappingContainerFixture.svelte.d.ts.map +1 -0
- package/dist/types.d.ts +15 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 0.5.2 (2026-08-07)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- server-side design pre-resolution against a fallback viewport [AIS-386] ([#119](https://github.com/contentful/experiences/pull/119))
|
|
6
|
+
|
|
7
|
+
### 🧱 Updated Dependencies
|
|
8
|
+
|
|
9
|
+
- Updated client to 0.3.1
|
|
10
|
+
- Updated design to 0.7.1
|
|
11
|
+
- Updated core to 0.7.1
|
|
12
|
+
|
|
13
|
+
## 0.5.1 (2026-08-07)
|
|
14
|
+
|
|
15
|
+
### 🚀 Features
|
|
16
|
+
|
|
17
|
+
- expose slot children as an array instead of a single wrapping node ([#122](https://github.com/contentful/experiences/pull/122))
|
|
18
|
+
|
|
1
19
|
## 0.5.0 (2026-08-07)
|
|
2
20
|
|
|
3
21
|
### 🩹 Fixes
|
package/README.md
CHANGED
|
@@ -35,14 +35,14 @@ resolveExperience(payload, config, opts?); // Async; walks payload, runs resolve
|
|
|
35
35
|
ServerExperienceRenderer; // SSR-safe; active viewport seeded from initialViewportId
|
|
36
36
|
ClientExperienceRenderer; // Subscribes to window.matchMedia via runes
|
|
37
37
|
MissingComponent; // Default fallback for unregistered component types
|
|
38
|
-
NodesRenderer; // Exposed so you can render non-`children` slots manually
|
|
38
|
+
NodesRenderer; // Exposed so you can render non-`children` slots manually (see Slot children)
|
|
39
39
|
useActiveViewport; // Rune-backed reactive object; you'll rarely need it directly
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
### Styling + runtime context (helpers)
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
|
-
getDesignValues<T>(); //
|
|
45
|
+
getDesignValues<T>(); // Optional helper: the same resolved design record that auto-fills props; read in a $derived to stay reactive
|
|
46
46
|
toCss(design, options?); // Turns a design record into a plain style object, keeping only real CSS keys
|
|
47
47
|
getExperience(); // RenderContext: debug, metadata, viewports, activeViewport
|
|
48
48
|
getContentfulComponent(); // Raw payload for the enclosing node (or undefined)
|
|
@@ -50,7 +50,7 @@ getContentfulExperienceTemplate(); // Same, for the page-level Experience Templa
|
|
|
50
50
|
type ToCssOptions;
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Resolved design values (viewport-cascaded + token-resolved server-side) are **auto-filled onto your component's props** by key, alongside content. Styling straight from `$props()` is the recommended path; `getDesignValues()` exposes the same record (read it in a `$derived` to stay reactive) for cases props don't cover. Token resolution is configured with `resolveToken` on your `Config` (`type ResolveToken`).
|
|
54
54
|
|
|
55
55
|
### Re-exported types and utilities
|
|
56
56
|
|
|
@@ -76,18 +76,21 @@ isCssProperty, toCssKey, CSS_PROPERTIES
|
|
|
76
76
|
## Quick reference
|
|
77
77
|
|
|
78
78
|
```svelte
|
|
79
|
-
<!-- Button.svelte: content
|
|
79
|
+
<!-- Button.svelte: content + resolved design both arrive as props -->
|
|
80
80
|
<script lang="ts">
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
interface ButtonProps {
|
|
82
|
+
label?: string;
|
|
83
|
+
url?: string;
|
|
84
|
+
backgroundColor?: string; // resolved design, auto-filled
|
|
85
|
+
color?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let { label = 'Button', url, backgroundColor, color }: ButtonProps = $props();
|
|
86
89
|
</script>
|
|
87
90
|
{#if url}
|
|
88
|
-
<a href={url} style={
|
|
91
|
+
<a href={url} style="background: {backgroundColor}; color: {color};">{label}</a>
|
|
89
92
|
{:else}
|
|
90
|
-
<button type="button" style={
|
|
93
|
+
<button type="button" style="background: {backgroundColor}; color: {color};">{label}</button>
|
|
91
94
|
{/if}
|
|
92
95
|
```
|
|
93
96
|
|
|
@@ -124,6 +127,37 @@ export const experienceConfig: Config = { components, resolveToken };
|
|
|
124
127
|
<ServerExperienceRenderer experience={data.experience} config={experienceConfig} />
|
|
125
128
|
```
|
|
126
129
|
|
|
130
|
+
### Slot children
|
|
131
|
+
|
|
132
|
+
A component's default slot children arrive as an **array of Snippets** (`Snippet[]`), one per child — not a single wrapping Snippet. For the common "just render them" case, iterate the array with `{#each}` and `{@render}`; to wrap, reorder, or drop children individually, do it inside that loop.
|
|
133
|
+
|
|
134
|
+
```svelte
|
|
135
|
+
<!-- Section.svelte -->
|
|
136
|
+
<script lang="ts">
|
|
137
|
+
import type { Snippet } from 'svelte';
|
|
138
|
+
|
|
139
|
+
let { children }: { children?: Snippet[] } = $props();
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<!-- Common case — render them all: -->
|
|
143
|
+
<div>
|
|
144
|
+
{#each children ?? [] as child}
|
|
145
|
+
{@render child()}
|
|
146
|
+
{/each}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- Or take control of each child: -->
|
|
150
|
+
<!--
|
|
151
|
+
<div>
|
|
152
|
+
{#each children ?? [] as child, i}
|
|
153
|
+
<div class="cell">{@render child()}</div>
|
|
154
|
+
{/each}
|
|
155
|
+
</div>
|
|
156
|
+
-->
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The default slot is injected as the `children` prop. Any **additional named slots** are not auto-injected — reach them from the raw payload via `getContentfulComponent().slots` (a `Record<string, PortableRenderNode[]>`) and render them yourself with `NodesRenderer`.
|
|
160
|
+
|
|
127
161
|
For the full getting-started walkthrough, the merge-precedence rules, viewport handling, and design rationale, see the [root README](../../README.md) and [`AGENTS.md`](../../AGENTS.md).
|
|
128
162
|
|
|
129
163
|
---
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
viewports: experience?.viewports ?? [],
|
|
55
55
|
activeViewport: experience?.viewports[0] ?? FALLBACK_VIEWPORT,
|
|
56
56
|
activeViewportIndex: 0,
|
|
57
|
+
fallbackViewportIndex: experience?.fallbackViewportIndex ?? 0,
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
setExperience(liveContext);
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
liveContext.viewports = experience.viewports;
|
|
65
66
|
liveContext.activeViewport = experience.viewports[idx] ?? FALLBACK_VIEWPORT;
|
|
66
67
|
liveContext.activeViewportIndex = idx;
|
|
68
|
+
liveContext.fallbackViewportIndex = experience.fallbackViewportIndex;
|
|
67
69
|
liveContext.debug = debug;
|
|
68
70
|
liveContext.metadata = { ...DEFAULT_CONTEXT.metadata, ...(metadata ?? {}) };
|
|
69
71
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ClientExperienceRenderer.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ClientExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ClientExperienceRenderer.svelte.ts"],"names":[],"mappings":"AA2FA,QAAA,MAAM,wBAAwB,kGAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { render } from '@testing-library/svelte';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
import DebugExperience from './DebugExperience.svelte';
|
|
4
|
-
const emptyPlan = { nodes: [], viewports: [] };
|
|
4
|
+
const emptyPlan = { nodes: [], viewports: [], fallbackViewportIndex: 0 };
|
|
5
5
|
function node(componentId, content = {}) {
|
|
6
6
|
return {
|
|
7
7
|
registration: { componentId },
|
|
8
|
-
props: { content, design: {} },
|
|
8
|
+
props: { content, design: {}, designRaw: {} },
|
|
9
9
|
slots: {},
|
|
10
10
|
};
|
|
11
11
|
}
|
|
@@ -25,7 +25,11 @@ describe('DebugExperience.svelte', () => {
|
|
|
25
25
|
it('summarizes the node count, pluralizing correctly', () => {
|
|
26
26
|
const { container: zero } = render(DebugExperience, { props: { experience: emptyPlan } });
|
|
27
27
|
expect(zero.innerHTML).toContain('Experience debug — 0 top-level nodes');
|
|
28
|
-
const one = {
|
|
28
|
+
const one = {
|
|
29
|
+
viewports: [],
|
|
30
|
+
nodes: [node('button')],
|
|
31
|
+
fallbackViewportIndex: 0,
|
|
32
|
+
};
|
|
29
33
|
const { container } = render(DebugExperience, { props: { experience: one } });
|
|
30
34
|
expect(container.innerHTML).toContain('Experience debug — 1 top-level node');
|
|
31
35
|
});
|
|
@@ -33,7 +37,11 @@ describe('DebugExperience.svelte', () => {
|
|
|
33
37
|
const plan = {
|
|
34
38
|
viewports: [],
|
|
35
39
|
nodes: [],
|
|
36
|
-
experienceTemplate: {
|
|
40
|
+
experienceTemplate: {
|
|
41
|
+
experienceTemplateId: 'page',
|
|
42
|
+
props: { content: {}, design: {}, designRaw: {} },
|
|
43
|
+
},
|
|
44
|
+
fallbackViewportIndex: 0,
|
|
37
45
|
};
|
|
38
46
|
const { container } = render(DebugExperience, { props: { experience: plan } });
|
|
39
47
|
expect(container.innerHTML).toContain('experience template: page');
|
|
@@ -42,6 +50,7 @@ describe('DebugExperience.svelte', () => {
|
|
|
42
50
|
const plan = {
|
|
43
51
|
viewports: [],
|
|
44
52
|
nodes: [node('button', { label: 'Go' })],
|
|
53
|
+
fallbackViewportIndex: 0,
|
|
45
54
|
};
|
|
46
55
|
const { container } = render(DebugExperience, { props: { experience: plan } });
|
|
47
56
|
expect(container.innerHTML).toContain('componentId');
|
|
@@ -51,7 +60,7 @@ describe('DebugExperience.svelte', () => {
|
|
|
51
60
|
it('degrades a circular reference to a placeholder instead of throwing', () => {
|
|
52
61
|
const n = node('button');
|
|
53
62
|
n.props.resolved = { self: n.props };
|
|
54
|
-
const plan = { viewports: [], nodes: [n] };
|
|
63
|
+
const plan = { viewports: [], nodes: [n], fallbackViewportIndex: 0 };
|
|
55
64
|
expect(() => render(DebugExperience, { props: { experience: plan } })).not.toThrow();
|
|
56
65
|
const { container } = render(DebugExperience, { props: { experience: plan } });
|
|
57
66
|
expect(container.innerHTML).toContain('[Circular]');
|
package/dist/NodeRenderer.svelte
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
import type { Snippet } from 'svelte';
|
|
8
8
|
|
|
9
9
|
import type { PortableRenderNode } from '@contentful/experiences-sdk-core';
|
|
10
|
-
import { applyTokenResolver, resolveDesignProperties } from '@contentful/experiences-design';
|
|
11
10
|
|
|
12
11
|
import { setContentfulComponent, setResolvedDesign } from './context.js';
|
|
12
|
+
import { selectResolvedDesign } from './design-utils.js';
|
|
13
13
|
import type { RenderUnknown } from './component-props.js';
|
|
14
14
|
import {
|
|
15
15
|
normalizeComponentRegistration,
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
config: Config;
|
|
24
24
|
experience: RenderContext;
|
|
25
25
|
renderUnknown: RenderUnknown;
|
|
26
|
-
children: Snippet;
|
|
26
|
+
children: Snippet[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
let { node, config, experience, renderUnknown, children }: NodeRendererProps = $props();
|
|
@@ -35,21 +35,20 @@
|
|
|
35
35
|
componentId: node.registration.componentId,
|
|
36
36
|
nodeId: node.nodeId,
|
|
37
37
|
content: node.props.content,
|
|
38
|
-
design: node.props.
|
|
38
|
+
design: node.props.designRaw,
|
|
39
39
|
resolved: node.props.resolved,
|
|
40
40
|
slots: node.slots,
|
|
41
41
|
};
|
|
42
42
|
setContentfulComponent(contentful);
|
|
43
43
|
|
|
44
|
-
// Cascade design + resolve tokens; published on context for getDesignValues(),
|
|
45
|
-
// never merged into props.
|
|
46
44
|
const tokenResolvedDesign = $derived.by(() => {
|
|
47
|
-
const
|
|
48
|
-
node.props
|
|
45
|
+
const { props, unresolved } = selectResolvedDesign(
|
|
46
|
+
node.props,
|
|
49
47
|
experience.viewports,
|
|
50
|
-
experience.activeViewportIndex
|
|
48
|
+
experience.activeViewportIndex,
|
|
49
|
+
experience.fallbackViewportIndex,
|
|
50
|
+
config.resolveToken
|
|
51
51
|
);
|
|
52
|
-
const { props, unresolved } = applyTokenResolver(resolvedDesign, config.resolveToken);
|
|
53
52
|
if (unresolved.length && typeof console !== 'undefined') {
|
|
54
53
|
console.warn(
|
|
55
54
|
`[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on "${node.registration.componentId}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
|
|
@@ -60,10 +59,12 @@
|
|
|
60
59
|
|
|
61
60
|
setResolvedDesign(() => tokenResolvedDesign);
|
|
62
61
|
|
|
62
|
+
// Merge precedence (last wins): defaults < design < content < resolveData < children.
|
|
63
63
|
const composed = $derived.by(() => {
|
|
64
64
|
if (!componentConfig) return null;
|
|
65
65
|
return {
|
|
66
66
|
...componentConfig.defaults,
|
|
67
|
+
...tokenResolvedDesign,
|
|
67
68
|
...node.props.content,
|
|
68
69
|
...node.props.resolved,
|
|
69
70
|
children,
|
|
@@ -7,7 +7,7 @@ interface NodeRendererProps {
|
|
|
7
7
|
config: Config;
|
|
8
8
|
experience: RenderContext;
|
|
9
9
|
renderUnknown: RenderUnknown;
|
|
10
|
-
children: Snippet;
|
|
10
|
+
children: Snippet[];
|
|
11
11
|
}
|
|
12
12
|
declare const NodeRenderer: import("svelte").Component<NodeRendererProps, {}, "">;
|
|
13
13
|
type NodeRenderer = ReturnType<typeof NodeRenderer>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodeRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAI3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAGH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,iBAAiB;IACzB,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"NodeRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodeRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAI3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAGH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,iBAAiB;IACzB,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAqEH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
* Recursive renderer over PortableRenderNodes. Customer components receive
|
|
3
3
|
* the merged content / design / resolveData prop bag plus the default
|
|
4
|
-
* `children` slot as
|
|
4
|
+
* `children` slot as an ARRAY of Snippets — one zero-arg Snippet per
|
|
5
|
+
* pre-rendered child node — declared as `children?: Snippet[]`. A component
|
|
6
|
+
* renders them all with an each-block + @render, or maps/filters/wraps the
|
|
7
|
+
* children individually. The array mirrors the React adapter's ReactNode[]
|
|
8
|
+
* slot contract; see the README for the consuming-a-slot example.
|
|
5
9
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* {text}
|
|
12
|
-
* {@render children?.()}
|
|
13
|
-
* </button>
|
|
14
|
-
*
|
|
15
|
-
* Only the `children` slot is rendered automatically — Svelte 5 Snippets are
|
|
16
|
-
* compile-time entities, so the renderer can't synthesize an unbounded set
|
|
17
|
-
* of named Snippet props from runtime payload data. Additional named slots
|
|
18
|
-
* are reachable through `getContentfulComponent().slots` and can be rendered
|
|
19
|
-
* with the exported `<NodesRenderer />` (see README).
|
|
10
|
+
* Only the default `children` slot is injected as a prop — Svelte 5 Snippets
|
|
11
|
+
* are compile-time entities, so the renderer can't synthesize an unbounded
|
|
12
|
+
* set of named Snippet props from runtime payload data. Additional named
|
|
13
|
+
* slots are reachable through `getContentfulComponent().slots` and can be
|
|
14
|
+
* rendered with the exported NodesRenderer (see README).
|
|
20
15
|
*
|
|
21
16
|
* The Experience runtime context and the raw Contentful payload are read
|
|
22
17
|
* via `getExperience` / `getContentfulComponent`.
|
|
@@ -25,10 +20,11 @@
|
|
|
25
20
|
* the active viewport is sourced (initial seed vs reactive matchMedia).
|
|
26
21
|
-->
|
|
27
22
|
<script lang="ts">
|
|
23
|
+
import type { Snippet } from 'svelte';
|
|
24
|
+
|
|
28
25
|
import type { PortableRenderNode } from '@contentful/experiences-sdk-core';
|
|
29
26
|
|
|
30
27
|
import NodeRenderer from './NodeRenderer.svelte';
|
|
31
|
-
import NodesRenderer from './NodesRenderer.svelte';
|
|
32
28
|
import type { RenderUnknown } from './component-props.js';
|
|
33
29
|
import type { Config, RenderContext } from './types.js';
|
|
34
30
|
|
|
@@ -40,12 +36,24 @@
|
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
let { nodes, config, experience, renderUnknown }: NodesRendererProps = $props();
|
|
39
|
+
|
|
40
|
+
// Curry the parameterized `renderNode` snippet into a zero-arg Snippet bound
|
|
41
|
+
// to one node. Svelte 5 invokes a snippet as `(anchor, ...argGetters)`, so we
|
|
42
|
+
// forward the anchor and supply the node through a getter. This is the one
|
|
43
|
+
// spot that leans on Svelte's snippet calling convention; pinned by the
|
|
44
|
+
// `children`-array tests in server-renderer.test.ts.
|
|
45
|
+
function toChildSnippet(node: PortableRenderNode): Snippet {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- programmatic snippet construction; see note above.
|
|
47
|
+
return ((anchor: unknown) => (renderNode as any)(anchor, () => node)) as Snippet;
|
|
48
|
+
}
|
|
43
49
|
</script>
|
|
44
50
|
|
|
45
|
-
{#
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
<NodesRenderer nodes={childrenNodes} {config} {experience} {renderUnknown} />
|
|
49
|
-
{/snippet}
|
|
51
|
+
{#snippet renderNode(node: PortableRenderNode)}
|
|
52
|
+
{@const childNodes = (node.slots.children ?? []) as PortableRenderNode[]}
|
|
53
|
+
{@const children = childNodes.map(toChildSnippet)}
|
|
50
54
|
<NodeRenderer {node} {config} {experience} {renderUnknown} {children} />
|
|
55
|
+
{/snippet}
|
|
56
|
+
|
|
57
|
+
{#each nodes as node, index (node.nodeId ?? index)}
|
|
58
|
+
{@render renderNode(node)}
|
|
51
59
|
{/each}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { PortableRenderNode } from '@contentful/experiences-sdk-core';
|
|
2
|
-
import NodesRenderer from './NodesRenderer.svelte';
|
|
3
2
|
import type { RenderUnknown } from './component-props.js';
|
|
4
3
|
import type { Config, RenderContext } from './types.js';
|
|
5
4
|
interface NodesRendererProps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodesRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodesRenderer.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"NodesRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodesRenderer.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGtD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;CAC9B;AAqCH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServerExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ServerExperienceRenderer.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"ServerExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ServerExperienceRenderer.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAsE1E,QAAA,MAAM,wBAAwB,mEAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
import type { Snippet } from 'svelte';
|
|
10
10
|
|
|
11
11
|
import type { PortableExperienceTemplate } from '@contentful/experiences-sdk-core';
|
|
12
|
-
import { applyTokenResolver, resolveDesignProperties } from '@contentful/experiences-design';
|
|
13
12
|
|
|
14
13
|
import { setContentfulExperienceTemplate, setResolvedDesign } from './context.js';
|
|
14
|
+
import { selectResolvedDesign } from './design-utils.js';
|
|
15
15
|
import {
|
|
16
16
|
normalizeExperienceTemplateRegistration,
|
|
17
17
|
type ContentfulExperienceTemplate,
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
setContentfulExperienceTemplate({
|
|
35
35
|
experienceTemplateId: experienceTemplate.experienceTemplateId,
|
|
36
36
|
content: experienceTemplate.props.content,
|
|
37
|
-
design: experienceTemplate.props.
|
|
37
|
+
design: experienceTemplate.props.designRaw,
|
|
38
38
|
resolved: experienceTemplate.props.resolved,
|
|
39
39
|
} satisfies ContentfulExperienceTemplate);
|
|
40
40
|
}
|
|
@@ -44,12 +44,13 @@
|
|
|
44
44
|
|
|
45
45
|
const tokenResolvedDesign = $derived.by(() => {
|
|
46
46
|
if (!experienceTemplate) return {};
|
|
47
|
-
const
|
|
48
|
-
experienceTemplate.props
|
|
47
|
+
const { props, unresolved } = selectResolvedDesign(
|
|
48
|
+
experienceTemplate.props,
|
|
49
49
|
experience.viewports,
|
|
50
|
-
experience.activeViewportIndex
|
|
50
|
+
experience.activeViewportIndex,
|
|
51
|
+
experience.fallbackViewportIndex,
|
|
52
|
+
config.resolveToken
|
|
51
53
|
);
|
|
52
|
-
const { props, unresolved } = applyTokenResolver(resolvedDesign, config.resolveToken);
|
|
53
54
|
if (unresolved.length && typeof console !== 'undefined') {
|
|
54
55
|
console.warn(
|
|
55
56
|
`[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on experience template "${experienceTemplate.experienceTemplateId}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
|
|
@@ -58,13 +59,15 @@
|
|
|
58
59
|
return props;
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
// A getter (not a snapshot) so getDesignValues() stays reactive
|
|
62
|
+
// A getter (not a snapshot) so getDesignValues() stays reactive.
|
|
62
63
|
setResolvedDesign(() => tokenResolvedDesign);
|
|
63
64
|
|
|
65
|
+
// Same precedence as component nodes.
|
|
64
66
|
const composed = $derived.by(() => {
|
|
65
67
|
if (!experienceTemplate || !experienceTemplateConfig) return null;
|
|
66
68
|
return {
|
|
67
69
|
...experienceTemplateConfig.defaults,
|
|
70
|
+
...tokenResolvedDesign,
|
|
68
71
|
...experienceTemplate.props.content,
|
|
69
72
|
...experienceTemplate.props.resolved,
|
|
70
73
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WrapWithExperienceTemplate.svelte.d.ts","sourceRoot":"","sources":["../src/WrapWithExperienceTemplate.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAInF,OAAO,EAGH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,+BAA+B;IACvC,kBAAkB,EAAE,0BAA0B,GAAG,SAAS,CAAC;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"WrapWithExperienceTemplate.svelte.d.ts","sourceRoot":"","sources":["../src/WrapWithExperienceTemplate.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAInF,OAAO,EAGH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,+BAA+B;IACvC,kBAAkB,EAAE,0BAA0B,GAAG,SAAS,CAAC;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AA6EH,QAAA,MAAM,0BAA0B,qEAAwC,CAAC;AACzE,KAAK,0BAA0B,GAAG,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAChF,eAAe,0BAA0B,CAAC"}
|
package/dist/design-utils.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { DesignPropValue, ResolveToken, ViewportDef } from '@contentful/experiences-sdk-core';
|
|
2
|
+
export declare function selectResolvedDesign(props: {
|
|
3
|
+
design: Record<string, unknown>;
|
|
4
|
+
designRaw: Record<string, DesignPropValue>;
|
|
5
|
+
}, viewports: ViewportDef[], activeViewportIndex: number, fallbackViewportIndex: number, resolveToken: ResolveToken | undefined): {
|
|
6
|
+
props: Record<string, unknown>;
|
|
7
|
+
unresolved: string[];
|
|
8
|
+
};
|
|
1
9
|
export interface ToCssOptions {
|
|
2
10
|
/** Keys to skip, matched against the original record key. */
|
|
3
11
|
exclude?: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"design-utils.d.ts","sourceRoot":"","sources":["../src/design-utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"design-utils.d.ts","sourceRoot":"","sources":["../src/design-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAUnG,wBAAgB,oBAAoB,CAClC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CAAE,EACtF,SAAS,EAAE,WAAW,EAAE,EACxB,mBAAmB,EAAE,MAAM,EAC3B,qBAAqB,EAAE,MAAM,EAC7B,YAAY,EAAE,YAAY,GAAG,SAAS,GACrC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CAM1D;AAED,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CACnB,MAAM,EAAE,MAAM,EACd,EAAE,OAAO,EAAE,OAAO,EAAE,GAAE,YAAiB,GACtC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAYjC"}
|
package/dist/design-utils.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import { isCssProperty, toCssKey } from '@contentful/experiences-design';
|
|
1
|
+
import { applyTokenResolver, isCssProperty, resolveDesignProperties, toCssKey, } from '@contentful/experiences-design';
|
|
2
|
+
// Use the server-resolved `props.design` when the active viewport matches the
|
|
3
|
+
// fallback; otherwise recompute the cascade from raw `props.designRaw`.
|
|
4
|
+
export function selectResolvedDesign(props, viewports, activeViewportIndex, fallbackViewportIndex, resolveToken) {
|
|
5
|
+
if (activeViewportIndex === fallbackViewportIndex) {
|
|
6
|
+
return { props: props.design, unresolved: [] };
|
|
7
|
+
}
|
|
8
|
+
const resolvedDesign = resolveDesignProperties(props.designRaw, viewports, activeViewportIndex);
|
|
9
|
+
return applyTokenResolver(resolvedDesign, resolveToken);
|
|
10
|
+
}
|
|
2
11
|
/**
|
|
3
12
|
* Convert a design record into a plain style object, keeping only keys that
|
|
4
13
|
* normalize to a known CSS property. Non-CSS keys (`variant`, `as`, …) and
|
|
@@ -9,6 +9,7 @@ import HeadingFixture from './test-fixtures/HeadingFixture.svelte';
|
|
|
9
9
|
import ItemFixture from './test-fixtures/ItemFixture.svelte';
|
|
10
10
|
import PrecedenceFixture from './test-fixtures/PrecedenceFixture.svelte';
|
|
11
11
|
import ExperienceTemplateFixture from './test-fixtures/ExperienceTemplateFixture.svelte';
|
|
12
|
+
import WrappingContainerFixture from './test-fixtures/WrappingContainerFixture.svelte';
|
|
12
13
|
import { captureSink } from './test-fixtures/capture-sink.js';
|
|
13
14
|
import { toCss } from './design-utils.js';
|
|
14
15
|
const VIEWPORTS = [
|
|
@@ -153,17 +154,18 @@ describe('ServerExperienceRenderer', () => {
|
|
|
153
154
|
};
|
|
154
155
|
const planWithMissing = {
|
|
155
156
|
viewports: VIEWPORTS,
|
|
157
|
+
fallbackViewportIndex: 0,
|
|
156
158
|
nodes: [
|
|
157
159
|
{
|
|
158
160
|
nodeId: 'root',
|
|
159
161
|
registration: { componentId: 'contentful-container' },
|
|
160
|
-
props: { content: {}, design: {} },
|
|
162
|
+
props: { content: {}, design: {}, designRaw: {} },
|
|
161
163
|
slots: {
|
|
162
164
|
children: [
|
|
163
165
|
{
|
|
164
166
|
nodeId: 'ghost',
|
|
165
167
|
registration: { componentId: 'NotRegistered' },
|
|
166
|
-
props: { content: {}, design: {} },
|
|
168
|
+
props: { content: {}, design: {}, designRaw: {} },
|
|
167
169
|
slots: {},
|
|
168
170
|
},
|
|
169
171
|
],
|
|
@@ -241,6 +243,7 @@ describe('ServerExperienceRenderer', () => {
|
|
|
241
243
|
};
|
|
242
244
|
const planWithResolved = {
|
|
243
245
|
viewports: VIEWPORTS,
|
|
246
|
+
fallbackViewportIndex: 0,
|
|
244
247
|
nodes: [
|
|
245
248
|
{
|
|
246
249
|
nodeId: 'r',
|
|
@@ -248,6 +251,7 @@ describe('ServerExperienceRenderer', () => {
|
|
|
248
251
|
props: {
|
|
249
252
|
content: { value: 'fromContent' },
|
|
250
253
|
design: {},
|
|
254
|
+
designRaw: {},
|
|
251
255
|
resolved: { value: 'fromResolveData' },
|
|
252
256
|
},
|
|
253
257
|
slots: {},
|
|
@@ -313,6 +317,66 @@ describe('ServerExperienceRenderer', () => {
|
|
|
313
317
|
warn.mockRestore();
|
|
314
318
|
});
|
|
315
319
|
});
|
|
320
|
+
describe('ServerExperienceRenderer — slot children as an array', () => {
|
|
321
|
+
const childrenPayload = () => ({
|
|
322
|
+
viewports: VIEWPORTS,
|
|
323
|
+
nodes: [
|
|
324
|
+
componentNode('contentful-container', {
|
|
325
|
+
id: 'c',
|
|
326
|
+
slots: {
|
|
327
|
+
children: [
|
|
328
|
+
componentNode('contentful-heading', {
|
|
329
|
+
id: 'a',
|
|
330
|
+
contentProperties: { text: 'one' },
|
|
331
|
+
}),
|
|
332
|
+
componentNode('contentful-heading', {
|
|
333
|
+
id: 'b',
|
|
334
|
+
contentProperties: { text: 'two' },
|
|
335
|
+
}),
|
|
336
|
+
componentNode('contentful-heading', {
|
|
337
|
+
id: 'd',
|
|
338
|
+
contentProperties: { text: 'three' },
|
|
339
|
+
}),
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
}),
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
it('passes slot children as a Snippet[] a component can wrap individually', async () => {
|
|
346
|
+
const cfg = {
|
|
347
|
+
components: {
|
|
348
|
+
'contentful-container': WrappingContainerFixture,
|
|
349
|
+
'contentful-heading': HeadingFixture,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
const plan = await resolveExperience(childrenPayload(), cfg);
|
|
353
|
+
const { container } = render(ServerExperienceRenderer, {
|
|
354
|
+
props: { experience: plan, config: cfg },
|
|
355
|
+
});
|
|
356
|
+
// The component received a real array of the right length.
|
|
357
|
+
expect(captureSink[0].props.childrenIsArray).toBe(true);
|
|
358
|
+
expect(captureSink[0].props.childCount).toBe(3);
|
|
359
|
+
// Each child got its own wrapper — proves per-child rendering control.
|
|
360
|
+
const wrappers = container.querySelectorAll('.wrap');
|
|
361
|
+
expect(wrappers.length).toBe(3);
|
|
362
|
+
expect(container.querySelector('[data-index="0"]')).not.toBeNull();
|
|
363
|
+
expect(container.querySelector('[data-index="2"]')).not.toBeNull();
|
|
364
|
+
expect(container.textContent).toContain('one');
|
|
365
|
+
expect(container.textContent).toContain('three');
|
|
366
|
+
});
|
|
367
|
+
it('renders an empty container when the slot has no children', async () => {
|
|
368
|
+
const cfg = {
|
|
369
|
+
components: { 'contentful-container': WrappingContainerFixture },
|
|
370
|
+
};
|
|
371
|
+
const plan = await resolveExperience({ viewports: VIEWPORTS, nodes: [componentNode('contentful-container', { id: 'c' })] }, cfg);
|
|
372
|
+
const { container } = render(ServerExperienceRenderer, {
|
|
373
|
+
props: { experience: plan, config: cfg },
|
|
374
|
+
});
|
|
375
|
+
expect(captureSink[0].props.childCount).toBe(0);
|
|
376
|
+
expect(container.querySelectorAll('.wrap').length).toBe(0);
|
|
377
|
+
expect(container.querySelector('[data-container]')).not.toBeNull();
|
|
378
|
+
});
|
|
379
|
+
});
|
|
316
380
|
describe('ServerExperienceRenderer — bare-component registrations', () => {
|
|
317
381
|
it('accepts a bare Svelte component as a registry entry', async () => {
|
|
318
382
|
const cfg = { components: { item: PrecedenceFixture } };
|
|
@@ -389,7 +453,7 @@ describe('ServerExperienceRenderer — getContentfulComponent()', () => {
|
|
|
389
453
|
});
|
|
390
454
|
});
|
|
391
455
|
describe('ServerExperienceRenderer — resolveToken', () => {
|
|
392
|
-
it('passes DesignToken
|
|
456
|
+
it('passes DesignToken values through the resolver before render', async () => {
|
|
393
457
|
const cfg = {
|
|
394
458
|
components: { 'contentful-button': ButtonFixture },
|
|
395
459
|
resolveToken: (ref) => (ref.value === 'color/surface/hero' ? '#4f39f6' : undefined),
|
|
@@ -410,7 +474,7 @@ describe('ServerExperienceRenderer — resolveToken', () => {
|
|
|
410
474
|
expect(container.innerHTML).toContain('data-bg="#4f39f6"');
|
|
411
475
|
expect(container.innerHTML).not.toContain('DesignToken');
|
|
412
476
|
});
|
|
413
|
-
it('warns and drops the key from the design
|
|
477
|
+
it('warns and drops the key from the design values when the resolver returns undefined', async () => {
|
|
414
478
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
415
479
|
const cfg = {
|
|
416
480
|
components: { 'contentful-button': CapturingComponent },
|
|
@@ -434,7 +498,7 @@ describe('ServerExperienceRenderer — resolveToken', () => {
|
|
|
434
498
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining('contentful-button'));
|
|
435
499
|
warn.mockRestore();
|
|
436
500
|
});
|
|
437
|
-
it('leaves
|
|
501
|
+
it('leaves token values untouched when no resolver is supplied (backward-compatible)', async () => {
|
|
438
502
|
const cfg = { components: { 'contentful-button': ButtonFixture } };
|
|
439
503
|
const plan = await resolveExperience({
|
|
440
504
|
viewports: VIEWPORTS,
|
|
@@ -449,8 +513,8 @@ describe('ServerExperienceRenderer — resolveToken', () => {
|
|
|
449
513
|
const { container } = render(ServerExperienceRenderer, {
|
|
450
514
|
props: { experience: plan, config: cfg },
|
|
451
515
|
});
|
|
452
|
-
// Svelte stringifies the
|
|
453
|
-
// attribute — the point is that the raw
|
|
516
|
+
// Svelte stringifies the token object into "[object Object]" on the
|
|
517
|
+
// attribute — the point is that the raw token value reaches the component
|
|
454
518
|
// unchanged, so the customer can inspect it or resolve it themselves.
|
|
455
519
|
expect(container.innerHTML).toContain('data-bg="[object Object]"');
|
|
456
520
|
});
|
|
@@ -474,17 +538,20 @@ describe('ServerExperienceRenderer — resolveToken', () => {
|
|
|
474
538
|
nodes: [componentNode('item', { id: 'i', contentProperties: { value: 'ok' } })],
|
|
475
539
|
};
|
|
476
540
|
const plan = await resolveExperience(tplPayload, tplConfig);
|
|
477
|
-
// Templates don't carry design props in the current XDA payload
|
|
478
|
-
// seed
|
|
479
|
-
|
|
541
|
+
// Experience Templates don't carry design props in the current XDA payload
|
|
542
|
+
// shape; seed a raw token on the resolved plan so we exercise the
|
|
543
|
+
// experienceTemplate path. Render at a non-fallback viewport (mobile ≠
|
|
544
|
+
// fallback index 0) so the adapter recomputes from the seeded raw design
|
|
545
|
+
// and runs resolveToken.
|
|
546
|
+
plan.experienceTemplate.props.designRaw = { cfBackground: dt('brand/canvas') };
|
|
480
547
|
const { container } = render(ServerExperienceRenderer, {
|
|
481
|
-
props: { experience: plan, config: tplConfig },
|
|
548
|
+
props: { experience: plan, config: tplConfig, initialViewportId: 'mobile' },
|
|
482
549
|
});
|
|
483
550
|
expect(container.innerHTML).toContain('data-bg="#111827"');
|
|
484
551
|
});
|
|
485
552
|
});
|
|
486
|
-
describe('ServerExperienceRenderer — design values
|
|
487
|
-
it('
|
|
553
|
+
describe('ServerExperienceRenderer — design values auto-fill props', () => {
|
|
554
|
+
it('spreads resolved design values onto component props by their raw key', async () => {
|
|
488
555
|
const cfg = { components: { capture: CapturingComponent } };
|
|
489
556
|
const plan = await resolveExperience({
|
|
490
557
|
viewports: VIEWPORTS,
|
|
@@ -499,20 +566,39 @@ describe('ServerExperienceRenderer — design values are not injected as props',
|
|
|
499
566
|
render(ServerExperienceRenderer, {
|
|
500
567
|
props: { experience: plan, config: cfg },
|
|
501
568
|
});
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
569
|
+
// Content flows as a prop, and design auto-fills props under its raw key.
|
|
570
|
+
expect(captureSink[0].props).toMatchObject({
|
|
571
|
+
label: 'keep me',
|
|
572
|
+
cfBackgroundColor: '#f00',
|
|
573
|
+
cfPadding: '10px',
|
|
574
|
+
});
|
|
507
575
|
// The values are still readable through getDesignValues().
|
|
508
576
|
expect(captureSink[0].designValues).toEqual({
|
|
509
577
|
cfBackgroundColor: '#f00',
|
|
510
578
|
cfPadding: '10px',
|
|
511
579
|
});
|
|
512
580
|
});
|
|
581
|
+
it('lets content override design on a key collision (content wins)', async () => {
|
|
582
|
+
const cfg = { components: { capture: CapturingComponent } };
|
|
583
|
+
const plan = await resolveExperience({
|
|
584
|
+
viewports: VIEWPORTS,
|
|
585
|
+
nodes: [
|
|
586
|
+
componentNode('capture', {
|
|
587
|
+
id: 'p',
|
|
588
|
+
// Same key in both channels: content must win.
|
|
589
|
+
contentProperties: { cfPadding: 'from-content' },
|
|
590
|
+
designProperties: { cfPadding: m('from-design') },
|
|
591
|
+
}),
|
|
592
|
+
],
|
|
593
|
+
}, cfg);
|
|
594
|
+
render(ServerExperienceRenderer, {
|
|
595
|
+
props: { experience: plan, config: cfg },
|
|
596
|
+
});
|
|
597
|
+
expect(captureSink[0].props).toMatchObject({ cfPadding: 'from-content' });
|
|
598
|
+
});
|
|
513
599
|
});
|
|
514
600
|
describe('ServerExperienceRenderer — getDesignValues()', () => {
|
|
515
|
-
it('returns the resolved design
|
|
601
|
+
it('returns the resolved design values for the current node', async () => {
|
|
516
602
|
const cfg = {
|
|
517
603
|
components: { capture: CapturingComponent },
|
|
518
604
|
resolveToken: (ref) => (ref.value === 'brand/primary' ? '#4f39f6' : undefined),
|
|
@@ -554,6 +640,46 @@ describe('ServerExperienceRenderer — getDesignValues()', () => {
|
|
|
554
640
|
expect(captureSink[0].designValues).toEqual({ cfPadding: '12px' });
|
|
555
641
|
});
|
|
556
642
|
});
|
|
643
|
+
describe('ServerExperienceRenderer — server pre-resolved design values', () => {
|
|
644
|
+
const probePayload = {
|
|
645
|
+
viewports: VIEWPORTS,
|
|
646
|
+
nodes: [
|
|
647
|
+
componentNode('contentful-container', {
|
|
648
|
+
id: 'p',
|
|
649
|
+
designProperties: { cfPadding: vbv({ desktop: m('40px'), mobile: m('12px') }) },
|
|
650
|
+
}),
|
|
651
|
+
],
|
|
652
|
+
};
|
|
653
|
+
it('consumes props.design as-is when the active viewport equals the fallback', async () => {
|
|
654
|
+
const plan = await resolveExperience(probePayload, config, { initialViewportId: 'mobile' });
|
|
655
|
+
// Tamper the precomputed values with a sentinel the cascade could never produce.
|
|
656
|
+
plan.nodes[0].props.design = { cfPadding: 'SENTINEL' };
|
|
657
|
+
const { container } = render(ServerExperienceRenderer, {
|
|
658
|
+
props: { experience: plan, config, initialViewportId: 'mobile' },
|
|
659
|
+
});
|
|
660
|
+
expect(container.innerHTML).toContain('data-padding="SENTINEL"');
|
|
661
|
+
});
|
|
662
|
+
it('recomputes from raw design properties when the active viewport differs from the fallback', async () => {
|
|
663
|
+
const plan = await resolveExperience(probePayload, config, { initialViewportId: 'mobile' });
|
|
664
|
+
plan.nodes[0].props.design = { cfPadding: 'SENTINEL' };
|
|
665
|
+
// Active viewport (desktop, idx 0) ≠ fallback (mobile, idx 2) → recompute.
|
|
666
|
+
const { container } = render(ServerExperienceRenderer, {
|
|
667
|
+
props: { experience: plan, config, initialViewportId: 'desktop' },
|
|
668
|
+
});
|
|
669
|
+
expect(container.innerHTML).toContain('data-padding="40px"');
|
|
670
|
+
expect(container.innerHTML).not.toContain('SENTINEL');
|
|
671
|
+
});
|
|
672
|
+
it('recomputes when the active viewport differs from the default fallback (viewport[0])', async () => {
|
|
673
|
+
const plan = await resolveExperience(probePayload, config);
|
|
674
|
+
// No fallback configured → pre-resolved against viewport[0] (desktop, idx 0).
|
|
675
|
+
expect(plan.fallbackViewportIndex).toBe(0);
|
|
676
|
+
// Active viewport is mobile (idx 2) ≠ fallback (idx 0) → recompute to 12px.
|
|
677
|
+
const { container } = render(ServerExperienceRenderer, {
|
|
678
|
+
props: { experience: plan, config, initialViewportId: 'mobile' },
|
|
679
|
+
});
|
|
680
|
+
expect(container.innerHTML).toContain('data-padding="12px"');
|
|
681
|
+
});
|
|
682
|
+
});
|
|
557
683
|
describe('toCss (Svelte)', () => {
|
|
558
684
|
it('converts bare (non-cf) CSS keys — the shape real payloads use', () => {
|
|
559
685
|
expect(toCss({ fontSize: '20px', backgroundColor: '#4f39f6' })).toEqual({
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import { getDesignValues } from '../get-design-values.js';
|
|
5
5
|
|
|
6
|
-
let { children }: { children?: Snippet } = $props();
|
|
6
|
+
let { children }: { children?: Snippet[] } = $props();
|
|
7
7
|
const design = $derived(getDesignValues());
|
|
8
8
|
</script>
|
|
9
9
|
|
|
10
10
|
<div data-padding={design.cfPadding as string}>
|
|
11
|
-
{#
|
|
11
|
+
{#each children ?? [] as child}
|
|
12
|
+
{@render child()}
|
|
13
|
+
{/each}
|
|
12
14
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
|
-
children?: Snippet;
|
|
3
|
+
children?: Snippet[];
|
|
4
4
|
};
|
|
5
5
|
declare const ContainerFixture: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
6
|
type ContainerFixture = ReturnType<typeof ContainerFixture>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContainerFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/ContainerFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIrC,KAAK,gBAAgB,GAAI;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"ContainerFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/ContainerFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIrC,KAAK,gBAAgB,GAAI;IAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC;AAkBnD,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Test fixture: proves slot children arrive as an array of Snippets that a
|
|
3
|
+
component can iterate and wrap individually. Also records the received array
|
|
4
|
+
length in the capture sink.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
8
|
+
|
|
9
|
+
import { getContentfulComponent, getExperience } from '../context.js';
|
|
10
|
+
import { captureSink } from './capture-sink.js';
|
|
11
|
+
|
|
12
|
+
let { children }: { children?: Snippet[] } = $props();
|
|
13
|
+
|
|
14
|
+
captureSink.push({
|
|
15
|
+
props: { childCount: children?.length ?? 0, childrenIsArray: Array.isArray(children) },
|
|
16
|
+
experience: getExperience(),
|
|
17
|
+
contentful: getContentfulComponent(),
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div data-container>
|
|
22
|
+
{#each children ?? [] as child, i}
|
|
23
|
+
<div class="wrap" data-index={i}>
|
|
24
|
+
{@render child()}
|
|
25
|
+
</div>
|
|
26
|
+
{/each}
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
children?: Snippet[];
|
|
4
|
+
};
|
|
5
|
+
declare const WrappingContainerFixture: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type WrappingContainerFixture = ReturnType<typeof WrappingContainerFixture>;
|
|
7
|
+
export default WrappingContainerFixture;
|
|
8
|
+
//# sourceMappingURL=WrappingContainerFixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WrappingContainerFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/WrappingContainerFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKrC,KAAK,gBAAgB,GAAI;IAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC;AA2BnD,QAAA,MAAM,wBAAwB,sDAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -9,9 +9,9 @@ export type { ResolveContext, ResolveToken };
|
|
|
9
9
|
* analytics on `nodeId`, rendering a raw-payload panel in preview, or
|
|
10
10
|
* rendering non-`children` slots through the exported `<NodesRenderer />`.
|
|
11
11
|
*
|
|
12
|
-
* Design properties stay in their **raw
|
|
13
|
-
* `ctx.design` carries inside `resolveData`). The viewport-cascaded,
|
|
14
|
-
* resolved values are what `getDesignValues()` returns.
|
|
12
|
+
* Design properties stay in their **raw discriminated form** here (the same
|
|
13
|
+
* shape `ctx.design` carries inside `resolveData`). The viewport-cascaded,
|
|
14
|
+
* token-resolved values are what `getDesignValues()` returns.
|
|
15
15
|
*/
|
|
16
16
|
export interface ContentfulComponent {
|
|
17
17
|
componentId: string;
|
|
@@ -46,12 +46,19 @@ export interface ContentfulExperienceTemplate {
|
|
|
46
46
|
export interface RenderContext extends ExperienceContext {
|
|
47
47
|
activeViewport: ViewportDef;
|
|
48
48
|
activeViewportIndex: number;
|
|
49
|
+
/**
|
|
50
|
+
* Viewport index the server pre-resolved design against (from
|
|
51
|
+
* `PortableRenderPlan.fallbackViewportIndex`). Renderers use `props.design`
|
|
52
|
+
* as-is when `activeViewportIndex` matches, and recompute otherwise. Always a
|
|
53
|
+
* number — the plan field is required, so the match is a real comparison.
|
|
54
|
+
*/
|
|
55
|
+
fallbackViewportIndex: number;
|
|
49
56
|
}
|
|
50
57
|
/**
|
|
51
58
|
* Customer-supplied configuration for a single component type. The `component`
|
|
52
|
-
* receives the merged
|
|
53
|
-
*
|
|
54
|
-
* `getDesignValues()`. Runtime context and raw payload come from
|
|
59
|
+
* receives the merged props (design + content + resolveData + `children`
|
|
60
|
+
* Snippet); resolved design values auto-fill matching props and are also
|
|
61
|
+
* readable via `getDesignValues()`. Runtime context and raw payload come from
|
|
55
62
|
* `getExperience()` / `getContentfulComponent()`.
|
|
56
63
|
*/
|
|
57
64
|
export interface ComponentConfig<Props extends object = Record<string, unknown>> {
|
|
@@ -83,8 +90,8 @@ export interface Config {
|
|
|
83
90
|
components: Components;
|
|
84
91
|
experienceTemplates?: ExperienceTemplates;
|
|
85
92
|
/**
|
|
86
|
-
* Resolves `DesignToken`
|
|
87
|
-
* component. If omitted,
|
|
93
|
+
* Resolves `DesignToken` values to runtime values before they reach a
|
|
94
|
+
* component. If omitted, they pass through unchanged. See `ResolveToken`
|
|
88
95
|
* in `@contentful/experiences-sdk-core` for the full contract.
|
|
89
96
|
*/
|
|
90
97
|
resolveToken?: ResolveToken;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,WAAW,EACZ,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C,oBAAoB,EAAE,MAAM,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,cAAc,EAAE,WAAW,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experiences-svelte",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Svelte adapter for Contentful Experiences — Server and Client renderers over a PortableRenderPlan",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@contentful/experiences-client": "0.3.
|
|
39
|
-
"@contentful/experiences-sdk-core": "0.7.
|
|
40
|
-
"@contentful/experiences-design": "0.7.
|
|
38
|
+
"@contentful/experiences-client": "0.3.1",
|
|
39
|
+
"@contentful/experiences-sdk-core": "0.7.1",
|
|
40
|
+
"@contentful/experiences-design": "0.7.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"svelte": "^5.55.7"
|