├── .circleci
└── config.yml
├── .contentful
└── vault-secrets.yaml
├── .eslintignore
├── .eslintrc.cjs
├── .github
├── CODEOWNERS
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
├── labeler.yml
├── semantic.yml
└── workflows
│ ├── auto-merge.yml
│ ├── build.yaml
│ ├── check.yaml
│ ├── cleanup.yaml
│ ├── clear-caches.yaml
│ ├── labeler.yml
│ ├── main.yml
│ ├── publish.yaml
│ └── vercel.yaml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
├── post-commit
└── pre-commit
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── catalog-info.yml
├── commitlint.config.cjs
├── docs
└── guides
│ └── caching.md
├── examples
├── README.md
├── astrojs-ssr
│ ├── .env.local.example
│ ├── .gitignore
│ ├── .prettierrc.mjs
│ ├── README.md
│ ├── astro.config.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ └── favicon.svg
│ ├── src
│ │ ├── components
│ │ │ └── Experience.tsx
│ │ ├── env.d.ts
│ │ ├── getExperience.ts
│ │ ├── layouts
│ │ │ └── Layout.astro
│ │ ├── pages
│ │ │ ├── [slug].astro
│ │ │ └── index.astro
│ │ └── studio-config.ts
│ └── tsconfig.json
├── gatsby
│ ├── gatsby-spa
│ │ ├── .env.development.example
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── gatsby-config.ts
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── pages
│ │ │ │ ├── 404.tsx
│ │ │ │ ├── [locale]
│ │ │ │ │ └── [slug].tsx
│ │ │ │ └── index.tsx
│ │ │ ├── studio-config.ts
│ │ │ └── utils
│ │ │ │ └── useContentfulClient.ts
│ │ └── tsconfig.json
│ └── gatsby-ssg
│ │ ├── .env.development.example
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── gatsby-config.ts
│ │ ├── gatsby-node.mjs
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── src
│ │ ├── pages
│ │ │ ├── 404.tsx
│ │ │ └── index.tsx
│ │ ├── studio-config.mjs
│ │ └── templates
│ │ │ └── ExperienceTemplate.tsx
│ │ └── tsconfig.json
├── next-app-router
│ ├── .env.local.example
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── .vscode
│ │ └── settings.json
│ ├── README.md
│ ├── next.config.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── src
│ │ ├── app
│ │ │ ├── [locale]
│ │ │ │ └── [slug]
│ │ │ │ │ └── page.tsx
│ │ │ ├── favicon.ico
│ │ │ ├── globals.css
│ │ │ └── layout.tsx
│ │ ├── components
│ │ │ └── Experience.tsx
│ │ ├── getExperience.ts
│ │ ├── middleware.ts
│ │ └── studio-config.ts
│ ├── tailwind.config.ts
│ └── tsconfig.json
└── next-pages-router
│ ├── .env.local.example
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── .vscode
│ └── settings.json
│ ├── README.md
│ ├── next.config.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── public
│ └── favicon.ico
│ ├── src
│ ├── getExperience.ts
│ ├── pages
│ │ ├── [slug].tsx
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ └── api
│ │ │ └── hello.ts
│ ├── studio-config.ts
│ └── styles
│ │ └── globals.css
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── lerna.json
├── nx.json
├── package-lock.json
├── package.json
├── packages
├── components
│ ├── .dependency-cruiser.cjs
│ ├── .storybook
│ │ ├── main.ts
│ │ └── preview.ts
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── cypress.config.ts
│ ├── cypress
│ │ ├── fixtures
│ │ │ └── example.json
│ │ └── support
│ │ │ ├── commands.ts
│ │ │ ├── component-index.html
│ │ │ └── component.ts
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ │ ├── components
│ │ │ ├── Assembly
│ │ │ │ ├── Assembly.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Button
│ │ │ │ ├── Button.css
│ │ │ │ ├── Button.cy.tsx
│ │ │ │ ├── Button.stories.ts
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── README.md
│ │ │ │ └── index.ts
│ │ │ ├── Carousel
│ │ │ │ ├── Carousel.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Columns
│ │ │ │ ├── ColumnTypes.ts
│ │ │ │ ├── Columns.css
│ │ │ │ ├── Columns.tsx
│ │ │ │ ├── SingleColumn.tsx
│ │ │ │ └── index.ts
│ │ │ ├── ContentfulContainer
│ │ │ │ ├── ContentfulContainer.css
│ │ │ │ ├── ContentfulContainer.tsx
│ │ │ │ ├── ContentfulContainerAsHyperlink.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Divider
│ │ │ │ ├── ContentfulDivider.css
│ │ │ │ ├── ContentfulDivider.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Heading
│ │ │ │ ├── Heading.css
│ │ │ │ ├── Heading.cy.tsx
│ │ │ │ ├── Heading.stories.ts
│ │ │ │ ├── Heading.tsx
│ │ │ │ ├── README.md
│ │ │ │ └── index.ts
│ │ │ ├── Image
│ │ │ │ ├── Image.css
│ │ │ │ ├── Image.cy.tsx
│ │ │ │ ├── Image.stories.ts
│ │ │ │ ├── Image.tsx
│ │ │ │ ├── README.md
│ │ │ │ └── index.ts
│ │ │ ├── Layout
│ │ │ │ └── Flex.tsx
│ │ │ ├── RichText
│ │ │ │ ├── README.md
│ │ │ │ ├── RichText.css
│ │ │ │ ├── RichText.cy.tsx
│ │ │ │ ├── RichText.stories.ts
│ │ │ │ ├── RichText.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Text
│ │ │ │ ├── README.md
│ │ │ │ ├── Text.css
│ │ │ │ ├── Text.cy.tsx
│ │ │ │ ├── Text.stories.ts
│ │ │ │ ├── Text.tsx
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ └── variables.css
│ │ ├── global.css
│ │ ├── global.d.ts
│ │ ├── index.ts
│ │ ├── stories
│ │ │ └── assets
│ │ │ │ ├── accessibility.png
│ │ │ │ ├── accessibility.svg
│ │ │ │ ├── addon-library.png
│ │ │ │ ├── assets.png
│ │ │ │ ├── context.png
│ │ │ │ ├── discord.svg
│ │ │ │ ├── docs.png
│ │ │ │ ├── figma-plugin.png
│ │ │ │ ├── github.svg
│ │ │ │ ├── share.png
│ │ │ │ ├── styling.png
│ │ │ │ ├── testing.png
│ │ │ │ ├── theming.png
│ │ │ │ ├── tutorials.svg
│ │ │ │ └── youtube.svg
│ │ └── utils
│ │ │ ├── combineClasses.cy.ts
│ │ │ ├── combineClasses.ts
│ │ │ └── constants.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── core
│ ├── .dependency-cruiser.cjs
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ │ ├── __fixtures__
│ │ │ ├── breakpoints.ts
│ │ │ └── designTokens.ts
│ │ ├── communication
│ │ │ ├── index.ts
│ │ │ └── sendMessage.ts
│ │ ├── constants.ts
│ │ ├── deep-binding
│ │ │ ├── DeepReference.spec.ts
│ │ │ ├── DeepReference.ts
│ │ │ └── index.ts
│ │ ├── definitions
│ │ │ ├── index.ts
│ │ │ └── styles.ts
│ │ ├── entity
│ │ │ ├── EditorEntityStore.spec.ts
│ │ │ ├── EditorEntityStore.ts
│ │ │ ├── EditorModeEntityStore.spec.ts
│ │ │ ├── EditorModeEntityStore.ts
│ │ │ ├── EditorStoreBase.spec.ts
│ │ │ ├── EntityStore.spec.ts
│ │ │ ├── EntityStore.ts
│ │ │ ├── EntityStoreBase.spec.ts
│ │ │ ├── EntityStoreBase.ts
│ │ │ ├── index.ts
│ │ │ └── value-transformers
│ │ │ │ ├── TransformAssetFile.ts
│ │ │ │ └── index.ts
│ │ ├── enums.ts
│ │ ├── exports.ts
│ │ ├── fetchers
│ │ │ ├── createExperience.spec.ts
│ │ │ ├── createExperience.ts
│ │ │ ├── fetchAllEntities.spec.ts
│ │ │ ├── fetchAllEntities.ts
│ │ │ ├── fetchById.spec.ts
│ │ │ ├── fetchById.ts
│ │ │ ├── fetchBySlug.spec.ts
│ │ │ ├── fetchBySlug.ts
│ │ │ ├── fetchExperienceEntry.spec.ts
│ │ │ ├── fetchExperienceEntry.ts
│ │ │ ├── fetchReferencedEntities.spec.ts
│ │ │ ├── fetchReferencedEntities.ts
│ │ │ ├── fetchers.spec.ts
│ │ │ ├── fetchers.ts
│ │ │ ├── gatherAutoFetchedReferentsFromIncludes.spec.ts
│ │ │ ├── gatherAutoFetchedReferentsFromIncludes.ts
│ │ │ ├── index.ts
│ │ │ ├── resolveDeepUsedComponents.spec.ts
│ │ │ ├── resolveDeepUsedComponents.ts
│ │ │ └── shared
│ │ │ │ ├── circularityCheckers.spec.ts
│ │ │ │ └── circularityCheckers.ts
│ │ ├── index.ts
│ │ ├── registries
│ │ │ ├── breakpointsRegistry.spec.ts
│ │ │ ├── breakpointsRegistry.ts
│ │ │ ├── designTokenRegistry.ts
│ │ │ └── index.ts
│ │ ├── test
│ │ │ └── __fixtures__
│ │ │ │ ├── asset.ts
│ │ │ │ ├── entities.ts
│ │ │ │ ├── entry.ts
│ │ │ │ └── experience.ts
│ │ ├── types.ts
│ │ └── utils
│ │ │ ├── breakpoints.spec.ts
│ │ │ ├── breakpoints.ts
│ │ │ ├── components.ts
│ │ │ ├── debugLogger.spec.ts
│ │ │ ├── debugLogger.ts
│ │ │ ├── domValues.spec.ts
│ │ │ ├── domValues.ts
│ │ │ ├── get.ts
│ │ │ ├── index.ts
│ │ │ ├── isLink.ts
│ │ │ ├── isLinkToAsset.spec.ts
│ │ │ ├── isLinkToAsset.ts
│ │ │ ├── localizeEntity.spec.ts
│ │ │ ├── localizeEntity.ts
│ │ │ ├── pathSchema.spec.ts
│ │ │ ├── pathSchema.ts
│ │ │ ├── resolveHyperlinkPattern.spec.ts
│ │ │ ├── resolveHyperlinkPattern.ts
│ │ │ ├── sanitizeNodeProps.ts
│ │ │ ├── styleUtils
│ │ │ ├── index.ts
│ │ │ ├── ssrStyles.spec.ts
│ │ │ ├── ssrStyles.ts
│ │ │ ├── ssrStylesCustomComponentWithBuiltInStyles.spec.tsx
│ │ │ ├── ssrStylesDesignTokenResolution.spec.tsx
│ │ │ ├── ssrStylesPattern.spec.ts
│ │ │ ├── ssrStylesStructureComponent.spec.ts
│ │ │ ├── styleTransformers.spec.ts
│ │ │ ├── styleTransformers.ts
│ │ │ ├── stylesUtils.spec.ts
│ │ │ └── stylesUtils.ts
│ │ │ ├── transformers
│ │ │ ├── getArrayValue.ts
│ │ │ ├── getBoundValue.ts
│ │ │ ├── getResolvedEntryFromLink.ts
│ │ │ ├── index.ts
│ │ │ ├── media
│ │ │ │ ├── getOptimizedBackgroundImageAsset.spec.ts
│ │ │ │ ├── getOptimizedBackgroundImageAsset.ts
│ │ │ │ ├── getOptimizedImageAsset.spec.ts
│ │ │ │ ├── getOptimizedImageAsset.ts
│ │ │ │ ├── getOptimizedImageUrl.spec.ts
│ │ │ │ ├── getOptimizedImageUrl.ts
│ │ │ │ ├── mediaUtils.ts
│ │ │ │ └── transformMedia.ts
│ │ │ ├── transformBoundContentValue.spec.ts
│ │ │ ├── transformBoundContentValue.ts
│ │ │ └── transformRichText.ts
│ │ │ ├── treeTraversal.ts
│ │ │ ├── typeguards.ts
│ │ │ ├── utils.spec.ts
│ │ │ ├── utils.ts
│ │ │ └── validations.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── create-studio-experiences
│ ├── .dependency-cruiser.cjs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── constants.ts
│ │ ├── content.ts
│ │ ├── copyTemplates.ts
│ │ ├── ctflClient.ts
│ │ ├── fsClient.ts
│ │ ├── index.ts
│ │ ├── models.ts
│ │ └── utils.ts
│ └── tsconfig.json
├── experience-builder-sdk
│ ├── .dependency-cruiser.cjs
│ ├── .gitignore
│ ├── .prettierignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── bin
│ │ ├── injectSDKVersion.cjs
│ │ └── searchAndReplaceSdkVersion.cjs
│ ├── catalog-info.yml
│ ├── jest.config.js
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ │ ├── ExperienceRoot.tsx
│ │ ├── blocks
│ │ │ ├── editor
│ │ │ │ ├── VisualEditorInjectScript.tsx
│ │ │ │ ├── VisualEditorLoader.tsx
│ │ │ │ └── VisualEditorRoot.tsx
│ │ │ └── preview
│ │ │ │ ├── CompositionBlock.spec.tsx
│ │ │ │ ├── CompositionBlock.tsx
│ │ │ │ ├── PreviewDeliveryRoot.test.tsx
│ │ │ │ ├── PreviewDeliveryRoot.tsx
│ │ │ │ └── PreviewUnboundImage.tsx
│ │ ├── components
│ │ │ ├── ErrorBoundary.tsx
│ │ │ └── Flex.tsx
│ │ ├── constants.ts
│ │ ├── core
│ │ │ ├── componentRegistry.test.tsx
│ │ │ ├── componentRegistry.ts
│ │ │ ├── index.ts
│ │ │ ├── preview
│ │ │ │ ├── assemblyUtils.spec.ts
│ │ │ │ └── assemblyUtils.ts
│ │ │ └── sdkFeatures.ts
│ │ ├── hooks
│ │ │ ├── index.ts
│ │ │ ├── useBreakpoints.spec.ts
│ │ │ ├── useBreakpoints.ts
│ │ │ ├── useCustomFetch.spec.ts
│ │ │ ├── useCustomFetch.ts
│ │ │ ├── useDetectCanvasMode.spec.tsx
│ │ │ ├── useDetectCanvasMode.tsx
│ │ │ ├── useFetchByBase.ts
│ │ │ ├── useFetchById.spec.ts
│ │ │ ├── useFetchById.ts
│ │ │ ├── useFetchBySlug.spec.ts
│ │ │ ├── useFetchBySlug.ts
│ │ │ ├── useInitializeVisualEditor.ts
│ │ │ ├── useInjectStylesheet.spec.ts
│ │ │ ├── useInjectStylesheet.ts
│ │ │ ├── useMediaQuery.spec.ts
│ │ │ ├── useMediaQuery.ts
│ │ │ └── usePrevious.ts
│ │ ├── index.ts
│ │ ├── styles
│ │ │ └── ErrorBoundary.css
│ │ ├── svg
│ │ │ └── emptyState.svg
│ │ ├── utils
│ │ │ ├── parseComponentProps.spec.ts
│ │ │ ├── parseComponentProps.ts
│ │ │ ├── prebindingUtils.spec.ts
│ │ │ ├── prebindingUtils.ts
│ │ │ ├── withComponentWrapper.spec.tsx
│ │ │ └── withComponentWrapper.tsx
│ │ └── window.d.ts
│ ├── test
│ │ ├── __fixtures__
│ │ │ ├── assembly.ts
│ │ │ ├── breakpoints.ts
│ │ │ ├── componentDefinition.ts
│ │ │ ├── componentTreeNode.ts
│ │ │ ├── composition.ts
│ │ │ └── entities.ts
│ │ ├── components
│ │ │ ├── Test.spec.tsx
│ │ │ └── Test.tsx
│ │ ├── fileMock.ts
│ │ └── styleMock.ts
│ ├── testing-library.js
│ └── tsconfig.json
├── storybook-addon
│ ├── .dependency-cruiser.cjs
│ ├── .storybook
│ │ ├── local-preset.js
│ │ ├── main.ts
│ │ ├── preview-head.html
│ │ └── preview.ts
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── assets
│ │ └── example.png
│ ├── manager.js
│ ├── package.json
│ ├── preview.js
│ ├── scripts
│ │ ├── eject-typescript.mjs
│ │ ├── prepublish-checks.mjs
│ │ └── welcome.js
│ ├── src
│ │ ├── Panel.tsx
│ │ ├── Tab.tsx
│ │ ├── TestSvg.tsx
│ │ ├── components
│ │ │ ├── ContentTab.tsx
│ │ │ ├── ExperienceBuilder.styles.ts
│ │ │ ├── FieldContentBindingTrigger
│ │ │ │ ├── ContentBindingPopover.tsx
│ │ │ │ ├── FieldContentBindingContext.tsx
│ │ │ │ ├── FieldContentBindingForm.tsx
│ │ │ │ ├── FieldContentBindingTrigger.tsx
│ │ │ │ ├── FieldManualContentBinding.tsx
│ │ │ │ └── index.ts
│ │ │ ├── List.tsx
│ │ │ ├── PanelContent.tsx
│ │ │ ├── SectionStyleInput.tsx
│ │ │ ├── SectionStyles.tsx
│ │ │ ├── StyleInputFields
│ │ │ │ ├── StyleInputBoolean.tsx
│ │ │ │ ├── StyleInputNumber.tsx
│ │ │ │ ├── StyleInputSelect.tsx
│ │ │ │ ├── StyleInputText.tsx
│ │ │ │ └── index.ts
│ │ │ ├── StyleSectionComponents
│ │ │ │ ├── AffectedBreakpoints.tsx
│ │ │ │ ├── AlignmentInput
│ │ │ │ │ ├── AlignmentInput.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── BackgroundColorInput
│ │ │ │ │ ├── BackgroundColorInput.tsx
│ │ │ │ │ ├── ColorInput.tsx
│ │ │ │ │ ├── ColorPicker.tsx
│ │ │ │ │ ├── HexInput.tsx
│ │ │ │ │ ├── OpacityInput.tsx
│ │ │ │ │ └── colorUtils.ts
│ │ │ │ ├── BackgroundImage
│ │ │ │ │ ├── BackgroundImageAlignmentInput.tsx
│ │ │ │ │ ├── BackgroundImageInput.tsx
│ │ │ │ │ └── BackgroundImageSettingsSelector.tsx
│ │ │ │ ├── BorderInput
│ │ │ │ │ ├── BorderInput.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── useBorderConstituents.ts
│ │ │ │ ├── BreakpointInheritanceTree.tsx
│ │ │ │ ├── DefaultInput.tsx
│ │ │ │ ├── DistributionComponent
│ │ │ │ │ ├── DistributionComponent.styles.ts
│ │ │ │ │ ├── DistributionComponent.tsx
│ │ │ │ │ ├── GapInput.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── useGapValue.ts
│ │ │ │ ├── SizeInput
│ │ │ │ │ ├── MaxWidthInput.tsx
│ │ │ │ │ ├── SizeInput.tsx
│ │ │ │ │ ├── SizeSelector.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── styles.ts
│ │ │ │ ├── SpacingComponent
│ │ │ │ │ ├── MarginGrid.tsx
│ │ │ │ │ ├── PaddingGrid.tsx
│ │ │ │ │ ├── SpacingComponent.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── styles.ts
│ │ │ │ │ └── useSpacingValue.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── utils.ts
│ │ │ ├── StylesTab.tsx
│ │ │ ├── TabContent.tsx
│ │ │ └── VariableIcon.tsx
│ │ ├── constants.ts
│ │ ├── context
│ │ │ └── useCompositionCanvasSubscriber.ts
│ │ ├── hooks
│ │ │ └── useVariableState.ts
│ │ ├── manager.tsx
│ │ ├── preview.ts
│ │ ├── stories
│ │ │ ├── Button.stories.ts
│ │ │ ├── Button.tsx
│ │ │ ├── Header.stories.ts
│ │ │ ├── Header.tsx
│ │ │ ├── Introduction.mdx
│ │ │ ├── Page.stories.ts
│ │ │ ├── Page.tsx
│ │ │ ├── assets
│ │ │ │ ├── code-brackets.svg
│ │ │ │ ├── colors.svg
│ │ │ │ ├── comments.svg
│ │ │ │ ├── direction.svg
│ │ │ │ ├── flow.svg
│ │ │ │ ├── plugin.svg
│ │ │ │ ├── repo.svg
│ │ │ │ └── stackalt.svg
│ │ │ ├── button.css
│ │ │ ├── header.css
│ │ │ └── page.css
│ │ ├── svg
│ │ │ └── composition
│ │ │ │ ├── AlignBottom.svg
│ │ │ │ ├── AlignCenterHorizontal.svg
│ │ │ │ ├── AlignCenterVertical.svg
│ │ │ │ ├── AlignLeft.svg
│ │ │ │ ├── AlignRight.svg
│ │ │ │ ├── AlignTop.svg
│ │ │ │ ├── ColumnGap.svg
│ │ │ │ ├── DesktopIcon.svg
│ │ │ │ ├── DesktopIconSelected.svg
│ │ │ │ ├── EditIcon.svg
│ │ │ │ ├── FlexDirectionColumn.svg
│ │ │ │ ├── FlexDirectionRow.svg
│ │ │ │ ├── MobileIcon.svg
│ │ │ │ ├── MobileIconSelected.svg
│ │ │ │ ├── RemoveIcon.svg
│ │ │ │ ├── RowGap.svg
│ │ │ │ ├── TabletIcon.svg
│ │ │ │ ├── TabletIconSelected.svg
│ │ │ │ ├── WidthIcon.svg
│ │ │ │ ├── border-width-lines.svg
│ │ │ │ ├── composition-fall-back-icon.svg
│ │ │ │ ├── composition-icon-code.svg
│ │ │ │ ├── composition-icon-date.svg
│ │ │ │ ├── composition-icon-location.svg
│ │ │ │ ├── composition-icon-text.svg
│ │ │ │ └── composition-layers-icon.svg
│ │ ├── types
│ │ │ ├── composition.d.ts
│ │ │ └── svg.d.ts
│ │ ├── utils
│ │ │ ├── definitions.ts
│ │ │ ├── strings.ts
│ │ │ └── variables.ts
│ │ ├── withGlobals.ts
│ │ └── withRoundTrip.ts
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ └── vite.config.ts
├── templates
│ ├── nextjs-marketing-demo
│ │ ├── .env.template
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── next.config.mjs
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── app
│ │ │ │ ├── [locale]
│ │ │ │ │ └── [slug]
│ │ │ │ │ │ └── page.tsx
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── fonts
│ │ │ │ │ ├── GeistMonoVF.woff
│ │ │ │ │ └── GeistVF.woff
│ │ │ │ ├── globals.css
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.module.css
│ │ │ ├── assets
│ │ │ │ └── Logo.svg
│ │ │ ├── components
│ │ │ │ ├── ButtonComponentRegistration.tsx
│ │ │ │ ├── CardComponentRegistration.tsx
│ │ │ │ ├── Container.tsx
│ │ │ │ ├── Experience.tsx
│ │ │ │ ├── Footer
│ │ │ │ │ ├── Footer.tsx
│ │ │ │ │ ├── FooterDebugging.tsx
│ │ │ │ │ ├── FooterNav.tsx
│ │ │ │ │ └── styles.module.css
│ │ │ │ ├── Header
│ │ │ │ │ ├── Header.tsx
│ │ │ │ │ ├── HeaderNav.tsx
│ │ │ │ │ └── styles.module.css
│ │ │ │ ├── Icon.tsx
│ │ │ │ └── RatingStarsComponentRegistration.tsx
│ │ │ ├── getExperience.ts
│ │ │ ├── middleware.ts
│ │ │ └── studio-config.ts
│ │ └── tsconfig.json
│ └── react-vite-ts
│ │ ├── .env.template
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── public
│ │ └── vite.svg
│ │ ├── src
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── assets
│ │ │ └── react.svg
│ │ ├── index.css
│ │ ├── main.tsx
│ │ ├── studio-config.ts
│ │ └── vite-env.d.ts
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
├── test-apps
│ ├── nextjs
│ │ ├── .dependency-cruiser.cjs
│ │ ├── .env.example
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── next-env.d.ts
│ │ ├── next.config.mjs
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── public
│ │ │ ├── next.svg
│ │ │ └── vercel.svg
│ │ ├── src
│ │ │ ├── app
│ │ │ │ ├── [[...slug]]
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── globals.css
│ │ │ │ └── layout.tsx
│ │ │ ├── components
│ │ │ │ ├── ComponentWithChildren.tsx
│ │ │ │ ├── CustomButton.tsx
│ │ │ │ ├── CustomImageComponent.tsx
│ │ │ │ ├── Experience.tsx
│ │ │ │ ├── KitchenSink.tsx
│ │ │ │ ├── LinkComponent.tsx
│ │ │ │ └── NestedSlots.tsx
│ │ │ ├── studio-config.ts
│ │ │ └── utils
│ │ │ │ └── getExperience.ts
│ │ ├── tailwind.config.ts
│ │ └── tsconfig.json
│ └── react-vite
│ │ ├── .dependency-cruiser.cjs
│ │ ├── .env.example
│ │ ├── .eslintrc.cjs
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src
│ │ ├── App.tsx
│ │ ├── Page.tsx
│ │ ├── components
│ │ │ ├── ColorfulBox
│ │ │ │ ├── ColorfulBox.module.css
│ │ │ │ └── ColorfulBox.tsx
│ │ │ ├── ComponentUsingReferences
│ │ │ │ ├── ComponentUsingReferences.module.css
│ │ │ │ ├── ComponentUsingReferences.tsx
│ │ │ │ └── index.ts
│ │ │ ├── ComponentWithChildren.tsx
│ │ │ ├── CustomImageComponent.tsx
│ │ │ ├── KitchenSink.tsx
│ │ │ ├── LinkComponent.tsx
│ │ │ ├── NestedSlots.tsx
│ │ │ ├── SpaceForm.tsx
│ │ │ └── SpaceSelector.tsx
│ │ ├── hooks
│ │ │ ├── useContentfulClient.ts
│ │ │ └── useContentfulConfig.ts
│ │ ├── main.tsx
│ │ ├── studio-config.ts
│ │ ├── styles.css
│ │ ├── utils
│ │ │ ├── ContentfulConfigProvider.tsx
│ │ │ ├── combineClasses.ts
│ │ │ ├── initializeConfigs.ts
│ │ │ └── models.ts
│ │ └── vite-env.d.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ ├── vercel.json
│ │ └── vite.config.ts
├── validators
│ ├── .dependency-cruiser.cjs
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ │ ├── index.ts
│ │ ├── schemas
│ │ │ ├── componentDefinition.ts
│ │ │ ├── index.ts
│ │ │ ├── latest.ts
│ │ │ ├── schemaVersions.ts
│ │ │ └── v2023_09_28
│ │ │ │ ├── common.ts
│ │ │ │ ├── experience.ts
│ │ │ │ └── pattern.ts
│ │ ├── test
│ │ │ └── __fixtures__
│ │ │ │ ├── componentDefinition.ts
│ │ │ │ └── v2023_09_28
│ │ │ │ ├── experience.ts
│ │ │ │ ├── experiencePattern.ts
│ │ │ │ └── index.ts
│ │ ├── types.ts
│ │ ├── utils
│ │ │ ├── zodToContentfulError.spec.ts
│ │ │ └── zodToContentfulError.ts
│ │ └── validators
│ │ │ ├── ValidatorReturnValue.ts
│ │ │ ├── index.ts
│ │ │ ├── tests
│ │ │ ├── componentSettings.spec.ts
│ │ │ ├── componentTree.spec.ts
│ │ │ ├── experienceValidator.spec.ts
│ │ │ ├── patternValidator.spec.ts
│ │ │ ├── validateBreakpointDefinitions.spec.ts
│ │ │ ├── validateComponentDefinition.spec.ts
│ │ │ ├── validateDataSource.spec.ts
│ │ │ ├── validateExperienceFields.spec.ts
│ │ │ ├── validateUnboundValues.spec.ts
│ │ │ └── validateUsedComponents.spec.ts
│ │ │ ├── validateBreakpointDefinitions.ts
│ │ │ ├── validateComponentDefinition.ts
│ │ │ ├── validateExperienceFields.ts
│ │ │ └── validatePatternFields.ts
│ ├── tsconfig.json
│ └── vite.config.mts
└── visual-editor
│ ├── .dependency-cruiser.cjs
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.mjs
│ ├── src
│ ├── __fixtures__
│ │ └── breakpoints.ts
│ ├── communication
│ │ ├── onComponentDrop.ts
│ │ ├── onComponentMoved.ts
│ │ ├── sendSelectedComponentCoordinates.spec.ts
│ │ └── sendSelectedComponentCoordinates.ts
│ ├── components
│ │ ├── DraggableBlock
│ │ │ ├── Dropzone.tsx
│ │ │ ├── Dropzone.types.ts
│ │ │ ├── DropzoneClone.tsx
│ │ │ ├── EditorBlock.tsx
│ │ │ ├── EditorBlockClone.tsx
│ │ │ ├── Tooltip.tsx
│ │ │ └── styles.module.css
│ │ ├── DraggableHelpers
│ │ │ ├── CircularDependencyErrorPlaceholder.tsx
│ │ │ ├── DraggableComponentList.tsx
│ │ │ ├── Hitboxes.tsx
│ │ │ ├── ImportedComponentErrorBoundary.tsx
│ │ │ ├── MissingComponentPlaceholder.tsx
│ │ │ ├── Placeholder.tsx
│ │ │ └── styles.module.css
│ │ ├── EmptyContainer
│ │ │ ├── EmptyContainer.module.css
│ │ │ └── EmptyContainer.tsx
│ │ ├── LoaderOverlay
│ │ │ ├── LoaderOverlay.tsx
│ │ │ └── overlay.module.css
│ │ ├── RootRenderer
│ │ │ ├── DNDProvider.tsx
│ │ │ ├── RootRenderer.tsx
│ │ │ ├── TestDNDContainer.tsx
│ │ │ └── render.module.css
│ │ └── VisualEditorRoot.tsx
│ ├── global.css
│ ├── global.d.ts
│ ├── hooks
│ │ ├── useBreakpoints.spec.ts
│ │ ├── useBreakpoints.ts
│ │ ├── useCanvasInteractions.ts
│ │ ├── useComponent.spec.ts
│ │ ├── useComponent.tsx
│ │ ├── useComponentProps.spec.ts
│ │ ├── useComponentProps.ts
│ │ ├── useDraggablePosition.ts
│ │ ├── useDropzoneDirection.ts
│ │ ├── useEditorModeClassName.spec.ts
│ │ ├── useEditorModeClassName.ts
│ │ ├── useEditorSubscriber.ts
│ │ ├── useInitializeEditor.ts
│ │ ├── useSelectedInstanceCoordinates.ts
│ │ └── useSingleColumn.ts
│ ├── index.tsx
│ ├── renderApp.tsx
│ ├── store
│ │ ├── draggedItem.ts
│ │ ├── editor.ts
│ │ ├── entityStore.ts
│ │ ├── registries.ts
│ │ ├── tree.ts
│ │ └── zone.ts
│ ├── types
│ │ ├── Config.tsx
│ │ ├── constants.ts
│ │ └── treeActions.ts
│ └── utils
│ │ ├── canvasToolsUtils.ts
│ │ ├── createTreeNode.ts
│ │ ├── dragState.ts
│ │ ├── generate-id.ts
│ │ ├── getComponentProps.ts
│ │ ├── getHitboxStyles.ts
│ │ ├── getItem.ts
│ │ ├── getTreeDiff.ts
│ │ ├── getUnboundValues.ts
│ │ ├── onDrop.ts
│ │ ├── simulateDnD.ts
│ │ ├── treeHelpers.ts
│ │ └── zone.ts
│ ├── test
│ ├── __fixtures__
│ │ ├── assembly.ts
│ │ ├── composition.ts
│ │ └── entities.ts
│ ├── fileMock.ts
│ └── styleMock.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── tools
└── .dependency-cruiser.cjs
└── tsconfig.base.json
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | orbs:
3 | vault: contentful/vault@1
4 |
5 | executors:
6 | docker-with-node:
7 | docker:
8 | - image: cimg/node:20.11
9 | working_directory: ~/build-and-test
10 |
11 | jobs:
12 | prepare:
13 | executor: docker-with-node
14 | steps:
15 | - checkout
16 | - run: node --version
17 |
18 | release-next:
19 | executor: docker-with-node
20 | steps:
21 | - checkout
22 | - use-vault
23 | - install-dependencies
24 | - vault/configure-lerna
25 | - run: echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ~/.npmrc
26 | - run: yarn build # This should not be needed, but without it it will not contain any files
27 | # Use the exact dependency version for local path specifiers. e.g. this avoids resolving `^0.0.2-alpha.15` to `0.0.2-dev-20240114T223125.0`
28 | - run: yarn lerna version --exact --no-private --conventional-commits --conventional-prerelease --preid next --create-release github --yes
29 | - run: yarn lerna publish from-git --pre-dist-tag next --yes
30 |
31 | workflows:
32 | version: 2
33 | build_and_test:
34 | jobs:
35 | - prepare
36 |
--------------------------------------------------------------------------------
/.contentful/vault-secrets.yaml:
--------------------------------------------------------------------------------
1 | version: 1
2 | services:
3 | github-action:
4 | policies:
5 | - dependabot
6 | - packages-read
7 | - semantic-release
8 | circleci:
9 | policies:
10 | - semantic-release-ecosystem
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | testing-library.js
2 | .eslintrc.cjs
3 | commitlint.config.cjs
4 | lint-staged.config.cjs
5 | node_modules/
6 | .coverage/
7 | .cache/
8 | dist/
9 | build/
10 | tests
11 | examples
12 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | node: true,
5 | es2021: true,
6 | },
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:react/recommended',
10 | 'plugin:react-hooks/recommended',
11 | 'plugin:@typescript-eslint/recommended',
12 | ],
13 | settings: {
14 | react: {
15 | version: 'detect',
16 | },
17 | },
18 | parser: '@typescript-eslint/parser',
19 | parserOptions: {
20 | ecmaFeatures: {
21 | jsx: true,
22 | },
23 | ecmaVersion: 'latest',
24 | sourceType: 'module',
25 | },
26 | plugins: ['react', 'react-hooks', '@typescript-eslint'],
27 | rules: {
28 | 'react-hooks/exhaustive-deps': 'warn',
29 | "@typescript-eslint/no-explicit-any": "warn",
30 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "ignoreRestSiblings": true }],
31 | "react/prop-types": "off",
32 | },
33 | }
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @contentful/team-sparks
2 |
3 | # Do not assign anyone to these files otherwise
4 | # there would be review requests for all dependabot PRs
5 | package.json
6 | package-lock.json
7 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Purpose
2 |
3 | ## Approach
4 |
5 |
17 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | registries:
3 | npm-registry-registry-npmjs-org:
4 | type: npm-registry
5 | url: https://registry.npmjs.org
6 | token: '${{secrets.NPM_REGISTRY_REGISTRY_NPMJS_ORG_TOKEN}}'
7 | npm-github:
8 | type: npm-registry
9 | url: https://npm.pkg.github.com
10 | token: ${{secrets.NPM_REGISTRY_REGISTRY_GH_ORG_TOKEN}}
11 |
12 | updates:
13 | - package-ecosystem: npm
14 | directory: '/'
15 | schedule:
16 | interval: weekly
17 | time: '00:00'
18 | timezone: Etc/UCT
19 | open-pull-requests-limit: 10
20 | target-branch: development
21 | labels:
22 | - dependencies
23 | - dependabot
24 | commit-message:
25 | prefix: chore
26 | registries:
27 | - npm-registry-registry-npmjs-org
28 | - npm-github
29 |
--------------------------------------------------------------------------------
/.github/labeler.yml:
--------------------------------------------------------------------------------
1 | package:$1:
2 | - packages/([^/]+)/.*
3 |
4 | configuration:
5 | - .circleci/**/*
6 | - .contentful/**/*
7 | - .github/**/*
8 | - .dependabot/**/*
9 | - .eslintignore
10 | - .eslintrc.js
11 | - .gitignore
12 | - .nvmrc
13 | - .prettierrc
14 | - lerna.json
15 | - LICENSE
16 | - nx.json
17 | - tsconfig.base.json
18 | - tsconfig.json
19 |
--------------------------------------------------------------------------------
/.github/semantic.yml:
--------------------------------------------------------------------------------
1 | # Always validate the PR title AND all the commits
2 | titleAndCommits: true
3 | anyCommit: true
4 | allowMergeCommits: false
5 | types:
6 | - feat
7 | - fix
8 | - improvement
9 | - docs
10 | - style
11 | - refactor
12 | - perf
13 | - test
14 | - build
15 | - ci
16 | - chore
17 | - revert
18 |
--------------------------------------------------------------------------------
/.github/workflows/auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: 'dependabot approve-and-request-merge'
2 |
3 | on: pull_request_target
4 |
5 | jobs:
6 | worker:
7 | permissions:
8 | contents: write
9 | id-token: write
10 | runs-on: ubuntu-latest
11 | if: github.actor == 'dependabot[bot]'
12 | steps:
13 | - name: Fetch Dependabot metadata
14 | id: dependabot-metadata
15 | uses: dependabot/fetch-metadata@v1
16 | with:
17 | github-token: '${{ secrets.GITHUB_TOKEN }}'
18 | - name: Auto-merge
19 | # Don't auto-merge major version upgrades
20 | if: ${{steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major'}}
21 | uses: contentful/github-auto-merge@v1
22 | with:
23 | VAULT_URL: ${{ secrets.VAULT_URL }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/clear-caches.yaml:
--------------------------------------------------------------------------------
1 | name: Clear all Github actions caches
2 | on:
3 | workflow_dispatch:
4 |
5 | jobs:
6 | my-job:
7 | name: Delete all caches
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Clear caches
12 | uses: easimon/wipe-cache@main
--------------------------------------------------------------------------------
/.github/workflows/labeler.yml:
--------------------------------------------------------------------------------
1 | name: 'Pull Request Labeler'
2 | on: [pull_request]
3 |
4 | jobs:
5 | triage:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/labeler@v4
9 | with:
10 | repo-token: '${{ secrets.GITHUB_TOKEN }}'
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 | *.tgz
10 | .npmrc
11 |
12 | node_modules
13 | build
14 | .yarn
15 | dist
16 | reports
17 | coverage
18 | dist-ssr
19 | *.local
20 | .yarnrc.yml
21 | vite.config.ts.timestamp-*
22 |
23 | # Editor directories and files
24 | .vscode/*
25 | !.vscode/extensions.json
26 | .idea
27 | .DS_Store
28 | *.suo
29 | *.ntvs*
30 | *.njsproj
31 | *.sln
32 | *.sw?
33 | .pnp.*
34 |
35 | tmp/
36 |
37 | .vercel
38 |
39 | # Ingore .nx local files
40 | .nx
41 |
42 | # Ingore .env local files
43 | .env
44 | # Allow env template files
45 | !.env.local.tmpl
46 |
47 | **/cypress/screenshots/**
48 | **/cypress/videos/**
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx commitlint --edit
2 |
--------------------------------------------------------------------------------
/.husky/post-commit:
--------------------------------------------------------------------------------
1 | rm -rf reports dist
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.11.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | examples
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "singleQuote": true,
6 | "bracketSameLine": true,
7 | "semi": true
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Contentful
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/catalog-info.yml:
--------------------------------------------------------------------------------
1 | apiVersion: backstage.io/v1alpha1
2 | kind: System
3 | metadata:
4 | name: experience-builder
5 | description: A monorepo for all NPM packages related to Experience Builder.
6 | annotations:
7 | circleci.com/project-slug: github/contentful/experience-builder
8 | github.com/project-slug: contentful/experience-builder
9 | contentful.com/ci-alert-slack: prd-experience-builder-alerts
10 | backstage.io/source-location: url:https://github.com/contentful/experience-builder/
11 | spec:
12 | type: library
13 | lifecycle: production
14 | owner: group:team-sparks
15 |
16 | ---
17 | apiVersion: backstage.io/v1alpha1
18 | kind: Location
19 | metadata:
20 | name: experience-builder-index
21 | spec:
22 | type: url
23 | targets:
24 | - ./packages/experience-builder-sdk/catalog-info.yaml
25 |
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Studio Experiences example apps
2 |
3 | This folder contains example apps that demonstrate how to use Studio Experiences with different frameworks.
4 |
5 | ## next-app-router
6 |
7 | Demonstrates how to set up a server rendered page with Studio Experiences using the Next.js App Router.
8 |
9 | ## next-pages-router
10 |
11 | Demonstrates how to set up a server rendered page with Studio Experiences using the Next.js Pages Router.
--------------------------------------------------------------------------------
/examples/astrojs-ssr/.env.local.example:
--------------------------------------------------------------------------------
1 | # Rename this file to .env.local and fill in the values for the following environment variables
2 | CTFL_ENVIRONMENT=
3 | CTFL_SPACE=
4 | CTFL_ACCESS_TOKEN=
5 | CTFL_PREVIEW_ACCESS_TOKEN=
6 | CTFL_EXPERIENCE_TYPE=
7 |
--------------------------------------------------------------------------------
/examples/astrojs-ssr/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
--------------------------------------------------------------------------------
/examples/astrojs-ssr/.prettierrc.mjs:
--------------------------------------------------------------------------------
1 | // .prettierrc.mjs
2 | /** @type {import("prettier").Config} */
3 | export default {
4 | plugins: ["prettier-plugin-astro"],
5 | };
6 |
--------------------------------------------------------------------------------
/examples/astrojs-ssr/astro.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { defineConfig } from 'astro/config';
3 | import node from '@astrojs/node';
4 | import react from '@astrojs/react';
5 |
6 | // https://astro.build/config
7 | export default defineConfig({
8 | integrations: [react()],
9 | output: 'server',
10 | i18n: {
11 | defaultLocale: 'en-US',
12 | locales: ['en-US', 'de'],
13 | },
14 | adapter: node({
15 | mode: 'standalone',
16 | }),
17 | });
18 |
--------------------------------------------------------------------------------
/examples/astrojs-ssr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "astro-studio-app",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro check && astro build",
9 | "preview": "astro preview",
10 | "astro": "astro"
11 | },
12 | "dependencies": {
13 | "@astrojs/check": "^0.9.4",
14 | "@astrojs/node": "^9.0.0",
15 | "@astrojs/react": "^4.1.1",
16 | "@contentful/experiences-sdk-react": "^1.26.0",
17 | "@types/react": "^19.0.1",
18 | "@types/react-dom": "^19.0.2",
19 | "astro": "^5.0.8",
20 | "contentful": "^11.3.2",
21 | "react": "^19.0.0",
22 | "react-dom": "^19.0.0",
23 | "typescript": "^5.6.3"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/astrojs-ssr/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/examples/astrojs-ssr/src/components/Experience.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ExperienceRoot } from '@contentful/experiences-sdk-react';
3 | import { type Experience as ExperienceType } from '@contentful/experiences-sdk-react';
4 |
5 | interface ExperienceProps {
6 | experience: ExperienceType;
7 | locale: string;
8 | }
9 |
10 | const Experience: React.FC = ({ experience, locale }) => {
11 | return ;
12 | };
13 |
14 | export default Experience;
15 |
--------------------------------------------------------------------------------
/examples/astrojs-ssr/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/examples/astrojs-ssr/src/pages/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from '../layouts/Layout.astro';
3 | import { detachExperienceStyles } from '@contentful/experiences-sdk-react';
4 | import Experience from '../components/Experience';
5 | import { getExperience } from '../getExperience';
6 | import '../studio-config';
7 |
8 | const slug = Astro.params.slug || 'home-page';
9 | const isPreview = Astro.url.searchParams.has('isPreview', 'true');
10 | const isEditorMode = Astro.url.searchParams.has('expEditorMode', 'true');
11 | const localeCode = Astro.preferredLocale || 'en-US';
12 |
13 | const { experience, error } = await getExperience(
14 | slug,
15 | localeCode,
16 | isPreview,
17 | isEditorMode
18 | );
19 |
20 | // extract the styles from the experience
21 | const stylesheet = experience ? detachExperienceStyles(experience) : null;
22 | ---
23 |
24 |
25 |
26 |
27 |
28 | {
29 | error ? (
30 | {error.message}
31 | ) : (
32 |
33 | )
34 | }
35 |
36 |
37 |
--------------------------------------------------------------------------------
/examples/astrojs-ssr/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | return Astro.redirect('/home-page')
3 | ---
--------------------------------------------------------------------------------
/examples/astrojs-ssr/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "jsxImportSource": "react"
6 | }
7 | }
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-spa/.env.development.example:
--------------------------------------------------------------------------------
1 | # Rename this file to .env.development and fill in the values for the following environment variables
2 | GATSBY_CTFL_ENVIRONMENT=
3 | GATSBY_CTFL_SPACE=
4 | GATSBY_CTFL_ACCESS_TOKEN=
5 | GATSBY_CTFL_PREVIEW_ACCESS_TOKEN=
6 | GATSBY_CTFL_EXPERIENCE_TYPE=
7 | GATSBY_CTFL_DOMAIN=
8 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-spa/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .cache/
3 | public
4 | src/gatsby-types.d.ts
5 | .env*
6 | !.env.development.example
7 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-spa/gatsby-config.ts:
--------------------------------------------------------------------------------
1 | import type { GatsbyConfig } from 'gatsby';
2 |
3 | const config: GatsbyConfig = {
4 | siteMetadata: {
5 | title: `gatsby-spa`,
6 | siteUrl: `https://www.yourdomain.tld`,
7 | },
8 | // More easily incorporate content into your pages through automatic TypeScript type generation and better GraphQL IntelliSense.
9 | // If you use VSCode you can also use the GraphQL plugin
10 | // Learn more at: https://gatsby.dev/graphql-typegen
11 | graphqlTypegen: true,
12 | plugins: [],
13 | };
14 |
15 | export default config;
16 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-spa/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-spa",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "gatsby-spa",
6 | "author": "ryunsong-contentful",
7 | "keywords": [
8 | "gatsby"
9 | ],
10 | "scripts": {
11 | "develop": "gatsby develop",
12 | "start": "gatsby develop",
13 | "build": "gatsby build",
14 | "serve": "gatsby serve",
15 | "clean": "gatsby clean",
16 | "typecheck": "tsc --noEmit"
17 | },
18 | "dependencies": {
19 | "@contentful/experiences-sdk-react": "^1.30.5",
20 | "contentful": "^11.3.2",
21 | "gatsby": "^5.14.3",
22 | "react": "^18.2.0",
23 | "react-dom": "^18.2.0",
24 | "react-markdown": "^9.0.3"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^20.11.19",
28 | "@types/react": "^18.2.55",
29 | "@types/react-dom": "^18.2.19",
30 | "typescript": "^5.3.3"
31 | },
32 | "overrides": {
33 | "gatsby": {
34 | "path-to-regexp": "0.1.12"
35 | },
36 | "@parcel/core": {
37 | "base-x": "3.0.11"
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-spa/src/pages/[locale]/[slug].tsx:
--------------------------------------------------------------------------------
1 | import { useFetchBySlug, ExperienceRoot } from '@contentful/experiences-sdk-react';
2 | import { PageProps } from 'gatsby';
3 | import React from 'react';
4 | import { useContentfulClient } from '../../utils/useContentfulClient';
5 |
6 | type PathParams = {
7 | locale: string;
8 | slug: string,
9 | isPreviewString: string;
10 | };
11 |
12 | const experienceTypeId = process.env.GATSBY_CTFL_EXPERIENCE_TYPE || '';
13 |
14 | export default function StudioExperiencePage(pageProps: PageProps) {
15 | const { locale = 'en-US', slug = 'home-page', } = pageProps.params as PathParams;
16 | const isPreview = new URL(pageProps.location.href).searchParams.get('isPreview') === 'true';
17 | const { client } = useContentfulClient({ isPreview });
18 |
19 | const { experience, error, isLoading } = useFetchBySlug({
20 | slug,
21 | localeCode: locale,
22 | client,
23 | experienceTypeId,
24 | });
25 |
26 | if (isLoading) return Loading...
;
27 | if (error) return {error.message}
;
28 |
29 | return ;
30 | }
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-spa/src/utils/useContentfulClient.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { createClient } from 'contentful';
3 | import '../studio-config'; // Update this file to set breakpoints, design tokens, and register components
4 |
5 | type Props = {
6 | isPreview?: boolean;
7 | }
8 |
9 | export const useContentfulClient = ({isPreview}: Props) => {
10 | const client = useMemo(() => {
11 | const clientConfig = {
12 | space: process.env.GATSBY_CTFL_SPACE || '',
13 | environment: process.env.GATSBY_CTFL_ENVIRONMENT || '',
14 | host: isPreview
15 | ? `preview.${process.env.GATSBY_CTFL_DOMAIN}` || 'preview.contentful.com'
16 | : `cdn.${process.env.GATSBY_CTFL_DOMAIN}` || 'cdn.contentful.com',
17 | accessToken: isPreview
18 | ? process.env.GATSBY_CTFL_PREVIEW_ACCESS_TOKEN || ''
19 | : process.env.GATSBY_CTFL_ACCESS_TOKEN || '',
20 | experienceTypeId: process.env.GATSBY_CTFL_EXPERIENCE_TYPE || '',
21 | };
22 | return createClient(clientConfig);
23 | }, []);
24 |
25 | return { client };
26 | };
27 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-spa/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "lib": [
5 | "dom",
6 | "esnext"
7 | ],
8 | "jsx": "react",
9 | "module": "esnext",
10 | "moduleResolution": "node",
11 | "esModuleInterop": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "strict": true,
14 | "skipLibCheck": true
15 | },
16 | "include": [
17 | "./src/**/*",
18 | "./gatsby-node.ts",
19 | "./gatsby-config.ts",
20 | "./plugins/**/*"
21 | ]
22 | }
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-ssg/.env.development.example:
--------------------------------------------------------------------------------
1 | # Rename this file to .env.development and fill in the values for the following environment variables
2 | GATSBY_CTFL_ENVIRONMENT=
3 | GATSBY_CTFL_SPACE=
4 | GATSBY_CTFL_ACCESS_TOKEN=
5 | GATSBY_CTFL_PREVIEW_ACCESS_TOKEN=
6 | GATSBY_CTFL_EXPERIENCE_TYPE=
7 | GATSBY_CTFL_DOMAIN=
8 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-ssg/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .cache/
3 | public
4 | src/gatsby-types.d.ts
5 | .env.development
6 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-ssg/gatsby-config.ts:
--------------------------------------------------------------------------------
1 | import type { GatsbyConfig } from "gatsby"
2 |
3 | const config: GatsbyConfig = {
4 | siteMetadata: {
5 | title: `gatsby-ssg`,
6 | siteUrl: `https://www.yourdomain.tld`,
7 | },
8 | // More easily incorporate content into your pages through automatic TypeScript type generation and better GraphQL IntelliSense.
9 | // If you use VSCode you can also use the GraphQL plugin
10 | // Learn more at: https://gatsby.dev/graphql-typegen
11 | graphqlTypegen: true,
12 | plugins: [],
13 | }
14 |
15 | export default config
16 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-ssg/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-ssg",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "gatsby-ssg",
6 | "author": "Ely Lucas",
7 | "keywords": [
8 | "gatsby"
9 | ],
10 | "scripts": {
11 | "develop": "gatsby develop",
12 | "start": "gatsby develop",
13 | "build": "gatsby build",
14 | "serve": "gatsby serve",
15 | "clean": "gatsby clean",
16 | "typecheck": "tsc --noEmit"
17 | },
18 | "dependencies": {
19 | "@contentful/experiences-sdk-react": "^1.28.1",
20 | "contentful": "^11.4.3",
21 | "gatsby": "^5.14.3",
22 | "react": "^18.2.0",
23 | "react-dom": "^18.2.0",
24 | "react-router-dom": "^7.5.2"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^20.11.19",
28 | "@types/react": "^18.2.55",
29 | "@types/react-dom": "^18.2.19",
30 | "typescript": "^5.3.3"
31 | },
32 | "overrides": {
33 | "gatsby": {
34 | "path-to-regexp": "0.1.12"
35 | },
36 | "@parcel/core": {
37 | "base-x": "3.0.11"
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-ssg/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import type { PageProps } from "gatsby"
3 | import { navigate } from 'gatsby';
4 |
5 | const IndexPage: React.FC = () => {
6 | React.useEffect(() => {
7 | navigate('/home-page');
8 | }, []);
9 |
10 | return null;
11 | }
12 |
13 | export default IndexPage
14 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-ssg/src/studio-config.mjs:
--------------------------------------------------------------------------------
1 | // File is mjs so it can be imported in gatsby-node.mjs as well
2 | import { defineDesignTokens } from '@contentful/experiences-sdk-react';
3 |
4 | defineDesignTokens({
5 | color: {
6 | MyBlue: 'cornflowerblue',
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-ssg/src/templates/ExperienceTemplate.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { PageProps } from 'gatsby';
3 | import { ExperienceRoot } from '@contentful/experiences-sdk-react';
4 | import '../studio-config.mjs';
5 |
6 | type PageContextProps = {
7 | experienceJson: string;
8 | stylesheet: string;
9 | localeCode: string;
10 | };
11 |
12 | export default function StudioExperiencePage(pageProps: PageProps) {
13 | const { experienceJson, stylesheet, localeCode } =
14 | pageProps.pageContext as PageContextProps;
15 |
16 | return (
17 | <>
18 |
19 |
20 | >
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/next-app-router/.env.local.example:
--------------------------------------------------------------------------------
1 | # Rename this file to .env.local and fill in the values for the following environment variables
2 | NEXT_PUBLIC_CTFL_ENVIRONMENT=
3 | NEXT_PUBLIC_CTFL_SPACE=
4 | NEXT_PUBLIC_CTFL_ACCESS_TOKEN=
5 | NEXT_PUBLIC_CTFL_PREVIEW_ACCESS_TOKEN=
6 | NEXT_PUBLIC_CTFL_EXPERIENCE_TYPE=
--------------------------------------------------------------------------------
/examples/next-app-router/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/next-app-router/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/next-app-router/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/examples/next-app-router/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | async redirects() {
4 | return [
5 | {
6 | source: '/',
7 | destination: '/home-page',
8 | permanent: true,
9 | }
10 | ];
11 | },
12 | };
13 |
14 | export default nextConfig;
15 |
--------------------------------------------------------------------------------
/examples/next-app-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-app-router",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@contentful/experiences-sdk-react": "^1.26.0",
13 | "contentful": "^11.3.2",
14 | "next": "15.2.3",
15 | "next-i18n-router": "^5.5.0",
16 | "react": "19.0.0",
17 | "react-dom": "19.0.0"
18 | },
19 | "devDependencies": {
20 | "@types/node": "^20",
21 | "@types/react": "19.0.1",
22 | "@types/react-dom": "19.0.2",
23 | "eslint": "^8",
24 | "eslint-config-next": "15.1.0",
25 | "postcss": "^8",
26 | "tailwindcss": "^3.4.1",
27 | "typescript": "^5"
28 | },
29 | "overrides": {
30 | "@types/react": "19.0.1",
31 | "@types/react-dom": "19.0.2"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/examples/next-app-router/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/examples/next-app-router/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/examples/next-app-router/src/app/favicon.ico
--------------------------------------------------------------------------------
/examples/next-app-router/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 |
12 | body {
13 | color: rgb(var(--foreground-rgb));
14 | }
15 |
16 | @layer utilities {
17 | .text-balance {
18 | text-wrap: balance;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/next-app-router/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 | import { Inter } from 'next/font/google';
3 | import './globals.css';
4 |
5 | const inter = Inter({ subsets: ['latin'] });
6 |
7 | export const metadata: Metadata = {
8 | title: 'Create Next App',
9 | description: 'Generated by create next app',
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: Readonly<{
15 | children: React.ReactNode;
16 | }>) {
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/next-app-router/src/components/Experience.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ExperienceRoot } from '@contentful/experiences-sdk-react';
4 | import React from 'react';
5 | // import the studio config client side
6 | import '@/studio-config';
7 |
8 | interface ExperienceProps {
9 | experienceJSON: string | null;
10 | locale: string;
11 | }
12 |
13 | const Experience: React.FC = ({ experienceJSON, locale }) => {
14 | return ;
15 | };
16 |
17 | export default Experience;
18 |
--------------------------------------------------------------------------------
/examples/next-app-router/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { i18nRouter } from 'next-i18n-router';
2 | import { NextRequest } from 'next/server';
3 |
4 | export function middleware(request: NextRequest) {
5 | const i18n = {
6 | locales: ['en-US', 'de'],
7 | defaultLocale: 'en-US',
8 | };
9 | const resp = i18nRouter(request, i18n);
10 | return resp;
11 | }
12 |
13 | // only applies this middleware to files in the app directory
14 | export const config = {
15 | matcher: '/((?!api|static|.*\\..*|_next).*)',
16 | };
17 |
--------------------------------------------------------------------------------
/examples/next-app-router/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | plugins: [],
10 | };
11 | export default config;
12 |
--------------------------------------------------------------------------------
/examples/next-app-router/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./src/*"
27 | ]
28 | },
29 | "target": "ES2017"
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/examples/next-pages-router/.env.local.example:
--------------------------------------------------------------------------------
1 | # Rename this file to .env.local and fill in the values for the following environment variables
2 | NEXT_PUBLIC_CTFL_SPACE=
3 | NEXT_PUBLIC_CTFL_ACCESS_TOKEN=
4 | NEXT_PUBLIC_CTFL_PREVIEW_ACCESS_TOKEN=
5 | NEXT_PUBLIC_CTFL_ENVIRONMENT=
6 | NEXT_PUBLIC_CTFL_EXPERIENCE_TYPE=
7 |
--------------------------------------------------------------------------------
/examples/next-pages-router/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/next-pages-router/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/next-pages-router/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/examples/next-pages-router/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | async redirects() {
4 | return [
5 | {
6 | source: '/',
7 | destination: '/home-page',
8 | permanent: true,
9 | },
10 | ];
11 | },
12 | i18n: {
13 | locales: ['en-US', 'de'],
14 | defaultLocale: 'en-US',
15 | },
16 | reactStrictMode: true,
17 | };
18 |
19 | export default nextConfig;
20 |
--------------------------------------------------------------------------------
/examples/next-pages-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-pages-router",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@contentful/experiences-sdk-react": "^1.30.5",
13 | "contentful": "^11.3.2",
14 | "next": "15.2.4",
15 | "react": "19.0.0",
16 | "react-dom": "19.0.0"
17 | },
18 | "devDependencies": {
19 | "@types/node": "^20",
20 | "@types/react": "19.0.1",
21 | "@types/react-dom": "19.0.2",
22 | "eslint": "^8",
23 | "eslint-config-next": "15.1.0",
24 | "postcss": "^8",
25 | "tailwindcss": "^3.4.1",
26 | "typescript": "^5"
27 | },
28 | "overrides": {
29 | "@types/react": "19.0.1",
30 | "@types/react-dom": "19.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/next-pages-router/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/examples/next-pages-router/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/examples/next-pages-router/public/favicon.ico
--------------------------------------------------------------------------------
/examples/next-pages-router/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css';
2 | import type { AppProps } from 'next/app';
3 |
4 | export default function App({ Component, pageProps }: AppProps) {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/examples/next-pages-router/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document';
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/examples/next-pages-router/src/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next';
3 |
4 | type Data = {
5 | name: string;
6 | };
7 |
8 | export default function handler(req: NextApiRequest, res: NextApiResponse) {
9 | res.status(200).json({ name: 'John Doe' });
10 | }
11 |
--------------------------------------------------------------------------------
/examples/next-pages-router/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | body {
12 | color: rgb(var(--foreground-rgb));
13 | }
14 |
15 | @layer utilities {
16 | .text-balance {
17 | text-wrap: balance;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/next-pages-router/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | plugins: [],
10 | };
11 | export default config;
12 |
--------------------------------------------------------------------------------
/examples/next-pages-router/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "paths": {
20 | "@/*": [
21 | "./src/*"
22 | ]
23 | },
24 | "target": "ES2017"
25 | },
26 | "include": [
27 | "next-env.d.ts",
28 | "**/*.ts",
29 | "**/*.tsx"
30 | ],
31 | "exclude": [
32 | "node_modules"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json",
3 | "npmClient": "npm",
4 | "version": "1.40.2-beta.0",
5 | "command": {
6 | "version": {
7 | "allowBranch": ["main", "next", "development"],
8 | "conventionalCommits": true,
9 | "message": "chore(release): updated release notes and package versions [ci skip]",
10 | "ignoreChanges": [
11 | "*.md",
12 | "*.mdx",
13 | "**/*.spec.*",
14 | "**/*.stories.*",
15 | "**/__fixtures__/**",
16 | "**/__tests__/**",
17 | "package-lock.lock",
18 | "lerna.json",
19 | ".eslintrc.js",
20 | ".eslintignore",
21 | ".prettierrc",
22 | "commitlist.config.js"
23 | ]
24 | }
25 | },
26 | "changelogPreset": {
27 | "name": "conventionalcommits"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasksRunnerOptions": {
3 | "default": {
4 | "runner": "nx/tasks-runners/default",
5 | "options": {
6 | "cacheableOperations": ["build", "lint", "tsc", "test", "test:ci", "coverage"]
7 | }
8 | }
9 | },
10 | "targetDefaults": {
11 | "build": {
12 | "outputs": ["{projectRoot}/dist"],
13 | "dependsOn": ["^build"]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/components/.dependency-cruiser.cjs:
--------------------------------------------------------------------------------
1 | // We need to have a local cruiser config to make the TS-specific resolve logic work
2 | module.exports = require('../../tools/.dependency-cruiser.cjs');
3 |
--------------------------------------------------------------------------------
/packages/components/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from '@storybook/react-vite';
2 |
3 | const config: StorybookConfig = {
4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
5 | addons: [
6 | '@storybook/addon-links',
7 | '@storybook/addon-essentials',
8 | '@storybook/addon-onboarding',
9 | '@storybook/addon-interactions',
10 | '@contentful/experiences-storybook-addon',
11 | ],
12 | framework: {
13 | name: '@storybook/react-vite',
14 | options: {},
15 | },
16 | docs: {
17 | autodocs: 'tag',
18 | },
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/packages/components/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import type { Preview } from '@storybook/react';
2 |
3 | const preview: Preview = {
4 | parameters: {
5 | actions: { argTypesRegex: '^on[A-Z].*' },
6 | controls: {
7 | matchers: {
8 | color: /(background|color)$/i,
9 | date: /Date$/,
10 | },
11 | },
12 | },
13 | };
14 |
15 | export default preview;
16 |
--------------------------------------------------------------------------------
/packages/components/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | component: {
5 | devServer: {
6 | framework: 'react',
7 | bundler: 'vite',
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/packages/components/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/packages/components/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components App
8 |
9 |
10 |
11 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/packages/components/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import typescript from '@rollup/plugin-typescript';
4 | import dts from 'rollup-plugin-dts';
5 | import postcss from 'rollup-plugin-postcss';
6 | import postcssImport from 'postcss-import';
7 |
8 | export default [
9 | {
10 | input: 'src/index.ts',
11 | output: [
12 | {
13 | file: './dist/index.js',
14 | format: 'esm',
15 | sourcemap: true,
16 | },
17 | ],
18 | plugins: [
19 | postcss({
20 | plugins: [postcssImport()],
21 | inject(cssVariableName) {
22 | return `import styleInject from 'style-inject';\nstyleInject(${cssVariableName});`;
23 | },
24 | }),
25 | resolve(),
26 | commonjs(),
27 | typescript({ tsconfig: './tsconfig.json', noEmitOnError: process.env.DEV ? false : true }),
28 | ],
29 | external: [/node_modules\/(?!tslib.*)/],
30 | },
31 | {
32 | input: 'src/index.ts',
33 | output: [{ file: 'dist/index.d.ts', format: 'es' }],
34 | plugins: [dts({ compilerOptions: { noEmitOnError: process.env.DEV ? false : true } })],
35 | external: [/.css/],
36 | },
37 | ];
38 |
--------------------------------------------------------------------------------
/packages/components/src/components/Assembly/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Assembly';
2 |
--------------------------------------------------------------------------------
/packages/components/src/components/Button/Button.css:
--------------------------------------------------------------------------------
1 | /* If the label is empty, still render the normal height by adding this pseudo element */
2 | .cf-button:empty::before {
3 | content: '';
4 | display: inline-block;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/components/src/components/Button/Button.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Button, ButtonComponentDefinition } from '.';
4 |
5 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
6 | const meta = {
7 | title: 'Example/Button',
8 | component: Button,
9 | parameters: {
10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
11 | layout: 'centered',
12 | experienceBuilder: ButtonComponentDefinition,
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
24 | export const Default: Story = {
25 | args: {
26 | children: 'Click me',
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/packages/components/src/components/Carousel/Carousel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { combineClasses } from '@/utils/combineClasses';
3 |
4 | interface CarouselProps {
5 | className?: string;
6 | children?: React.ReactNode;
7 | }
8 |
9 | export const Carousel: React.FC = ({ className, children, ...props }) => {
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/packages/components/src/components/Carousel/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CONTENTFUL_COMPONENTS,
3 | CONTENTFUL_DEFAULT_CATEGORY,
4 | } from '@contentful/experiences-core/constants';
5 | import { ComponentDefinition } from '@contentful/experiences-core/types';
6 |
7 | export { Carousel } from './Carousel';
8 |
9 | export const carouselDefinition: ComponentDefinition = {
10 | id: CONTENTFUL_COMPONENTS.carousel.id,
11 | name: CONTENTFUL_COMPONENTS.carousel.name,
12 | category: CONTENTFUL_DEFAULT_CATEGORY,
13 | children: true,
14 | builtInStyles: [
15 | 'cfPadding',
16 | 'cfMargin',
17 | 'cfHeight',
18 | 'cfWidth',
19 | 'cfMaxWidth',
20 | 'cfBorderRadius',
21 | 'cfBackgroundColor',
22 | 'cfBorder',
23 | ],
24 | variables: {},
25 | tooltip: {
26 | description: 'Drop onto the canvas to add a carousel.',
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/packages/components/src/components/Columns/Columns.css:
--------------------------------------------------------------------------------
1 | .cf-columns {
2 | display: flex;
3 | gap: 24px;
4 | grid-template-columns: repeat(12, 1fr);
5 | flex-direction: column;
6 | min-height: 0; /* NEW */
7 | min-width: 0; /* NEW; needed for Firefox */
8 | }
9 |
10 | @media (min-width: 768px) {
11 | .cf-columns {
12 | display: grid;
13 | }
14 | }
15 |
16 | .cf-single-column-wrapper {
17 | position: relative;
18 | display: flex;
19 | }
20 |
21 | .cf-single-column {
22 | pointer-events: all;
23 | }
24 |
25 | .cf-single-column-wrapper:after {
26 | content: '';
27 | display: block;
28 | position: absolute;
29 | pointer-events: none;
30 | top: 0;
31 | left: 0;
32 | right: 0;
33 | bottom: 0;
34 | display: flex;
35 | justify-content: center;
36 | align-items: center;
37 | overflow-x: clip;
38 | font-family: var(--exp-builder-font-stack-primary);
39 | font-size: 12px;
40 | color: var(--exp-builder-gray400);
41 | z-index: 1;
42 | }
43 |
44 | .cf-single-column-label:after {
45 | content: 'Column';
46 | }
47 |
--------------------------------------------------------------------------------
/packages/components/src/components/Columns/SingleColumn.tsx:
--------------------------------------------------------------------------------
1 | import { combineClasses } from '@/utils/combineClasses';
2 | import React from 'react';
3 | import { SingleColumnProps } from './ColumnTypes';
4 | import { Flex } from '@components/Layout/Flex';
5 |
6 | export const SingleColumn: React.FC = (props) => {
7 | const { className, editorMode, children } = props;
8 |
9 | if (editorMode === false) {
10 | return {children};
11 | }
12 |
13 | const {
14 | renderDropzone,
15 | node,
16 | className: _className,
17 | dragProps = {},
18 | cfColumnSpan,
19 | editorMode: edit,
20 | ...editorProps
21 | } = props;
22 |
23 | const isEmpty = !node.children.length;
24 |
25 | return renderDropzone(node, {
26 | ['data-test-id']: 'contentful-single-column',
27 | id: 'ContentfulSingleColumn',
28 | className: combineClasses(
29 | 'cf-single-column-wrapper',
30 | className,
31 | isEmpty ? 'cf-single-column-label' : '',
32 | ),
33 | WrapperComponent: Flex,
34 | dragProps,
35 | ...editorProps,
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/packages/components/src/components/Divider/ContentfulDivider.css:
--------------------------------------------------------------------------------
1 | .cf-divider {
2 | display: contents;
3 | position: relative;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | .cf-divider hr {
9 | border: none;
10 | }
11 |
12 | /* For the editor mode add this 10px tolerance to make it easier picking up the divider component.
13 | * Using the DND zone as precondition makes sure that we don't render this pseudo element in delivery. */
14 | [data-ctfl-zone-id='root'] .cf-divider::before {
15 | content: "";
16 | position: absolute;
17 | top: -5px;
18 | left: -5px;
19 | bottom: -5px;
20 | right: -5px;
21 | pointer-events: all;
22 | }
23 |
--------------------------------------------------------------------------------
/packages/components/src/components/Divider/ContentfulDivider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './ContentfulDivider.css';
3 |
4 | export type ContentfulDividerProps = {
5 | className?: string;
6 | dragProps?: unknown;
7 | };
8 |
9 | export const ContentfulDivider = ({
10 | className = '',
11 | // We have to exclude this explicitly from rendering as withComponentWrapper is not used
12 | dragProps: _,
13 | ...props
14 | }: ContentfulDividerProps) => {
15 | return (
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/packages/components/src/components/Divider/index.ts:
--------------------------------------------------------------------------------
1 | import { dividerBuiltInStyles } from '@contentful/experiences-core';
2 | import {
3 | CONTENTFUL_COMPONENTS,
4 | CONTENTFUL_DEFAULT_CATEGORY,
5 | } from '@contentful/experiences-core/constants';
6 | import { ComponentDefinition } from '@contentful/experiences-core/types';
7 |
8 | export { ContentfulDivider } from './ContentfulDivider';
9 |
10 | export const dividerDefinition: ComponentDefinition = {
11 | id: CONTENTFUL_COMPONENTS.divider.id,
12 | name: CONTENTFUL_COMPONENTS.divider.name,
13 | category: CONTENTFUL_DEFAULT_CATEGORY,
14 | children: false,
15 | variables: dividerBuiltInStyles,
16 | tooltip: {
17 | description: 'Drop onto the canvas to add a divider.',
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/packages/components/src/components/Heading/Heading.css:
--------------------------------------------------------------------------------
1 | .cf-heading {
2 | white-space: pre-line;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/components/src/components/Heading/Heading.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Heading, HeadingComponentDefinition } from '.';
4 |
5 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
6 | const meta = {
7 | title: 'Example/Heading',
8 | component: Heading,
9 | parameters: {
10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
11 | layout: 'centered',
12 | experienceBuilder: HeadingComponentDefinition,
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
24 | export const Default: Story = {
25 | args: {
26 | text: 'My heading',
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/packages/components/src/components/Image/Image.css:
--------------------------------------------------------------------------------
1 | @import url('../variables.css');
2 |
3 | div.cf-placeholder-wrapper {
4 | /* Required for the absolute positioned icon to render in the center */
5 | position: relative;
6 | }
7 |
8 | img.cf-placeholder-image {
9 | background-color: var(--cf-color-gray100);
10 | outline-offset: -2px;
11 | outline: 2px solid rgba(var(--cf-color-gray400-rgb), 0.5);
12 | width: 100%;
13 | height: 100%;
14 | }
15 |
16 | svg.cf-placeholder-icon {
17 | position: absolute;
18 | top: 50%;
19 | left: 50%;
20 | transform: translate(-50%, -50%);
21 | height: var(--cf-text-3xl);
22 | width: var(--cf-text-3xl);
23 | max-height: 100%;
24 | max-width: 100%;
25 | }
26 |
27 | svg.cf-placeholder-icon path {
28 | fill: var(--cf-color-gray400);
29 | }
30 |
--------------------------------------------------------------------------------
/packages/components/src/components/Image/README.md:
--------------------------------------------------------------------------------
1 | ## Image
2 |
3 | Renders an image tag.
4 |
5 | Extends `React.ImgHTMLAttributes` and can be used as a normal image.
6 |
7 | ## CSS Class
8 |
9 | Has a default class of `cf-image`, which can be used to style the component.
10 |
11 | ## Usage
12 |
13 | ```tsx
14 | import { Image } from '@contentful/experiences-components-react';
15 |
16 | const MyPage = () => ;
17 | ```
18 |
--------------------------------------------------------------------------------
/packages/components/src/components/Image/index.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentDefinition } from '@contentful/experiences-core/types';
2 | import {
3 | CONTENTFUL_COMPONENTS,
4 | CONTENTFUL_DEFAULT_CATEGORY,
5 | } from '@contentful/experiences-core/constants';
6 |
7 | export * from './Image';
8 |
9 | export const ImageComponentDefinition: ComponentDefinition = {
10 | id: CONTENTFUL_COMPONENTS.image.id,
11 | name: CONTENTFUL_COMPONENTS.image.name,
12 | category: CONTENTFUL_DEFAULT_CATEGORY,
13 | builtInStyles: ['cfMargin', 'cfPadding', 'cfImageAsset', 'cfImageOptions', 'cfBorderRadius'],
14 | tooltip: {
15 | description: 'Drop onto the canvas to upload an image.',
16 | },
17 | variables: {
18 | alt: {
19 | displayName: 'Alt text',
20 | type: 'Text',
21 | description: 'Alternative text for the image',
22 | validations: {
23 | bindingSourceType: ['entry', 'manual', 'asset'],
24 | },
25 | },
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/packages/components/src/components/RichText/RichText.css:
--------------------------------------------------------------------------------
1 | .cf-richtext {
2 | white-space: pre-line;
3 | }
4 |
5 | .cf-richtext > *:first-child {
6 | margin-top: 0;
7 | }
8 |
9 | .cf-richtext > *:last-child {
10 | margin-bottom: 0;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/components/src/components/Text/Text.css:
--------------------------------------------------------------------------------
1 | .cf-text {
2 | white-space: pre-line;
3 | }
4 |
5 | .cf-text-link .cf-text {
6 | margin: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/components/src/components/Text/Text.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Text, TextComponentDefinition } from '.';
4 |
5 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
6 | const meta = {
7 | title: 'Example/Text',
8 | component: Text,
9 | parameters: {
10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
11 | layout: 'centered',
12 | experienceBuilder: TextComponentDefinition,
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
24 | export const Default: Story = {
25 | args: {
26 | value: 'Lorem ipsum',
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/packages/components/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Button';
2 | export * from './Heading';
3 | export * from './RichText';
4 | export * from './Text';
5 | export * from './Image';
6 | export * from './ContentfulContainer';
7 | export * from './Divider';
8 | export * from './Columns';
9 | export * from './Assembly';
10 | export * from './Carousel';
11 |
--------------------------------------------------------------------------------
/packages/components/src/global.css:
--------------------------------------------------------------------------------
1 | /* Initially added with PR #253 for each component, this is now a global setting
2 | * It is recommended on MDN to use this as a default for layouting.
3 | */
4 | * {
5 | box-sizing: border-box;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/components/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg?react';
2 |
--------------------------------------------------------------------------------
/packages/components/src/index.ts:
--------------------------------------------------------------------------------
1 | import './global.css';
2 |
3 | export * from './components';
4 |
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/accessibility.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/accessibility.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/addon-library.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/addon-library.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/assets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/assets.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/context.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/context.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/docs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/docs.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/figma-plugin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/figma-plugin.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/share.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/styling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/styling.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/testing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/testing.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/theming.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/contentful/experience-builder/179d28e4e3ed662c36f20cd45c449ac2f8598312/packages/components/src/stories/assets/theming.png
--------------------------------------------------------------------------------
/packages/components/src/stories/assets/youtube.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/packages/components/src/utils/combineClasses.ts:
--------------------------------------------------------------------------------
1 | export function combineClasses(...classes: (string | undefined | null)[]) {
2 | return classes
3 | .filter((val) => val)
4 | .map((c) => c!.trim())
5 | .join(' ');
6 | }
7 |
--------------------------------------------------------------------------------
/packages/components/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const placeholderImage =
2 | 'https://images.ctfassets.net/tofsejyzyo24/5owPX1vp6cXDZr7QOabwzT/d5580f5b4dbad3f74c87ce2f03efa581/Image_container.png';
3 |
--------------------------------------------------------------------------------
/packages/components/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "esModuleInterop": true,
5 | "strict": true,
6 | "skipLibCheck": true,
7 | "jsx": "react",
8 | "module": "ESNext",
9 | "sourceMap": true,
10 | "outDir": "dist",
11 | "moduleResolution": "node",
12 | "allowSyntheticDefaultImports": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "noEmitOnError": true,
15 | "baseUrl": ".",
16 | "paths": {
17 | "@components/*": ["./src/components/*"],
18 | "@/*": ["./src/*"]
19 | }
20 | },
21 | "include": ["src/**/*", "cypress"],
22 | "exclude": ["dist", "node_modules", "src/**/*.test.tsx", "src/**/*.stories.tsx"]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/components/vite.config.ts:
--------------------------------------------------------------------------------
1 | // Vite is included so components can be loaded in Cypress Component Testing.
2 |
3 | import { defineConfig } from 'vite';
4 | import react from '@vitejs/plugin-react';
5 | import path from 'path';
6 | import svgr from 'vite-plugin-svgr';
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | resolve: {
11 | alias: {
12 | '@': path.resolve(__dirname, './src'),
13 | '@components': path.resolve(__dirname, './src/components'),
14 | },
15 | },
16 | plugins: [svgr(), react()],
17 | });
18 |
--------------------------------------------------------------------------------
/packages/core/.dependency-cruiser.cjs:
--------------------------------------------------------------------------------
1 | // We need to have a local cruiser config to make the TS-specific resolve logic work
2 | module.exports = require('../../tools/.dependency-cruiser.cjs');
3 |
--------------------------------------------------------------------------------
/packages/core/src/__fixtures__/breakpoints.ts:
--------------------------------------------------------------------------------
1 | import { Breakpoint } from '@/types';
2 |
3 | export const createBreakpoints = (): Breakpoint[] => [
4 | {
5 | id: 'desktop',
6 | query: '*',
7 | displayName: 'All sizes',
8 | previewSize: '993px',
9 | },
10 | {
11 | id: 'tablet',
12 | query: '<992px',
13 | displayName: 'Tablet',
14 | previewSize: '820px',
15 | },
16 | {
17 | id: 'mobile',
18 | query: '<576px',
19 | displayName: 'Mobile',
20 | previewSize: '390px',
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/packages/core/src/communication/index.ts:
--------------------------------------------------------------------------------
1 | export * from './sendMessage';
2 |
--------------------------------------------------------------------------------
/packages/core/src/communication/sendMessage.ts:
--------------------------------------------------------------------------------
1 | import { SendMessageParams, OutgoingMessage } from '@/types';
2 |
3 | export const sendMessage: SendMessageParams = (eventType, data) => {
4 | if (typeof window === 'undefined') {
5 | return;
6 | }
7 |
8 | console.debug(`[experiences-sdk-react::sendMessage] Sending message [${eventType}]`, {
9 | source: 'customer-app',
10 | eventType,
11 | payload: data,
12 | });
13 |
14 | window.parent?.postMessage(
15 | {
16 | source: 'customer-app',
17 | eventType,
18 | payload: data,
19 | } as OutgoingMessage,
20 | '*',
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/packages/core/src/deep-binding/index.ts:
--------------------------------------------------------------------------------
1 | export * from './DeepReference';
2 |
--------------------------------------------------------------------------------
/packages/core/src/definitions/index.ts:
--------------------------------------------------------------------------------
1 | export * from './styles';
2 |
--------------------------------------------------------------------------------
/packages/core/src/entity/index.ts:
--------------------------------------------------------------------------------
1 | export * from './EditorModeEntityStore';
2 | export * from './EntityStore';
3 | export * from './EntityStoreBase';
4 |
--------------------------------------------------------------------------------
/packages/core/src/entity/value-transformers/TransformAssetFile.ts:
--------------------------------------------------------------------------------
1 | import { AssetFile } from 'contentful';
2 |
3 | export function transformAssetFileToUrl(
4 | fieldValue: string | AssetFile | undefined,
5 | ): string | undefined {
6 | return fieldValue && typeof fieldValue == 'object' && (fieldValue as AssetFile).url
7 | ? (fieldValue as AssetFile).url
8 | : (fieldValue as string | undefined);
9 | }
10 |
--------------------------------------------------------------------------------
/packages/core/src/entity/value-transformers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './TransformAssetFile';
2 |
--------------------------------------------------------------------------------
/packages/core/src/enums.ts:
--------------------------------------------------------------------------------
1 | export enum VisualEditorMode {
2 | LazyLoad = 'lazyLoad',
3 | InjectScript = 'injectScript',
4 | }
5 |
--------------------------------------------------------------------------------
/packages/core/src/exports.ts:
--------------------------------------------------------------------------------
1 | //this file maintains the exports (ie: '@contentful/experiences-core/constants') used in the package.json file
2 | export * from './constants';
3 | export * from './types';
4 |
--------------------------------------------------------------------------------
/packages/core/src/fetchers/fetchers.ts:
--------------------------------------------------------------------------------
1 | import type { ContentfulClientApi } from 'contentful';
2 | import { fetchExperienceEntry } from './fetchExperienceEntry';
3 | import { fetchReferencedEntities } from './fetchReferencedEntities';
4 |
5 | type fetchExperienceArgs = {
6 | client: ContentfulClientApi;
7 | experienceTypeId: string;
8 | locale: string;
9 | identifier: { slug: string } | { id: string };
10 | };
11 |
12 | export const fetchExperience = async ({
13 | client,
14 | experienceTypeId,
15 | locale,
16 | identifier,
17 | }: fetchExperienceArgs) => {
18 | const entry = await fetchExperienceEntry({ client, experienceTypeId, locale, identifier });
19 |
20 | if (!entry) {
21 | return { experienceEntry: undefined, referencedAssets: [], referencedEntries: [] };
22 | }
23 |
24 | const { assets, entries } = await fetchReferencedEntities({
25 | client,
26 | experienceEntry: entry,
27 | locale,
28 | });
29 |
30 | return {
31 | experienceEntry: entry,
32 | referencedAssets: assets,
33 | referencedEntries: entries,
34 | };
35 | };
36 |
--------------------------------------------------------------------------------
/packages/core/src/fetchers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './fetchBySlug';
2 | export * from './fetchById';
3 | export * from './fetchAllEntities';
4 | export * from './createExperience';
5 | // Exposing the underlying fetcher methods to allow caching of API responses
6 | export * from './fetchReferencedEntities';
7 | export * from './fetchExperienceEntry';
8 |
--------------------------------------------------------------------------------
/packages/core/src/fetchers/resolveDeepUsedComponents.ts:
--------------------------------------------------------------------------------
1 | import { ExperienceEntry, ExperienceFields } from '@/types';
2 | export const resolveDeepUsedComponents = ({
3 | experienceEntryFields,
4 | parentComponents,
5 | }: {
6 | experienceEntryFields?: ExperienceFields;
7 | parentComponents: Set;
8 | }): ExperienceEntry[] => {
9 | const totalUsedComponents: ExperienceEntry[] = [];
10 | const usedComponents = experienceEntryFields?.usedComponents as ExperienceEntry[];
11 | if (!usedComponents || usedComponents.length === 0) {
12 | return [];
13 | }
14 | for (const component of usedComponents) {
15 | if ('fields' in component) {
16 | totalUsedComponents.push(component);
17 | if (parentComponents.has(component.sys.id)) {
18 | continue;
19 | }
20 |
21 | totalUsedComponents.push(
22 | ...resolveDeepUsedComponents({
23 | experienceEntryFields: component.fields,
24 | parentComponents: new Set([...parentComponents, component.sys.id]),
25 | }),
26 | );
27 | }
28 | }
29 | return totalUsedComponents;
30 | };
31 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './utils';
2 | export * from './definitions';
3 | export * from './entity';
4 | export * from './communication';
5 | export * from './enums';
6 | export * from './fetchers';
7 | export * from './registries';
8 | export * from './deep-binding';
9 |
--------------------------------------------------------------------------------
/packages/core/src/registries/breakpointsRegistry.ts:
--------------------------------------------------------------------------------
1 | import { Breakpoint, validateBreakpointsDefinition } from '@contentful/experiences-validators';
2 |
3 | export let breakpointsRegistry: Breakpoint[] = [];
4 |
5 | /**
6 | * Register custom breakpoints
7 | * @param breakpoints - [{[key:string]: string}]
8 | * @returns void
9 | */
10 | export const defineBreakpoints = (breakpoints: Breakpoint[]) => {
11 | Object.assign(breakpointsRegistry, breakpoints);
12 | };
13 |
14 | export const runBreakpointsValidation = () => {
15 | if (!breakpointsRegistry.length) return;
16 |
17 | const validation = validateBreakpointsDefinition(breakpointsRegistry);
18 | if (!validation.success) {
19 | throw new Error(
20 | `Invalid breakpoints definition. Failed with errors: \n${JSON.stringify(validation.errors, null, 2)}`,
21 | );
22 | }
23 | };
24 |
25 | // Used in the tests to get a breakpoint registration
26 | export const getBreakpointRegistration = (id: string) =>
27 | breakpointsRegistry.find((breakpoint) => breakpoint.id === id);
28 |
29 | // Used in the tests to reset the registry
30 | export const resetBreakpointsRegistry = () => {
31 | breakpointsRegistry = [];
32 | };
33 |
--------------------------------------------------------------------------------
/packages/core/src/registries/index.ts:
--------------------------------------------------------------------------------
1 | export * from './designTokenRegistry';
2 | export * from './breakpointsRegistry';
3 |
--------------------------------------------------------------------------------
/packages/core/src/utils/get.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
2 | export function get(obj: Record, path: string[]): T | undefined {
3 | if (!path.length) {
4 | return obj as T;
5 | }
6 |
7 | try {
8 | const [currentPath, ...nextPath] = path;
9 | return get(obj[currentPath], nextPath);
10 | } catch (err) {
11 | return undefined;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/core/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components';
2 | export * from './breakpoints';
3 | export * from './debugLogger';
4 | export * from './domValues';
5 | export * from './isLinkToAsset';
6 | export * from './isLink';
7 | export * from './localizeEntity';
8 | export * from './pathSchema';
9 | export * from './resolveHyperlinkPattern';
10 | export * from './sanitizeNodeProps';
11 | export * from './styleUtils';
12 | export * from './transformers';
13 | export * from './treeTraversal';
14 | export * from './typeguards';
15 | export * from './utils';
16 | export * from './validations';
17 |
--------------------------------------------------------------------------------
/packages/core/src/utils/isLink.ts:
--------------------------------------------------------------------------------
1 | import { Asset, Entry, UnresolvedLink } from 'contentful';
2 |
3 | export const isLink = (
4 | maybeLink: UnresolvedLink<'Entry' | 'Asset'> | Entry | Asset | string | unknown,
5 | ): maybeLink is UnresolvedLink<'Entry' | 'Asset'> => {
6 | if (maybeLink === null) return false;
7 | if (typeof maybeLink !== 'object') return false;
8 |
9 | const link = maybeLink as {
10 | sys?: {
11 | id?: string;
12 | type?: string;
13 | };
14 | };
15 |
16 | return Boolean(link.sys?.id) && link.sys?.type === 'Link';
17 | };
18 |
--------------------------------------------------------------------------------
/packages/core/src/utils/isLinkToAsset.ts:
--------------------------------------------------------------------------------
1 | import type { Link } from '@/types';
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
4 | export const isLinkToAsset = (variable: any): variable is Link<'Asset'> => {
5 | if (!variable) return false;
6 | if (typeof variable !== 'object') return false;
7 |
8 | return (
9 | variable.sys?.linkType === 'Asset' &&
10 | typeof variable.sys?.id === 'string' &&
11 | !!variable.sys?.id &&
12 | variable.sys?.type === 'Link'
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/packages/core/src/utils/sanitizeNodeProps.ts:
--------------------------------------------------------------------------------
1 | import { omit } from 'lodash-es';
2 | import { CF_STYLE_ATTRIBUTES } from '../constants';
3 | import type { PrimitiveValue } from '../types';
4 |
5 | const stylesToKeep = ['cfImageAsset'];
6 | const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
7 | const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
8 |
9 | export const sanitizeNodeProps = (nodeProps: Record) => {
10 | return omit(nodeProps, stylesToRemove, propsToRemove);
11 | };
12 |
--------------------------------------------------------------------------------
/packages/core/src/utils/styleUtils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './stylesUtils';
2 | export * from './ssrStyles';
3 | export { transformVisibility } from './styleTransformers';
4 |
--------------------------------------------------------------------------------
/packages/core/src/utils/transformers/getBoundValue.ts:
--------------------------------------------------------------------------------
1 | import { PrimitiveValue } from '@/types';
2 | import { Asset, AssetFile, Entry } from 'contentful';
3 | import { get } from '../get';
4 |
5 | export const getBoundValue = (entryOrAsset: Entry | Asset, path: string): PrimitiveValue => {
6 | const value = get(entryOrAsset, path.split('/').slice(2, -1));
7 | return value && typeof value == 'object' && (value as AssetFile).url
8 | ? (value as AssetFile).url
9 | : (value as string | undefined);
10 | };
11 |
--------------------------------------------------------------------------------
/packages/core/src/utils/transformers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './transformBoundContentValue';
2 |
--------------------------------------------------------------------------------
/packages/core/src/utils/transformers/media/getOptimizedImageUrl.ts:
--------------------------------------------------------------------------------
1 | export function getOptimizedImageUrl(
2 | url: string,
3 | width?: number,
4 | quality?: number,
5 | format?: string,
6 | ) {
7 | if (url.startsWith('//')) {
8 | url = 'https:' + url;
9 | }
10 |
11 | const params = new URLSearchParams();
12 | if (width) {
13 | params.append('w', width.toString());
14 | }
15 | if (quality && quality > 0 && quality < 100) {
16 | params.append('q', quality.toString());
17 | }
18 | if (format) {
19 | params.append('fm', format);
20 | }
21 |
22 | const queryString = params.toString();
23 | return `${url}${queryString ? '?' + queryString : ''}`;
24 | }
25 |
--------------------------------------------------------------------------------
/packages/core/src/utils/transformers/media/mediaUtils.ts:
--------------------------------------------------------------------------------
1 | import { SUPPORTED_IMAGE_FORMATS } from '@/constants';
2 | import { AssetFile } from 'contentful';
3 |
4 | export type ValidFormats = (typeof SUPPORTED_IMAGE_FORMATS)[number];
5 |
6 | export interface AssetFileWithRequiredImage extends AssetFile {
7 | details: Required;
8 | }
9 |
10 | export function validateParams(
11 | file: AssetFile,
12 | quality: number,
13 | format?: ValidFormats,
14 | ): file is AssetFileWithRequiredImage {
15 | if (!file.details.image) {
16 | throw Error('No image in file asset to transform');
17 | }
18 | if (quality < 0 || quality > 100) {
19 | throw Error('Quality must be between 0 and 100');
20 | }
21 | if (format && !SUPPORTED_IMAGE_FORMATS.includes(format)) {
22 | throw Error(`Format must be one of ${SUPPORTED_IMAGE_FORMATS.join(', ')}`);
23 | }
24 | return true;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/core/src/utils/typeguards.ts:
--------------------------------------------------------------------------------
1 | import type { ExperienceEntry } from '@/types';
2 | import { Entry } from 'contentful';
3 |
4 | export const isExperienceEntry = (entry: ExperienceEntry | Entry): entry is ExperienceEntry => {
5 | return (
6 | entry?.sys?.type === 'Entry' &&
7 | !!entry.fields?.title &&
8 | !!entry.fields?.slug &&
9 | !!entry.fields?.componentTree &&
10 | Array.isArray((entry as ExperienceEntry).fields.componentTree.breakpoints) &&
11 | Array.isArray((entry as ExperienceEntry).fields.componentTree.children) &&
12 | typeof (entry as ExperienceEntry).fields.componentTree.schemaVersion === 'string'
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "esModuleInterop": true,
5 | "strict": true,
6 | "skipLibCheck": true,
7 | "jsx": "react",
8 | "module": "ESNext",
9 | "outDir": "dist",
10 | "moduleResolution": "node",
11 | "allowSyntheticDefaultImports": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "baseUrl": ".",
14 | "noImplicitAny": false,
15 | "noEmitOnError": true,
16 | "paths": {
17 | "@/*": ["./src/*"]
18 | }
19 | },
20 | "include": ["src/**/*", "cypress"],
21 | "exclude": [
22 | "dist",
23 | "node_modules",
24 | "src/**/*.test.tsx",
25 | "src/**/*.spec.tsx",
26 | "src/**/*.stories.tsx"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/packages/core/vitest.config.ts:
--------------------------------------------------------------------------------
1 | // vitest.config.ts
2 | import { defineConfig } from 'vitest/config';
3 | import tsconfigPaths from 'vite-tsconfig-paths';
4 |
5 | export default defineConfig({
6 | //@ts-expect-error ignore
7 | plugins: [
8 | tsconfigPaths({
9 | // Don't parse other tsconfig files outside of this package
10 | ignoreConfigErrors: true,
11 | }),
12 | ],
13 | test: {
14 | globals: true,
15 | coverage: {
16 | reporter: ['text', 'json', 'html'],
17 | reportsDirectory: 'reports',
18 | },
19 | environment: 'happy-dom',
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/packages/create-studio-experiences/.dependency-cruiser.cjs:
--------------------------------------------------------------------------------
1 | // We need to have a local cruiser config to make the TS-specific resolve logic work
2 | module.exports = require('../../tools/.dependency-cruiser.cjs');
3 |
--------------------------------------------------------------------------------
/packages/create-studio-experiences/.gitignore:
--------------------------------------------------------------------------------
1 | # CLI generated project (default path)
2 | studio-experiences-react-app
3 |
4 | templates
5 |
--------------------------------------------------------------------------------
/packages/create-studio-experiences/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const CONSTANTS = {
2 | contentType: 'ExperiencesLayout',
3 | environment: 'master',
4 | locale: 'en-US',
5 | slug: 'home-page',
6 | title: 'Home Page',
7 | };
8 |
--------------------------------------------------------------------------------
/packages/create-studio-experiences/src/copyTemplates.ts:
--------------------------------------------------------------------------------
1 | import { cp } from 'fs/promises';
2 |
3 | const excludedPathsToCopy = ['node_modules', 'dist', '.next', 'package.json', '.env.local'];
4 |
5 | async function copyFolder(src: string, dest: string) {
6 | try {
7 | await cp(src, dest, {
8 | recursive: true,
9 | filter: (src) => {
10 | if (excludedPathsToCopy.some((exclude) => src.includes(exclude))) {
11 | return false;
12 | }
13 | return true;
14 | },
15 | });
16 | } catch (err) {
17 | console.error(`Error copying folder: ${err}`);
18 | }
19 | }
20 |
21 | await copyFolder('../templates', './templates');
22 |
--------------------------------------------------------------------------------
/packages/create-studio-experiences/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts a string to a valid Content Type ID format, i.e. only alphanumeric characters
3 | * @param {string} input - The input string to be converted.
4 | * @returns {string} The converted string.
5 | */
6 | export const generateContentTypeId = (input: string): string => {
7 | // Remove non-alphanumeric characters and split by them
8 | const words = input.split(/[\W_]+/);
9 |
10 | // Convert the first word to lowercase, and the rest to lowercase with the first letter capitalized
11 | return words
12 | .map((word: string, index: number) => {
13 | if (index === 0) {
14 | return word.toLowerCase();
15 | } else {
16 | return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
17 | }
18 | })
19 | .join('');
20 | };
21 |
22 | export const isValidPackageName = (projectName: string) => {
23 | return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(projectName);
24 | };
25 |
--------------------------------------------------------------------------------
/packages/create-studio-experiences/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "declaration": true,
5 | "strict": true,
6 | "skipLibCheck": true,
7 | "module": "Node16",
8 | "target": "ES2022",
9 | "sourceMap": true,
10 | "outDir": "dist",
11 | "forceConsistentCasingInFileNames": true,
12 | "baseUrl": ".",
13 | "noEmitOnError": true,
14 | "paths": {
15 | "@/*": ["./src/*"]
16 | }
17 | },
18 | "include": ["src/**/*"]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/.dependency-cruiser.cjs:
--------------------------------------------------------------------------------
1 | // We need to have a local cruiser config to make the TS-specific resolve logic work
2 | module.exports = require('../../tools/.dependency-cruiser.cjs');
3 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/.gitignore:
--------------------------------------------------------------------------------
1 | src/sdkVersion.ts
2 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
4 | out
5 | typings
6 | reports
7 | tmp
8 |
9 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/bin/injectSDKVersion.cjs:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef
2 | const fs = require('fs');
3 | // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef
4 | const path = require('path');
5 |
6 | // eslint-disable-next-line no-undef
7 | const sdkVersionFilePath = path.resolve(__dirname, '../src/sdkVersion.ts');
8 |
9 | /*
10 | This script creates a file `sdkVersion.ts` in the `src` directory with the current SDK version, and
11 | is usually ran before each build to ensure the file exists
12 | */
13 | // eslint-disable-next-line no-undef
14 | fs.writeFileSync(
15 | sdkVersionFilePath,
16 | `export const SDK_VERSION = '${process.env.npm_package_version}';\n`,
17 | { flag: 'w' },
18 | );
19 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/catalog-info.yml:
--------------------------------------------------------------------------------
1 | apiVersion: backstage.io/v1alpha1
2 | kind: Component
3 | metadata:
4 | name: experience-builder-sdk
5 | description: The official public SDK to integrate applications with the Experience Builder.
6 | annotations:
7 | circleci.com/project-slug: github/contentful/experience-builder
8 | github.com/project-slug: contentful/experience-builder
9 | contentful.com/ci-alert-slack: prd-experience-builder-alerts
10 | backstage.io/source-location: url:https://github.com/contentful/experience-builder/tree/development/packages/experience-builder-sdk
11 | spec:
12 | type: library
13 | lifecycle: production
14 | owner: group:team-sparks
15 | system: experience-builder
16 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/jest.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | resetMocks: true,
3 | roots: ['./src'],
4 | preset: 'ts-jest',
5 | testEnvironment: 'jsdom',
6 | coverageDirectory: './reports',
7 | setupFilesAfterEnv: ['./testing-library.js'],
8 | moduleNameMapper: {
9 | '\\.(css)$': '/test/styleMock.ts',
10 | '^.+\\.svg$': '/test/fileMock.ts',
11 | '^.+\\.svg\\?react$': '/test/fileMock.ts',
12 | 'lodash-es': 'lodash', // hack to make lodash-es work with jest
13 | },
14 | transform: {
15 | '\\.[jt]sx?$': ['ts-jest'],
16 | },
17 | transformIgnorePatterns: ['node_modules/(?!(@contentful/.*/.*|style-inject|lodash-es)/)'],
18 | };
19 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/src/constants.ts:
--------------------------------------------------------------------------------
1 | import type { SchemaVersions } from '@contentful/experiences-core/types';
2 |
3 | // this is the array of version which currently LATEST_SCHEMA_VERSION is compatible with
4 | export const compatibleVersions: SchemaVersions[] = ['2023-09-28'];
5 |
6 | export { SDK_VERSION } from './sdkVersion';
7 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/src/core/index.ts:
--------------------------------------------------------------------------------
1 | export { Flex } from '../components/Flex';
2 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/src/core/sdkFeatures.ts:
--------------------------------------------------------------------------------
1 | // Object to store the SDK features that are enabled/disabled
2 | export const sdkFeatures: Record = {
3 | hasSDKVersionUI: true,
4 | hasVersionHistory: true,
5 | cfVisibility: true,
6 | patternResolution: true,
7 | };
8 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { useBreakpoints } from './useBreakpoints';
2 | export * from './useFetchById';
3 | export * from './useFetchBySlug';
4 | export * from './useCustomFetch';
5 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/src/hooks/useInjectStylesheet.spec.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react';
2 | import { useInjectStylesheet } from './useInjectStylesheet';
3 |
4 | describe('useInjectStylesheet', () => {
5 | const css = 'body { background: red; }';
6 |
7 | it('should inject and remove styles', () => {
8 | const { unmount } = renderHook(() => useInjectStylesheet(css));
9 |
10 | const styleTag = document.head.querySelector('style');
11 | expect(styleTag).not.toBeNull();
12 | expect(styleTag?.innerHTML).toBe(css);
13 |
14 | unmount();
15 |
16 | expect(document.head.querySelector('style')).toBeNull();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/packages/experience-builder-sdk/src/hooks/useInjectStylesheet.ts:
--------------------------------------------------------------------------------
1 | import { useInsertionEffect } from 'react';
2 |
3 | /**
4 | * This hook injects the passed styles on the client side within a new