@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 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>(); // Resolved design values for the current node; read in a $derived to stay reactive
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
- Design is **not** injected as props; components read it via `getDesignValues()` from the top of their `<script>` block. Token resolution is configured with `resolveToken` on your `Config` (`type ResolveToken`).
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 via props, design via the helper -->
79
+ <!-- Button.svelte: content + resolved design both arrive as props -->
80
80
  <script lang="ts">
81
- import { getDesignValues, toCss } from '@contentful/experiences-svelte';
82
-
83
- let { label = 'Button', url }: ButtonProps = $props();
84
- const design = $derived(getDesignValues());
85
- const style = $derived(toCss(design));
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={style}>{label}</a>
91
+ <a href={url} style="background: {backgroundColor}; color: {color};">{label}</a>
89
92
  {:else}
90
- <button type="button" style={style}>{label}</button>
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":"AAyFA,QAAA,MAAM,wBAAwB,kGAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
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 = { viewports: [], nodes: [node('button')] };
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: { experienceTemplateId: 'page', props: { content: {}, design: {} } },
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]');
@@ -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.design,
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 resolvedDesign = resolveDesignProperties(
48
- node.props.design,
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;CACnB;AAoEH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,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 a named Snippet:
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
- * <script lang="ts">
7
- * import type { Snippet } from 'svelte';
8
- * let { text, children }: { text?: string; children?: Snippet } = $props();
9
- * </script>
10
- * <button>
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
- {#each nodes as node, index (node.nodeId ?? index)}
46
- {#snippet children()}
47
- {@const childrenNodes = (node.slots.children ?? []) as PortableRenderNode[]}
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":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,OAAO,aAAa,MAAM,wBAAwB,CAAC;AACnD,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;AAwBH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
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"}
@@ -51,6 +51,7 @@
51
51
  viewports,
52
52
  activeViewport: experience?.viewports[idx] ?? FALLBACK_VIEWPORT,
53
53
  activeViewportIndex: idx,
54
+ fallbackViewportIndex: experience?.fallbackViewportIndex ?? 0,
54
55
  };
55
56
  }
56
57
 
@@ -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;AAqE1E,QAAA,MAAM,wBAAwB,mEAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,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.design,
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 resolvedDesign = resolveDesignProperties(
48
- experienceTemplate.props.design,
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; not merged into props.
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;AA0EH,QAAA,MAAM,0BAA0B,qEAAwC,CAAC;AACzE,KAAK,0BAA0B,GAAG,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAChF,eAAe,0BAA0B,CAAC"}
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"}
@@ -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":"AAEA,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"}
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"}
@@ -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 envelopes through the resolver before render', async () => {
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 bag when the resolver returns undefined', async () => {
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 envelopes untouched when no resolver is supplied', async () => {
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 envelope object into "[object Object]" on the
453
- // attribute — the point is that the raw envelope reaches the component
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 shape;
478
- // seed one on the resolved plan so we exercise the experienceTemplate path.
479
- plan.experienceTemplate.props.design = { cfBackground: dt('brand/canvas') };
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 are not injected as props', () => {
487
- it('does NOT spread resolved design values onto component props', async () => {
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
- const keys = Object.keys(captureSink[0].props);
503
- // Content still flows as a prop; design does not.
504
- expect(keys).toContain('label');
505
- expect(keys).not.toContain('cfBackgroundColor');
506
- expect(keys).not.toContain('cfPadding');
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 bag for the current node', async () => {
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
- {#if children}{@render children()}{/if}
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;AAgBjD,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,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 envelope form** here (the same shape
13
- * `ctx.design` carries inside `resolveData`). The viewport-cascaded, token-
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 prop bag (content + resolveData + `children` Snippet).
53
- * Design values are not injected — a component reads them via
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` envelopes to runtime values before they reach a
87
- * component. If omitted, envelopes pass through unchanged. See `ResolveToken`
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;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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.0",
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.0",
39
- "@contentful/experiences-sdk-core": "0.7.0",
40
- "@contentful/experiences-design": "0.7.0"
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"