@contentful/experiences-react 0.5.1
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 +75 -0
- package/README.md +117 -0
- package/dist/client-renderer.d.ts +17 -0
- package/dist/client-renderer.js +60 -0
- package/dist/client-renderer.js.map +1 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +60 -0
- package/dist/context.js.map +1 -0
- package/dist/design-utils.d.ts +18 -0
- package/dist/design-utils.js +17 -0
- package/dist/design-utils.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/missing-component.d.ts +10 -0
- package/dist/missing-component.js +36 -0
- package/dist/missing-component.js.map +1 -0
- package/dist/nodes-renderer.d.ts +28 -0
- package/dist/nodes-renderer.js +134 -0
- package/dist/nodes-renderer.js.map +1 -0
- package/dist/server-renderer.d.ts +19 -0
- package/dist/server-renderer.js +59 -0
- package/dist/server-renderer.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -0
- package/dist/use-active-viewport.d.ts +8 -0
- package/dist/use-active-viewport.js +61 -0
- package/dist/use-active-viewport.js.map +1 -0
- package/dist/use-design-values.d.ts +14 -0
- package/dist/use-design-values.js +9 -0
- package/dist/use-design-values.js.map +1 -0
- package/package.json +46 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
## 0.5.1 (2026-07-17)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- design-token resolution via resolveToken + useDesignValues [AIS-149] ([#53](https://github.com/contentful/experiences/pull/53))
|
|
6
|
+
|
|
7
|
+
### 🧱 Updated Dependencies
|
|
8
|
+
|
|
9
|
+
- Updated client to 0.1.1
|
|
10
|
+
- Updated design to 0.5.1
|
|
11
|
+
- Updated core to 0.5.1
|
|
12
|
+
|
|
13
|
+
## 0.5.0 (2026-07-17)
|
|
14
|
+
|
|
15
|
+
### 🚀 Features
|
|
16
|
+
|
|
17
|
+
- add packages/client with fetchExperience and delivery client [AIS-147] ([#30](https://github.com/contentful/experiences/pull/30), [#27](https://github.com/contentful/experiences/issues/27))
|
|
18
|
+
|
|
19
|
+
### 🧱 Updated Dependencies
|
|
20
|
+
|
|
21
|
+
- Updated client to 0.1.0
|
|
22
|
+
- Updated design to 0.5.0
|
|
23
|
+
- Updated core to 0.5.0
|
|
24
|
+
|
|
25
|
+
## 0.4.0 (2026-07-08)
|
|
26
|
+
|
|
27
|
+
### 🚀 Features
|
|
28
|
+
|
|
29
|
+
- idiomatic adapters — bare components + context hooks ([0365bfb](https://github.com/contentful/experiences/commit/0365bfb))
|
|
30
|
+
|
|
31
|
+
### 🧱 Updated Dependencies
|
|
32
|
+
|
|
33
|
+
- Updated design to 0.4.0
|
|
34
|
+
- Updated core to 0.4.0
|
|
35
|
+
|
|
36
|
+
## 0.3.0 (2026-06-24)
|
|
37
|
+
|
|
38
|
+
### 🚀 Features
|
|
39
|
+
|
|
40
|
+
- more-robust examples + simple/advanced README split + contentful prop ([#18](https://github.com/contentful/experiences/pull/18))
|
|
41
|
+
|
|
42
|
+
### 🧱 Updated Dependencies
|
|
43
|
+
|
|
44
|
+
- Updated design to 0.3.0
|
|
45
|
+
- Updated core to 0.3.0
|
|
46
|
+
|
|
47
|
+
## 0.2.0 (2026-06-24)
|
|
48
|
+
|
|
49
|
+
### 🧱 Updated Dependencies
|
|
50
|
+
|
|
51
|
+
- Updated design to 0.2.0
|
|
52
|
+
- Updated core to 0.2.0
|
|
53
|
+
|
|
54
|
+
## 0.1.2 (2026-06-24)
|
|
55
|
+
|
|
56
|
+
### 🧱 Updated Dependencies
|
|
57
|
+
|
|
58
|
+
- Updated design to 0.1.2
|
|
59
|
+
|
|
60
|
+
## 0.1.1 (2026-06-23)
|
|
61
|
+
|
|
62
|
+
### 🧱 Updated Dependencies
|
|
63
|
+
|
|
64
|
+
- Updated design to 0.1.1
|
|
65
|
+
|
|
66
|
+
## 0.1.0 (2026-06-23)
|
|
67
|
+
|
|
68
|
+
### 🚀 Features
|
|
69
|
+
|
|
70
|
+
- initial Experiences SDK monorepo with React adapter [] ([#16](https://github.com/contentful/experiences/pull/16))
|
|
71
|
+
|
|
72
|
+
### 🧱 Updated Dependencies
|
|
73
|
+
|
|
74
|
+
- Updated design to 0.1.0
|
|
75
|
+
- Updated core to 0.1.0
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @contentful/experiences-react
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Pre-alpha.** Not yet published to npm. APIs are unstable and will change.
|
|
4
|
+
|
|
5
|
+
The React adapter for the Contentful Experiences SDK suite. Renders Experience payloads from the Experience Delivery API (XDA) using customer-supplied React components.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @contentful/experiences-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This is the **only SDK package customers install** — it re-exports everything from `@contentful/experiences-core`, `@contentful/experiences-design`, and `@contentful/experiences-client` that consumers need. The other packages are workspace-internal.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Public API
|
|
16
|
+
|
|
17
|
+
### Authoring
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
defineComponent<Props>(config); // Type-narrowing identity for component-type configs
|
|
21
|
+
defineTemplate<Props>(config); // Same shape, for page-level template wrappers
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Fetching
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
fetchExperience(experienceOptions, clientOptions, resolveOptions) // Async — fetches from XDA + resolves in one call
|
|
28
|
+
createClient(options) // Functional constructor matching the SDK's option shape
|
|
29
|
+
ContentfulViewDeliveryClient // Re-exported delivery client for advanced use cases
|
|
30
|
+
NotFoundError // Thrown when the Experience ID doesn't exist
|
|
31
|
+
ContentfulViewDelivery // Full error namespace from the delivery client
|
|
32
|
+
type ExperienceOptions, ClientOptions, ResolveOptions, CreateClientOptions
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Resolver
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
resolveExperience(payload, config, opts?) // Async — walks payload, runs resolveData, returns a PortableRenderPlan
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Renderers
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
ServerExperienceRenderer; // RSC-friendly, active viewport seeded from initialViewportId
|
|
45
|
+
ClientExperienceRenderer; // 'use client', subscribes to window.matchMedia, alias: ExperienceRenderer
|
|
46
|
+
MissingComponent; // Default fallback for unregistered component types
|
|
47
|
+
useActiveViewport; // Hook used inside ClientExperienceRenderer (rarely needed by consumers)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Re-exported types and utilities
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// From core
|
|
54
|
+
type Config, Components, Templates,
|
|
55
|
+
type ComponentConfig, TemplateConfig,
|
|
56
|
+
type ComponentProps, TemplateProps,
|
|
57
|
+
type RenderContext,
|
|
58
|
+
type ExperiencePayload, ExperienceNode, ComponentTypeNode, TemplateNode,
|
|
59
|
+
type PortableRenderPlan, PortableRenderNode, PortableTemplate,
|
|
60
|
+
type DesignPropValue, ManualDesignValue, DesignToken, ValuesByViewport,
|
|
61
|
+
type ViewportDef, ExperienceContext, ResolveContext,
|
|
62
|
+
type ResolverConfig, ResolveExperienceOptions
|
|
63
|
+
|
|
64
|
+
// From design (for customers who want to do their own viewport-aware resolution)
|
|
65
|
+
getValueForViewport, getViewportIndex, resolveDesignProperties, toCssMediaQuery
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Quick reference
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { notFound } from 'next/navigation';
|
|
74
|
+
import {
|
|
75
|
+
defineComponent,
|
|
76
|
+
defineTemplate,
|
|
77
|
+
fetchExperience,
|
|
78
|
+
NotFoundError,
|
|
79
|
+
ServerExperienceRenderer,
|
|
80
|
+
type Config,
|
|
81
|
+
type Components,
|
|
82
|
+
type Templates,
|
|
83
|
+
} from '@contentful/experiences-react';
|
|
84
|
+
|
|
85
|
+
import { Button, type ButtonProps } from './components/Button';
|
|
86
|
+
|
|
87
|
+
const components: Components = {
|
|
88
|
+
button: defineComponent<ButtonProps>({
|
|
89
|
+
defaults: { type: 'primary' },
|
|
90
|
+
resolveData: ({ content }) => ({ url: ensureScheme(content.url) }),
|
|
91
|
+
render: Button,
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const experienceConfig: Config = { components };
|
|
96
|
+
|
|
97
|
+
// In a server component:
|
|
98
|
+
try {
|
|
99
|
+
const experience = await fetchExperience(
|
|
100
|
+
{ spaceId: process.env.SPACE_ID!, environmentId: 'master', experienceId: slug },
|
|
101
|
+
{ accessToken: process.env.CDA_TOKEN! },
|
|
102
|
+
{ config: experienceConfig }
|
|
103
|
+
);
|
|
104
|
+
return <ServerExperienceRenderer experience={experience} config={experienceConfig} />;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err instanceof NotFoundError) notFound();
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
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).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT. See the repository [`LICENSE`](../../LICENSE) and [`NOTICE`](../../NOTICE) for full attribution.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { PortableRenderPlan, ExperienceContext } from '@contentful/experiences-core';
|
|
3
|
+
import { RenderUnknown } from './nodes-renderer.js';
|
|
4
|
+
import { Config } from './types.js';
|
|
5
|
+
import './missing-component.js';
|
|
6
|
+
|
|
7
|
+
interface ClientExperienceRendererProps {
|
|
8
|
+
experience: PortableRenderPlan | null | undefined;
|
|
9
|
+
config: Config;
|
|
10
|
+
/** Initial viewport seed; should match what the server-rendered output used. */
|
|
11
|
+
initialViewportId?: string;
|
|
12
|
+
context?: Partial<ExperienceContext>;
|
|
13
|
+
renderUnknown?: RenderUnknown;
|
|
14
|
+
}
|
|
15
|
+
declare function ClientExperienceRenderer({ experience, config, initialViewportId, context, renderUnknown, }: ClientExperienceRendererProps): ReactNode;
|
|
16
|
+
|
|
17
|
+
export { ClientExperienceRenderer, type ClientExperienceRendererProps };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ExperienceProvider } from "./context";
|
|
4
|
+
import { MissingComponent } from "./missing-component";
|
|
5
|
+
import { NodesRenderer, WrapWithTemplate } from "./nodes-renderer";
|
|
6
|
+
import { useActiveViewport } from "./use-active-viewport";
|
|
7
|
+
const DEFAULT_CONTEXT = {
|
|
8
|
+
isPreview: false,
|
|
9
|
+
metadata: {},
|
|
10
|
+
viewports: []
|
|
11
|
+
};
|
|
12
|
+
const FALLBACK_VIEWPORT = {
|
|
13
|
+
id: "_",
|
|
14
|
+
query: "*",
|
|
15
|
+
displayName: "Default",
|
|
16
|
+
previewSize: "100%"
|
|
17
|
+
};
|
|
18
|
+
function ClientExperienceRenderer({
|
|
19
|
+
experience,
|
|
20
|
+
config,
|
|
21
|
+
initialViewportId,
|
|
22
|
+
context,
|
|
23
|
+
renderUnknown = MissingComponent
|
|
24
|
+
}) {
|
|
25
|
+
if (!experience) return null;
|
|
26
|
+
const { activeViewportIndex } = useActiveViewport(experience.viewports, initialViewportId);
|
|
27
|
+
const contextViewports = experience.viewports.map((v) => ({ ...v }));
|
|
28
|
+
const activeViewport = { ...experience.viewports[activeViewportIndex] ?? FALLBACK_VIEWPORT };
|
|
29
|
+
const renderContext = {
|
|
30
|
+
...DEFAULT_CONTEXT,
|
|
31
|
+
...context,
|
|
32
|
+
metadata: { ...DEFAULT_CONTEXT.metadata, ...context?.metadata ?? {} },
|
|
33
|
+
viewports: contextViewports,
|
|
34
|
+
activeViewport,
|
|
35
|
+
activeViewportIndex
|
|
36
|
+
};
|
|
37
|
+
return /* @__PURE__ */ jsx(ExperienceProvider, { value: renderContext, children: /* @__PURE__ */ jsx(
|
|
38
|
+
WrapWithTemplate,
|
|
39
|
+
{
|
|
40
|
+
template: experience.template,
|
|
41
|
+
config,
|
|
42
|
+
viewports: experience.viewports,
|
|
43
|
+
activeViewportIndex,
|
|
44
|
+
children: /* @__PURE__ */ jsx(
|
|
45
|
+
NodesRenderer,
|
|
46
|
+
{
|
|
47
|
+
nodes: experience.nodes,
|
|
48
|
+
config,
|
|
49
|
+
viewports: experience.viewports,
|
|
50
|
+
activeViewportIndex,
|
|
51
|
+
renderUnknown
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
) });
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
ClientExperienceRenderer
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=client-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client-renderer.tsx"],"sourcesContent":["/*\n * Client Experience renderer. First paint matches the server renderer; after\n * hydration `useActiveViewport` takes over via `matchMedia`. Server-safe: the\n * hook returns the seeded index and registers no listeners when there's no\n * window, so SSR output matches `<ServerExperienceRenderer>`.\n */\n\n'use client';\n\nimport type { ReactNode } from 'react';\n\nimport type {\n ExperienceContext,\n PortableRenderPlan,\n ViewportDef,\n} from '@contentful/experiences-core';\n\nimport { ExperienceProvider } from './context';\nimport { MissingComponent } from './missing-component';\nimport { NodesRenderer, WrapWithTemplate, type RenderUnknown } from './nodes-renderer';\nimport type { Config, RenderContext } from './types';\nimport { useActiveViewport } from './use-active-viewport';\n\nconst DEFAULT_CONTEXT: ExperienceContext = {\n isPreview: false,\n metadata: {},\n viewports: [],\n};\n\nconst FALLBACK_VIEWPORT: ViewportDef = {\n id: '_',\n query: '*',\n displayName: 'Default',\n previewSize: '100%',\n};\n\nexport interface ClientExperienceRendererProps {\n experience: PortableRenderPlan | null | undefined;\n config: Config;\n /** Initial viewport seed; should match what the server-rendered output used. */\n initialViewportId?: string;\n context?: Partial<ExperienceContext>;\n renderUnknown?: RenderUnknown;\n}\n\nexport function ClientExperienceRenderer({\n experience,\n config,\n initialViewportId,\n context,\n renderUnknown = MissingComponent,\n}: ClientExperienceRendererProps): ReactNode {\n if (!experience) return null;\n const { activeViewportIndex } = useActiveViewport(experience.viewports, initialViewportId);\n // Copy so the context shares no object identity with the plan arrays — see\n // the note in `server-renderer.tsx`.\n const contextViewports = experience.viewports.map((v) => ({ ...v }));\n const activeViewport = { ...(experience.viewports[activeViewportIndex] ?? FALLBACK_VIEWPORT) };\n\n const renderContext: RenderContext = {\n ...DEFAULT_CONTEXT,\n ...context,\n metadata: { ...DEFAULT_CONTEXT.metadata, ...(context?.metadata ?? {}) },\n viewports: contextViewports,\n activeViewport,\n activeViewportIndex,\n };\n\n return (\n <ExperienceProvider value={renderContext}>\n <WrapWithTemplate\n template={experience.template}\n config={config}\n viewports={experience.viewports}\n activeViewportIndex={activeViewportIndex}\n >\n <NodesRenderer\n nodes={experience.nodes}\n config={config}\n viewports={experience.viewports}\n activeViewportIndex={activeViewportIndex}\n renderUnknown={renderUnknown}\n />\n </WrapWithTemplate>\n </ExperienceProvider>\n );\n}\n"],"mappings":";AA4EQ;AA3DR,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,eAAe,wBAA4C;AAEpE,SAAS,yBAAyB;AAElC,MAAM,kBAAqC;AAAA,EACzC,WAAW;AAAA,EACX,UAAU,CAAC;AAAA,EACX,WAAW,CAAC;AACd;AAEA,MAAM,oBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AAAA,EACb,aAAa;AACf;AAWO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAA6C;AAC3C,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,EAAE,oBAAoB,IAAI,kBAAkB,WAAW,WAAW,iBAAiB;AAGzF,QAAM,mBAAmB,WAAW,UAAU,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;AACnE,QAAM,iBAAiB,EAAE,GAAI,WAAW,UAAU,mBAAmB,KAAK,kBAAmB;AAE7F,QAAM,gBAA+B;AAAA,IACnC,GAAG;AAAA,IACH,GAAG;AAAA,IACH,UAAU,EAAE,GAAG,gBAAgB,UAAU,GAAI,SAAS,YAAY,CAAC,EAAG;AAAA,IACtE,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EACF;AAEA,SACE,oBAAC,sBAAmB,OAAO,eACzB;AAAA,IAAC;AAAA;AAAA,MACC,UAAU,WAAW;AAAA,MACrB;AAAA,MACA,WAAW,WAAW;AAAA,MACtB;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,WAAW;AAAA,UAClB;AAAA,UACA,WAAW,WAAW;AAAA,UACtB;AAAA,UACA;AAAA;AAAA,MACF;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ContentfulComponent, ContentfulTemplate, RenderContext } from './types.js';
|
|
3
|
+
import '@contentful/experiences-core';
|
|
4
|
+
|
|
5
|
+
declare function ExperienceProvider({ value, children, }: {
|
|
6
|
+
value: RenderContext;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}): ReactNode;
|
|
9
|
+
declare function ContentfulComponentProvider({ value, children, }: {
|
|
10
|
+
value: ContentfulComponent;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}): ReactNode;
|
|
13
|
+
declare function ContentfulTemplateProvider({ value, children, }: {
|
|
14
|
+
value: ContentfulTemplate;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}): ReactNode;
|
|
17
|
+
declare function ResolvedDesignProvider({ value, children, }: {
|
|
18
|
+
value: Record<string, unknown>;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}): ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* Read the current Experience runtime context — viewports, the active
|
|
23
|
+
* viewport, preview flag, and free-form metadata. Throws when called outside
|
|
24
|
+
* any Experience renderer subtree.
|
|
25
|
+
*/
|
|
26
|
+
declare function useExperience(): RenderContext;
|
|
27
|
+
/**
|
|
28
|
+
* Read the raw Contentful payload for the nearest enclosing Experience node.
|
|
29
|
+
* Returns `null` outside any rendered node (e.g. from a template's chrome
|
|
30
|
+
* that wraps the nodes but isn't itself a node).
|
|
31
|
+
*/
|
|
32
|
+
declare function useContentfulComponent(): ContentfulComponent | null;
|
|
33
|
+
/**
|
|
34
|
+
* Read the raw Contentful payload for the page-level template, if one is in
|
|
35
|
+
* scope. Returns `null` outside a template's render tree.
|
|
36
|
+
*/
|
|
37
|
+
declare function useContentfulTemplate(): ContentfulTemplate | null;
|
|
38
|
+
/**
|
|
39
|
+
* Read the design values the renderer already resolved for the enclosing
|
|
40
|
+
* node or template — viewport-cascaded and token-resolved, the same bag that
|
|
41
|
+
* feeds the component's props. Returns `null` outside a rendered node/
|
|
42
|
+
* template, which `useDesignValues` treats as "nothing to read."
|
|
43
|
+
*/
|
|
44
|
+
declare function useResolvedDesign(): Record<string, unknown> | null;
|
|
45
|
+
|
|
46
|
+
export { ContentfulComponentProvider, ContentfulTemplateProvider, ExperienceProvider, ResolvedDesignProvider, useContentfulComponent, useContentfulTemplate, useExperience, useResolvedDesign };
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
const ExperienceContext = createContext(null);
|
|
5
|
+
const ContentfulComponentContext = createContext(null);
|
|
6
|
+
const ContentfulTemplateContext = createContext(null);
|
|
7
|
+
const ResolvedDesignContext = createContext(null);
|
|
8
|
+
function ExperienceProvider({
|
|
9
|
+
value,
|
|
10
|
+
children
|
|
11
|
+
}) {
|
|
12
|
+
return /* @__PURE__ */ jsx(ExperienceContext.Provider, { value, children });
|
|
13
|
+
}
|
|
14
|
+
function ContentfulComponentProvider({
|
|
15
|
+
value,
|
|
16
|
+
children
|
|
17
|
+
}) {
|
|
18
|
+
return /* @__PURE__ */ jsx(ContentfulComponentContext.Provider, { value, children });
|
|
19
|
+
}
|
|
20
|
+
function ContentfulTemplateProvider({
|
|
21
|
+
value,
|
|
22
|
+
children
|
|
23
|
+
}) {
|
|
24
|
+
return /* @__PURE__ */ jsx(ContentfulTemplateContext.Provider, { value, children });
|
|
25
|
+
}
|
|
26
|
+
function ResolvedDesignProvider({
|
|
27
|
+
value,
|
|
28
|
+
children
|
|
29
|
+
}) {
|
|
30
|
+
return /* @__PURE__ */ jsx(ResolvedDesignContext.Provider, { value, children });
|
|
31
|
+
}
|
|
32
|
+
function useExperience() {
|
|
33
|
+
const ctx = useContext(ExperienceContext);
|
|
34
|
+
if (ctx === null) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"useExperience() must be called inside a <ServerExperienceRenderer> or <ClientExperienceRenderer> subtree."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return ctx;
|
|
40
|
+
}
|
|
41
|
+
function useContentfulComponent() {
|
|
42
|
+
return useContext(ContentfulComponentContext);
|
|
43
|
+
}
|
|
44
|
+
function useContentfulTemplate() {
|
|
45
|
+
return useContext(ContentfulTemplateContext);
|
|
46
|
+
}
|
|
47
|
+
function useResolvedDesign() {
|
|
48
|
+
return useContext(ResolvedDesignContext);
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
ContentfulComponentProvider,
|
|
52
|
+
ContentfulTemplateProvider,
|
|
53
|
+
ExperienceProvider,
|
|
54
|
+
ResolvedDesignProvider,
|
|
55
|
+
useContentfulComponent,
|
|
56
|
+
useContentfulTemplate,
|
|
57
|
+
useExperience,
|
|
58
|
+
useResolvedDesign
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx"],"sourcesContent":["/*\n * React context providers + hooks that expose runtime context and the raw\n * Contentful payload to descendants of a rendered Experience. Customer\n * components receive only their declared props via the renderer; anything\n * that needs the Experience runtime context or the underlying payload calls\n * the hook explicitly at the top of its component body.\n */\n\n'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\n\nimport type { ContentfulComponent, ContentfulTemplate, RenderContext } from './types';\n\nconst ExperienceContext = createContext<RenderContext | null>(null);\nconst ContentfulComponentContext = createContext<ContentfulComponent | null>(null);\nconst ContentfulTemplateContext = createContext<ContentfulTemplate | null>(null);\n// Plain data only (never the Config or its functions) so it crosses RSC cleanly.\nconst ResolvedDesignContext = createContext<Record<string, unknown> | null>(null);\n\n// Wrapped as real client-component functions, not re-exported `Context.Provider`\n// objects — the server renderer imports these across the RSC boundary and\n// Next.js requires a function reference there (\"Element type is invalid\" otherwise).\n\nexport function ExperienceProvider({\n value,\n children,\n}: {\n value: RenderContext;\n children: ReactNode;\n}): ReactNode {\n return <ExperienceContext.Provider value={value}>{children}</ExperienceContext.Provider>;\n}\n\nexport function ContentfulComponentProvider({\n value,\n children,\n}: {\n value: ContentfulComponent;\n children: ReactNode;\n}): ReactNode {\n return (\n <ContentfulComponentContext.Provider value={value}>\n {children}\n </ContentfulComponentContext.Provider>\n );\n}\n\nexport function ContentfulTemplateProvider({\n value,\n children,\n}: {\n value: ContentfulTemplate;\n children: ReactNode;\n}): ReactNode {\n return (\n <ContentfulTemplateContext.Provider value={value}>\n {children}\n </ContentfulTemplateContext.Provider>\n );\n}\n\nexport function ResolvedDesignProvider({\n value,\n children,\n}: {\n value: Record<string, unknown>;\n children: ReactNode;\n}): ReactNode {\n return <ResolvedDesignContext.Provider value={value}>{children}</ResolvedDesignContext.Provider>;\n}\n\n/**\n * Read the current Experience runtime context — viewports, the active\n * viewport, preview flag, and free-form metadata. Throws when called outside\n * any Experience renderer subtree.\n */\nexport function useExperience(): RenderContext {\n const ctx = useContext(ExperienceContext);\n if (ctx === null) {\n throw new Error(\n 'useExperience() must be called inside a <ServerExperienceRenderer> or <ClientExperienceRenderer> subtree.'\n );\n }\n return ctx;\n}\n\n/**\n * Read the raw Contentful payload for the nearest enclosing Experience node.\n * Returns `null` outside any rendered node (e.g. from a template's chrome\n * that wraps the nodes but isn't itself a node).\n */\nexport function useContentfulComponent(): ContentfulComponent | null {\n return useContext(ContentfulComponentContext);\n}\n\n/**\n * Read the raw Contentful payload for the page-level template, if one is in\n * scope. Returns `null` outside a template's render tree.\n */\nexport function useContentfulTemplate(): ContentfulTemplate | null {\n return useContext(ContentfulTemplateContext);\n}\n\n/**\n * Read the design values the renderer already resolved for the enclosing\n * node or template — viewport-cascaded and token-resolved, the same bag that\n * feeds the component's props. Returns `null` outside a rendered node/\n * template, which `useDesignValues` treats as \"nothing to read.\"\n */\nexport function useResolvedDesign(): Record<string, unknown> | null {\n return useContext(ResolvedDesignContext);\n}\n"],"mappings":";AA+BS;AArBT,SAAS,eAAe,kBAAkC;AAI1D,MAAM,oBAAoB,cAAoC,IAAI;AAClE,MAAM,6BAA6B,cAA0C,IAAI;AACjF,MAAM,4BAA4B,cAAyC,IAAI;AAE/E,MAAM,wBAAwB,cAA8C,IAAI;AAMzE,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AACF,GAGc;AACZ,SAAO,oBAAC,kBAAkB,UAAlB,EAA2B,OAAe,UAAS;AAC7D;AAEO,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AACF,GAGc;AACZ,SACE,oBAAC,2BAA2B,UAA3B,EAAoC,OAClC,UACH;AAEJ;AAEO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AACF,GAGc;AACZ,SACE,oBAAC,0BAA0B,UAA1B,EAAmC,OACjC,UACH;AAEJ;AAEO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AACF,GAGc;AACZ,SAAO,oBAAC,sBAAsB,UAAtB,EAA+B,OAAe,UAAS;AACjE;AAOO,SAAS,gBAA+B;AAC7C,QAAM,MAAM,WAAW,iBAAiB;AACxC,MAAI,QAAQ,MAAM;AAChB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,yBAAqD;AACnE,SAAO,WAAW,0BAA0B;AAC9C;AAMO,SAAS,wBAAmD;AACjE,SAAO,WAAW,yBAAyB;AAC7C;AAQO,SAAS,oBAAoD;AAClE,SAAO,WAAW,qBAAqB;AACzC;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ToCssOptions {
|
|
4
|
+
/** Keys to skip, matched against the original record key. */
|
|
5
|
+
exclude?: string[];
|
|
6
|
+
/** When set, only these keys pass through (still whitelist-filtered). */
|
|
7
|
+
include?: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Convert a design record into a `CSSProperties` object, keeping only keys
|
|
11
|
+
* that normalize to a known CSS property. Non-CSS keys (`variant`, `as`, …)
|
|
12
|
+
* are dropped; read those off the `useDesignValues()` record directly.
|
|
13
|
+
*
|
|
14
|
+
* toCss({ fontSize: '20px', variant: 'h1' }) //=> { fontSize: '20px' }
|
|
15
|
+
*/
|
|
16
|
+
declare function toCss(design: object, { include, exclude }?: ToCssOptions): CSSProperties;
|
|
17
|
+
|
|
18
|
+
export { type ToCssOptions, toCss };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isCssProperty, toCssKey } from "@contentful/experiences-design";
|
|
2
|
+
function toCss(design, { include, exclude } = {}) {
|
|
3
|
+
const out = {};
|
|
4
|
+
for (const [key, value] of Object.entries(design)) {
|
|
5
|
+
if (include && !include.includes(key)) continue;
|
|
6
|
+
if (exclude?.includes(key)) continue;
|
|
7
|
+
if (value === void 0 || value === null) continue;
|
|
8
|
+
const cssKey = toCssKey(key);
|
|
9
|
+
if (!isCssProperty(cssKey)) continue;
|
|
10
|
+
out[cssKey] = value;
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
toCss
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=design-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/design-utils.ts"],"sourcesContent":["import type { CSSProperties } from 'react';\n\nimport { isCssProperty, toCssKey } from '@contentful/experiences-design';\n\nexport interface ToCssOptions {\n /** Keys to skip, matched against the original record key. */\n exclude?: string[];\n /** When set, only these keys pass through (still whitelist-filtered). */\n include?: string[];\n}\n\n/**\n * Convert a design record into a `CSSProperties` object, keeping only keys\n * that normalize to a known CSS property. Non-CSS keys (`variant`, `as`, …)\n * are dropped; read those off the `useDesignValues()` record directly.\n *\n * toCss({ fontSize: '20px', variant: 'h1' }) //=> { fontSize: '20px' }\n */\nexport function toCss(design: object, { include, exclude }: ToCssOptions = {}): CSSProperties {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(design)) {\n if (include && !include.includes(key)) continue;\n if (exclude?.includes(key)) continue;\n if (value === undefined || value === null) continue;\n const cssKey = toCssKey(key);\n if (!isCssProperty(cssKey)) continue;\n out[cssKey] = value;\n }\n return out as CSSProperties;\n}\n"],"mappings":"AAEA,SAAS,eAAe,gBAAgB;AAgBjC,SAAS,MAAM,QAAgB,EAAE,SAAS,QAAQ,IAAkB,CAAC,GAAkB;AAC5F,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,WAAW,CAAC,QAAQ,SAAS,GAAG,EAAG;AACvC,QAAI,SAAS,SAAS,GAAG,EAAG;AAC5B,QAAI,UAAU,UAAa,UAAU,KAAM;AAC3C,UAAM,SAAS,SAAS,GAAG;AAC3B,QAAI,CAAC,cAAc,MAAM,EAAG;AAC5B,QAAI,MAAM,IAAI;AAAA,EAChB;AACA,SAAO;AACT;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { ClientExperienceRenderer, ClientExperienceRendererProps, ClientExperienceRenderer as ExperienceRenderer, ClientExperienceRendererProps as ExperienceRendererProps } from './client-renderer.js';
|
|
2
|
+
export { ServerExperienceRenderer, ServerExperienceRendererProps } from './server-renderer.js';
|
|
3
|
+
export { MissingComponent, MissingComponentProps } from './missing-component.js';
|
|
4
|
+
export { UseActiveViewportResult, useActiveViewport } from './use-active-viewport.js';
|
|
5
|
+
export { RenderUnknown } from './nodes-renderer.js';
|
|
6
|
+
export { useContentfulComponent, useContentfulTemplate, useExperience } from './context.js';
|
|
7
|
+
export { useDesignValues } from './use-design-values.js';
|
|
8
|
+
export { ToCssOptions, toCss } from './design-utils.js';
|
|
9
|
+
export { ComponentConfig, Components, Config, ContentfulComponent, ContentfulTemplate, Registration, RenderContext, TemplateConfig, TemplateRegistration, Templates, defineComponent, defineTemplate, normalizeComponentRegistration, normalizeTemplateRegistration } from './types.js';
|
|
10
|
+
export { ComponentTypeNode, ComponentTypeRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, PortableTemplate, ResolveContext, ResolveExperienceOptions, ResolveToken, ResolverConfig, TemplateNode, TemplateRef, ValuesByViewport, ViewportDef, resolveExperience } from '@contentful/experiences-core';
|
|
11
|
+
export { CSS_PROPERTIES, getValueForViewport, getViewportIndex, isCssProperty, resolveDesignProperties, toCssKey, toCssMediaQuery } from '@contentful/experiences-design';
|
|
12
|
+
export { ClientOptions, ContentfulViewDelivery, ContentfulViewDeliveryClient, CreateClientOptions, ExperienceOptions, NotFoundError, ResolveOptions, createClient, fetchExperience } from '@contentful/experiences-client';
|
|
13
|
+
import 'react';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClientExperienceRenderer,
|
|
3
|
+
ClientExperienceRenderer as ClientExperienceRenderer2
|
|
4
|
+
} from "./client-renderer";
|
|
5
|
+
import { ServerExperienceRenderer } from "./server-renderer";
|
|
6
|
+
import { MissingComponent } from "./missing-component";
|
|
7
|
+
import { useActiveViewport } from "./use-active-viewport";
|
|
8
|
+
import { useExperience, useContentfulComponent, useContentfulTemplate } from "./context";
|
|
9
|
+
import { useDesignValues } from "./use-design-values";
|
|
10
|
+
import { toCss } from "./design-utils";
|
|
11
|
+
import {
|
|
12
|
+
defineComponent,
|
|
13
|
+
defineTemplate,
|
|
14
|
+
normalizeComponentRegistration,
|
|
15
|
+
normalizeTemplateRegistration
|
|
16
|
+
} from "./types";
|
|
17
|
+
import { resolveExperience } from "@contentful/experiences-core";
|
|
18
|
+
import {
|
|
19
|
+
CSS_PROPERTIES,
|
|
20
|
+
getValueForViewport,
|
|
21
|
+
getViewportIndex,
|
|
22
|
+
isCssProperty,
|
|
23
|
+
resolveDesignProperties,
|
|
24
|
+
toCssKey,
|
|
25
|
+
toCssMediaQuery
|
|
26
|
+
} from "@contentful/experiences-design";
|
|
27
|
+
import {
|
|
28
|
+
ContentfulViewDelivery,
|
|
29
|
+
ContentfulViewDeliveryClient,
|
|
30
|
+
NotFoundError,
|
|
31
|
+
createClient,
|
|
32
|
+
fetchExperience
|
|
33
|
+
} from "@contentful/experiences-client";
|
|
34
|
+
export {
|
|
35
|
+
CSS_PROPERTIES,
|
|
36
|
+
ClientExperienceRenderer2 as ClientExperienceRenderer,
|
|
37
|
+
ContentfulViewDelivery,
|
|
38
|
+
ContentfulViewDeliveryClient,
|
|
39
|
+
ClientExperienceRenderer as ExperienceRenderer,
|
|
40
|
+
MissingComponent,
|
|
41
|
+
NotFoundError,
|
|
42
|
+
ServerExperienceRenderer,
|
|
43
|
+
createClient,
|
|
44
|
+
defineComponent,
|
|
45
|
+
defineTemplate,
|
|
46
|
+
fetchExperience,
|
|
47
|
+
getValueForViewport,
|
|
48
|
+
getViewportIndex,
|
|
49
|
+
isCssProperty,
|
|
50
|
+
normalizeComponentRegistration,
|
|
51
|
+
normalizeTemplateRegistration,
|
|
52
|
+
resolveDesignProperties,
|
|
53
|
+
resolveExperience,
|
|
54
|
+
toCss,
|
|
55
|
+
toCssKey,
|
|
56
|
+
toCssMediaQuery,
|
|
57
|
+
useActiveViewport,
|
|
58
|
+
useContentfulComponent,
|
|
59
|
+
useContentfulTemplate,
|
|
60
|
+
useDesignValues,
|
|
61
|
+
useExperience
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/*\n * Public API surface for `@contentful/experiences-react`.\n *\n * Customers add ONLY this package to their app's dependencies. Everything\n * needed to render an Experience — types, the resolver, viewport utilities,\n * the renderer components, the authoring helpers — is re-exported from here.\n * The internal `@contentful/experiences-core` and\n * `@contentful/experiences-design` packages are workspace-only implementation\n * details; they are not part of the public API.\n *\n * `ExperienceRenderer` is an alias for `ClientExperienceRenderer`; SSR\n * consumers explicitly import `ServerExperienceRenderer`.\n */\n\n// ─── Renderers ─────────────────────────────────────────────────────────────\nexport {\n ClientExperienceRenderer as ExperienceRenderer,\n ClientExperienceRenderer,\n} from './client-renderer';\nexport type { ClientExperienceRendererProps as ExperienceRendererProps } from './client-renderer';\nexport type { ClientExperienceRendererProps } from './client-renderer';\n\nexport { ServerExperienceRenderer } from './server-renderer';\nexport type { ServerExperienceRendererProps } from './server-renderer';\n\nexport { MissingComponent } from './missing-component';\nexport type { MissingComponentProps } from './missing-component';\n\nexport { useActiveViewport } from './use-active-viewport';\nexport type { UseActiveViewportResult } from './use-active-viewport';\n\nexport type { RenderUnknown } from './nodes-renderer';\n\n// ─── Runtime context hooks ───────────────────────────────────────────────\nexport { useExperience, useContentfulComponent, useContentfulTemplate } from './context';\nexport { useDesignValues } from './use-design-values';\nexport { toCss } from './design-utils';\nexport type { ToCssOptions } from './design-utils';\n\n// ─── Authoring helpers + Config types ─────────────────────────────────────\nexport {\n defineComponent,\n defineTemplate,\n normalizeComponentRegistration,\n normalizeTemplateRegistration,\n} from './types';\nexport type {\n ComponentConfig,\n Components,\n Config,\n ContentfulComponent,\n ContentfulTemplate,\n Registration,\n RenderContext,\n ResolveToken,\n TemplateConfig,\n TemplateRegistration,\n Templates,\n} from './types';\n\n// ─── Resolver (re-exported from render-core) ──────────────────────────────\nexport { resolveExperience } from '@contentful/experiences-core';\nexport type { ResolverConfig, ResolveExperienceOptions } from '@contentful/experiences-core';\n\n// ─── Core IR + payload types (re-exported from render-core) ───────────────\nexport type {\n ComponentTypeNode,\n ComponentTypeRef,\n DesignPropValue,\n DesignToken,\n ExperienceContext,\n ExperienceNode,\n ExperiencePayload,\n ExperienceSys,\n ManualDesignValue,\n PortableRegistration,\n PortableRenderNode,\n PortableRenderPlan,\n PortableTemplate,\n ResolveContext,\n TemplateNode,\n TemplateRef,\n ValuesByViewport,\n ViewportDef,\n} from '@contentful/experiences-core';\n\n// ─── Design utilities (re-exported from design) ───────────────────────────\nexport {\n CSS_PROPERTIES,\n getValueForViewport,\n getViewportIndex,\n isCssProperty,\n resolveDesignProperties,\n toCssKey,\n toCssMediaQuery,\n} from '@contentful/experiences-design';\n\n// ─── Delivery client + fetchExperience ────────────────────────────────────\nexport {\n ContentfulViewDelivery,\n ContentfulViewDeliveryClient,\n NotFoundError,\n createClient,\n fetchExperience,\n} from '@contentful/experiences-client';\nexport type {\n ClientOptions,\n CreateClientOptions,\n ExperienceOptions,\n ResolveOptions,\n} from '@contentful/experiences-client';\n"],"mappings":"AAeA;AAAA,EAC8B;AAAA,EAC5B,4BAAAA;AAAA,OACK;AAIP,SAAS,gCAAgC;AAGzC,SAAS,wBAAwB;AAGjC,SAAS,yBAAyB;AAMlC,SAAS,eAAe,wBAAwB,6BAA6B;AAC7E,SAAS,uBAAuB;AAChC,SAAS,aAAa;AAItB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAgBP,SAAS,yBAAyB;AA0BlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":["ClientExperienceRenderer"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface MissingComponentProps {
|
|
4
|
+
componentTypeId: string;
|
|
5
|
+
/** Optional — only present when the payload supplied an id for this node. */
|
|
6
|
+
nodeId?: string;
|
|
7
|
+
}
|
|
8
|
+
declare function MissingComponent({ componentTypeId, nodeId }: MissingComponentProps): ReactNode;
|
|
9
|
+
|
|
10
|
+
export { MissingComponent, type MissingComponentProps };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useExperience } from "./context";
|
|
3
|
+
function MissingComponent({ componentTypeId, nodeId }) {
|
|
4
|
+
const { isPreview } = useExperience();
|
|
5
|
+
if (typeof console !== "undefined") {
|
|
6
|
+
const idLabel = nodeId ? ` (nodeId: ${nodeId})` : "";
|
|
7
|
+
console.warn(
|
|
8
|
+
`[@contentful/experiences] No component registered for type "${componentTypeId}"${idLabel}.`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
if (!isPreview) return null;
|
|
12
|
+
return /* @__PURE__ */ jsxs(
|
|
13
|
+
"div",
|
|
14
|
+
{
|
|
15
|
+
style: {
|
|
16
|
+
border: "2px solid red",
|
|
17
|
+
padding: "1rem",
|
|
18
|
+
color: "red",
|
|
19
|
+
background: "#fff"
|
|
20
|
+
},
|
|
21
|
+
"data-experiences-missing": componentTypeId,
|
|
22
|
+
children: [
|
|
23
|
+
/* @__PURE__ */ jsxs("strong", { children: [
|
|
24
|
+
"Missing component \u2018",
|
|
25
|
+
componentTypeId,
|
|
26
|
+
"\u2019"
|
|
27
|
+
] }),
|
|
28
|
+
/* @__PURE__ */ jsx("p", { children: "This component is referenced by the Experience payload but is not registered in the Config." })
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
MissingComponent
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=missing-component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/missing-component.tsx"],"sourcesContent":["/*\n * Default fallback rendered when an instance references a component type\n * not present in the Config.\n *\n * Behavior: a visible red error box in preview, silent null in production.\n * Customers can override per-render via <ExperienceRenderer renderUnknown=...>.\n */\n\nimport type { ReactNode } from 'react';\n\nimport { useExperience } from './context';\n\nexport interface MissingComponentProps {\n componentTypeId: string;\n /** Optional — only present when the payload supplied an id for this node. */\n nodeId?: string;\n}\n\nexport function MissingComponent({ componentTypeId, nodeId }: MissingComponentProps): ReactNode {\n const { isPreview } = useExperience();\n if (typeof console !== 'undefined') {\n const idLabel = nodeId ? ` (nodeId: ${nodeId})` : '';\n console.warn(\n `[@contentful/experiences] No component registered for type \"${componentTypeId}\"${idLabel}.`\n );\n }\n if (!isPreview) return null;\n return (\n <div\n style={{\n border: '2px solid red',\n padding: '1rem',\n color: 'red',\n background: '#fff',\n }}\n data-experiences-missing={componentTypeId}\n >\n <strong>Missing component ‘{componentTypeId}’</strong>\n <p>\n This component is referenced by the Experience payload but is not registered in the Config.\n </p>\n </div>\n );\n}\n"],"mappings":"AAqCM,SACA,KADA;AA3BN,SAAS,qBAAqB;AAQvB,SAAS,iBAAiB,EAAE,iBAAiB,OAAO,GAAqC;AAC9F,QAAM,EAAE,UAAU,IAAI,cAAc;AACpC,MAAI,OAAO,YAAY,aAAa;AAClC,UAAM,UAAU,SAAS,aAAa,MAAM,MAAM;AAClD,YAAQ;AAAA,MACN,+DAA+D,eAAe,IAAI,OAAO;AAAA,IAC3F;AAAA,EACF;AACA,MAAI,CAAC,UAAW,QAAO;AACvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,QACP,YAAY;AAAA,MACd;AAAA,MACA,4BAA0B;AAAA,MAE1B;AAAA,6BAAC,YAAO;AAAA;AAAA,UAA0B;AAAA,UAAgB;AAAA,WAAO;AAAA,QACzD,oBAAC,OAAE,yGAEH;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { PortableRenderNode, ViewportDef, PortableTemplate } from '@contentful/experiences-core';
|
|
3
|
+
import { MissingComponentProps } from './missing-component.js';
|
|
4
|
+
import { Config } from './types.js';
|
|
5
|
+
|
|
6
|
+
type RenderUnknown = (props: MissingComponentProps) => ReactNode;
|
|
7
|
+
interface NodesRendererProps {
|
|
8
|
+
nodes: PortableRenderNode[];
|
|
9
|
+
config: Config;
|
|
10
|
+
viewports: ViewportDef[];
|
|
11
|
+
activeViewportIndex: number;
|
|
12
|
+
renderUnknown: RenderUnknown;
|
|
13
|
+
}
|
|
14
|
+
declare function NodesRenderer({ nodes, config, viewports, activeViewportIndex, renderUnknown, }: NodesRendererProps): ReactNode;
|
|
15
|
+
interface WrapWithTemplateProps {
|
|
16
|
+
template: PortableTemplate | undefined;
|
|
17
|
+
config: Config;
|
|
18
|
+
viewports: ViewportDef[];
|
|
19
|
+
activeViewportIndex: number;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Wraps the rendered nodes with the page-level template. If the template is
|
|
24
|
+
* referenced but unregistered, warns once and renders children unwrapped.
|
|
25
|
+
*/
|
|
26
|
+
declare function WrapWithTemplate({ template, config, viewports, activeViewportIndex, children, }: WrapWithTemplateProps): ReactNode;
|
|
27
|
+
|
|
28
|
+
export { NodesRenderer, type NodesRendererProps, type RenderUnknown, WrapWithTemplate, type WrapWithTemplateProps };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, createElement } from "react";
|
|
3
|
+
import { applyTokenResolver, resolveDesignProperties } from "@contentful/experiences-design";
|
|
4
|
+
import {
|
|
5
|
+
ContentfulComponentProvider,
|
|
6
|
+
ContentfulTemplateProvider,
|
|
7
|
+
ResolvedDesignProvider
|
|
8
|
+
} from "./context";
|
|
9
|
+
import {
|
|
10
|
+
normalizeComponentRegistration,
|
|
11
|
+
normalizeTemplateRegistration
|
|
12
|
+
} from "./types";
|
|
13
|
+
function NodesRenderer({
|
|
14
|
+
nodes,
|
|
15
|
+
config,
|
|
16
|
+
viewports,
|
|
17
|
+
activeViewportIndex,
|
|
18
|
+
renderUnknown
|
|
19
|
+
}) {
|
|
20
|
+
if (!nodes.length) return null;
|
|
21
|
+
return /* @__PURE__ */ jsx(Fragment, { children: nodes.map((node, index) => /* @__PURE__ */ jsx(
|
|
22
|
+
NodeRenderer,
|
|
23
|
+
{
|
|
24
|
+
node,
|
|
25
|
+
config,
|
|
26
|
+
viewports,
|
|
27
|
+
activeViewportIndex,
|
|
28
|
+
renderUnknown
|
|
29
|
+
},
|
|
30
|
+
node.nodeId ?? index
|
|
31
|
+
)) });
|
|
32
|
+
}
|
|
33
|
+
function NodeRenderer({
|
|
34
|
+
node,
|
|
35
|
+
config,
|
|
36
|
+
viewports,
|
|
37
|
+
activeViewportIndex,
|
|
38
|
+
renderUnknown
|
|
39
|
+
}) {
|
|
40
|
+
const { componentTypeId } = node.registration;
|
|
41
|
+
const entry = config.components[componentTypeId];
|
|
42
|
+
if (!entry) {
|
|
43
|
+
return renderUnknown({ componentTypeId, nodeId: node.nodeId });
|
|
44
|
+
}
|
|
45
|
+
const componentConfig = normalizeComponentRegistration(entry);
|
|
46
|
+
const slotProps = {};
|
|
47
|
+
for (const [slotName, children] of Object.entries(node.slots)) {
|
|
48
|
+
slotProps[slotName] = /* @__PURE__ */ jsx(
|
|
49
|
+
NodesRenderer,
|
|
50
|
+
{
|
|
51
|
+
nodes: children,
|
|
52
|
+
config,
|
|
53
|
+
viewports,
|
|
54
|
+
activeViewportIndex,
|
|
55
|
+
renderUnknown
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const resolvedDesign = resolveDesignProperties(node.props.design, viewports, activeViewportIndex);
|
|
60
|
+
const { props: tokenResolvedDesign, unresolved } = applyTokenResolver(
|
|
61
|
+
resolvedDesign,
|
|
62
|
+
config.resolveToken
|
|
63
|
+
);
|
|
64
|
+
if (unresolved.length && typeof console !== "undefined") {
|
|
65
|
+
console.warn(
|
|
66
|
+
`[@contentful/experiences-react] resolveToken returned undefined for token id(s) on "${componentTypeId}": ${unresolved.join(", ")}. useDesignValues() will omit those keys.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const contentful = {
|
|
70
|
+
componentTypeId,
|
|
71
|
+
nodeId: node.nodeId,
|
|
72
|
+
content: node.props.content,
|
|
73
|
+
design: node.props.design,
|
|
74
|
+
resolved: node.props.resolved
|
|
75
|
+
};
|
|
76
|
+
const composed = {
|
|
77
|
+
...componentConfig.defaults,
|
|
78
|
+
...node.props.content,
|
|
79
|
+
...node.props.resolved,
|
|
80
|
+
...slotProps
|
|
81
|
+
};
|
|
82
|
+
return /* @__PURE__ */ jsx(ContentfulComponentProvider, { value: contentful, children: /* @__PURE__ */ jsx(ResolvedDesignProvider, { value: tokenResolvedDesign, children: createElement(componentConfig.component, composed) }) });
|
|
83
|
+
}
|
|
84
|
+
function WrapWithTemplate({
|
|
85
|
+
template,
|
|
86
|
+
config,
|
|
87
|
+
viewports,
|
|
88
|
+
activeViewportIndex,
|
|
89
|
+
children
|
|
90
|
+
}) {
|
|
91
|
+
if (!template) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
92
|
+
const entry = config.templates?.[template.templateId];
|
|
93
|
+
if (!entry) {
|
|
94
|
+
if (typeof console !== "undefined") {
|
|
95
|
+
console.warn(
|
|
96
|
+
`[@contentful/experiences-react] No template registered for id "${template.templateId}". Rendering nodes without the template wrapper.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
100
|
+
}
|
|
101
|
+
const templateConfig = normalizeTemplateRegistration(entry);
|
|
102
|
+
const resolvedDesign = resolveDesignProperties(
|
|
103
|
+
template.props.design,
|
|
104
|
+
viewports,
|
|
105
|
+
activeViewportIndex
|
|
106
|
+
);
|
|
107
|
+
const { props: tokenResolvedDesign, unresolved } = applyTokenResolver(
|
|
108
|
+
resolvedDesign,
|
|
109
|
+
config.resolveToken
|
|
110
|
+
);
|
|
111
|
+
if (unresolved.length && typeof console !== "undefined") {
|
|
112
|
+
console.warn(
|
|
113
|
+
`[@contentful/experiences-react] resolveToken returned undefined for token id(s) on template "${template.templateId}": ${unresolved.join(", ")}. useDesignValues() will omit those keys.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const contentful = {
|
|
117
|
+
templateId: template.templateId,
|
|
118
|
+
content: template.props.content,
|
|
119
|
+
design: template.props.design,
|
|
120
|
+
resolved: template.props.resolved
|
|
121
|
+
};
|
|
122
|
+
const composed = {
|
|
123
|
+
...templateConfig.defaults,
|
|
124
|
+
...template.props.content,
|
|
125
|
+
...template.props.resolved,
|
|
126
|
+
children
|
|
127
|
+
};
|
|
128
|
+
return /* @__PURE__ */ jsx(ContentfulTemplateProvider, { value: contentful, children: /* @__PURE__ */ jsx(ResolvedDesignProvider, { value: tokenResolvedDesign, children: createElement(templateConfig.component, composed) }) });
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
NodesRenderer,
|
|
132
|
+
WrapWithTemplate
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=nodes-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/nodes-renderer.tsx"],"sourcesContent":["/*\n * Recursive renderer over PortableRenderNodes. Customer components receive the\n * merged prop bag (defaults + content + resolveData + slots); design values\n * are read via `useDesignValues()`, not injected as props.\n */\n\nimport { Fragment, createElement, type ReactNode } from 'react';\n\nimport type {\n PortableRenderNode,\n PortableTemplate,\n ViewportDef,\n} from '@contentful/experiences-core';\nimport { applyTokenResolver, resolveDesignProperties } from '@contentful/experiences-design';\n\nimport {\n ContentfulComponentProvider,\n ContentfulTemplateProvider,\n ResolvedDesignProvider,\n} from './context';\nimport type { MissingComponentProps } from './missing-component';\nimport {\n normalizeComponentRegistration,\n normalizeTemplateRegistration,\n type ContentfulComponent,\n type ContentfulTemplate,\n type Config,\n} from './types';\n\nexport type RenderUnknown = (props: MissingComponentProps) => ReactNode;\n\n// Internal renderers take `viewports` + `activeViewportIndex`, not the whole\n// RenderContext object — the context is published once via ExperienceProvider,\n// and re-threading it as an element prop makes React's RSC serializer back-patch\n// a shared reference into frozen props (\"Cannot assign to read only property\").\nexport interface NodesRendererProps {\n nodes: PortableRenderNode[];\n config: Config;\n viewports: ViewportDef[];\n activeViewportIndex: number;\n renderUnknown: RenderUnknown;\n}\n\nexport function NodesRenderer({\n nodes,\n config,\n viewports,\n activeViewportIndex,\n renderUnknown,\n}: NodesRendererProps): ReactNode {\n if (!nodes.length) return null;\n return (\n <Fragment>\n {nodes.map((node, index) => (\n <NodeRenderer\n key={node.nodeId ?? index}\n node={node}\n config={config}\n viewports={viewports}\n activeViewportIndex={activeViewportIndex}\n renderUnknown={renderUnknown}\n />\n ))}\n </Fragment>\n );\n}\n\ninterface NodeRendererProps {\n node: PortableRenderNode;\n config: Config;\n viewports: ViewportDef[];\n activeViewportIndex: number;\n renderUnknown: RenderUnknown;\n}\n\nfunction NodeRenderer({\n node,\n config,\n viewports,\n activeViewportIndex,\n renderUnknown,\n}: NodeRendererProps): ReactNode {\n const { componentTypeId } = node.registration;\n const entry = config.components[componentTypeId];\n if (!entry) {\n return renderUnknown({ componentTypeId, nodeId: node.nodeId });\n }\n const componentConfig = normalizeComponentRegistration(entry);\n\n // Pre-render slot subtrees so components receive ReactNodes by slot name.\n const slotProps: Record<string, ReactNode> = {};\n for (const [slotName, children] of Object.entries(node.slots)) {\n slotProps[slotName] = (\n <NodesRenderer\n nodes={children}\n config={config}\n viewports={viewports}\n activeViewportIndex={activeViewportIndex}\n renderUnknown={renderUnknown}\n />\n );\n }\n\n // Cascade design to the active viewport, then resolve DesignToken envelopes.\n // Published on context for useDesignValues() — never spread onto props.\n const resolvedDesign = resolveDesignProperties(node.props.design, viewports, activeViewportIndex);\n const { props: tokenResolvedDesign, unresolved } = applyTokenResolver(\n resolvedDesign,\n config.resolveToken\n );\n if (unresolved.length && typeof console !== 'undefined') {\n console.warn(\n `[@contentful/experiences-react] resolveToken returned undefined for token id(s) on \"${componentTypeId}\": ${unresolved.join(', ')}. useDesignValues() will omit those keys.`\n );\n }\n\n const contentful: ContentfulComponent = {\n componentTypeId,\n nodeId: node.nodeId,\n content: node.props.content,\n design: node.props.design,\n resolved: node.props.resolved,\n };\n\n // Merge precedence (last wins): defaults < content < resolveData < slots.\n const composed = {\n ...componentConfig.defaults,\n ...node.props.content,\n ...node.props.resolved,\n ...slotProps,\n };\n\n return (\n <ContentfulComponentProvider value={contentful}>\n <ResolvedDesignProvider value={tokenResolvedDesign}>\n {createElement(componentConfig.component, composed)}\n </ResolvedDesignProvider>\n </ContentfulComponentProvider>\n );\n}\n\nexport interface WrapWithTemplateProps {\n template: PortableTemplate | undefined;\n config: Config;\n viewports: ViewportDef[];\n activeViewportIndex: number;\n children: ReactNode;\n}\n\n/**\n * Wraps the rendered nodes with the page-level template. If the template is\n * referenced but unregistered, warns once and renders children unwrapped.\n */\nexport function WrapWithTemplate({\n template,\n config,\n viewports,\n activeViewportIndex,\n children,\n}: WrapWithTemplateProps): ReactNode {\n if (!template) return <Fragment>{children}</Fragment>;\n const entry = config.templates?.[template.templateId];\n if (!entry) {\n if (typeof console !== 'undefined') {\n console.warn(\n `[@contentful/experiences-react] No template registered for id \"${template.templateId}\". Rendering nodes without the template wrapper.`\n );\n }\n return <Fragment>{children}</Fragment>;\n }\n const templateConfig = normalizeTemplateRegistration(entry);\n\n const resolvedDesign = resolveDesignProperties(\n template.props.design,\n viewports,\n activeViewportIndex\n );\n\n const { props: tokenResolvedDesign, unresolved } = applyTokenResolver(\n resolvedDesign,\n config.resolveToken\n );\n if (unresolved.length && typeof console !== 'undefined') {\n console.warn(\n `[@contentful/experiences-react] resolveToken returned undefined for token id(s) on template \"${template.templateId}\": ${unresolved.join(', ')}. useDesignValues() will omit those keys.`\n );\n }\n\n const contentful: ContentfulTemplate = {\n templateId: template.templateId,\n content: template.props.content,\n design: template.props.design,\n resolved: template.props.resolved,\n };\n\n const composed = {\n ...templateConfig.defaults,\n ...template.props.content,\n ...template.props.resolved,\n children,\n };\n\n return (\n <ContentfulTemplateProvider value={contentful}>\n <ResolvedDesignProvider value={tokenResolvedDesign}>\n {createElement(templateConfig.component, composed)}\n </ResolvedDesignProvider>\n </ContentfulTemplateProvider>\n );\n}\n"],"mappings":"AAsDQ;AAhDR,SAAS,UAAU,qBAAqC;AAOxD,SAAS,oBAAoB,+BAA+B;AAE5D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAgBA,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAkC;AAChC,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,SACE,oBAAC,YACE,gBAAM,IAAI,CAAC,MAAM,UAChB;AAAA,IAAC;AAAA;AAAA,MAEC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,IALK,KAAK,UAAU;AAAA,EAMtB,CACD,GACH;AAEJ;AAUA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiC;AAC/B,QAAM,EAAE,gBAAgB,IAAI,KAAK;AACjC,QAAM,QAAQ,OAAO,WAAW,eAAe;AAC/C,MAAI,CAAC,OAAO;AACV,WAAO,cAAc,EAAE,iBAAiB,QAAQ,KAAK,OAAO,CAAC;AAAA,EAC/D;AACA,QAAM,kBAAkB,+BAA+B,KAAK;AAG5D,QAAM,YAAuC,CAAC;AAC9C,aAAW,CAAC,UAAU,QAAQ,KAAK,OAAO,QAAQ,KAAK,KAAK,GAAG;AAC7D,cAAU,QAAQ,IAChB;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,EAEJ;AAIA,QAAM,iBAAiB,wBAAwB,KAAK,MAAM,QAAQ,WAAW,mBAAmB;AAChG,QAAM,EAAE,OAAO,qBAAqB,WAAW,IAAI;AAAA,IACjD;AAAA,IACA,OAAO;AAAA,EACT;AACA,MAAI,WAAW,UAAU,OAAO,YAAY,aAAa;AACvD,YAAQ;AAAA,MACN,uFAAuF,eAAe,MAAM,WAAW,KAAK,IAAI,CAAC;AAAA,IACnI;AAAA,EACF;AAEA,QAAM,aAAkC;AAAA,IACtC;AAAA,IACA,QAAQ,KAAK;AAAA,IACb,SAAS,KAAK,MAAM;AAAA,IACpB,QAAQ,KAAK,MAAM;AAAA,IACnB,UAAU,KAAK,MAAM;AAAA,EACvB;AAGA,QAAM,WAAW;AAAA,IACf,GAAG,gBAAgB;AAAA,IACnB,GAAG,KAAK,MAAM;AAAA,IACd,GAAG,KAAK,MAAM;AAAA,IACd,GAAG;AAAA,EACL;AAEA,SACE,oBAAC,+BAA4B,OAAO,YAClC,8BAAC,0BAAuB,OAAO,qBAC5B,wBAAc,gBAAgB,WAAW,QAAQ,GACpD,GACF;AAEJ;AAcO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAqC;AACnC,MAAI,CAAC,SAAU,QAAO,oBAAC,YAAU,UAAS;AAC1C,QAAM,QAAQ,OAAO,YAAY,SAAS,UAAU;AACpD,MAAI,CAAC,OAAO;AACV,QAAI,OAAO,YAAY,aAAa;AAClC,cAAQ;AAAA,QACN,kEAAkE,SAAS,UAAU;AAAA,MACvF;AAAA,IACF;AACA,WAAO,oBAAC,YAAU,UAAS;AAAA,EAC7B;AACA,QAAM,iBAAiB,8BAA8B,KAAK;AAE1D,QAAM,iBAAiB;AAAA,IACrB,SAAS,MAAM;AAAA,IACf;AAAA,IACA;AAAA,EACF;AAEA,QAAM,EAAE,OAAO,qBAAqB,WAAW,IAAI;AAAA,IACjD;AAAA,IACA,OAAO;AAAA,EACT;AACA,MAAI,WAAW,UAAU,OAAO,YAAY,aAAa;AACvD,YAAQ;AAAA,MACN,gGAAgG,SAAS,UAAU,MAAM,WAAW,KAAK,IAAI,CAAC;AAAA,IAChJ;AAAA,EACF;AAEA,QAAM,aAAiC;AAAA,IACrC,YAAY,SAAS;AAAA,IACrB,SAAS,SAAS,MAAM;AAAA,IACxB,QAAQ,SAAS,MAAM;AAAA,IACvB,UAAU,SAAS,MAAM;AAAA,EAC3B;AAEA,QAAM,WAAW;AAAA,IACf,GAAG,eAAe;AAAA,IAClB,GAAG,SAAS,MAAM;AAAA,IAClB,GAAG,SAAS,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,SACE,oBAAC,8BAA2B,OAAO,YACjC,8BAAC,0BAAuB,OAAO,qBAC5B,wBAAc,eAAe,WAAW,QAAQ,GACnD,GACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { PortableRenderPlan, ExperienceContext } from '@contentful/experiences-core';
|
|
3
|
+
import { RenderUnknown } from './nodes-renderer.js';
|
|
4
|
+
import { Config } from './types.js';
|
|
5
|
+
import './missing-component.js';
|
|
6
|
+
|
|
7
|
+
interface ServerExperienceRendererProps {
|
|
8
|
+
experience: PortableRenderPlan | null | undefined;
|
|
9
|
+
config: Config;
|
|
10
|
+
/** Initial viewport seed (e.g. derived from User-Agent). Defaults to viewport[0]. */
|
|
11
|
+
initialViewportId?: string;
|
|
12
|
+
/** Per-render context shallow-merged onto defaults. */
|
|
13
|
+
context?: Partial<ExperienceContext>;
|
|
14
|
+
/** Override the fallback rendered for unregistered component types. */
|
|
15
|
+
renderUnknown?: RenderUnknown;
|
|
16
|
+
}
|
|
17
|
+
declare function ServerExperienceRenderer({ experience, config, initialViewportId, context, renderUnknown, }: ServerExperienceRendererProps): ReactNode;
|
|
18
|
+
|
|
19
|
+
export { ServerExperienceRenderer, type ServerExperienceRendererProps };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { getViewportIndex } from "@contentful/experiences-design";
|
|
3
|
+
import { ExperienceProvider } from "./context";
|
|
4
|
+
import { MissingComponent } from "./missing-component";
|
|
5
|
+
import { NodesRenderer, WrapWithTemplate } from "./nodes-renderer";
|
|
6
|
+
const DEFAULT_CONTEXT = {
|
|
7
|
+
isPreview: false,
|
|
8
|
+
metadata: {},
|
|
9
|
+
viewports: []
|
|
10
|
+
};
|
|
11
|
+
const FALLBACK_VIEWPORT = {
|
|
12
|
+
id: "_",
|
|
13
|
+
query: "*",
|
|
14
|
+
displayName: "Default",
|
|
15
|
+
previewSize: "100%"
|
|
16
|
+
};
|
|
17
|
+
function ServerExperienceRenderer({
|
|
18
|
+
experience,
|
|
19
|
+
config,
|
|
20
|
+
initialViewportId,
|
|
21
|
+
context,
|
|
22
|
+
renderUnknown = MissingComponent
|
|
23
|
+
}) {
|
|
24
|
+
if (!experience) return null;
|
|
25
|
+
const activeViewportIndex = getViewportIndex(experience.viewports, initialViewportId);
|
|
26
|
+
const contextViewports = experience.viewports.map((v) => ({ ...v }));
|
|
27
|
+
const activeViewport = { ...experience.viewports[activeViewportIndex] ?? FALLBACK_VIEWPORT };
|
|
28
|
+
const renderContext = {
|
|
29
|
+
...DEFAULT_CONTEXT,
|
|
30
|
+
...context,
|
|
31
|
+
metadata: { ...DEFAULT_CONTEXT.metadata, ...context?.metadata ?? {} },
|
|
32
|
+
viewports: contextViewports,
|
|
33
|
+
activeViewport,
|
|
34
|
+
activeViewportIndex
|
|
35
|
+
};
|
|
36
|
+
return /* @__PURE__ */ jsx(ExperienceProvider, { value: renderContext, children: /* @__PURE__ */ jsx(
|
|
37
|
+
WrapWithTemplate,
|
|
38
|
+
{
|
|
39
|
+
template: experience.template,
|
|
40
|
+
config,
|
|
41
|
+
viewports: experience.viewports,
|
|
42
|
+
activeViewportIndex,
|
|
43
|
+
children: /* @__PURE__ */ jsx(
|
|
44
|
+
NodesRenderer,
|
|
45
|
+
{
|
|
46
|
+
nodes: experience.nodes,
|
|
47
|
+
config,
|
|
48
|
+
viewports: experience.viewports,
|
|
49
|
+
activeViewportIndex,
|
|
50
|
+
renderUnknown
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
) });
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
ServerExperienceRenderer
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=server-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server-renderer.tsx"],"sourcesContent":["/*\n * Server-safe Experience renderer. Resolves the active viewport once from\n * `initialViewportId` and renders without any reactive subscription.\n */\n\nimport type { ReactNode } from 'react';\n\nimport type {\n ExperienceContext,\n PortableRenderPlan,\n ViewportDef,\n} from '@contentful/experiences-core';\nimport { getViewportIndex } from '@contentful/experiences-design';\n\nimport { ExperienceProvider } from './context';\nimport { MissingComponent } from './missing-component';\nimport { NodesRenderer, WrapWithTemplate, type RenderUnknown } from './nodes-renderer';\nimport type { Config, RenderContext } from './types';\n\nconst DEFAULT_CONTEXT: ExperienceContext = {\n isPreview: false,\n metadata: {},\n viewports: [],\n};\n\n// Keeps `activeViewport` non-null when a payload declares no viewports.\nconst FALLBACK_VIEWPORT: ViewportDef = {\n id: '_',\n query: '*',\n displayName: 'Default',\n previewSize: '100%',\n};\n\nexport interface ServerExperienceRendererProps {\n experience: PortableRenderPlan | null | undefined;\n config: Config;\n /** Initial viewport seed (e.g. derived from User-Agent). Defaults to viewport[0]. */\n initialViewportId?: string;\n /** Per-render context shallow-merged onto defaults. */\n context?: Partial<ExperienceContext>;\n /** Override the fallback rendered for unregistered component types. */\n renderUnknown?: RenderUnknown;\n}\n\nexport function ServerExperienceRenderer({\n experience,\n config,\n initialViewportId,\n context,\n renderUnknown = MissingComponent,\n}: ServerExperienceRendererProps): ReactNode {\n if (!experience) return null;\n\n const activeViewportIndex = getViewportIndex(experience.viewports, initialViewportId);\n\n // Copy viewports/activeViewport so the context (serialized + frozen by RSC)\n // shares no object identity with the plan arrays the renderers read below —\n // a shared reference makes Flight back-patch into frozen props and throw.\n const contextViewports = experience.viewports.map((v) => ({ ...v }));\n const activeViewport = { ...(experience.viewports[activeViewportIndex] ?? FALLBACK_VIEWPORT) };\n\n const renderContext: RenderContext = {\n ...DEFAULT_CONTEXT,\n ...context,\n metadata: { ...DEFAULT_CONTEXT.metadata, ...(context?.metadata ?? {}) },\n viewports: contextViewports,\n activeViewport,\n activeViewportIndex,\n };\n\n return (\n <ExperienceProvider value={renderContext}>\n <WrapWithTemplate\n template={experience.template}\n config={config}\n viewports={experience.viewports}\n activeViewportIndex={activeViewportIndex}\n >\n <NodesRenderer\n nodes={experience.nodes}\n config={config}\n viewports={experience.viewports}\n activeViewportIndex={activeViewportIndex}\n renderUnknown={renderUnknown}\n />\n </WrapWithTemplate>\n </ExperienceProvider>\n );\n}\n"],"mappings":"AA8EQ;AAlER,SAAS,wBAAwB;AAEjC,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,eAAe,wBAA4C;AAGpE,MAAM,kBAAqC;AAAA,EACzC,WAAW;AAAA,EACX,UAAU,CAAC;AAAA,EACX,WAAW,CAAC;AACd;AAGA,MAAM,oBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AAAA,EACb,aAAa;AACf;AAaO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAA6C;AAC3C,MAAI,CAAC,WAAY,QAAO;AAExB,QAAM,sBAAsB,iBAAiB,WAAW,WAAW,iBAAiB;AAKpF,QAAM,mBAAmB,WAAW,UAAU,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;AACnE,QAAM,iBAAiB,EAAE,GAAI,WAAW,UAAU,mBAAmB,KAAK,kBAAmB;AAE7F,QAAM,gBAA+B;AAAA,IACnC,GAAG;AAAA,IACH,GAAG;AAAA,IACH,UAAU,EAAE,GAAG,gBAAgB,UAAU,GAAI,SAAS,YAAY,CAAC,EAAG;AAAA,IACtE,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EACF;AAEA,SACE,oBAAC,sBAAmB,OAAO,eACzB;AAAA,IAAC;AAAA;AAAA,MACC,UAAU,WAAW;AAAA,MACrB;AAAA,MACA,WAAW,WAAW;AAAA,MACtB;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,WAAW;AAAA,UAClB;AAAA,UACA,WAAW,WAAW;AAAA,UACtB;AAAA,UACA;AAAA;AAAA,MACF;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from 'react';
|
|
2
|
+
import { ResolveContext, ResolveToken, DesignPropValue, ExperienceContext, ViewportDef } from '@contentful/experiences-core';
|
|
3
|
+
export { ResolveContext, ResolveToken } from '@contentful/experiences-core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The full Contentful-side payload for a single component instance, exposed
|
|
7
|
+
* via `useContentfulComponent()` to any descendant of a rendered Experience
|
|
8
|
+
* node. Useful for custom design-property resolution outside the SDK's
|
|
9
|
+
* cascade, branching by `componentTypeId` in a generic wrapper, keying
|
|
10
|
+
* analytics on `nodeId`, or rendering a raw-payload panel in preview.
|
|
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 `useDesignValues()` returns.
|
|
15
|
+
*/
|
|
16
|
+
interface ContentfulComponent {
|
|
17
|
+
componentTypeId: string;
|
|
18
|
+
nodeId?: string;
|
|
19
|
+
content: Record<string, unknown>;
|
|
20
|
+
design: Record<string, DesignPropValue>;
|
|
21
|
+
resolved?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Same shape as `ContentfulComponent`, but for the page-level template.
|
|
25
|
+
* Exposed via `useContentfulTemplate()` inside a template's component tree.
|
|
26
|
+
*/
|
|
27
|
+
interface ContentfulTemplate {
|
|
28
|
+
templateId: string;
|
|
29
|
+
content: Record<string, unknown>;
|
|
30
|
+
design: Record<string, DesignPropValue>;
|
|
31
|
+
resolved?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Render-time experience context. Extends the core `ExperienceContext` with
|
|
35
|
+
* the active viewport — a render-time value that resolvers cannot see (they
|
|
36
|
+
* run once before viewport resolution and are not re-triggered by viewport
|
|
37
|
+
* changes). Exposed via `useExperience()` to any descendant of the renderer.
|
|
38
|
+
*/
|
|
39
|
+
interface RenderContext extends ExperienceContext {
|
|
40
|
+
activeViewport: ViewportDef;
|
|
41
|
+
activeViewportIndex: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Customer-supplied configuration for a single component type. The `component`
|
|
45
|
+
* receives the merged prop bag (content + resolveData + slots). Design values
|
|
46
|
+
* are not injected — a component reads them via `useDesignValues()`. Runtime
|
|
47
|
+
* context and raw payload come from `useExperience()` / `useContentfulComponent()`.
|
|
48
|
+
*/
|
|
49
|
+
interface ComponentConfig<Props extends object = Record<string, unknown>> {
|
|
50
|
+
/**
|
|
51
|
+
* Lowest-precedence prop bag. Merged in before content / resolveData /
|
|
52
|
+
* slots. Useful for variant fallbacks the editorial layer doesn't always supply.
|
|
53
|
+
*/
|
|
54
|
+
defaults?: Partial<Props>;
|
|
55
|
+
/**
|
|
56
|
+
* Optional sync-or-async hook that derives final props from the raw
|
|
57
|
+
* Experience inputs. Runs once during `resolveExperience`, before render —
|
|
58
|
+
* does NOT re-fire on viewport changes.
|
|
59
|
+
*/
|
|
60
|
+
resolveData?: (ctx: ResolveContext) => Partial<Props> | Promise<Partial<Props>>;
|
|
61
|
+
/**
|
|
62
|
+
* The React component to render. Receives the merged prop bag as its props.
|
|
63
|
+
*/
|
|
64
|
+
component: ComponentType<Props>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Registry value. Register a bare React component for the common case, or
|
|
68
|
+
* the full `ComponentConfig` shape when you need defaults or a `resolveData`
|
|
69
|
+
* hook.
|
|
70
|
+
*
|
|
71
|
+
* button: Button, // bare
|
|
72
|
+
* header: { component: Header, defaults: {...} }, // with defaults
|
|
73
|
+
* card: defineComponent<CardProps>({ component: Card, resolveData: ... }),
|
|
74
|
+
*/
|
|
75
|
+
type Registration<Props extends object = Record<string, unknown>> = ComponentType<Props> | ComponentConfig<Props>;
|
|
76
|
+
/**
|
|
77
|
+
* Customer-supplied configuration for a page-level template. The template's
|
|
78
|
+
* component receives the rendered Experience nodes as `children`.
|
|
79
|
+
*/
|
|
80
|
+
interface TemplateConfig<Props extends object = Record<string, unknown>> {
|
|
81
|
+
defaults?: Partial<Props>;
|
|
82
|
+
resolveData?: (ctx: ResolveContext) => Partial<Props> | Promise<Partial<Props>>;
|
|
83
|
+
component: ComponentType<Props & {
|
|
84
|
+
children?: ReactNode;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Registry value for templates. Same dual-shape as component registrations.
|
|
89
|
+
*/
|
|
90
|
+
type TemplateRegistration<Props extends object = Record<string, unknown>> = ComponentType<Props & {
|
|
91
|
+
children?: ReactNode;
|
|
92
|
+
}> | TemplateConfig<Props>;
|
|
93
|
+
/**
|
|
94
|
+
* Identity helper — returns the config as-is, but narrows the `resolveData`
|
|
95
|
+
* and `component` parameter types to your declared `Props`.
|
|
96
|
+
*/
|
|
97
|
+
declare function defineComponent<Props extends object = Record<string, unknown>>(config: ComponentConfig<Props>): ComponentConfig<Props>;
|
|
98
|
+
/**
|
|
99
|
+
* Identity helper — returns the template config as-is, with `Props` narrowing.
|
|
100
|
+
*/
|
|
101
|
+
declare function defineTemplate<Props extends object = Record<string, unknown>>(config: TemplateConfig<Props>): TemplateConfig<Props>;
|
|
102
|
+
/**
|
|
103
|
+
* Component registry — keyed by `componentTypeId` (last slash-segment of
|
|
104
|
+
* `componentType.sys.urn`). Per-entry prop narrowing happens at
|
|
105
|
+
* `defineComponent<Props>` or at the bare-component's own type.
|
|
106
|
+
*/
|
|
107
|
+
type Components = Record<string, Registration<any>>;
|
|
108
|
+
/**
|
|
109
|
+
* Template registry — keyed by `templateId` (last slash-segment of
|
|
110
|
+
* `payload.sys.template.sys.urn`).
|
|
111
|
+
*/
|
|
112
|
+
type Templates = Record<string, TemplateRegistration<any>>;
|
|
113
|
+
interface Config {
|
|
114
|
+
components: Components;
|
|
115
|
+
templates?: Templates;
|
|
116
|
+
/**
|
|
117
|
+
* Resolves `DesignToken` envelopes to runtime values before they reach a
|
|
118
|
+
* component. If omitted, envelopes pass through unchanged. See `ResolveToken`
|
|
119
|
+
* in `@contentful/experiences-core` for the full contract.
|
|
120
|
+
*/
|
|
121
|
+
resolveToken?: ResolveToken;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Normalize a registry entry — bare component OR config object — into the
|
|
125
|
+
* common `ComponentConfig` shape used by the renderer.
|
|
126
|
+
*
|
|
127
|
+
* React function components are plain functions; `React.memo` /
|
|
128
|
+
* `React.forwardRef` yield objects carrying a `$$typeof` symbol. Both count
|
|
129
|
+
* as bare components; a config object is discriminated by having a
|
|
130
|
+
* `component` field and no `$$typeof`.
|
|
131
|
+
*/
|
|
132
|
+
declare function normalizeComponentRegistration<P extends object>(reg: Registration<P>): ComponentConfig<P>;
|
|
133
|
+
declare function normalizeTemplateRegistration<P extends object>(reg: TemplateRegistration<P>): TemplateConfig<P>;
|
|
134
|
+
|
|
135
|
+
export { type ComponentConfig, type Components, type Config, type ContentfulComponent, type ContentfulTemplate, type Registration, type RenderContext, type TemplateConfig, type TemplateRegistration, type Templates, defineComponent, defineTemplate, normalizeComponentRegistration, normalizeTemplateRegistration };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function defineComponent(config) {
|
|
2
|
+
return config;
|
|
3
|
+
}
|
|
4
|
+
function defineTemplate(config) {
|
|
5
|
+
return config;
|
|
6
|
+
}
|
|
7
|
+
function normalizeComponentRegistration(reg) {
|
|
8
|
+
if (typeof reg === "object" && reg !== null && !("$$typeof" in reg) && "component" in reg) {
|
|
9
|
+
return reg;
|
|
10
|
+
}
|
|
11
|
+
return { component: reg };
|
|
12
|
+
}
|
|
13
|
+
function normalizeTemplateRegistration(reg) {
|
|
14
|
+
if (typeof reg === "object" && reg !== null && !("$$typeof" in reg) && "component" in reg) {
|
|
15
|
+
return reg;
|
|
16
|
+
}
|
|
17
|
+
return { component: reg };
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
defineComponent,
|
|
21
|
+
defineTemplate,
|
|
22
|
+
normalizeComponentRegistration,
|
|
23
|
+
normalizeTemplateRegistration
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { ComponentType, ReactNode } from 'react';\n\nimport type {\n DesignPropValue,\n ExperienceContext,\n ResolveContext,\n ResolveToken,\n ViewportDef,\n} from '@contentful/experiences-core';\n\n/**\n * Re-exported for ergonomics: customer code authoring `resolveData` doesn't\n * have to dig into the core package for the context type.\n */\nexport type { ResolveContext, ResolveToken };\n\n/**\n * The full Contentful-side payload for a single component instance, exposed\n * via `useContentfulComponent()` to any descendant of a rendered Experience\n * node. Useful for custom design-property resolution outside the SDK's\n * cascade, branching by `componentTypeId` in a generic wrapper, keying\n * analytics on `nodeId`, or rendering a raw-payload panel in preview.\n *\n * Design properties stay in their **raw envelope form** here (the same shape\n * `ctx.design` carries inside `resolveData`). The viewport-cascaded, token-\n * resolved values are what `useDesignValues()` returns.\n */\nexport interface ContentfulComponent {\n componentTypeId: string;\n nodeId?: string;\n content: Record<string, unknown>;\n design: Record<string, DesignPropValue>;\n resolved?: Record<string, unknown>;\n}\n\n/**\n * Same shape as `ContentfulComponent`, but for the page-level template.\n * Exposed via `useContentfulTemplate()` inside a template's component tree.\n */\nexport interface ContentfulTemplate {\n templateId: string;\n content: Record<string, unknown>;\n design: Record<string, DesignPropValue>;\n resolved?: Record<string, unknown>;\n}\n\n/**\n * Render-time experience context. Extends the core `ExperienceContext` with\n * the active viewport — a render-time value that resolvers cannot see (they\n * run once before viewport resolution and are not re-triggered by viewport\n * changes). Exposed via `useExperience()` to any descendant of the renderer.\n */\nexport interface RenderContext extends ExperienceContext {\n activeViewport: ViewportDef;\n activeViewportIndex: number;\n}\n\n/**\n * Customer-supplied configuration for a single component type. The `component`\n * receives the merged prop bag (content + resolveData + slots). Design values\n * are not injected — a component reads them via `useDesignValues()`. Runtime\n * context and raw payload come from `useExperience()` / `useContentfulComponent()`.\n */\nexport interface ComponentConfig<Props extends object = Record<string, unknown>> {\n /**\n * Lowest-precedence prop bag. Merged in before content / resolveData /\n * slots. Useful for variant fallbacks the editorial layer doesn't always supply.\n */\n defaults?: Partial<Props>;\n /**\n * Optional sync-or-async hook that derives final props from the raw\n * Experience inputs. Runs once during `resolveExperience`, before render —\n * does NOT re-fire on viewport changes.\n */\n resolveData?: (ctx: ResolveContext) => Partial<Props> | Promise<Partial<Props>>;\n /**\n * The React component to render. Receives the merged prop bag as its props.\n */\n component: ComponentType<Props>;\n}\n\n/**\n * Registry value. Register a bare React component for the common case, or\n * the full `ComponentConfig` shape when you need defaults or a `resolveData`\n * hook.\n *\n * button: Button, // bare\n * header: { component: Header, defaults: {...} }, // with defaults\n * card: defineComponent<CardProps>({ component: Card, resolveData: ... }),\n */\nexport type Registration<Props extends object = Record<string, unknown>> =\n | ComponentType<Props>\n | ComponentConfig<Props>;\n\n/**\n * Customer-supplied configuration for a page-level template. The template's\n * component receives the rendered Experience nodes as `children`.\n */\nexport interface TemplateConfig<Props extends object = Record<string, unknown>> {\n defaults?: Partial<Props>;\n resolveData?: (ctx: ResolveContext) => Partial<Props> | Promise<Partial<Props>>;\n component: ComponentType<Props & { children?: ReactNode }>;\n}\n\n/**\n * Registry value for templates. Same dual-shape as component registrations.\n */\nexport type TemplateRegistration<Props extends object = Record<string, unknown>> =\n | ComponentType<Props & { children?: ReactNode }>\n | TemplateConfig<Props>;\n\n/**\n * Identity helper — returns the config as-is, but narrows the `resolveData`\n * and `component` parameter types to your declared `Props`.\n */\nexport function defineComponent<Props extends object = Record<string, unknown>>(\n config: ComponentConfig<Props>\n): ComponentConfig<Props> {\n return config;\n}\n\n/**\n * Identity helper — returns the template config as-is, with `Props` narrowing.\n */\nexport function defineTemplate<Props extends object = Record<string, unknown>>(\n config: TemplateConfig<Props>\n): TemplateConfig<Props> {\n return config;\n}\n\n/**\n * Component registry — keyed by `componentTypeId` (last slash-segment of\n * `componentType.sys.urn`). Per-entry prop narrowing happens at\n * `defineComponent<Props>` or at the bare-component's own type.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- per-entry narrowing happens at the registration author's call site.\nexport type Components = Record<string, Registration<any>>;\n\n/**\n * Template registry — keyed by `templateId` (last slash-segment of\n * `payload.sys.template.sys.urn`).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- per-entry narrowing happens at the registration author's call site.\nexport type Templates = Record<string, TemplateRegistration<any>>;\n\nexport interface Config {\n components: Components;\n templates?: Templates;\n /**\n * Resolves `DesignToken` envelopes to runtime values before they reach a\n * component. If omitted, envelopes pass through unchanged. See `ResolveToken`\n * in `@contentful/experiences-core` for the full contract.\n */\n resolveToken?: ResolveToken;\n}\n\n/**\n * Normalize a registry entry — bare component OR config object — into the\n * common `ComponentConfig` shape used by the renderer.\n *\n * React function components are plain functions; `React.memo` /\n * `React.forwardRef` yield objects carrying a `$$typeof` symbol. Both count\n * as bare components; a config object is discriminated by having a\n * `component` field and no `$$typeof`.\n */\nexport function normalizeComponentRegistration<P extends object>(\n reg: Registration<P>\n): ComponentConfig<P> {\n if (typeof reg === 'object' && reg !== null && !('$$typeof' in reg) && 'component' in reg) {\n return reg as ComponentConfig<P>;\n }\n return { component: reg as ComponentType<P> };\n}\n\nexport function normalizeTemplateRegistration<P extends object>(\n reg: TemplateRegistration<P>\n): TemplateConfig<P> {\n if (typeof reg === 'object' && reg !== null && !('$$typeof' in reg) && 'component' in reg) {\n return reg as TemplateConfig<P>;\n }\n return { component: reg as ComponentType<P & { children?: ReactNode }> };\n}\n"],"mappings":"AAmHO,SAAS,gBACd,QACwB;AACxB,SAAO;AACT;AAKO,SAAS,eACd,QACuB;AACvB,SAAO;AACT;AAqCO,SAAS,+BACd,KACoB;AACpB,MAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,EAAE,cAAc,QAAQ,eAAe,KAAK;AACzF,WAAO;AAAA,EACT;AACA,SAAO,EAAE,WAAW,IAAwB;AAC9C;AAEO,SAAS,8BACd,KACmB;AACnB,MAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,EAAE,cAAc,QAAQ,eAAe,KAAK;AACzF,WAAO;AAAA,EACT;AACA,SAAO,EAAE,WAAW,IAAmD;AACzE;","names":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ViewportDef } from '@contentful/experiences-core';
|
|
2
|
+
|
|
3
|
+
interface UseActiveViewportResult {
|
|
4
|
+
activeViewportIndex: number;
|
|
5
|
+
}
|
|
6
|
+
declare function useActiveViewport(viewports: ViewportDef[], initialViewportId?: string): UseActiveViewportResult;
|
|
7
|
+
|
|
8
|
+
export { type UseActiveViewportResult, useActiveViewport };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { getViewportIndex, toCssMediaQuery } from "@contentful/experiences-design";
|
|
4
|
+
function createMediaQueryMatchers(viewports) {
|
|
5
|
+
const mediaQueryMatches = new Array(viewports.length).fill(false);
|
|
6
|
+
mediaQueryMatches[0] = true;
|
|
7
|
+
const mediaQueryMatchers = [];
|
|
8
|
+
if (typeof window === "undefined") {
|
|
9
|
+
return [mediaQueryMatchers, mediaQueryMatches];
|
|
10
|
+
}
|
|
11
|
+
for (let index = 0; index < viewports.length; index++) {
|
|
12
|
+
const viewport = viewports[index];
|
|
13
|
+
if (!viewport) continue;
|
|
14
|
+
const cssQuery = toCssMediaQuery(viewport);
|
|
15
|
+
if (!cssQuery) continue;
|
|
16
|
+
const matcher = window.matchMedia(cssQuery);
|
|
17
|
+
mediaQueryMatches[index] = matcher.matches;
|
|
18
|
+
mediaQueryMatchers.push({ index, signal: matcher });
|
|
19
|
+
}
|
|
20
|
+
return [mediaQueryMatchers, mediaQueryMatches];
|
|
21
|
+
}
|
|
22
|
+
function useMediaQueryMatchers(viewports) {
|
|
23
|
+
const [, initialMatches] = createMediaQueryMatchers(viewports);
|
|
24
|
+
const [matches, setMatches] = useState(initialMatches);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const [matchers] = createMediaQueryMatchers(viewports);
|
|
27
|
+
const listeners = matchers.map(({ index, signal }) => {
|
|
28
|
+
const onChange = () => setMatches((prev) => {
|
|
29
|
+
const next = [...prev];
|
|
30
|
+
next[index] = signal.matches;
|
|
31
|
+
return next;
|
|
32
|
+
});
|
|
33
|
+
signal.addEventListener("change", onChange);
|
|
34
|
+
return onChange;
|
|
35
|
+
});
|
|
36
|
+
return () => {
|
|
37
|
+
listeners.forEach((listener, i) => {
|
|
38
|
+
const matcher = matchers[i];
|
|
39
|
+
if (matcher) matcher.signal.removeEventListener("change", listener);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
}, [viewports]);
|
|
43
|
+
return matches;
|
|
44
|
+
}
|
|
45
|
+
function useActiveViewport(viewports, initialViewportId) {
|
|
46
|
+
const matches = useMediaQueryMatchers(viewports);
|
|
47
|
+
const [activeViewportIndex, setActiveViewportIndex] = useState(
|
|
48
|
+
() => getViewportIndex(viewports, initialViewportId)
|
|
49
|
+
);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const currentIndex = matches.findLastIndex((isMatch) => isMatch);
|
|
52
|
+
if (currentIndex !== -1 && currentIndex !== activeViewportIndex) {
|
|
53
|
+
setActiveViewportIndex(currentIndex);
|
|
54
|
+
}
|
|
55
|
+
}, [activeViewportIndex, matches]);
|
|
56
|
+
return { activeViewportIndex };
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
useActiveViewport
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=use-active-viewport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/use-active-viewport.ts"],"sourcesContent":["/*\n * Reactive viewport-matcher hook.\n *\n * \"Active viewport\" = the last-matching media query in the ordered viewports\n * list. With desktop-first ordering (`*`, `<992px`, `<576px`), the narrowest\n * matching query wins. With mobile-first ordering, the widest wins. Either\n * way: the last `findLastIndex` of `mediaQueryMatches[i] === true`.\n *\n * Pass `initialViewportId` (typically derived from User-Agent on the server)\n * to seed the first render so SSR + client agree on viewport before\n * window.matchMedia takes over.\n */\n\n'use client';\n\nimport { useEffect, useState } from 'react';\n\nimport type { ViewportDef } from '@contentful/experiences-core';\nimport { getViewportIndex, toCssMediaQuery } from '@contentful/experiences-design';\n\ninterface MediaQueryMatcher {\n index: number;\n signal: MediaQueryList;\n}\n\nfunction createMediaQueryMatchers(viewports: ViewportDef[]): [MediaQueryMatcher[], boolean[]] {\n const mediaQueryMatches: boolean[] = new Array(viewports.length).fill(false);\n // The first viewport is the wildcard \"*\" and always matches.\n mediaQueryMatches[0] = true;\n\n const mediaQueryMatchers: MediaQueryMatcher[] = [];\n if (typeof window === 'undefined') {\n return [mediaQueryMatchers, mediaQueryMatches];\n }\n for (let index = 0; index < viewports.length; index++) {\n const viewport = viewports[index];\n if (!viewport) continue;\n const cssQuery = toCssMediaQuery(viewport);\n if (!cssQuery) continue;\n const matcher = window.matchMedia(cssQuery);\n mediaQueryMatches[index] = matcher.matches;\n mediaQueryMatchers.push({ index, signal: matcher });\n }\n return [mediaQueryMatchers, mediaQueryMatches];\n}\n\nfunction useMediaQueryMatchers(viewports: ViewportDef[]): boolean[] {\n const [, initialMatches] = createMediaQueryMatchers(viewports);\n const [matches, setMatches] = useState<boolean[]>(initialMatches);\n\n useEffect(() => {\n const [matchers] = createMediaQueryMatchers(viewports);\n const listeners = matchers.map(({ index, signal }) => {\n const onChange = (): void =>\n setMatches((prev) => {\n const next = [...prev];\n next[index] = signal.matches;\n return next;\n });\n signal.addEventListener('change', onChange);\n return onChange;\n });\n return () => {\n listeners.forEach((listener, i) => {\n const matcher = matchers[i];\n if (matcher) matcher.signal.removeEventListener('change', listener);\n });\n };\n }, [viewports]);\n\n return matches;\n}\n\nexport interface UseActiveViewportResult {\n activeViewportIndex: number;\n}\n\nexport function useActiveViewport(\n viewports: ViewportDef[],\n initialViewportId?: string\n): UseActiveViewportResult {\n const matches = useMediaQueryMatchers(viewports);\n const [activeViewportIndex, setActiveViewportIndex] = useState<number>(() =>\n getViewportIndex(viewports, initialViewportId)\n );\n\n useEffect(() => {\n const currentIndex = matches.findLastIndex((isMatch) => isMatch);\n if (currentIndex !== -1 && currentIndex !== activeViewportIndex) {\n setActiveViewportIndex(currentIndex);\n }\n }, [activeViewportIndex, matches]);\n\n return { activeViewportIndex };\n}\n"],"mappings":";AAeA,SAAS,WAAW,gBAAgB;AAGpC,SAAS,kBAAkB,uBAAuB;AAOlD,SAAS,yBAAyB,WAA4D;AAC5F,QAAM,oBAA+B,IAAI,MAAM,UAAU,MAAM,EAAE,KAAK,KAAK;AAE3E,oBAAkB,CAAC,IAAI;AAEvB,QAAM,qBAA0C,CAAC;AACjD,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,CAAC,oBAAoB,iBAAiB;AAAA,EAC/C;AACA,WAAS,QAAQ,GAAG,QAAQ,UAAU,QAAQ,SAAS;AACrD,UAAM,WAAW,UAAU,KAAK;AAChC,QAAI,CAAC,SAAU;AACf,UAAM,WAAW,gBAAgB,QAAQ;AACzC,QAAI,CAAC,SAAU;AACf,UAAM,UAAU,OAAO,WAAW,QAAQ;AAC1C,sBAAkB,KAAK,IAAI,QAAQ;AACnC,uBAAmB,KAAK,EAAE,OAAO,QAAQ,QAAQ,CAAC;AAAA,EACpD;AACA,SAAO,CAAC,oBAAoB,iBAAiB;AAC/C;AAEA,SAAS,sBAAsB,WAAqC;AAClE,QAAM,CAAC,EAAE,cAAc,IAAI,yBAAyB,SAAS;AAC7D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAoB,cAAc;AAEhE,YAAU,MAAM;AACd,UAAM,CAAC,QAAQ,IAAI,yBAAyB,SAAS;AACrD,UAAM,YAAY,SAAS,IAAI,CAAC,EAAE,OAAO,OAAO,MAAM;AACpD,YAAM,WAAW,MACf,WAAW,CAAC,SAAS;AACnB,cAAM,OAAO,CAAC,GAAG,IAAI;AACrB,aAAK,KAAK,IAAI,OAAO;AACrB,eAAO;AAAA,MACT,CAAC;AACH,aAAO,iBAAiB,UAAU,QAAQ;AAC1C,aAAO;AAAA,IACT,CAAC;AACD,WAAO,MAAM;AACX,gBAAU,QAAQ,CAAC,UAAU,MAAM;AACjC,cAAM,UAAU,SAAS,CAAC;AAC1B,YAAI,QAAS,SAAQ,OAAO,oBAAoB,UAAU,QAAQ;AAAA,MACpE,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAEd,SAAO;AACT;AAMO,SAAS,kBACd,WACA,mBACyB;AACzB,QAAM,UAAU,sBAAsB,SAAS;AAC/C,QAAM,CAAC,qBAAqB,sBAAsB,IAAI;AAAA,IAAiB,MACrE,iBAAiB,WAAW,iBAAiB;AAAA,EAC/C;AAEA,YAAU,MAAM;AACd,UAAM,eAAe,QAAQ,cAAc,CAAC,YAAY,OAAO;AAC/D,QAAI,iBAAiB,MAAM,iBAAiB,qBAAqB;AAC/D,6BAAuB,YAAY;AAAA,IACrC;AAAA,EACF,GAAG,CAAC,qBAAqB,OAAO,CAAC;AAEjC,SAAO,EAAE,oBAAoB;AAC/B;","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the current node's resolved design values (viewport-cascaded +
|
|
3
|
+
* token-resolved). Returns `{}` when there's no design in scope.
|
|
4
|
+
*
|
|
5
|
+
* Pass a type argument to shape the bag, like `useState<T>()`:
|
|
6
|
+
*
|
|
7
|
+
* const design = useDesignValues<{ as?: 'h1' | 'h2' }>();
|
|
8
|
+
*
|
|
9
|
+
* `T` is a convenience assertion, not validated at runtime — treat keys as
|
|
10
|
+
* possibly `undefined`.
|
|
11
|
+
*/
|
|
12
|
+
declare function useDesignValues<T extends object = Record<string, unknown>>(): T;
|
|
13
|
+
|
|
14
|
+
export { useDesignValues };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/use-design-values.tsx"],"sourcesContent":["'use client';\n\nimport { useResolvedDesign } from './context';\n\n/**\n * Read the current node's resolved design values (viewport-cascaded +\n * token-resolved). Returns `{}` when there's no design in scope.\n *\n * Pass a type argument to shape the bag, like `useState<T>()`:\n *\n * const design = useDesignValues<{ as?: 'h1' | 'h2' }>();\n *\n * `T` is a convenience assertion, not validated at runtime — treat keys as\n * possibly `undefined`.\n */\nexport function useDesignValues<T extends object = Record<string, unknown>>(): T {\n return (useResolvedDesign() ?? {}) as T;\n}\n"],"mappings":";AAEA,SAAS,yBAAyB;AAa3B,SAAS,kBAAiE;AAC/E,SAAQ,kBAAkB,KAAK,CAAC;AAClC;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contentful/experiences-react",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "React adapter for Contentful Experiences — Server and Client renderers over a PortableRenderPlan",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/contentful/experiences.git",
|
|
30
|
+
"directory": "packages/adapter-react"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@contentful/experiences-client": "0.1.1",
|
|
34
|
+
"@contentful/experiences-core": "0.5.1",
|
|
35
|
+
"@contentful/experiences-design": "0.5.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
39
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"react-dom": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|