├── .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 | 2 | 3 | 9 | 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 = () => My Image; 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 | 2 | 3 | 4 | 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