├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── ci.yml │ ├── main-preview.yml │ ├── playwright.yml │ ├── pr-playwright-report.yml │ ├── pr-preview-build.yml │ ├── pr-preview-deploy.yml │ ├── release-4.x.x.yml │ ├── release-5.x.x.yml │ ├── release-6.x.x.yml │ ├── release-alpha.yml │ ├── release-beta.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .mocks ├── blogPage.json ├── contentBlocks.json ├── forms.json ├── layoutBlock.json ├── navigation.json ├── page.json ├── post.json ├── posts.json ├── services.json ├── suggestedPosts.json ├── tags.json └── utils.ts ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .storybook ├── addons │ ├── addon-yaml │ │ ├── AddonYaml.css │ │ ├── preset.ts │ │ └── register.tsx │ └── theme-addon │ │ └── register.tsx ├── decorators │ ├── DocsDecorator │ │ ├── DocsDecorator.scss │ │ └── DocsDecorator.tsx │ ├── withLang.tsx │ ├── withMobile.tsx │ └── withTheme.tsx ├── main.ts ├── manager-head.html ├── manager.ts ├── preview.tsx ├── stories │ └── Changelog.mdx └── theme.ts ├── .stylelintrc ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README-ru.md ├── README.md ├── commitlint.config.js ├── gulpfile.js ├── jest.config.js ├── package-lock.json ├── package.json ├── playwright ├── README.md ├── core │ ├── constants.ts │ ├── delays.ts │ ├── expectScreenshotFixture.ts │ ├── index.ts │ ├── mountFixture.tsx │ └── types.ts ├── playwright.config.ts └── playwright │ ├── Providers.tsx │ ├── index.html │ ├── index.scss │ └── index.tsx ├── scripts └── playwright-docker.sh ├── src ├── blocks │ ├── Author │ │ ├── Author.scss │ │ ├── Author.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Author.visual.test.tsx-snapshots │ │ │ │ ├── Author-render-stories-Default-light-chromium-linux.png │ │ │ │ └── Author-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── Author.mdx │ │ │ └── Author.stories.tsx │ │ ├── __tests__ │ │ │ ├── Author.test.tsx │ │ │ ├── Author.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── Banner │ │ ├── Banner.scss │ │ ├── Banner.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Banner.visual.test.tsx-snapshots │ │ │ │ ├── Banner-render-stories-Default-light-chromium-linux.png │ │ │ │ └── Banner-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── Banner.mdx │ │ │ └── Banner.stories.tsx │ │ ├── __tests__ │ │ │ ├── Banner.test.tsx │ │ │ ├── Banner.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── CTA │ │ ├── CTA.scss │ │ ├── CTA.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── CTA.visual.test.tsx-snapshots │ │ │ │ ├── CTA-render-stories-Default-light-chromium-linux.png │ │ │ │ └── CTA-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── CTA.mdx │ │ │ └── CTA.stories.tsx │ │ ├── __tests__ │ │ │ ├── CTA.test.tsx │ │ │ ├── CTA.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── ColoredText │ │ ├── ColoredText.scss │ │ ├── ColoredText.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── ColoredText.visual.test.tsx-snapshots │ │ │ │ ├── ColoredText-render-stories-Default-light-chromium-linux.png │ │ │ │ └── ColoredText-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── ColoredText.mdx │ │ │ └── ColoredText.stories.tsx │ │ ├── __tests__ │ │ │ ├── ColoredText.test.tsx │ │ │ ├── ColoredText.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── Feed │ │ ├── Feed.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Feed.visual.test.tsx-snapshots │ │ │ │ ├── Feed-render-stories-Default-light-chromium-linux.png │ │ │ │ └── Feed-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── Feed.mdx │ │ │ └── Feed.stories.tsx │ │ ├── __tests__ │ │ │ ├── Feed.visual.test.tsx │ │ │ └── helpers.tsx │ │ ├── reducer.ts │ │ └── schema.ts │ ├── Form │ │ ├── Form.scss │ │ ├── Form.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Form.visual.test.tsx-snapshots │ │ │ │ ├── Form-render-stories-Default-light-chromium-linux.png │ │ │ │ ├── Form-render-stories-Default-light-webkit-linux.png │ │ │ │ ├── Form-render-stories-FormData-light-chromium-linux.png │ │ │ │ └── Form-render-stories-FormData-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── Form.mdx │ │ │ └── Form.stories.tsx │ │ ├── __tests__ │ │ │ ├── Form.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── Header │ │ ├── Header.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Header.visual.test.tsx-snapshots │ │ │ │ ├── Header-render-stories-Default-light-chromium-linux.png │ │ │ │ └── Header-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── Header.mdx │ │ │ └── Header.stories.tsx │ │ ├── __tests__ │ │ │ ├── Header.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── Layout │ │ ├── Layout.scss │ │ ├── Layout.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Layout.visual.test.tsx-snapshots │ │ │ │ ├── Layout-render-stories-Default-light-chromium-linux.png │ │ │ │ └── Layout-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── Layout.mdx │ │ │ └── Layout.stories.tsx │ │ ├── __tests__ │ │ │ ├── Layout.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── Media │ │ ├── Media.scss │ │ ├── Media.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Media.visual.test.tsx-snapshots │ │ │ │ ├── Media-render-stories-Default-light-chromium-linux.png │ │ │ │ └── Media-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── Media.mdx │ │ │ └── Media.stories.tsx │ │ ├── __tests__ │ │ │ ├── Media.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── Meta │ │ ├── Meta.scss │ │ ├── Meta.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Meta.visual.test.tsx-snapshots │ │ │ │ ├── Meta-render-stories-Default-light-chromium-linux.png │ │ │ │ └── Meta-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── Meta.mdx │ │ │ └── Meta.stories.tsx │ │ ├── __tests__ │ │ │ ├── Meta.test.tsx │ │ │ ├── Meta.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── Suggest │ │ ├── README.md │ │ ├── Suggest.tsx │ │ ├── __snapshots__ │ │ │ └── Suggest.visual.test.tsx-snapshots │ │ │ │ ├── Suggest-render-stories-Default-light-chromium-linux.png │ │ │ │ └── Suggest-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── Suggest.mdx │ │ │ └── Suggest.stories.tsx │ │ ├── __tests__ │ │ │ ├── Suggest.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ ├── YFM │ │ ├── README.md │ │ ├── YFM.tsx │ │ ├── __snapshots__ │ │ │ └── YFM.visual.test.tsx-snapshots │ │ │ │ ├── YFM-render-stories-Default-light-chromium-linux.png │ │ │ │ └── YFM-render-stories-Default-light-webkit-linux.png │ │ ├── __stories__ │ │ │ ├── YFM.mdx │ │ │ └── YFM.stories.tsx │ │ ├── __tests__ │ │ │ ├── YFM.test.tsx │ │ │ ├── YFM.visual.test.tsx │ │ │ └── helpers.tsx │ │ └── schema.ts │ └── constants.ts ├── components │ ├── FeedHeader │ │ ├── FeedHeader.scss │ │ ├── FeedHeader.tsx │ │ └── components │ │ │ ├── Controls │ │ │ ├── Controls.scss │ │ │ ├── Controls.tsx │ │ │ └── customRenders.tsx │ │ │ ├── CustomSelectOption │ │ │ ├── CustomSelectOption.scss │ │ │ └── CustomSelectOption.tsx │ │ │ └── CustomSwitcher │ │ │ ├── CustomSwitcher.scss │ │ │ └── CustomSwitcher.tsx │ ├── MetaWrapper │ │ └── MetaWrapper.tsx │ ├── Paginator │ │ ├── Paginator.scss │ │ ├── Paginator.tsx │ │ ├── components │ │ │ ├── NavigationButton.tsx │ │ │ └── PaginatorItem.tsx │ │ ├── types.ts │ │ └── utils.ts │ ├── PostCard │ │ ├── PostCard.scss │ │ └── PostCard.tsx │ ├── PostInfo │ │ ├── PostInfo.scss │ │ ├── PostInfo.tsx │ │ ├── SuggestPostInfo.tsx │ │ └── components │ │ │ ├── Date.tsx │ │ │ ├── ReadingTime.tsx │ │ │ ├── Save.tsx │ │ │ └── Sharing.tsx │ ├── Posts │ │ ├── Posts.scss │ │ └── Posts.tsx │ ├── PostsEmpty │ │ ├── PostsEmpty.scss │ │ └── PostsEmpty.tsx │ ├── PostsError │ │ ├── PostError.scss │ │ └── PostsError.tsx │ ├── Prompt │ │ ├── Prompt.scss │ │ └── Prompt.tsx │ ├── PromptSignIn │ │ ├── PromptSignIn.tsx │ │ ├── README.md │ │ ├── __stories__ │ │ │ ├── PromptSignIn.mdx │ │ │ └── PromptSignIn.stories.tsx │ │ └── hooks │ │ │ └── usePromptSignInProps.ts │ ├── Search │ │ ├── Search.scss │ │ └── Search.tsx │ └── Wrapper │ │ ├── Wrapper.scss │ │ └── Wrapper.tsx ├── constants.ts ├── constructor ├── containers │ ├── BlogPage │ │ ├── BlogPage.scss │ │ ├── BlogPage.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── BlogPage.visual.test.tsx-snapshots │ │ │ │ ├── BlogPage-render-stories-Default-light-chromium-linux.png │ │ │ │ ├── BlogPage-render-stories-Default-light-webkit-linux.png │ │ │ │ ├── BlogPage-render-stories-Default-with-opened-select-light-chromium-linux.png │ │ │ │ ├── BlogPage-render-stories-Default-with-opened-select-light-webkit-linux.png │ │ │ │ ├── BlogPage-render-stories-WithNavigation-light-chromium-linux.png │ │ │ │ └── BlogPage-render-stories-WithNavigation-light-webkit-linux.png │ │ ├── __stories__ │ │ │ └── BlogPage.stories.tsx │ │ └── __tests__ │ │ │ ├── BlogPage.visual.test.tsx │ │ │ └── helpers.tsx │ └── BlogPostPage │ │ ├── BlogPostPage.scss │ │ ├── BlogPostPage.tsx │ │ ├── README.md │ │ ├── __snapshots__ │ │ └── BlogPostPage.visual.test.tsx-snapshots │ │ │ ├── BlogPostPage-render-stories-Default-light-chromium-linux.png │ │ │ ├── BlogPostPage-render-stories-Default-light-webkit-linux.png │ │ │ ├── BlogPostPage-render-stories-WithNavigation-light-chromium-linux.png │ │ │ └── BlogPostPage-render-stories-WithNavigation-light-webkit-linux.png │ │ ├── __stories__ │ │ └── BlogPostPage.stories.tsx │ │ └── __tests__ │ │ ├── BlogPostPage.visual.test.tsx │ │ └── helpers.tsx ├── contexts │ ├── DeviceContext.ts │ ├── FeedContext.ts │ ├── LikesContext.ts │ ├── LocaleContext.ts │ ├── MobileContext.ts │ ├── PostPageContext.ts │ ├── RouterContext.ts │ ├── SettingsContext.ts │ └── theme │ │ ├── ThemeContext.ts │ │ ├── ThemeProvider.tsx │ │ ├── ThemeValueContext.ts │ │ ├── index.ts │ │ ├── useTheme.ts │ │ ├── useThemeValue.ts │ │ ├── withTheme.tsx │ │ └── withThemeValue.tsx ├── counters │ ├── metrika.ts │ └── utils.ts ├── data │ ├── config.ts │ ├── contentFilter.ts │ ├── createReadableContent.ts │ ├── sanitizeMeta.ts │ ├── transformPageContent.ts │ └── transformPost.ts ├── demo │ ├── StoryTemplate.mdx │ └── mocks.ts ├── hooks │ ├── useAriaAttributes.ts │ ├── useExtendedComponentMap.ts │ ├── useHover.ts │ ├── useIsIPhone.ts │ ├── useLikes.ts │ └── useOpenCloseTimer.ts ├── i18n │ └── index.ts ├── icons │ ├── Close.tsx │ ├── DropdownArrow.tsx │ ├── Save.tsx │ ├── SaveFilled.tsx │ ├── SearchIcon.tsx │ ├── ShareArrowUp.tsx │ └── Time.tsx ├── index.ts ├── internal-typings │ └── global.d.ts ├── models │ ├── blocks.ts │ ├── common.ts │ ├── locale.ts │ └── paddings.ts ├── schema │ ├── README.md │ ├── blocks.ts │ ├── common.ts │ ├── headers.ts │ ├── index.ts │ └── utils.ts ├── server.ts └── utils │ ├── cn.ts │ ├── common.ts │ ├── date.ts │ ├── index.ts │ └── svg.ts ├── styles ├── fonts.scss ├── mixins.scss ├── root.scss ├── storybook │ ├── common.scss │ ├── index.scss │ ├── palette.scss │ └── typography.scss ├── styles.scss ├── variables.scss └── yfm.scss ├── svgo.config.js ├── test-utils ├── constants.ts ├── custom-environment.ts ├── setup-tests-after.ts ├── setup-tests.ts └── shared │ ├── common.tsx │ └── content.tsx ├── test.txt ├── tsconfig.configure.json ├── tsconfig.json ├── tsconfig.publish.json ├── tsconfig.server.json └── tsconfig.test.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "shippedProposals": true, 8 | "useBuiltIns": "usage", 9 | "corejs": 3, 10 | "modules": false, 11 | "targets": { 12 | "chrome": 100 13 | } 14 | } 15 | ], 16 | [ 17 | "@babel/preset-typescript", 18 | { 19 | "allowDeclareFields": true 20 | } 21 | ], 22 | [ 23 | "@babel/preset-react", 24 | { 25 | "runtime": "automatic" 26 | } 27 | ] 28 | ], 29 | "plugins": [] 30 | } 31 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{*.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /build 4 | /storybook-static 5 | /server 6 | 7 | /playwright/playwright/.cache/ 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@gravity-ui/eslint-config", 4 | "@gravity-ui/eslint-config/prettier", 5 | "@gravity-ui/eslint-config/client", 6 | "@gravity-ui/eslint-config/a11y", 7 | "plugin:react/jsx-runtime" 8 | ], 9 | "root": true, 10 | "env": { 11 | "node": true, 12 | "jest": true 13 | }, 14 | "plugins": ["no-not-accumulator-reassign"], 15 | "rules": { 16 | "no-param-reassign": ["warn", {"props": false}], 17 | "no-not-accumulator-reassign/no-not-accumulator-reassign": [ 18 | "warn", 19 | ["reduce"], 20 | {"props": true} 21 | ], 22 | "no-restricted-syntax": [ 23 | "error", 24 | { 25 | "selector": "ImportDeclaration[source.value='react'] :matches(ImportDefaultSpecifier, ImportSpecifier)", 26 | "message": "Please use `import * as React from 'react'` instead." 27 | } 28 | ], 29 | "import/no-extraneous-dependencies": 0 30 | }, 31 | "overrides": [ 32 | { 33 | "files": ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"], 34 | "extends": ["plugin:testing-library/react"] 35 | }, 36 | { 37 | "files": ["**/__stories__/**/*.[jt]s?(x)"], 38 | "rules": { 39 | "no-console": "off" 40 | } 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | verify_files: 11 | name: Verify Files 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Setup Node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | cache: npm 23 | - name: Install Packages 24 | run: npm ci 25 | - name: Lint Files 26 | run: npm run lint 27 | - name: Typecheck 28 | run: npm run typecheck 29 | - name: Typecheck (React 18) 30 | run: | 31 | npm i --no-save @types/react@18 @types/react-dom@18 32 | npm run typecheck 33 | 34 | tests: 35 | name: Tests 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 0 42 | - name: Setup Node 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: 20 46 | cache: npm 47 | - name: Install Packages 48 | run: npm ci 49 | - name: Unit Tests 50 | run: npm run test 51 | -------------------------------------------------------------------------------- /.github/workflows/main-preview.yml: -------------------------------------------------------------------------------- 1 | name: Main Preview 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | main: 9 | name: Build and Deploy 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | - name: Install Packages 21 | run: npm ci 22 | shell: bash 23 | - name: Build Storybook 24 | run: npx sb build 25 | shell: bash 26 | - name: Upload to S3 27 | uses: gravity-ui/preview-upload-to-s3-action@v1 28 | with: 29 | src-path: storybook-static 30 | dest-path: /blog-constructor/main/ 31 | s3-key-id: ${{ secrets.STORYBOOK_S3_KEY_ID }} 32 | s3-secret-key: ${{ secrets.STORYBOOK_S3_SECRET_KEY }} 33 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test: 8 | name: Test component 9 | runs-on: ubuntu-latest 10 | container: 11 | image: mcr.microsoft.com/playwright:v1.45.3-jammy 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 20 17 | cache: npm 18 | - name: Install dependencies 19 | run: npm ci 20 | - name: Run Playwright tests 21 | run: npm run playwright 22 | env: 23 | CI: 'true' 24 | - name: Upload Playwright playwright report to GitHub Actions Artifacts 25 | if: always() 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: playwright-report 29 | path: ./playwright-report 30 | retention-days: 1 31 | - name: Save PR ID 32 | if: always() 33 | run: | 34 | pr="${{ github.event.pull_request.number }}" 35 | echo $pr > ./pr-id.txt 36 | shell: bash 37 | - name: Create PR Artifact 38 | if: always() 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: pr 42 | path: ./pr-id.txt 43 | -------------------------------------------------------------------------------- /.github/workflows/pr-playwright-report.yml: -------------------------------------------------------------------------------- 1 | name: PR Playwright Report 2 | on: 3 | workflow_run: 4 | workflows: ['Playwright Tests'] 5 | types: 6 | - completed 7 | 8 | jobs: 9 | comment: 10 | name: Upload Playwright report to s3 11 | if: github.event.workflow_run.event == 'pull_request' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Download Artifacts 15 | uses: actions/download-artifact@v4 16 | with: 17 | github-token: ${{ secrets.GRAVITY_UI_BOT_GITHUB_TOKEN }} 18 | run-id: ${{ github.event.workflow_run.id }} 19 | - name: Extract PR Number 20 | id: pr 21 | run: echo "::set-output name=id::$( 13 | github.event.workflow_run.event == 'pull_request' && 14 | github.event.workflow_run.conclusion == 'success' 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: gravity-ui/preview-deploy-action@v1 18 | with: 19 | project: blog-constructor 20 | github-token: ${{ secrets.GRAVITY_UI_BOT_GITHUB_TOKEN }} 21 | s3-key-id: ${{ secrets.STORYBOOK_S3_KEY_ID }} 22 | s3-secret-key: ${{ secrets.STORYBOOK_S3_SECRET_KEY }} 23 | -------------------------------------------------------------------------------- /.github/workflows/release-4.x.x.yml: -------------------------------------------------------------------------------- 1 | name: Release version 4.x.x. 2 | 3 | on: 4 | push: 5 | branches: [version-4.x.x] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: gravity-ui/release-action@v1 12 | with: 13 | github-token: ${{ secrets.GRAVITY_UI_BOT_GITHUB_TOKEN }} 14 | npm-token: ${{ secrets.GRAVITY_UI_BOT_NPM_TOKEN }} 15 | node-version: 20 16 | npm-dist-tag: untagged 17 | default-branch: version-4.x.x 18 | -------------------------------------------------------------------------------- /.github/workflows/release-5.x.x.yml: -------------------------------------------------------------------------------- 1 | name: Release version 5.x.x. 2 | 3 | on: 4 | push: 5 | branches: [version-5.x.x] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: gravity-ui/release-action@v1 12 | with: 13 | github-token: ${{ secrets.GRAVITY_UI_BOT_GITHUB_TOKEN }} 14 | npm-token: ${{ secrets.GRAVITY_UI_BOT_NPM_TOKEN }} 15 | node-version: 20 16 | npm-dist-tag: untagged 17 | default-branch: version-5.x.x 18 | -------------------------------------------------------------------------------- /.github/workflows/release-6.x.x.yml: -------------------------------------------------------------------------------- 1 | name: Release version 6.x.x. 2 | 3 | on: 4 | push: 5 | branches: [version-6.x.x] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: gravity-ui/release-action@v1 12 | with: 13 | github-token: ${{ secrets.GRAVITY_UI_BOT_GITHUB_TOKEN }} 14 | npm-token: ${{ secrets.GRAVITY_UI_BOT_NPM_TOKEN }} 15 | node-version: 18 16 | npm-dist-tag: untagged 17 | default-branch: version-6.x.x 18 | -------------------------------------------------------------------------------- /.github/workflows/release-alpha.yml: -------------------------------------------------------------------------------- 1 | name: Release alpha version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | manualVersion: 7 | type: string 8 | required: false 9 | description: 'If your build failed and the version is already exists you can set version of package manually, e.g. 3.0.0-alpha.0. Use the prefix `alpha` otherwise you will get error.' 10 | prerelease: 11 | type: choice 12 | description: Release type, patch is used by default 13 | default: 'prerelease' 14 | options: 15 | - prerelease 16 | - prepatch 17 | - preminor 18 | - premajor 19 | 20 | jobs: 21 | release: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: gravity-ui/release-action@v1 25 | with: 26 | github-token: ${{ secrets.GRAVITY_UI_BOT_GITHUB_TOKEN }} 27 | npm-token: ${{ secrets.GRAVITY_UI_BOT_NPM_TOKEN }} 28 | node-version: 20 29 | npm-dist-tag: alpha 30 | npm-preid: alpha 31 | npm-version: ${{ github.event.inputs.manualVersion || github.event.inputs.prerelease }} 32 | -------------------------------------------------------------------------------- /.github/workflows/release-beta.yml: -------------------------------------------------------------------------------- 1 | # Build and publish -beta tag for @gravity-ui/blog-constructor 2 | # Runs manually in Actions tabs in github 3 | # Runs on any branch except main 4 | 5 | name: Release beta version 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | version: 11 | type: string 12 | required: false 13 | description: 'If your build failed and the version is already exists you can set version of package manually, e.g. 3.0.0-beta.0. Use the prefix `beta` otherwise you will get error.' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - run: | 20 | if [ "${{ github.event.inputs.version }}" != "" ]; then 21 | if [[ "${{ github.event.inputs.version }}" != *"beta"* ]]; then 22 | echo "version set incorrectly! Check that is contains beta in it's name" 23 | exit 1 24 | fi 25 | fi 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | registry-url: 'https://registry.npmjs.org' 31 | - name: Install Packages 32 | run: npm ci 33 | - name: Lint Files 34 | run: npm run lint 35 | - name: Typecheck 36 | run: npm run typecheck 37 | - name: Test 38 | run: npm test 39 | - name: Bump version 40 | run: | 41 | echo ${{ github.event.inputs.version }} 42 | 43 | if [ "${{ github.event.inputs.version }}" == "" ]; then 44 | npm version prerelease --preid=beta --git-tag-version=false 45 | else 46 | npm version ${{ github.event.inputs.version }} --git-tag-version=false 47 | fi 48 | - name: Publish version 49 | run: npm publish --tag beta --access public 50 | env: 51 | NODE_AUTH_TOKEN: ${{ secrets.GRAVITY_UI_BOT_NPM_TOKEN }} 52 | shell: bash 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: gravity-ui/release-action@v1 12 | with: 13 | github-token: ${{ secrets.GRAVITY_UI_BOT_GITHUB_TOKEN }} 14 | npm-token: ${{ secrets.GRAVITY_UI_BOT_NPM_TOKEN }} 15 | node-version: 20 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Settings 2 | .idea 3 | .DS_Store 4 | .vscode 5 | 6 | # Libs 7 | node_modules 8 | 9 | # Generated content 10 | /build 11 | /storybook-static 12 | /server 13 | 14 | *.tgz 15 | .env 16 | 17 | /styles/*css 18 | 19 | /test-results/ 20 | /playwright-report* 21 | /blob-report/ 22 | .cache* 23 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.mocks/blogPage.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 969, 3 | "name": "blog", 4 | "createdAt": "2022-08-24T15:40:07.513Z", 5 | "updatedAt": "2022-08-24T15:40:07.513Z", 6 | "type": "default", 7 | "isDeleted": false, 8 | "pageId": 969, 9 | "locale": "ru", 10 | "publishedVersionId": 17498, 11 | "lastVersionId": 17498, 12 | "content": { 13 | "blocks": [ 14 | { 15 | "type": "blog-feed-block", 16 | "resetPaddings": true, 17 | "image": "https://storage.yandexcloud.net/cloud-www-assets/constructor/storybook/images/img_8-12_light.png", 18 | "title": { 19 | "text": "Blog", 20 | "textSize": "l" 21 | } 22 | } 23 | ] 24 | }, 25 | "title": "Blog", 26 | "noIndex": false, 27 | "shareTitle": null, 28 | "shareDescription": null, 29 | "shareImage": null, 30 | "pageLocaleId": 1261, 31 | "author": "author", 32 | "metaDescription": null, 33 | "keywords": [], 34 | "shareGenImage": null, 35 | "shareGenTitle": null, 36 | "solution": null, 37 | "service": null, 38 | "regions": [], 39 | "locales": [ 40 | { 41 | "locale": "en", 42 | "publishedVersionId": null 43 | }, 44 | { 45 | "locale": "ru", 46 | "publishedVersionId": 17498 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.mocks/forms.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "type": "blog-form-block", 4 | "paddingTop": "xl", 5 | "formData": { 6 | "hubspot": { 7 | "region": "eu1", 8 | "portalId": "25764979", 9 | "formId": "a3eb06a6-e8ce-45d4-81bd-7fadb7dab313" 10 | } 11 | } 12 | }, 13 | "yandexForm": { 14 | "formData": { 15 | "yandex": { 16 | "theme": "default", 17 | "id": "61a4e639d4d24e0dbba36f5c", 18 | "customFormSection": "cloud" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | legacy-peer-deps=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # System 2 | .idea 3 | .vscode 4 | .history 5 | 6 | # Build 7 | build/ 8 | server/ 9 | storybook-static/ 10 | /styles/*.css 11 | 12 | # npm files 13 | package.json 14 | 15 | # docs 16 | CHANGELOG.md 17 | CONTRIBUTING.md 18 | 19 | /playwright/playwright/.cache/ 20 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@gravity-ui/prettier-config'); 2 | -------------------------------------------------------------------------------- /.storybook/addons/addon-yaml/AddonYaml.css: -------------------------------------------------------------------------------- 1 | .addon-yaml { 2 | padding: 11px; 3 | } 4 | 5 | .addon-yaml pre { 6 | margin: 10px 0 0; 7 | padding: 0; 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/addons/addon-yaml/preset.ts: -------------------------------------------------------------------------------- 1 | function managerEntries(entry = []) { 2 | return [...entry, require.resolve('./register.tsx')]; 3 | } 4 | 5 | module.exports = {managerEntries}; 6 | -------------------------------------------------------------------------------- /.storybook/addons/addon-yaml/register.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {ClipboardButton, ThemeProvider} from '@gravity-ui/uikit'; 4 | import {addons, types, useArgs} from '@storybook/manager-api'; 5 | import {AddonPanel} from '@storybook/components'; 6 | import {useGlobals} from '@storybook/manager-api'; 7 | import yaml from 'js-yaml'; 8 | 9 | import './AddonYaml.css'; 10 | 11 | const ADDON_ID = 'yamladdon'; 12 | const PANEL_ID = `${ADDON_ID}/panel`; 13 | 14 | const YamlPanel = () => { 15 | const [params] = useArgs(); 16 | const [globals] = useGlobals(); 17 | 18 | const content = React.useMemo( 19 | () => 20 | yaml.dump([params], { 21 | flowLevel: -1, 22 | lineWidth: -1, 23 | forceQuotes: true, 24 | skipInvalid: true, 25 | }), 26 | [params], 27 | ); 28 | 29 | return ( 30 | 31 |
32 | 33 |
{content}
34 |
35 |
36 | ); 37 | }; 38 | 39 | addons.register(ADDON_ID, () => { 40 | addons.add(PANEL_ID, { 41 | type: types.PANEL, 42 | title: 'YAML', 43 | render: ({active, key}) => ( 44 | 45 | 46 | 47 | ), 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /.storybook/addons/theme-addon/register.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {addons, types, useGlobals} from '@storybook/manager-api'; 3 | import type {API} from '@storybook/manager-api'; 4 | 5 | import {themes} from '../../theme'; 6 | 7 | const ADDON_ID = 'g-theme-addon'; 8 | const TOOL_ID = `${ADDON_ID}tool`; 9 | 10 | addons.register(ADDON_ID, (api) => { 11 | addons.add(TOOL_ID, { 12 | type: types.TOOL, 13 | title: 'Theme', 14 | render: () => { 15 | return ; 16 | }, 17 | }); 18 | }); 19 | 20 | function Tool({api}: {api: API}) { 21 | const [{theme}] = useGlobals(); 22 | React.useEffect(() => { 23 | api.setOptions({theme: themes[theme]}); 24 | }, [theme]); 25 | return null; 26 | } 27 | -------------------------------------------------------------------------------- /.storybook/decorators/DocsDecorator/DocsDecorator.scss: -------------------------------------------------------------------------------- 1 | @use '../../../styles/mixins'; 2 | 3 | $offset-xs: 4px; 4 | $offset-s: 8px; 5 | $offset-m: 16px; 6 | $offset-l: 40px; 7 | 8 | $root: '.docs-decorator'; 9 | 10 | #{$root}#{$root}#{$root}#{$root}#{$root} { 11 | .sbdocs-wrapper { 12 | padding: $offset-l; 13 | 14 | h1 { 15 | margin-bottom: 32px; 16 | } 17 | } 18 | 19 | .sbdocs-content { 20 | max-width: 1680px; 21 | } 22 | 23 | .innerZoomElementWrapper { 24 | overflow: hidden; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.storybook/decorators/DocsDecorator/DocsDecorator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {DocsContainer} from '@storybook/addon-docs'; 4 | import type {DocsContainerProps} from '@storybook/addon-docs'; 5 | 6 | import {themes} from '../../../.storybook/theme'; 7 | import {MobileContext} from '../../../src/contexts/MobileContext'; 8 | import {cn} from '../../../src/utils/cn'; 9 | import {ThemeProvider} from '@gravity-ui/uikit'; 10 | 11 | import './DocsDecorator.scss'; 12 | 13 | export interface DocsDecoratorProps extends React.PropsWithChildren {} 14 | 15 | const b = cn('docs-decorator'); 16 | 17 | export function DocsDecorator({children, context}: DocsDecoratorProps) { 18 | // @ts-expect-error 19 | const theme = context.store.userGlobals.globals.theme; 20 | 21 | return ( 22 |
23 | 24 | 25 | {children} 26 | 27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /.storybook/decorators/withLang.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {StoryFn, StoryContext} from '@storybook/react'; 3 | 4 | import {Lang, configure} from '@gravity-ui/uikit'; 5 | 6 | configure({lang: Lang.En}); 7 | 8 | export function withLang(Story: StoryFn, context: StoryContext) { 9 | const lang = context.globals.lang || Lang.En; 10 | configure({lang}); 11 | 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /.storybook/decorators/withMobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {MobileProvider} from '@gravity-ui/uikit'; 4 | import {StoryFn, StoryContext} from '@storybook/react'; 5 | 6 | export function withMobile(Story: StoryFn, context: StoryContext) { 7 | const platform = context.globals.platform; 8 | 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.storybook/decorators/withTheme.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {StoryFn, StoryContext} from '@storybook/react'; 3 | import {useTheme} from '../../src/contexts/theme'; 4 | 5 | export function withTheme(Story: StoryFn, context: StoryContext) { 6 | const themeValue = context.globals.theme; 7 | const [theme, setTheme] = useTheme(); // eslint-disable-line react-hooks/rules-of-hooks 8 | 9 | // eslint-disable-next-line react-hooks/rules-of-hooks 10 | React.useEffect(() => { 11 | if (theme !== themeValue) { 12 | setTheme(themeValue); 13 | } 14 | }, [theme, themeValue, setTheme]); 15 | 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type {StorybookConfig} from '@storybook/react-webpack5'; 2 | 3 | const config: StorybookConfig = { 4 | framework: { 5 | name: '@storybook/react-webpack5', 6 | options: {fastRefresh: true}, 7 | }, 8 | stories: ['./stories/**/*.mdx', '../src/**/__stories__/*.mdx', '../src/**/*.stories.@(ts|tsx)'], 9 | docs: { 10 | defaultName: 'Docs', 11 | }, 12 | addons: [ 13 | '@storybook/preset-scss', 14 | {name: '@storybook/addon-essentials', options: {backgrounds: false, actions: false}}, 15 | './addons/addon-yaml/preset', 16 | './addons/theme-addon/register.tsx', 17 | '@storybook/addon-mdx-gfm', 18 | '@storybook/addon-webpack5-compiler-babel', 19 | ], 20 | }; 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import {addons} from '@storybook/manager-api'; 2 | 3 | import {themes} from './theme'; 4 | 5 | addons.setConfig({ 6 | theme: themes.light, 7 | }); 8 | -------------------------------------------------------------------------------- /.storybook/stories/Changelog.mdx: -------------------------------------------------------------------------------- 1 | import {Markdown} from '@storybook/blocks'; 2 | 3 | import CHANGELOG from '../../CHANGELOG.md?raw'; 4 | 5 | {CHANGELOG} -------------------------------------------------------------------------------- /.storybook/theme.ts: -------------------------------------------------------------------------------- 1 | import {create} from '@storybook/theming'; 2 | 3 | export const CloudThemeLight = create({ 4 | base: 'light', 5 | 6 | colorPrimary: '#027bf3', 7 | colorSecondary: 'rgba(2, 123, 243, 0.6)', 8 | 9 | // Typography 10 | fontBase: 'Arial, sans-serif', 11 | fontCode: 12 | '"SF Mono", "Menlo", "Monaco", "Consolas", "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", "Courier", monospace', 13 | 14 | // Text colors 15 | textColor: 'black', 16 | textInverseColor: 'black', 17 | 18 | // Toolbar default and active colors 19 | barTextColor: 'silver', 20 | barSelectedColor: '#027bf3', 21 | // barBg: '#027bf3', 22 | 23 | // Form colors 24 | inputBg: 'white', 25 | inputBorder: 'silver', 26 | inputTextColor: 'black', 27 | inputBorderRadius: 4, 28 | 29 | brandUrl: 'https://github.com/gravity-ui/blog-constructor', 30 | brandTitle: `
Blog Constructor
31 |
Gravity UI Guidelines
`, 32 | }); 33 | 34 | export const CloudThemeDark = create({ 35 | base: 'dark', 36 | }); 37 | 38 | export const themes = { 39 | light: CloudThemeLight, 40 | dark: CloudThemeDark, 41 | }; 42 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@gravity-ui/stylelint-config", "@gravity-ui/stylelint-config/prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @NikitaCG 2 | 3 | @niktverd 4 | @gorgeousvlad 5 | @yuberdysheva 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Notice to external contributors 2 | 3 | ## General info 4 | 5 | Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Yandex Contributor License Agreement (the “**CLA**”). The current version of the CLA can be found here: 6 | 7 | 1. https://yandex.ru/legal/cla/?lang=en (in English) and 8 | 2. https://yandex.ru/legal/cla/?lang=ru (in Russian). 9 | 10 | By adopting the CLA, you state the following: 11 | 12 | - You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, 13 | - You have read the terms and conditions of the CLA and agree with them in full, 14 | - You are legally able to provide and license your contributions as stated, 15 | - We may use your contributions for our open source projects and for any other our project too, 16 | - We rely on your assurances concerning the rights of third parties in relation to your contributions. 17 | 18 | If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have already read and adopt our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. 19 | 20 | ## Provide contributions 21 | 22 | If you have already adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: 23 | 24 | ``` 25 | I hereby agree to the terms of the CLA available at: [link]. 26 | ``` 27 | 28 | Replace the bracketed text as follows: 29 | 30 | - [link] is the link to the current version of the CLA: https://yandex.ru/legal/cla/?lang=en (in English) or https://yandex.ru/legal/cla/?lang=ru (in Russian). 31 | 32 | It is enough to provide us such notification once. 33 | 34 | ## Other questions 35 | 36 | If you have any questions, please mail us at opensource@yandex-team.ru. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 YANDEX LLC 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = {extends: ['@commitlint/config-conventional']}; 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], 4 | rootDir: '.', 5 | transform: { 6 | '^.+\\.tsx?$': ['ts-jest', {tsconfig: './tsconfig.test.json'}], 7 | }, 8 | transformIgnorePatterns: ['node_modules/(?!(@gravity-ui|tinygesture)/)'], 9 | coverageDirectory: './coverage', 10 | collectCoverageFrom: [ 11 | 'src/blocks/**/*.{ts,tsx,js,jsx}', 12 | 'src/components/**/*.{ts,tsx,js,jsx}', 13 | 'src/containers/**/*.{ts,tsx,js,jsx}', 14 | '!src/demo/**/*', 15 | '!**/__stories__/**/*', 16 | '!**/*/*.stories.{ts,tsx}', 17 | ], 18 | testEnvironment: '/test-utils/custom-environment.ts', 19 | setupFiles: ['/test-utils/setup-tests.ts'], 20 | setupFilesAfterEnv: ['/test-utils/setup-tests-after.ts'], 21 | moduleNameMapper: { 22 | '\\.(css|less|scss|sass)$': 'jest-transform-css', 23 | }, 24 | testMatch: ['**/*.test.[jt]s?(x)'], 25 | testPathIgnorePatterns: [ 26 | '/node_modules', 27 | '/build', 28 | '/server', 29 | '/.storybook', 30 | '.visual.', 31 | ], 32 | }; 33 | -------------------------------------------------------------------------------- /playwright/core/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_MOUNT_TEST_DELAY = 1000; // ms 2 | -------------------------------------------------------------------------------- /playwright/core/delays.ts: -------------------------------------------------------------------------------- 1 | import {DEFAULT_MOUNT_TEST_DELAY} from './constants'; 2 | import type {PlaywrightFixture} from './types'; 3 | 4 | export const defaultDelayFixture: PlaywrightFixture<() => Promise> = async ({page}, use) => { 5 | const defaultDelay = async () => await page.waitForTimeout(DEFAULT_MOUNT_TEST_DELAY); 6 | await use(defaultDelay); 7 | }; 8 | 9 | export const delayFixture: PlaywrightFixture<(delay: number) => Promise> = async ( 10 | {page}, 11 | use, 12 | ) => { 13 | const delayFunction = async (delay: number) => await page.waitForTimeout(delay); 14 | await use(delayFunction); 15 | }; 16 | -------------------------------------------------------------------------------- /playwright/core/index.ts: -------------------------------------------------------------------------------- 1 | import {test as base} from '@playwright/experimental-ct-react'; 2 | 3 | import {defaultDelayFixture, delayFixture} from './delays'; 4 | import {expectScreenshotFixture} from './expectScreenshotFixture'; 5 | import {mountFixture} from './mountFixture'; 6 | import type {Fixtures} from './types'; 7 | 8 | export const test = base.extend({ 9 | mount: mountFixture, 10 | expectScreenshot: expectScreenshotFixture, 11 | defaultDelay: defaultDelayFixture, 12 | delay: delayFixture, 13 | }); 14 | 15 | export {expect} from '@playwright/experimental-ct-react'; 16 | -------------------------------------------------------------------------------- /playwright/core/mountFixture.tsx: -------------------------------------------------------------------------------- 1 | import type {MountOptions} from '@playwright/experimental-ct-react'; 2 | 3 | import type {MountFixture, PlaywrightFixture} from './types'; 4 | 5 | export const mountFixture: PlaywrightFixture = async ({mount: baseMount}, use) => { 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | const mount = async (component: JSX.Element, options?: MountOptions | undefined) => { 8 | return await baseMount( 9 |
13 | {component} 14 |
, 15 | options, 16 | ); 17 | }; 18 | 19 | await use(mount); 20 | }; 21 | -------------------------------------------------------------------------------- /playwright/core/types.ts: -------------------------------------------------------------------------------- 1 | import type {MountOptions, MountResult} from '@playwright/experimental-ct-react'; 2 | import type { 3 | Locator, 4 | PageScreenshotOptions, 5 | PlaywrightTestArgs, 6 | PlaywrightTestOptions, 7 | PlaywrightWorkerArgs, 8 | PlaywrightWorkerOptions, 9 | TestFixture, 10 | } from '@playwright/test'; 11 | 12 | interface ComponentFixtures { 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | mount( 15 | component: JSX.Element, 16 | options?: MountOptions, 17 | style?: React.CSSProperties, 18 | ): Promise; 19 | } 20 | 21 | type PlaywrightTestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures; 22 | type PlaywrightWorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions; 23 | type PlaywrightFixtures = PlaywrightTestFixtures & PlaywrightWorkerFixtures; 24 | export type PlaywrightFixture = TestFixture; 25 | 26 | export type Fixtures = { 27 | mount: MountFixture; 28 | expectScreenshot: ExpectScreenshotFixture; 29 | defaultDelay: () => Promise; 30 | delay: (delay: number) => Promise; 31 | }; 32 | 33 | export type MountFixture = ComponentFixtures['mount']; 34 | 35 | export interface ExpectScreenshotFixture { 36 | (props?: CaptureScreenshotParams): Promise; 37 | } 38 | 39 | interface CaptureScreenshotParams extends PageScreenshotOptions { 40 | screenshotName?: string; 41 | component?: Locator; 42 | skipTheme?: 'light' | 'dark'; 43 | } 44 | -------------------------------------------------------------------------------- /playwright/playwright/Providers.tsx: -------------------------------------------------------------------------------- 1 | import {Theme} from '@gravity-ui/page-constructor'; 2 | import { 3 | MobileProvider, 4 | ThemeProvider, 5 | Toaster, 6 | ToasterComponent, 7 | ToasterProvider, 8 | } from '@gravity-ui/uikit'; 9 | import * as React from 'react'; 10 | import {BlogConstructorProvider} from '../../src/constructor/BlogConstructorProvider'; 11 | 12 | const toaster = new Toaster(); 13 | 14 | export const Providers = ({children}) => { 15 | const [theme, setTheme] = React.useState(Theme.Light); 16 | 17 | React.useEffect(() => { 18 | const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 19 | const updateTheme = (event) => { 20 | setTheme(event.matches ? Theme.Dark : Theme.Light); 21 | }; 22 | 23 | setTheme(darkModeMediaQuery.matches ? Theme.Dark : Theme.Light); 24 | 25 | darkModeMediaQuery.addEventListener('change', updateTheme); 26 | 27 | return () => { 28 | darkModeMediaQuery.removeEventListener('change', updateTheme); 29 | }; 30 | }, []); 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | {children} 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /playwright/playwright/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Testing Page 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /playwright/playwright/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/styles.scss'; 2 | @import '../../styles/storybook/index.scss'; 3 | @import '@gravity-ui/uikit/styles/styles.scss'; 4 | @import '../../styles/root.scss'; 5 | @import '../../styles/yfm.scss'; 6 | @import '../../styles/fonts.scss'; 7 | -------------------------------------------------------------------------------- /playwright/playwright/index.tsx: -------------------------------------------------------------------------------- 1 | import {beforeMount} from '@playwright/experimental-ct-react/hooks'; 2 | 3 | import {Providers} from './Providers'; 4 | import './index.scss'; 5 | 6 | beforeMount(async ({App}) => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /scripts/playwright-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | IMAGE_NAME="mcr.microsoft.com/playwright" 6 | IMAGE_TAG="v1.45.3-jammy" # This version have to be synchronized with playwright version from package.json 7 | 8 | NODE_MODULES_CACHE_DIR="$HOME/.cache/blog-constructor-playwright-docker-node-modules" 9 | 10 | command_exists() { 11 | command -v "$1" >/dev/null 2>&1 12 | } 13 | 14 | run_command() { 15 | $CONTAINER_TOOL run --rm --network host -it -w /work \ 16 | -v $(pwd):/work \ 17 | -v "$NODE_MODULES_CACHE_DIR:/work/node_modules" \ 18 | -e IS_DOCKER=1 \ 19 | "$IMAGE_NAME:$IMAGE_TAG" \ 20 | /bin/bash -c "$1" 21 | } 22 | 23 | if command_exists docker; then 24 | CONTAINER_TOOL="docker" 25 | elif command_exists podman; then 26 | CONTAINER_TOOL="podman" 27 | else 28 | echo "Neither Docker nor Podman is installed on the system." 29 | exit 1 30 | fi 31 | 32 | if [[ "$1" = "clear-cache" ]]; then 33 | rm -rf "$NODE_MODULES_CACHE_DIR" 34 | rm -rf "./playwright/.cache-docker" 35 | exit 0 36 | fi 37 | 38 | if [[ ! -d "$NODE_MODULES_CACHE_DIR" ]]; then 39 | mkdir -p "$NODE_MODULES_CACHE_DIR" 40 | run_command 'npm ci' 41 | fi 42 | 43 | run_command "$1" 44 | -------------------------------------------------------------------------------- /src/blocks/Author/Author.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables.scss'; 2 | @import '../../../styles/mixins.scss'; 3 | 4 | $block: '.#{$namespace}author'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | &__layout { 10 | display: flex; 11 | align-items: flex-start; 12 | flex-direction: row; 13 | overflow: hidden; 14 | } 15 | 16 | @media (max-width: map-get($gridBreakpoints, 'lg')) { 17 | &__layout { 18 | width: 50%; 19 | } 20 | } 21 | 22 | @media (max-width: map-get($gridBreakpoints, 'sm')) { 23 | &__layout { 24 | width: 100%; 25 | } 26 | } 27 | 28 | &__description { 29 | color: var(--g-color-text-primary); 30 | } 31 | 32 | &__content { 33 | display: flex; 34 | flex-wrap: wrap; 35 | justify-content: flex-start; 36 | /* stylelint-disable-next-line declaration-block-no-redundant-longhand-properties */ 37 | align-content: flex-start; 38 | position: relative; 39 | } 40 | 41 | &__container { 42 | background-color: var(--pc-color-base-silver); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/blocks/Author/Author.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {AuthorType, Author as PCAuthor} from '@gravity-ui/page-constructor'; 4 | 5 | import {Wrapper} from '../../components/Wrapper/Wrapper'; 6 | import {PostPageContext} from '../../contexts/PostPageContext'; 7 | import {AuthorProps} from '../../models/blocks'; 8 | import {PaddingsDirections} from '../../models/paddings'; 9 | import {block} from '../../utils/cn'; 10 | 11 | import './Author.scss'; 12 | 13 | const b = block('author'); 14 | 15 | export const Author = (props: AuthorProps) => { 16 | const {image, paddingTop, paddingBottom, authorId, qa} = props; 17 | 18 | const {post} = React.useContext(PostPageContext); 19 | 20 | const author = post?.authors?.find(({id}: {id: number | string}) => id === authorId); 21 | 22 | const authorItem = React.useMemo(() => { 23 | const imageUrl = author?.avatar ?? image; 24 | const authorAvatar = author; 25 | 26 | return { 27 | firstName: author?.firstName || '', 28 | secondName: author?.secondName || '', 29 | description: author?.shortDescription || '', 30 | avatar: authorAvatar, 31 | }; 32 | }, [author?.avatar, author?.firstName, author?.shortDescription, author?.secondName, image]); 33 | 34 | if (!authorItem?.firstName || !authorItem?.secondName) { 35 | return null; 36 | } 37 | 38 | return ( 39 | 47 |
48 | 53 |
54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/blocks/Author/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------ | :-------------------- | :------- | :------------------------- | 3 | | className | `string` | `false` | Component className | 4 | | authorId | `number` | `true` | Author id | 5 | | image | `string` | `true` | Link to the author's image | 6 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 7 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 8 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 9 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 10 | -------------------------------------------------------------------------------- /src/blocks/Author/__snapshots__/Author.visual.test.tsx-snapshots/Author-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Author/__snapshots__/Author.visual.test.tsx-snapshots/Author-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Author/__snapshots__/Author.visual.test.tsx-snapshots/Author-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Author/__snapshots__/Author.visual.test.tsx-snapshots/Author-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Author/__stories__/Author.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as AuthorStories from './Author.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/Author/__stories__/Author.stories.tsx: -------------------------------------------------------------------------------- 1 | import {Block, PageConstructor} from '@gravity-ui/page-constructor'; 2 | import type {Meta, StoryFn} from '@storybook/react'; 3 | 4 | import {blockMockData, getDefaultStoryArgs} from '../../../../.mocks/utils'; 5 | import customBlocks from '../../../constructor/blocksMap'; 6 | import {PostPageContext} from '../../../contexts/PostPageContext'; 7 | import {AuthorProps} from '../../../models/blocks'; 8 | import {BlockType} from '../../../models/common'; 9 | import {Author} from '../Author'; 10 | 11 | export default { 12 | title: 'Blocks/Author', 13 | component: Author, 14 | args: { 15 | theme: 'light', 16 | }, 17 | } as Meta; 18 | 19 | type AuthorModel = { 20 | type: BlockType.Author; 21 | } & AuthorProps; 22 | 23 | const DefaultTemplate: StoryFn = (args) => ( 24 | 25 | 26 | 27 | ); 28 | 29 | export const Default = DefaultTemplate.bind({}); 30 | 31 | Default.args = { 32 | type: BlockType.Author, 33 | authorId: 290, 34 | ...getDefaultStoryArgs(), 35 | } as AuthorModel; 36 | -------------------------------------------------------------------------------- /src/blocks/Author/__tests__/Author.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('Author', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/Author/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as AuthorStories from '../__stories__/Author.stories'; 4 | 5 | export const {Default} = composeStories(AuthorStories); 6 | -------------------------------------------------------------------------------- /src/blocks/Author/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | const { 4 | common: {BlockBaseProps}, 5 | } = validators; 6 | 7 | import {BlockType} from '../../models/common'; 8 | import {BlogBlockBase} from '../../schema/common'; 9 | 10 | export const Author = { 11 | [BlockType.Author]: { 12 | type: 'object', 13 | additionalProperties: false, 14 | required: ['authorId'], 15 | properties: { 16 | ...BlockBaseProps, 17 | ...BlogBlockBase, 18 | authorId: { 19 | type: 'number', 20 | }, 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/blocks/Banner/Banner.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables.scss'; 2 | @import '../../../styles/mixins.scss'; 3 | 4 | $block: '.#{$namespace}banner'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | margin-top: $indentXS; 10 | padding-bottom: $indentXS; 11 | 12 | &__content { 13 | width: 100%; 14 | border-radius: var(--bc-border-radius); 15 | position: relative; 16 | display: flex; 17 | flex-direction: row; 18 | } 19 | 20 | &__container { 21 | width: 100%; 22 | border-radius: var(--bc-border-radius); 23 | display: flex; 24 | 25 | min-height: 220px; 26 | 27 | &_offset_large { 28 | padding: calc(#{$indentXXXL} - #{$indentXXL}) 0 $indentXS; 29 | margin-bottom: -$indentL; 30 | } 31 | } 32 | 33 | &__info, 34 | &__image { 35 | flex: 1; 36 | } 37 | 38 | &__info { 39 | padding: $indentM; 40 | display: flex; 41 | justify-content: space-between; 42 | flex-direction: column; 43 | align-items: baseline; 44 | } 45 | 46 | &__image { 47 | @include card-image(); 48 | width: calc(100% - #{$indentXXXS}); 49 | height: calc(100% - #{$indentXXXS}); 50 | 51 | object-fit: cover; 52 | } 53 | 54 | &__image-container { 55 | height: 100%; 56 | } 57 | 58 | &__image-container { 59 | &_image-size { 60 | &_s { 61 | width: 25%; 62 | } 63 | 64 | &_m { 65 | width: 50%; 66 | } 67 | } 68 | } 69 | 70 | @media (max-width: map-get($gridBreakpoints, 'sm')) { 71 | &__content { 72 | flex-direction: column; 73 | } 74 | 75 | &__image-container { 76 | &_image-size { 77 | &_s, 78 | &_m { 79 | width: 100%; 80 | height: 236px; 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/blocks/Banner/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------- | :------------------------ | :------- | :------------------------------------ | 3 | | color | `string` | `false` | Color value | 4 | | image | `string` | `false` | Image link | 5 | | imageSize | `s - m` | `false` | Image size | 6 | | title | `TitleBaseProps - string` | `false` | Content title | 7 | | text | `string` | `false` | Content text | 8 | | additionalInfo | `string` | `false` | Content additional info | 9 | | links | `LinkProps[]` | `false` | Content links | 10 | | buttons | `ButtonProps[]` | `false` | Content buttons | 11 | | size | `s - l` | `false` | Content size | 12 | | colSizes | `GridColumnSizesType` | `false` | Content columns sizes for breakpoints | 13 | | centered | `boolean` | `false` | Flag for content for center alignment | 14 | | theme | `light - dark - default` | `false` | Content theme | 15 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 16 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 17 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 18 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 19 | 20 | To get more information about `content` see this [page](https://preview.gravity-ui.com/page-constructor/?path=/story/components-content--default) 21 | -------------------------------------------------------------------------------- /src/blocks/Banner/__snapshots__/Banner.visual.test.tsx-snapshots/Banner-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Banner/__snapshots__/Banner.visual.test.tsx-snapshots/Banner-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Banner/__snapshots__/Banner.visual.test.tsx-snapshots/Banner-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Banner/__snapshots__/Banner.visual.test.tsx-snapshots/Banner-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Banner/__stories__/Banner.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as BannerStories from './Banner.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/Banner/__stories__/Banner.stories.tsx: -------------------------------------------------------------------------------- 1 | import {Block, PageConstructor} from '@gravity-ui/page-constructor'; 2 | import type {Meta, StoryFn} from '@storybook/react'; 3 | 4 | import {blockMockData, getDefaultStoryArgs} from '../../../../.mocks/utils'; 5 | import customBlocks from '../../../constructor/blocksMap'; 6 | import {PostPageContext} from '../../../contexts/PostPageContext'; 7 | import {BannerProps} from '../../../models/blocks'; 8 | import {BlockType} from '../../../models/common'; 9 | import {Banner} from '../Banner'; 10 | 11 | export default { 12 | title: 'Blocks/Banner', 13 | component: Banner, 14 | args: { 15 | theme: 'light', 16 | }, 17 | argTypes: { 18 | color: { 19 | control: {type: 'color'}, 20 | }, 21 | }, 22 | } as Meta; 23 | 24 | type BannerModel = { 25 | type: BlockType.Banner; 26 | } & BannerProps; 27 | 28 | const DefaultTemplate: StoryFn = (args) => ( 29 | 30 | 31 | 32 | ); 33 | 34 | export const Default = DefaultTemplate.bind({}); 35 | 36 | Default.args = { 37 | type: BlockType.Banner, 38 | color: '#7ccea0', 39 | ...getDefaultStoryArgs(), 40 | title: 'Lorem', 41 | text: 'Lorem ipsum dolor sit amet', 42 | additionalInfo: 'consectetur adipiscing elit', 43 | } as BannerModel; 44 | -------------------------------------------------------------------------------- /src/blocks/Banner/__tests__/Banner.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('Banner', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/Banner/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as BannerStories from '../__stories__/Banner.stories'; 4 | 5 | export const {Default} = composeStories(BannerStories); 6 | -------------------------------------------------------------------------------- /src/blocks/Banner/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | import {BlockType} from '../../models/common'; 4 | import {BlogBlockBase} from '../../schema/common'; 5 | 6 | const { 7 | subBlocks: {ContentBase}, 8 | common: {BlockBaseProps}, 9 | } = validators; 10 | 11 | export const Banner = { 12 | [BlockType.Banner]: { 13 | type: 'object', 14 | additionalProperties: false, 15 | required: ['title', 'text', 'image'], 16 | properties: { 17 | ...BlockBaseProps, 18 | ...BlogBlockBase, 19 | ...ContentBase, 20 | color: { 21 | type: 'string', 22 | }, 23 | image: { 24 | type: 'string', 25 | }, 26 | imageSize: { 27 | type: 'string', 28 | enum: ['s', 'm'], 29 | }, 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/blocks/CTA/CTA.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables.scss'; 2 | 3 | $block: '.#{$namespace}cta'; 4 | 5 | #{$block} { 6 | $root: &; 7 | 8 | &__card { 9 | display: flex; 10 | background-color: var(--bc-cta-card-bg, var(--pc-color-base-silver)); 11 | border-radius: var(--bc-border-radius); 12 | min-height: 80px; 13 | align-content: center; 14 | /* stylelint-disable-next-line declaration-block-no-redundant-longhand-properties */ 15 | justify-content: center; 16 | flex-direction: column; 17 | flex-grow: 1; 18 | padding: $indentM; 19 | width: calc((100% / 3) - (#{$indentM} / 2)); 20 | } 21 | 22 | &__content { 23 | display: flex; 24 | flex-flow: row wrap; 25 | gap: $indentXS; 26 | } 27 | 28 | @media (max-width: map-get($gridBreakpoints, 'md')) { 29 | &__content { 30 | flex-wrap: wrap; 31 | } 32 | 33 | &__card { 34 | width: calc((100% / 2) - #{$indentXS}); 35 | flex-grow: 1; 36 | } 37 | } 38 | 39 | @media (max-width: map-get($gridBreakpoints, 'sm')) { 40 | &__content { 41 | flex-direction: column; 42 | } 43 | 44 | &__card { 45 | width: 100%; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/blocks/CTA/CTA.tsx: -------------------------------------------------------------------------------- 1 | import {Content, ContentBlockProps} from '@gravity-ui/page-constructor'; 2 | 3 | import {Wrapper} from '../../components/Wrapper/Wrapper'; 4 | import {CTAProps} from '../../models/blocks'; 5 | import {PaddingsDirections} from '../../models/paddings'; 6 | import {block} from '../../utils/cn'; 7 | import { 8 | getMergedAnalyticsEvents, 9 | getQaAttributes, 10 | prepareAnalyticsEvent, 11 | updateContentSizes, 12 | } from '../../utils/common'; 13 | import {DefaultGoalIds} from '../../constants'; 14 | import {AnalyticsCounter} from '../../counters/utils'; 15 | 16 | import './CTA.scss'; 17 | 18 | const b = block('cta'); 19 | 20 | const linkGoals = prepareAnalyticsEvent({ 21 | name: DefaultGoalIds.cta, 22 | counter: AnalyticsCounter.CrossSite, 23 | }); 24 | 25 | export const CTA = ({items, paddingTop, paddingBottom, qa}: CTAProps) => { 26 | const qaAttributes = getQaAttributes(qa, 'card'); 27 | 28 | return ( 29 | 37 | {items.map((content: ContentBlockProps, index: number) => { 38 | const contentData = updateContentSizes(content); 39 | 40 | contentData.links?.forEach((link) => { 41 | // eslint-disable-next-line no-not-accumulator-reassign/no-not-accumulator-reassign 42 | link.analyticsEvents = getMergedAnalyticsEvents( 43 | linkGoals, 44 | link.analyticsEvents, 45 | ); 46 | }); 47 | 48 | return ( 49 |
50 | 51 |
52 | ); 53 | })} 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/blocks/CTA/README.md: -------------------------------------------------------------------------------- 1 | ## Block "Call to action" 2 | 3 | | Property | Type | Required | Description | 4 | | :------------ | :-------------------- | :------- | :------------------ | 5 | | items | `ContentBlockProps[]` | `true` | Items with content | 6 | | columnCount | `number` | `false` | Column count | 7 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 8 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 9 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 10 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 11 | 12 | To get more information about `content` see this [page](https://preview.gravity-ui.com/page-constructor/?path=/story/components-content--default) 13 | -------------------------------------------------------------------------------- /src/blocks/CTA/__snapshots__/CTA.visual.test.tsx-snapshots/CTA-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/CTA/__snapshots__/CTA.visual.test.tsx-snapshots/CTA-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/CTA/__snapshots__/CTA.visual.test.tsx-snapshots/CTA-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/CTA/__snapshots__/CTA.visual.test.tsx-snapshots/CTA-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/CTA/__stories__/CTA.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as CTAStories from './CTA.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/CTA/__tests__/CTA.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('CTA', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/CTA/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as CTAStories from '../__stories__/CTA.stories'; 4 | 5 | export const {Default} = composeStories(CTAStories); 6 | -------------------------------------------------------------------------------- /src/blocks/CTA/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | import {BlockType} from '../../models/common'; 4 | import {BlogBlockBase} from '../../schema/common'; 5 | 6 | const { 7 | subBlocks: {ContentBase}, 8 | common: {BlockBaseProps}, 9 | } = validators; 10 | 11 | export const CTA = { 12 | [BlockType.CTA]: { 13 | type: 'object', 14 | additionalProperties: false, 15 | required: ['items'], 16 | properties: { 17 | ...BlockBaseProps, 18 | ...BlogBlockBase, 19 | items: { 20 | type: 'array', 21 | items: { 22 | type: 'object', 23 | additionalProperties: false, 24 | required: ['title', 'links'], 25 | properties: { 26 | ...ContentBase, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/blocks/ColoredText/ColoredText.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables.scss'; 2 | 3 | $block: '.#{$namespace}colored-text'; 4 | 5 | #{$block} { 6 | $root: &; 7 | 8 | &__container { 9 | display: flex; 10 | border-radius: var(--bc-border-radius); 11 | overflow: hidden; 12 | position: relative; 13 | } 14 | 15 | &__picture-container { 16 | position: absolute; 17 | overflow: hidden; 18 | width: 100%; 19 | height: 100%; 20 | border-radius: var(--bc-border-radius); 21 | z-index: 1; 22 | top: 0; 23 | left: 0; 24 | } 25 | 26 | &__picture { 27 | object-fit: cover; 28 | height: 100%; 29 | width: 100%; 30 | } 31 | 32 | &__text-content { 33 | position: inherit; 34 | z-index: 2; 35 | height: 100%; 36 | width: 100%; 37 | padding: $indentM; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/blocks/ColoredText/ColoredText.tsx: -------------------------------------------------------------------------------- 1 | import {BackgroundImage, Content} from '@gravity-ui/page-constructor'; 2 | 3 | import {Wrapper} from '../../components/Wrapper/Wrapper'; 4 | import {ColoredTextProps} from '../../models/blocks'; 5 | import {PaddingsDirections} from '../../models/paddings'; 6 | import {block} from '../../utils/cn'; 7 | import {getQaAttributes, updateContentSizes} from '../../utils/common'; 8 | 9 | import './ColoredText.scss'; 10 | 11 | const b = block('colored-text'); 12 | 13 | export const ColoredText = ({ 14 | background, 15 | paddingTop, 16 | paddingBottom, 17 | qa, 18 | ...content 19 | }: ColoredTextProps) => { 20 | const contentData = updateContentSizes(content); 21 | const qaAttributes = getQaAttributes(qa); 22 | 23 | return ( 24 | 31 |
36 |
37 | {background?.image && ( 38 | 43 | )} 44 |
45 |
46 | 47 |
48 |
49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/blocks/ColoredText/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------- | :---------------------------------- | :------- | :------------------------------------ | 3 | | background | `{color?, image?, altText?} string` | `false` | Background settings | 4 | | title | `TitleBaseProps - string` | `false` | Content title | 5 | | text | `string` | `false` | Content text | 6 | | additionalInfo | `string` | `false` | Content additional info | 7 | | links | `LinkProps[]` | `false` | Content links | 8 | | buttons | `ButtonProps[]` | `false` | Content buttons | 9 | | size | `s - l` | `false` | Content size | 10 | | colSizes | `GridColumnSizesType` | `false` | Content columns sizes for breakpoints | 11 | | centered | `boolean` | `false` | Flag for content for center alignment | 12 | | theme | `light - dark - default` | `false` | Content theme | 13 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 14 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 15 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 16 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 17 | 18 | To get more information about `content` see this [page](https://preview.gravity-ui.com/page-constructor/?path=/story/components-content--default) 19 | -------------------------------------------------------------------------------- /src/blocks/ColoredText/__snapshots__/ColoredText.visual.test.tsx-snapshots/ColoredText-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/ColoredText/__snapshots__/ColoredText.visual.test.tsx-snapshots/ColoredText-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/ColoredText/__snapshots__/ColoredText.visual.test.tsx-snapshots/ColoredText-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/ColoredText/__snapshots__/ColoredText.visual.test.tsx-snapshots/ColoredText-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/ColoredText/__stories__/ColoredText.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as ColoredTextStories from './ColoredText.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/ColoredText/__stories__/ColoredText.stories.tsx: -------------------------------------------------------------------------------- 1 | import {Block, PageConstructor} from '@gravity-ui/page-constructor'; 2 | import type {Meta, StoryFn} from '@storybook/react'; 3 | 4 | import {blockMockData} from '../../../../.mocks/utils'; 5 | import customBlocks from '../../../constructor/blocksMap'; 6 | import {PostPageContext} from '../../../contexts/PostPageContext'; 7 | import {ColoredTextProps} from '../../../models/blocks'; 8 | import {BlockType} from '../../../models/common'; 9 | import {ColoredText} from '../ColoredText'; 10 | 11 | export default { 12 | title: 'Blocks/ColoredText', 13 | component: ColoredText, 14 | args: { 15 | theme: 'light', 16 | }, 17 | } as Meta; 18 | 19 | type ColoredTextModel = { 20 | type: BlockType.ColoredText; 21 | } & ColoredTextProps; 22 | 23 | const DefaultTemplate: StoryFn = (args) => ( 24 | 25 | 26 | 27 | ); 28 | 29 | export const Default = DefaultTemplate.bind({}); 30 | 31 | Default.args = { 32 | type: BlockType.ColoredText, 33 | background: { 34 | color: '#000', 35 | image: 'https://storage.yandexcloud.net/cloud-www-assets/constructor/storybook/images/img_8-12_light.png', 36 | altText: 'test', 37 | }, 38 | text: 'Lorem ipsum dolor sit amet', 39 | paddingBottom: 'l', 40 | paddingTop: 'l', 41 | }; 42 | -------------------------------------------------------------------------------- /src/blocks/ColoredText/__tests__/ColoredText.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('ColoredText', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/ColoredText/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as ColoredTextStories from '../__stories__/ColoredText.stories'; 4 | 5 | export const {Default} = composeStories(ColoredTextStories); 6 | -------------------------------------------------------------------------------- /src/blocks/ColoredText/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | import {BlockType} from '../../models/common'; 4 | import {BlogBlockBase} from '../../schema/common'; 5 | 6 | const { 7 | subBlocks: {ContentBase}, 8 | components: {ImageProps}, 9 | common: {BlockBaseProps}, 10 | } = validators; 11 | 12 | const BackgroundProps = { 13 | type: 'object', 14 | additionalProperties: false, 15 | properties: { 16 | image: ImageProps, 17 | color: { 18 | type: 'string', 19 | }, 20 | altText: { 21 | type: 'string', 22 | contentType: 'text', 23 | }, 24 | }, 25 | }; 26 | 27 | export const ColoredText = { 28 | [BlockType.ColoredText]: { 29 | type: 'object', 30 | additionalProperties: false, 31 | required: ['text'], 32 | properties: { 33 | ...BlockBaseProps, 34 | ...BlogBlockBase, 35 | ...ContentBase, 36 | background: BackgroundProps, 37 | }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/blocks/Feed/README.md: -------------------------------------------------------------------------------- 1 | The data in this component is taken from contexts. 2 | 3 | | Property | Type | Required | Description | 4 | | :------- | :------- | :------- | :-------------------- | 5 | | image | `string` | `true` | Image for feed header | 6 | -------------------------------------------------------------------------------- /src/blocks/Feed/__snapshots__/Feed.visual.test.tsx-snapshots/Feed-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Feed/__snapshots__/Feed.visual.test.tsx-snapshots/Feed-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Feed/__snapshots__/Feed.visual.test.tsx-snapshots/Feed-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Feed/__snapshots__/Feed.visual.test.tsx-snapshots/Feed-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Feed/__stories__/Feed.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as FeedStories from './Feed.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/Feed/__tests__/Feed.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('Feed', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/Feed/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as FeedStories from '../__stories__/Feed.stories'; 4 | 5 | export const {Default} = composeStories(FeedStories); 6 | -------------------------------------------------------------------------------- /src/blocks/Feed/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | const { 4 | common: {TitleProps, BlockBaseProps}, 5 | } = validators; 6 | 7 | import {BlockType} from '../../models/common'; 8 | 9 | export const Feed = { 10 | [BlockType.Feed]: { 11 | additionalProperties: false, 12 | required: [], 13 | properties: { 14 | ...BlockBaseProps, 15 | title: TitleProps, 16 | image: { 17 | type: 'string', 18 | }, 19 | description: { 20 | type: 'string', 21 | contentType: 'text', 22 | }, 23 | size: { 24 | type: 'string', 25 | enum: ['s', 'm'], 26 | }, 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/blocks/Form/Form.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins'; 2 | @import '../../../styles/variables'; 3 | 4 | $block: '.#{$namespace}form-block'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | &__container { 10 | border-radius: $borderRadius; 11 | position: relative; 12 | overflow: hidden; 13 | padding: 24px; 14 | 15 | &_border { 16 | &_shadow { 17 | @include image-shadow(); 18 | } 19 | 20 | &_line { 21 | border: 1px solid var(--g-color-line-generic); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/blocks/Form/Form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {InnerForm} from '@gravity-ui/page-constructor'; 4 | 5 | import {Wrapper} from '../../components/Wrapper/Wrapper'; 6 | import {FormProps} from '../../models/blocks'; 7 | import {PaddingsDirections} from '../../models/paddings'; 8 | import {block} from '../../utils/cn'; 9 | 10 | import './Form.scss'; 11 | 12 | const b = block('form-block'); 13 | 14 | export const Form = ({paddingTop, paddingBottom, ...props}: FormProps) => { 15 | const {formData, border = 'shadow'} = props; 16 | const [contentLoaded, setContentLoaded] = React.useState(false); 17 | 18 | const onContentLoad = () => { 19 | setContentLoaded(true); 20 | }; 21 | 22 | if (!formData) { 23 | return null; 24 | } 25 | 26 | return ( 27 | 34 |
35 | 40 |
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/blocks/Form/__snapshots__/Form.visual.test.tsx-snapshots/Form-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Form/__snapshots__/Form.visual.test.tsx-snapshots/Form-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Form/__snapshots__/Form.visual.test.tsx-snapshots/Form-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Form/__snapshots__/Form.visual.test.tsx-snapshots/Form-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Form/__snapshots__/Form.visual.test.tsx-snapshots/Form-render-stories-FormData-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Form/__snapshots__/Form.visual.test.tsx-snapshots/Form-render-stories-FormData-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Form/__snapshots__/Form.visual.test.tsx-snapshots/Form-render-stories-FormData-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Form/__snapshots__/Form.visual.test.tsx-snapshots/Form-render-stories-FormData-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Form/__stories__/Form.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as FormStories from './Form.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/Form/__tests__/Form.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default, FormData} from './helpers'; 4 | 5 | const FORM_DEALAY = 15 * 1000; 6 | 7 | test.describe('Form', () => { 8 | // skip because yandex forms falls shows Captcha 9 | test.skip('render stories ', async ({mount, expectScreenshot, delay}) => { 10 | await mount(); 11 | await delay(FORM_DEALAY); 12 | await expectScreenshot({skipTheme: 'dark'}); 13 | }); 14 | 15 | test.skip('render stories ', async ({mount, expectScreenshot, delay}) => { 16 | await mount(); 17 | await delay(FORM_DEALAY); 18 | await expectScreenshot({skipTheme: 'dark'}); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/blocks/Form/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as FormStories from '../__stories__/Form.stories'; 4 | 5 | export const {Default, FormData} = composeStories(FormStories); 6 | -------------------------------------------------------------------------------- /src/blocks/Form/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | import {BlockType} from '../../models/common'; 4 | import {BlogBlockBase} from '../../schema/common'; 5 | 6 | const { 7 | common: {BlockBaseProps}, 8 | components: {YandexFormProps}, 9 | subBlocks: {HubspotFormProps}, 10 | } = validators; 11 | 12 | export const Media = { 13 | [BlockType.Media]: { 14 | type: 'object', 15 | additionalProperties: false, 16 | properties: { 17 | ...BlockBaseProps, 18 | ...BlogBlockBase, 19 | formData: { 20 | oneOf: [ 21 | { 22 | type: 'object', 23 | optionName: 'yandex', 24 | properties: { 25 | yandex: YandexFormProps, 26 | }, 27 | }, 28 | { 29 | type: 'object', 30 | optionName: 'hubspot', 31 | properties: { 32 | hubspot: HubspotFormProps, 33 | }, 34 | }, 35 | ], 36 | }, 37 | border: { 38 | type: 'string', 39 | enum: ['shadow', 'line', 'none'], 40 | }, 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/blocks/Header/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------ | :-------------------- | :------- | :------------------ | 3 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 4 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 5 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 6 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 7 | 8 | and `HeaderBlockProps` 9 | 10 | To get more information about `HeaderBlockProps` see this [page](https://preview.gravity-ui.com/page-constructor/?path=/docs/blocks-header--default) 11 | -------------------------------------------------------------------------------- /src/blocks/Header/__snapshots__/Header.visual.test.tsx-snapshots/Header-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Header/__snapshots__/Header.visual.test.tsx-snapshots/Header-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Header/__snapshots__/Header.visual.test.tsx-snapshots/Header-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Header/__snapshots__/Header.visual.test.tsx-snapshots/Header-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Header/__stories__/Header.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as HeaderStories from './Header.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/Header/__stories__/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import {Block, PageConstructor} from '@gravity-ui/page-constructor'; 2 | import {Meta, StoryFn} from '@storybook/react'; 3 | 4 | import {blockMockData, getDefaultStoryArgs} from '../../../../.mocks/utils'; 5 | import customBlocks from '../../../constructor/blocksMap'; 6 | import {PostPageContext} from '../../../contexts/PostPageContext'; 7 | import {HeaderProps} from '../../../models/blocks'; 8 | import {BlockType} from '../../../models/common'; 9 | import {Header} from '../Header'; 10 | 11 | export default { 12 | title: 'Blocks/Header', 13 | component: Header, 14 | args: { 15 | theme: 'light', 16 | }, 17 | } as Meta; 18 | 19 | type HeaderModel = { 20 | type: BlockType.Header; 21 | } & HeaderProps; 22 | 23 | const DefaultTemplate: StoryFn = (args) => ( 24 | 25 | 26 | 27 | ); 28 | 29 | export const Default = DefaultTemplate.bind({}); 30 | 31 | Default.args = { 32 | type: BlockType.Header, 33 | ...getDefaultStoryArgs(), 34 | } as unknown as HeaderModel; 35 | -------------------------------------------------------------------------------- /src/blocks/Header/__tests__/Header.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('Header', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/Header/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as HeaderStories from '../__stories__/Header.stories'; 4 | 5 | export const {Default} = composeStories(HeaderStories); 6 | -------------------------------------------------------------------------------- /src/blocks/Header/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | import {BlockType} from '../../models/common'; 4 | import {BlogBlockBase} from '../../schema/common'; 5 | 6 | const { 7 | blocks: {HeaderProperties}, 8 | common: {BlockBaseProps}, 9 | } = validators; 10 | 11 | export const Header = { 12 | [BlockType.Header]: { 13 | type: 'object', 14 | additionalProperties: false, 15 | properties: { 16 | ...BlockBaseProps, 17 | ...BlogBlockBase, 18 | ...HeaderProperties, 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/blocks/Layout/Layout.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins'; 2 | @import '../../../styles/variables'; 3 | 4 | $block: '.#{$namespace}layout'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | margin: 0; 10 | padding: 0; 11 | 12 | &__left-col { 13 | @include add-specificity(&) { 14 | padding: 0; 15 | } 16 | } 17 | 18 | &__right-col { 19 | @include add-specificity(&) { 20 | padding: 0; 21 | } 22 | } 23 | 24 | &__row { 25 | padding: 0; 26 | margin: 0; 27 | } 28 | 29 | &__item { 30 | margin: 0; 31 | padding: 0 $indentXXXS; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/blocks/Layout/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------ | :-------------------- | :------- | :---------------------------- | 3 | | fullWidth | `boolean` | `false` | Full width flag | 4 | | mobileOrder | `reverse` | `false` | Mobile order for layout value | 5 | | children | `ReactElement[]` | `true` | Children element | 6 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 7 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 8 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 9 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 10 | -------------------------------------------------------------------------------- /src/blocks/Layout/__snapshots__/Layout.visual.test.tsx-snapshots/Layout-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Layout/__snapshots__/Layout.visual.test.tsx-snapshots/Layout-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Layout/__snapshots__/Layout.visual.test.tsx-snapshots/Layout-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Layout/__snapshots__/Layout.visual.test.tsx-snapshots/Layout-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Layout/__stories__/Layout.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as LayoutStories from './Layout.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/Layout/__stories__/Layout.stories.tsx: -------------------------------------------------------------------------------- 1 | import {Block, PageConstructor} from '@gravity-ui/page-constructor'; 2 | import type {Meta, StoryFn} from '@storybook/react'; 3 | 4 | import {blockMockData, getDefaultStoryArgs} from '../../../../.mocks/utils'; 5 | import customBlocks from '../../../constructor/blocksMap'; 6 | import {PostPageContext} from '../../../contexts/PostPageContext'; 7 | import {LayoutProps} from '../../../models/blocks'; 8 | import {BlockType} from '../../../models/common'; 9 | import {Layout} from '../Layout'; 10 | 11 | import layoutBlock from '../../../../.mocks/layoutBlock.json'; 12 | 13 | export default { 14 | title: 'Blocks/Layout', 15 | component: Layout, 16 | args: { 17 | theme: 'light', 18 | }, 19 | } as Meta; 20 | 21 | type LayoutModel = { 22 | type: BlockType.Layout; 23 | } & LayoutProps; 24 | 25 | const DefaultTemplate: StoryFn = (args) => ( 26 | 27 | 28 | 29 | ); 30 | 31 | export const Default = DefaultTemplate.bind({}); 32 | 33 | Default.args = { 34 | type: BlockType.Layout, 35 | ...getDefaultStoryArgs(), 36 | children: layoutBlock.children, 37 | } as unknown as LayoutModel; 38 | -------------------------------------------------------------------------------- /src/blocks/Layout/__tests__/Layout.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('Layout', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/Layout/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as LayoutStories from '../__stories__/Layout.stories'; 4 | 5 | export const {Default} = composeStories(LayoutStories); 6 | -------------------------------------------------------------------------------- /src/blocks/Layout/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | const { 4 | common: {BlockBaseProps, ChildrenProps}, 5 | } = validators; 6 | 7 | import {BlockType} from '../../models/common'; 8 | import {BlogBlockBase} from '../../schema/common'; 9 | 10 | export const Layout = { 11 | [BlockType.Layout]: { 12 | type: 'object', 13 | additionalProperties: false, 14 | required: ['children'], 15 | properties: { 16 | ...BlockBaseProps, 17 | ...BlogBlockBase, 18 | children: ChildrenProps, 19 | mobileOrder: { 20 | type: 'string', 21 | enum: ['reverse', 'straight'], 22 | }, 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/blocks/Media/Media.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins'; 2 | @import '../../../styles/variables'; 3 | 4 | $block: '.#{$namespace}media'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | &__text-content { 10 | padding-top: $indentXS; 11 | } 12 | 13 | &__container { 14 | position: relative; 15 | height: 100%; 16 | } 17 | 18 | &__border { 19 | position: relative; 20 | height: 100%; 21 | width: 100%; 22 | border-radius: calc(var(--bc-border-radius) + 1px); 23 | border: 1px solid var(--g-color-line-generic); 24 | overflow: hidden; 25 | } 26 | 27 | &__content { 28 | position: relative; 29 | top: 0; 30 | right: 0; 31 | width: 100%; 32 | height: 100%; 33 | overflow: hidden; 34 | object-fit: cover; 35 | 36 | border-radius: var(--bc-border-radius); 37 | } 38 | 39 | &__video { 40 | height: 100%; 41 | 42 | > video { 43 | width: 100%; 44 | height: auto; 45 | border-radius: var(--bc-border-radius); 46 | } 47 | } 48 | 49 | &__image { 50 | width: 100%; 51 | height: 100%; 52 | object-fit: cover; 53 | border-radius: var(--bc-border-radius); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/blocks/Media/Media.tsx: -------------------------------------------------------------------------------- 1 | import {Media as PCMedia, YFMWrapper} from '@gravity-ui/page-constructor'; 2 | 3 | import {Wrapper} from '../../components/Wrapper/Wrapper'; 4 | import {MediaProps} from '../../models/blocks'; 5 | import {PaddingsDirections} from '../../models/paddings'; 6 | import {block} from '../../utils/cn'; 7 | 8 | import './Media.scss'; 9 | 10 | const b = block('media'); 11 | 12 | export const Media = ({text, paddingTop, paddingBottom, ...mediaProps}: MediaProps) => ( 13 | 20 |
21 | 27 |
28 | {text && ( 29 |
30 | 38 |
39 | )} 40 |
41 | ); 42 | -------------------------------------------------------------------------------- /src/blocks/Media/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------- | :----------------------------- | :------- | :------------------------ | 3 | | className | `string` | `false` | Component className | 4 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 5 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 6 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 7 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 8 | | text | `string` | `false` | Text | 9 | | previewImg | `string` | `false` | Image for preview | 10 | | youtube | `MediaProps['youtube']` | `false` | Youtube media content | 11 | | videoIframe | `MediaProps['videoIframe']` | `false` | Video iframe src | 12 | | image | `MediaProps['image']` | `false` | Image media content | 13 | | video | `MediaProps['video']` | `false` | Video media content | 14 | | dataLens | `MediaProps['dataLens']` | `false` | dataLens media content | 15 | | videoMicrodata | `MediaProps['videoMicrodata']` | `false` | microdata for video media | 16 | 17 | To get more information about `MediaProps` see this [page](https://preview.gravity-ui.com/page-constructor/?path=/docs/blocks-media--default) 18 | -------------------------------------------------------------------------------- /src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Media/__stories__/Media.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as MediaStories from './Media.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/Media/__stories__/Media.stories.tsx: -------------------------------------------------------------------------------- 1 | import {Meta, StoryFn} from '@storybook/react'; 2 | 3 | import { 4 | dataLensSrc, 5 | getDefaultStoryArgs, 6 | getVideoStoryArgs, 7 | youtubeSrc, 8 | } from '../../../../.mocks/utils'; 9 | import {MediaProps} from '../../../models/blocks'; 10 | import {BlockType} from '../../../models/common'; 11 | import {Media} from '../Media'; 12 | 13 | export default { 14 | title: 'Blocks/Media', 15 | component: Media, 16 | args: { 17 | theme: 'light', 18 | }, 19 | } as Meta; 20 | 21 | type MediaModel = { 22 | type: BlockType.Media; 23 | } & MediaProps; 24 | 25 | const DefaultTemplate: StoryFn = (args) => ( 26 |
27 | 28 | 29 | 30 | 31 |
32 | ); 33 | 34 | export const Default = DefaultTemplate.bind({}); 35 | 36 | Default.args = { 37 | type: BlockType.Media, 38 | ...getDefaultStoryArgs(), 39 | } as MediaModel; 40 | -------------------------------------------------------------------------------- /src/blocks/Media/__tests__/Media.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | const MEDIA_DELAY = 10 * 1000; 6 | 7 | test.describe('Media', () => { 8 | test.skip('render stories ', async ({mount, expectScreenshot, delay, page}) => { 9 | await mount(); 10 | await delay(MEDIA_DELAY); 11 | await expectScreenshot({ 12 | skipTheme: 'dark', 13 | mask: [ 14 | page.locator('.bc-media__video'), 15 | page.locator('.pc-media-component-data-lens__wrap'), 16 | ], 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/blocks/Media/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as MediaStories from '../__stories__/Media.stories'; 4 | 5 | export const {Default} = composeStories(MediaStories); 6 | -------------------------------------------------------------------------------- /src/blocks/Media/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | import {BlockType} from '../../models/common'; 4 | import {BlogBlockBase} from '../../schema/common'; 5 | 6 | const { 7 | common: {BlockBaseProps, MediaProps}, 8 | } = validators; 9 | 10 | export const Media = { 11 | [BlockType.Media]: { 12 | type: 'object', 13 | additionalProperties: false, 14 | properties: { 15 | ...BlockBaseProps, 16 | ...BlogBlockBase, 17 | ...MediaProps, 18 | text: { 19 | type: 'string', 20 | contentType: 'text', 21 | }, 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/blocks/Meta/Meta.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables.scss'; 2 | 3 | $block: '.#{$namespace}meta'; 4 | 5 | #{$block} { 6 | $root: &; 7 | 8 | padding: 0; 9 | margin: 0; 10 | 11 | &__breadcrumbs { 12 | display: inline-block; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/blocks/Meta/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------ | :----------------------- | :------- | :------------------ | 3 | | locale | `string` | `true` | Locale value | 4 | | theme | `light - dark - default` | `false` | Content theme | 5 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 6 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 7 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 8 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 9 | -------------------------------------------------------------------------------- /src/blocks/Meta/__snapshots__/Meta.visual.test.tsx-snapshots/Meta-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Meta/__snapshots__/Meta.visual.test.tsx-snapshots/Meta-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Meta/__snapshots__/Meta.visual.test.tsx-snapshots/Meta-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Meta/__snapshots__/Meta.visual.test.tsx-snapshots/Meta-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Meta/__stories__/Meta.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as MetaStories from './Meta.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/Meta/__stories__/Meta.stories.tsx: -------------------------------------------------------------------------------- 1 | import {Block, PageConstructor} from '@gravity-ui/page-constructor'; 2 | import {Meta, StoryFn} from '@storybook/react'; 3 | 4 | import {blockMockData} from '../../../../.mocks/utils'; 5 | import customBlocks from '../../../constructor/blocksMap'; 6 | import {PostPageContext} from '../../../contexts/PostPageContext'; 7 | import {MetaProps} from '../../../models/blocks'; 8 | import {BlockType} from '../../../models/common'; 9 | import {Meta as MetaBlock} from '../Meta'; 10 | 11 | export default { 12 | title: 'Blocks/Meta', 13 | component: MetaBlock, 14 | args: { 15 | theme: 'light', 16 | }, 17 | } as Meta; 18 | 19 | type MetaModel = { 20 | type: BlockType.Meta; 21 | } & MetaProps; 22 | 23 | const DefaultTemplate: StoryFn = (args) => ( 24 | 25 | 26 | 27 | ); 28 | 29 | export const Default = DefaultTemplate.bind({}); 30 | 31 | Default.args = { 32 | type: BlockType.Meta, 33 | paddingBottom: 'l', 34 | paddingTop: 'l', 35 | }; 36 | -------------------------------------------------------------------------------- /src/blocks/Meta/__tests__/Meta.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('Meta', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/Meta/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as MetaStories from '../__stories__/Meta.stories'; 4 | 5 | export const {Default} = composeStories(MetaStories); 6 | -------------------------------------------------------------------------------- /src/blocks/Meta/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | const { 4 | common: {BlockBaseProps}, 5 | } = validators; 6 | 7 | import {BlockType} from '../../models/common'; 8 | import {BlogBlockBase} from '../../schema/common'; 9 | 10 | export const Meta = { 11 | [BlockType.Meta]: { 12 | type: 'object', 13 | additionalProperties: false, 14 | properties: { 15 | ...BlockBaseProps, 16 | ...BlogBlockBase, 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/blocks/Suggest/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------ | :-------------------- | :------- | :------------------ | 3 | | className | `string` | `false` | Component className | 4 | | posts | `PostData[]` | `true` | Suggest posts data | 5 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 6 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 7 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 8 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 9 | -------------------------------------------------------------------------------- /src/blocks/Suggest/Suggest.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {SliderBlock} from '@gravity-ui/page-constructor'; 4 | 5 | import {PostCard} from '../../components/PostCard/PostCard'; 6 | import {Wrapper} from '../../components/Wrapper/Wrapper'; 7 | import {PostPageContext} from '../../contexts/PostPageContext'; 8 | import {Keyset, i18n} from '../../i18n'; 9 | import {SuggestProps} from '../../models/blocks'; 10 | import {PaddingsDirections} from '../../models/paddings'; 11 | import {prepareAnalyticsEvent} from '../../utils/common'; 12 | import {DefaultGoalIds} from '../../constants'; 13 | import {AnalyticsCounter} from '../../counters/utils'; 14 | 15 | const suggestGoals = prepareAnalyticsEvent({ 16 | name: DefaultGoalIds.suggest, 17 | counter: AnalyticsCounter.CrossSite, 18 | }); 19 | 20 | /** 21 | * Suggested posts block 22 | * 23 | * @param posts - suggested posts list 24 | * @param paddingTop - padding top code 25 | * @param paddingBottom - padding bottom code 26 | * 27 | * @returns -jsx 28 | */ 29 | export const Suggest = ({paddingTop = 'l', paddingBottom = 'l'}: SuggestProps) => { 30 | const {suggestedPosts} = React.useContext(PostPageContext); 31 | 32 | if (suggestedPosts.length === 0) { 33 | return null; 34 | } 35 | 36 | return ( 37 | 43 | 47 | {suggestedPosts.map((post) => ( 48 | 49 | ))} 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/blocks/Suggest/__snapshots__/Suggest.visual.test.tsx-snapshots/Suggest-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Suggest/__snapshots__/Suggest.visual.test.tsx-snapshots/Suggest-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/Suggest/__snapshots__/Suggest.visual.test.tsx-snapshots/Suggest-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/Suggest/__snapshots__/Suggest.visual.test.tsx-snapshots/Suggest-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/Suggest/__stories__/Suggest.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as SuggestStories from './Suggest.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/Suggest/__stories__/Suggest.stories.tsx: -------------------------------------------------------------------------------- 1 | import {Block, PageConstructor} from '@gravity-ui/page-constructor'; 2 | import {Meta, StoryFn} from '@storybook/react'; 3 | 4 | import {blockMockData} from '../../../../.mocks/utils'; 5 | import customBlocks from '../../../constructor/blocksMap'; 6 | import {PostPageContext} from '../../../contexts/PostPageContext'; 7 | import {SuggestProps} from '../../../models/blocks'; 8 | import {BlockType} from '../../../models/common'; 9 | import {Suggest} from '../Suggest'; 10 | 11 | export default { 12 | title: 'Blocks/Suggest', 13 | component: Suggest, 14 | args: { 15 | theme: 'light', 16 | }, 17 | } as Meta; 18 | 19 | type SuggestModel = { 20 | type: BlockType.Suggest; 21 | } & SuggestProps; 22 | 23 | const DefaultTemplate: StoryFn = (args) => ( 24 | 25 | 26 | 27 | ); 28 | 29 | export const Default = DefaultTemplate.bind({}); 30 | 31 | Default.args = { 32 | type: BlockType.Suggest, 33 | paddingBottom: 'l', 34 | paddingTop: 'l', 35 | }; 36 | -------------------------------------------------------------------------------- /src/blocks/Suggest/__tests__/Suggest.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('Suggest', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/Suggest/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as SuggestStories from '../__stories__/Suggest.stories'; 4 | 5 | export const {Default} = composeStories(SuggestStories); 6 | -------------------------------------------------------------------------------- /src/blocks/Suggest/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | const { 4 | common: {BlockBaseProps}, 5 | } = validators; 6 | 7 | import {BlockType} from '../../models/common'; 8 | import {BlogBlockBase} from '../../schema/common'; 9 | 10 | export const Suggest = { 11 | [BlockType.Suggest]: { 12 | type: 'object', 13 | additionalProperties: false, 14 | properties: { 15 | ...BlockBaseProps, 16 | ...BlogBlockBase, 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/blocks/YFM/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------ | :-------------------- | :------- | :------------------ | 3 | | text | `string` | `true` | Text | 4 | | paddingTop | `xs - s - m - l - xl` | `false` | Top padding size | 5 | | paddingBottom | `xs - s - m - l - xl` | `false` | Bottom padding size | 6 | | paddingRight | `xs - s - m - l - xl` | `false` | Right padding size | 7 | | paddingLeft | `xs - s - m - l - xl` | `false` | Left padding size | 8 | -------------------------------------------------------------------------------- /src/blocks/YFM/YFM.tsx: -------------------------------------------------------------------------------- 1 | import {YFMWrapper} from '@gravity-ui/page-constructor'; 2 | 3 | import {Wrapper} from '../../components/Wrapper/Wrapper'; 4 | import {YFMProps} from '../../models/blocks'; 5 | import {PaddingsDirections} from '../../models/paddings'; 6 | import {cn} from '../../utils/cn'; 7 | import {getQaAttributes} from '../../utils/common'; 8 | 9 | const b = cn('yfm'); 10 | 11 | export const YFM = (props: YFMProps) => { 12 | const {text, paddingTop, paddingBottom, qa} = props; 13 | const qaAttributes = getQaAttributes(qa); 14 | 15 | return ( 16 | 23 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/blocks/YFM/__snapshots__/YFM.visual.test.tsx-snapshots/YFM-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/YFM/__snapshots__/YFM.visual.test.tsx-snapshots/YFM-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/blocks/YFM/__snapshots__/YFM.visual.test.tsx-snapshots/YFM-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/blocks/YFM/__snapshots__/YFM.visual.test.tsx-snapshots/YFM-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/blocks/YFM/__stories__/YFM.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as YFMStories from './YFM.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/blocks/YFM/__stories__/YFM.stories.tsx: -------------------------------------------------------------------------------- 1 | import {Block, PageConstructor} from '@gravity-ui/page-constructor'; 2 | import type {Meta, StoryFn} from '@storybook/react'; 3 | 4 | import {blockMockData, getDefaultStoryArgs} from '../../../../.mocks/utils'; 5 | import customBlocks from '../../../constructor/blocksMap'; 6 | import {PostPageContext} from '../../../contexts/PostPageContext'; 7 | import {YFMProps} from '../../../models/blocks'; 8 | import {BlockType} from '../../../models/common'; 9 | import {YFM} from '../YFM'; 10 | 11 | export default { 12 | title: 'Blocks/YFM', 13 | component: YFM, 14 | args: { 15 | theme: 'light', 16 | }, 17 | } as Meta; 18 | 19 | type YFMModel = { 20 | type: BlockType.YFM; 21 | } & YFMProps; 22 | 23 | const DefaultTemplate: StoryFn = (args) => ( 24 | 25 | 26 | 27 | ); 28 | 29 | export const Default = DefaultTemplate.bind({}); 30 | 31 | Default.args = { 32 | type: BlockType.YFM, 33 | ...getDefaultStoryArgs(), 34 | text: '

Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

', 35 | } as YFMModel; 36 | -------------------------------------------------------------------------------- /src/blocks/YFM/__tests__/YFM.test.tsx: -------------------------------------------------------------------------------- 1 | import {render, screen} from '@testing-library/react'; 2 | 3 | import {PADDING_SIZES} from '../../../../test-utils/constants'; 4 | import {testPaddingBottom, testPaddingTop} from '../../../../test-utils/shared/common'; 5 | import {YFMProps} from '../../../models/blocks'; 6 | import {PaddingSize} from '../../../models/paddings'; 7 | import {getQaAttributes} from '../../../utils/common'; 8 | import {YFM} from '../YFM'; 9 | 10 | const yfmProps = { 11 | text: 'YFM block', 12 | qa: 'yfm-block', 13 | }; 14 | 15 | const qaAttributes = getQaAttributes(yfmProps.qa); 16 | 17 | describe('YFM', () => { 18 | test('render yfm by default', async () => { 19 | render(); 20 | const yfm = screen.getByText(yfmProps.text); 21 | expect(yfm).toHaveClass('yfm'); 22 | }); 23 | 24 | test.each(new Array(...PADDING_SIZES))( 25 | 'render with given "%s" paddingTop size', 26 | (size: PaddingSize) => { 27 | testPaddingTop({ 28 | component: YFM, 29 | props: {...yfmProps, paddingTop: size}, 30 | options: {qaId: qaAttributes.wrapper}, 31 | }); 32 | }, 33 | ); 34 | 35 | test.each(new Array(...PADDING_SIZES))( 36 | 'render with given "%s" paddingBottom size', 37 | (size: PaddingSize) => { 38 | testPaddingBottom({ 39 | component: YFM, 40 | props: {...yfmProps, paddingBottom: size}, 41 | options: {qaId: qaAttributes.wrapper}, 42 | }); 43 | }, 44 | ); 45 | }); 46 | -------------------------------------------------------------------------------- /src/blocks/YFM/__tests__/YFM.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default} from './helpers'; 4 | 5 | test.describe('YFM', () => { 6 | test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { 7 | await mount(); 8 | await defaultDelay(); 9 | await expectScreenshot({skipTheme: 'dark'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/blocks/YFM/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as YFMStories from '../__stories__/YFM.stories'; 4 | 5 | export const {Default} = composeStories(YFMStories); 6 | -------------------------------------------------------------------------------- /src/blocks/YFM/schema.ts: -------------------------------------------------------------------------------- 1 | import {validators} from '@gravity-ui/page-constructor'; 2 | 3 | const { 4 | common: {BlockBaseProps}, 5 | } = validators; 6 | 7 | import {BlockType} from '../../models/common'; 8 | import {BlogBlockBase} from '../../schema/common'; 9 | 10 | export const YFM = { 11 | [BlockType.YFM]: { 12 | type: 'object', 13 | additionalProperties: false, 14 | required: ['text'], 15 | properties: { 16 | ...BlockBaseProps, 17 | ...BlogBlockBase, 18 | text: { 19 | type: 'string', 20 | contentType: 'yfm', 21 | }, 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/blocks/constants.ts: -------------------------------------------------------------------------------- 1 | export const CONTENT_DEFAULT_SIZE = 's'; 2 | 3 | export const CONTENT_DEFAULT_COL_SIZES = {all: 12, md: 12}; 4 | 5 | export const CONTENT_DEFAULT_THEME = 'default'; 6 | 7 | export const DEFAULT_PAGE = 1; 8 | 9 | export const DEFAULT_ROWS_PER_PAGE = 12; 10 | -------------------------------------------------------------------------------- /src/components/FeedHeader/FeedHeader.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins.scss'; 2 | @import '../../../styles/variables.scss'; 3 | 4 | $block: '.#{$namespace}feed-header'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | margin-top: $indentXS; 10 | 11 | &__header { 12 | padding: $indentXS 0; 13 | } 14 | 15 | &__content { 16 | position: relative; 17 | height: 100%; 18 | padding-bottom: 0; 19 | 20 | &_offset_large { 21 | padding: calc(#{$indentXXXL} - #{$indentXXL}) 0 $indentXS; 22 | margin-bottom: -$indentL; 23 | } 24 | 25 | &_theme_dark { 26 | @include add-specificity(&) { 27 | #{$root}__title, 28 | #{$root}__description * { 29 | color: var(--g-color-text-light-primary); 30 | } 31 | } 32 | } 33 | } 34 | 35 | &_has-background { 36 | height: calc(100% + #{$indentXXL}); 37 | } 38 | 39 | &__content { 40 | position: relative; 41 | z-index: 10; 42 | } 43 | 44 | &__background, 45 | &__background-media { 46 | z-index: 5; 47 | } 48 | 49 | &__background { 50 | position: absolute; 51 | top: 0; 52 | left: 50%; 53 | width: 1440px; 54 | transform: translateX(-50%); 55 | max-width: 98vw; 56 | height: 100%; 57 | border-radius: var(--bc-border-radius); 58 | } 59 | 60 | @media (max-width: map-get($gridBreakpoints, 'md')) { 61 | &_has-background { 62 | #{$root}__background-img { 63 | display: none; 64 | } 65 | 66 | #{$root}__content_vertical-offset { 67 | &_s, 68 | &_m, 69 | &_l, 70 | &_xl { 71 | padding: calc(#{$indentXXL} - #{$indentXS}) 0; 72 | } 73 | } 74 | } 75 | } 76 | 77 | .mobile & { 78 | &_has-background { 79 | #{$root}__title { 80 | @include text-size(display-2); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/FeedHeader/components/CustomSelectOption/CustomSelectOption.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../styles/mixins.scss'; 2 | @import '../../../../../styles/variables.scss'; 3 | 4 | $block: '.#{$namespace}feed-custom-select-option'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | @include text-size(body-2); 10 | 11 | display: flex; 12 | 13 | &__icon { 14 | margin-right: 6px; 15 | 16 | & > svg { 17 | width: 20px; 18 | height: 20px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/FeedHeader/components/CustomSelectOption/CustomSelectOption.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {SelectOption as SelectOptionType} from '@gravity-ui/uikit'; 4 | 5 | import {block} from '../../../../utils/cn'; 6 | 7 | import './CustomSelectOption.scss'; 8 | 9 | const b = block('feed-custom-select-option'); 10 | 11 | export type CustomSelectOptionProps = { 12 | data: { 13 | icon?: React.ReactElement; 14 | } & SelectOptionType; 15 | }; 16 | 17 | export const CustomSelectOption = ({data}: CustomSelectOptionProps) => ( 18 |
19 | {data.icon} 20 | {data.content} 21 |
22 | ); 23 | -------------------------------------------------------------------------------- /src/components/MetaWrapper/MetaWrapper.tsx: -------------------------------------------------------------------------------- 1 | import {Helmet} from 'react-helmet'; 2 | 3 | import {MetaProps} from '../../models/common'; 4 | 5 | /** 6 | * Wrapper on meta data of page 7 | * 8 | * @param needHelmetWrapper - component needs helmet wrapper 9 | * @param metaComponent - meta data component 10 | * 11 | * @returns jsx 12 | */ 13 | export const MetaWrapper = ({needHelmetWrapper = false, metaComponent}: MetaProps) => 14 | needHelmetWrapper ? {metaComponent} : metaComponent; 15 | -------------------------------------------------------------------------------- /src/components/Paginator/Paginator.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins.scss'; 2 | @import '../../../styles/variables.scss'; 3 | 4 | $block: '.#{$namespace}paginator'; 5 | 6 | $itemDimensions: 44px; 7 | 8 | #{$block} { 9 | $block: &; 10 | 11 | @include reset-list-style(); 12 | 13 | display: flex; 14 | align-items: center; 15 | 16 | &__item { 17 | $item: &; 18 | 19 | display: inline-flex; 20 | align-items: center; 21 | justify-content: center; 22 | 23 | min-width: $itemDimensions; 24 | min-height: $itemDimensions; 25 | margin-left: 4px; 26 | 27 | color: var(--g-color-text-primary); 28 | @include text-size(body-2); 29 | 30 | &_type { 31 | &_page { 32 | --bc-border-radius: 10px; 33 | width: $itemDimensions; 34 | height: $itemDimensions; 35 | 36 | cursor: pointer; 37 | border-radius: var(--bc-border-radius); 38 | 39 | &:hover { 40 | background: var(--g-color-base-simple-hover); 41 | } 42 | 43 | &#{$item}_active { 44 | background: var(--g-color-base-simple-hover); 45 | cursor: default; 46 | } 47 | } 48 | } 49 | } 50 | 51 | &__pagination { 52 | display: flex; 53 | flex-direction: row; 54 | justify-content: center; 55 | align-items: center; 56 | width: 100%; 57 | 58 | @media (max-width: map-get($gridBreakpoints, 'sm')) { 59 | flex-direction: column; 60 | } 61 | } 62 | 63 | &__pagination-block { 64 | display: flex; 65 | flex-direction: row; 66 | justify-content: center; 67 | align-items: center; 68 | margin-bottom: 4px; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/Paginator/components/NavigationButton.tsx: -------------------------------------------------------------------------------- 1 | import {Keyset, i18n} from '../../../i18n'; 2 | import {block} from '../../../utils/cn'; 3 | import {ArrowType} from '../types'; 4 | 5 | import '../Paginator.scss'; 6 | 7 | const b = block('paginator'); 8 | 9 | export type NavigationButtonProps = { 10 | arrowType: ArrowType; 11 | disabled?: boolean; 12 | }; 13 | 14 | export const NavigationButton = ({arrowType, disabled}: NavigationButtonProps) => 15 | disabled ? null : ( 16 |
17 | {arrowType === ArrowType.Prev ? i18n(Keyset.ButtonBegin) : i18n(Keyset.ButtonFarther)} 18 |
19 | ); 20 | -------------------------------------------------------------------------------- /src/components/Paginator/types.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import type {NoStrictEntityMods} from '@bem-react/classname'; 4 | 5 | import type {ClassNameProps, Query} from '../../models/common'; 6 | 7 | export interface PaginatorItemProps { 8 | key: string | ArrowType; 9 | dataKey: string | ArrowType; 10 | mods: NoStrictEntityMods; 11 | content: React.ReactNode; 12 | queryParams: Query; 13 | onClick?: (key: number | ArrowType) => void; 14 | loading?: boolean; 15 | index: number; 16 | } 17 | 18 | export type PaginatorProps = { 19 | page: number; 20 | totalItems: number; 21 | itemsPerPage: number; 22 | maxPages: number; 23 | onPageChange: (page: number) => void; 24 | pageCountForShowSupportButtons?: number; 25 | queryParams: Query; 26 | } & ClassNameProps; 27 | 28 | export enum ArrowType { 29 | Prev = 'prev', 30 | Next = 'next', 31 | } 32 | 33 | export type GetPageConfigParams = { 34 | page: number; 35 | pagesCount: number; 36 | queryParams: Query; 37 | handlePageClick: (key: number | ArrowType) => void; 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/Paginator/utils.ts: -------------------------------------------------------------------------------- 1 | import {GetPageConfigParams, PaginatorItemProps, PaginatorProps} from './types'; 2 | 3 | const MAX_VISIBLE_PAGES = 5; 4 | 5 | export const getPageConfigs = ({ 6 | page, 7 | queryParams, 8 | pagesCount, 9 | handlePageClick, 10 | }: GetPageConfigParams) => { 11 | const paginatorItems: Array = []; 12 | // it is calculating the middle of visible pages below 13 | const pageOffset = (MAX_VISIBLE_PAGES - 1) / 2; 14 | let startPage = page - pageOffset; 15 | let endPage = page + pageOffset; 16 | 17 | if (startPage < 1) { 18 | endPage = page + pageOffset - startPage + 1; 19 | startPage = 1; 20 | } 21 | 22 | endPage = Math.min(endPage, pagesCount); 23 | 24 | for (let i = startPage; i <= endPage; i++) { 25 | paginatorItems.push({ 26 | key: String(i), 27 | dataKey: String(i), 28 | index: i, 29 | mods: {type: 'page', active: page === i}, 30 | queryParams, 31 | onClick: handlePageClick, 32 | content: i, 33 | }); 34 | } 35 | 36 | return paginatorItems; 37 | }; 38 | 39 | export const getPagesCount = ( 40 | props: Pick, 41 | ) => { 42 | const totalPages = Math.ceil(props.totalItems / props.itemsPerPage); 43 | 44 | return Math.min(totalPages, props.maxPages); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/PostInfo/components/Date.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {LocaleContext} from '../../../contexts/LocaleContext'; 4 | import {PostCardSize, QAProps} from '../../../models/common'; 5 | import {block} from '../../../utils/cn'; 6 | import {format} from '../../../utils/date'; 7 | 8 | import '../PostInfo.scss'; 9 | 10 | const b = block('post-info'); 11 | 12 | type DateProps = QAProps & { 13 | date: string | number; 14 | size?: PostCardSize; 15 | id?: string; 16 | }; 17 | 18 | export const Date = ({date, size = PostCardSize.SMALL, id, qa}: DateProps) => { 19 | const {locale} = React.useContext(LocaleContext); 20 | 21 | return ( 22 |
23 | {format(date, 'longDate', locale?.code)} 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/PostInfo/components/ReadingTime.tsx: -------------------------------------------------------------------------------- 1 | import {Icon} from '@gravity-ui/uikit'; 2 | 3 | import {Keyset, i18n} from '../../../i18n'; 4 | import {Time} from '../../../icons/Time'; 5 | import {block} from '../../../utils/cn'; 6 | 7 | import {QAProps} from '../../../models/common'; 8 | import '../PostInfo.scss'; 9 | 10 | const b = block('post-info'); 11 | 12 | const ICON_SIZE = 16; 13 | 14 | type ReadingTimeProps = QAProps & { 15 | readingTime: number; 16 | size?: 's' | 'm'; 17 | id?: string; 18 | }; 19 | 20 | export const ReadingTime = ({readingTime, size = 's', id, qa}: ReadingTimeProps) => ( 21 |
22 | 23 | 24 | 25 | {i18n(Keyset.ContextReadingTime, {count: readingTime})} 26 |
27 | ); 28 | -------------------------------------------------------------------------------- /src/components/Posts/Posts.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins.scss'; 2 | @import '../../../styles/variables.scss'; 3 | 4 | $block: '.#{$namespace}posts'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | position: relative; 10 | 11 | &__cards-container, 12 | &__pinned-container { 13 | padding-top: $indentSM; 14 | scroll-margin: $indentL; 15 | } 16 | 17 | &__cards-container { 18 | &_isLoading { 19 | opacity: 0.7; 20 | } 21 | } 22 | 23 | &__pagination { 24 | display: flex; 25 | flex-direction: column; 26 | 27 | align-items: center; 28 | justify-content: center; 29 | 30 | padding-top: $indentL; 31 | padding-bottom: $indentXL; 32 | } 33 | 34 | &__more-button { 35 | margin-bottom: $indentXXS; 36 | } 37 | 38 | &__error-show-more { 39 | display: flex; 40 | flex-direction: column; 41 | align-items: center; 42 | justify-content: center; 43 | 44 | color: var(--g-color-base-danger-medium); 45 | padding-bottom: $indentXXS; 46 | } 47 | 48 | &__paginator { 49 | padding-top: $indentXXS; 50 | } 51 | 52 | &__loaderContainer { 53 | z-index: 6; 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | width: 70%; 58 | height: 100%; 59 | 60 | background: linear-gradient( 61 | 90deg, 62 | rgba(255, 255, 255, 0), 63 | rgba(255, 255, 255, 0.3), 64 | rgba(255, 255, 255, 0.5), 65 | rgba(255, 255, 255, 0.3), 66 | rgba(255, 255, 255, 0) 67 | ); 68 | 69 | animation: shimmer 2s infinite linear; 70 | } 71 | } 72 | 73 | @keyframes shimmer { 74 | from { 75 | transform: translateX(-200%); 76 | } 77 | to { 78 | transform: translateX(300%); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/PostsEmpty/PostsEmpty.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins.scss'; 2 | @import '../../../styles/variables.scss'; 3 | 4 | $block: '.#{$namespace}posts-empty'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | &__container { 10 | display: flex; 11 | flex-direction: column; 12 | 13 | justify-content: center; 14 | align-items: center; 15 | } 16 | 17 | &__title, 18 | &__subtitle { 19 | text-align: center; 20 | word-wrap: break-word; 21 | max-width: 400px; 22 | width: 100%; 23 | } 24 | 25 | &__title { 26 | margin-top: $indentSM; 27 | @include text-size(display-2); 28 | font-weight: var(--g-text-accent-font-weight); 29 | } 30 | 31 | &__subtitle { 32 | margin-top: $indentXS; 33 | @include text-size(body-3); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/PostsEmpty/PostsEmpty.tsx: -------------------------------------------------------------------------------- 1 | import {Keyset, i18n} from '../../i18n'; 2 | import {block} from '../../utils/cn'; 3 | 4 | import './PostsEmpty.scss'; 5 | 6 | const b = block('posts-empty'); 7 | 8 | export const PostsEmpty = () => ( 9 |
10 |
{i18n(Keyset.TitleEmptyContainer)}
11 |
{i18n(Keyset.ContextEmptyContainer)}
12 |
13 | ); 14 | -------------------------------------------------------------------------------- /src/components/PostsError/PostError.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins.scss'; 2 | @import '../../../styles/variables.scss'; 3 | 4 | $block: '.#{$namespace}posts-error'; 5 | 6 | #{$block} { 7 | $root: &; 8 | 9 | &__container { 10 | display: flex; 11 | flex-direction: column; 12 | 13 | justify-content: center; 14 | align-items: center; 15 | padding-top: $indentSM; 16 | padding-bottom: $indentXL; 17 | } 18 | 19 | &__title, 20 | &__subtitle { 21 | text-align: center; 22 | word-wrap: break-word; 23 | max-width: 400px; 24 | width: 100%; 25 | } 26 | 27 | &__title { 28 | margin-top: $indentSM; 29 | @include text-size(display-2); 30 | font-weight: var(--g-text-accent-font-weight); 31 | } 32 | 33 | &__subtitle { 34 | margin-top: $indentXS; 35 | @include text-size(body-3); 36 | } 37 | 38 | &__button { 39 | padding: $indentSM 0 $indentL; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/PostsError/PostsError.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from '@gravity-ui/uikit'; 2 | 3 | import {Keyset, i18n} from '../../i18n'; 4 | import {block} from '../../utils/cn'; 5 | 6 | import './PostError.scss'; 7 | 8 | const b = block('posts-error'); 9 | 10 | type PostsErrorContainerProps = { 11 | onButtonClick?: () => void | Promise; 12 | }; 13 | 14 | export const PostsError = ({onButtonClick}: PostsErrorContainerProps) => { 15 | const handleClick = () => (onButtonClick ? onButtonClick() : window.location.reload()); 16 | 17 | return ( 18 |
19 |
{i18n(Keyset.ErrorTitle)}
20 |
{i18n(Keyset.PostLoadError)}
21 |
22 | 25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Prompt/Prompt.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/variables.scss'; 2 | @import '../../../styles/mixins.scss'; 3 | 4 | $prompt: '#{$namespace}prompt'; 5 | $block: '.#{$prompt}'; 6 | 7 | @keyframes #{$prompt}_open { 8 | 0% { 9 | opacity: 0; 10 | transform: translateY(100%); 11 | } 12 | 100% { 13 | opacity: 1; 14 | transform: translateY(0%); 15 | } 16 | } 17 | 18 | @keyframes #{$prompt}_close { 19 | 0% { 20 | opacity: 1; 21 | transform: translateY(0%); 22 | } 23 | 100% { 24 | opacity: 0; 25 | transform: translateY(100%); 26 | } 27 | } 28 | 29 | #{$block} { 30 | $duration: $animationDuration * 2; 31 | 32 | display: flex; 33 | width: 100%; 34 | justify-content: center; 35 | overflow: hidden; // prevent scrollbar 36 | position: fixed; 37 | bottom: 0; 38 | 39 | &:not(#{$block}_mounted) { 40 | display: none; 41 | } 42 | 43 | &__content { 44 | @extend %shadow; 45 | display: flex; 46 | flex-flow: row wrap; 47 | gap: $indentXS; 48 | align-items: center; 49 | margin: $indentSM; 50 | padding: $indentXS $indentS; 51 | border-radius: calc($borderRadius / 2); 52 | background-color: var(--g-color-base-float); 53 | font-size: var(--g-text-body-2-font-size); 54 | } 55 | 56 | &_close { 57 | pointer-events: none; 58 | } 59 | 60 | &_open > #{$block}__content { 61 | opacity: 0; 62 | transform: translateY(100%); 63 | animation: #{$prompt}_open $duration forwards; 64 | } 65 | 66 | &_close > #{$block}__content { 67 | opacity: 1; 68 | transform: translateY(0%); 69 | animation: #{$prompt}_close $duration forwards; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/Prompt/Prompt.tsx: -------------------------------------------------------------------------------- 1 | import {Button, ButtonProps} from '@gravity-ui/uikit'; 2 | 3 | import {useHover} from '../../hooks/useHover'; 4 | import {useOpenCloseTimer} from '../../hooks/useOpenCloseTimer'; 5 | import {block} from '../../utils/cn'; 6 | 7 | import './Prompt.scss'; 8 | 9 | const b = block('prompt'); 10 | 11 | export interface PromptProps { 12 | // Prompt message 13 | text: string; 14 | actions: ButtonProps[]; 15 | // Unix Timestamp in milliseconds when the Prompt opens 16 | openTimestamp?: number; 17 | // Milliseconds to remain visible 18 | openDuration?: number; 19 | className?: string; 20 | theme?: 'grey' | 'beige' | 'white'; 21 | } 22 | 23 | /** 24 | * Popup that appears with text message and button(s) for given `actions`. 25 | * Features: 26 | * - Automatically disappears after `openDuration` in milliseconds 27 | * - `openTimestamp` (`Date.now()`) resets the visible duration 28 | * @returns {JSX|null} 29 | */ 30 | export const Prompt = ({ 31 | text, 32 | actions, 33 | className, 34 | openTimestamp = 0, 35 | openDuration, 36 | theme, 37 | }: PromptProps) => { 38 | const [ref, hovering] = useHover(); 39 | const {open: isOpen} = useOpenCloseTimer(openTimestamp, openDuration); 40 | const open = isOpen || hovering; 41 | const mounted = openTimestamp > 0; 42 | 43 | return ( 44 |
45 |
46 | {text} 47 |
48 | {actions.map(({view = 'action', className: btnClass, ...btnProps}, i) => ( 49 |
57 |
58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/PromptSignIn/PromptSignIn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {Keyset, i18n} from '../../i18n'; 4 | import {Prompt, PromptProps} from '../Prompt/Prompt'; 5 | 6 | export interface PromptSignInProps extends Partial { 7 | onClickSignIn?: React.EventHandler; 8 | } 9 | 10 | /** 11 | * Authentication Popup that appears when user action requires login 12 | * @returns {JSX|null} 13 | */ 14 | export const PromptSignIn = ({ 15 | text = i18n(Keyset.PromptSignInOnLike), 16 | onClickSignIn = () => alert(i18n(Keyset.SignIn)), 17 | actions = [ 18 | { 19 | children: i18n(Keyset.SignIn), 20 | onClick: onClickSignIn, 21 | size: 'l', 22 | }, 23 | ], 24 | ...props 25 | }: PromptSignInProps) => ; 26 | -------------------------------------------------------------------------------- /src/components/PromptSignIn/README.md: -------------------------------------------------------------------------------- 1 | | Property | Type | Required | Description | 2 | | :------------ | :--------------- | :------- | :--------------------------------------------------- | ------- | -------------------- | 3 | | text | `string` | `false` | Prompt message | 4 | | actions | `ButtonProps[]` | `false` | Actions in prompt | 5 | | openTimestamp | `number` | `false` | Unix Timestamp in milliseconds when the Prompt opens | 6 | | openDuration | `number` | `false` | Open duration | 7 | | className | `string` | `false` | Class name | 8 | | theme | `'grey' | 'beige' | 'white'` | `false` | Theme for components | 9 | | onClickSignIn | `SyntheticEvent` | `false` | Event on click | 10 | -------------------------------------------------------------------------------- /src/components/PromptSignIn/__stories__/PromptSignIn.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Markdown} from '@storybook/blocks'; 2 | 3 | import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; 4 | import * as PromptSignIntories from './PromptSignIn.stories.tsx'; 5 | import README from '../README.md?raw'; 6 | 7 | 8 | 9 | {README} 10 | -------------------------------------------------------------------------------- /src/components/PromptSignIn/__stories__/PromptSignIn.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {Button} from '@gravity-ui/uikit'; 4 | import type {Meta, StoryFn} from '@storybook/react'; 5 | 6 | import {UIKIT_ROOT_CLASS, UIKIT_THEME_LIGHT_CLASS} from '../../../constants'; 7 | import {PromptProps} from '../../Prompt/Prompt'; 8 | import {PromptSignIn} from '../PromptSignIn'; 9 | 10 | export default { 11 | title: 'Components/PromptSignIn', 12 | component: PromptSignIn, 13 | } as Meta; 14 | 15 | const styleBtn = {margin: '1em'}; 16 | 17 | const DefaultTemplate: StoryFn = (args) => { 18 | const {openTimestamp = 0} = args; 19 | const [timestamp, setTime] = React.useState(openTimestamp); 20 | const props = {...args, openTimestamp: timestamp}; 21 | const onClick = React.useCallback(() => { 22 | setTime(Date.now()); 23 | }, [setTime]); 24 | 25 | return ( 26 |
27 | 30 | 33 | 34 |
35 | ); 36 | }; 37 | 38 | export const Default = DefaultTemplate.bind({}); 39 | -------------------------------------------------------------------------------- /src/components/PromptSignIn/hooks/usePromptSignInProps.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function usePromptSignInProps(onClickSignIn?: React.EventHandler) { 4 | const [openTimestamp, setTime] = React.useState(0); 5 | const requireSignIn = React.useMemo(() => { 6 | return onClickSignIn 7 | ? () => { 8 | setTime(Date.now()); 9 | } 10 | : undefined; 11 | }, [onClickSignIn, setTime]); 12 | 13 | return {onClickSignIn, openTimestamp, requireSignIn}; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Search/Search.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins.scss'; 2 | @import '../../../styles/variables.scss'; 3 | 4 | $block: '.#{$namespace}search'; 5 | 6 | #{$block} { 7 | $root: &; 8 | $searchHeight: 44px; 9 | 10 | @include text-size(body-1); 11 | 12 | position: relative; 13 | display: flex; 14 | align-items: center; 15 | justify-content: flex-end; 16 | 17 | transition: width 0.3s; 18 | 19 | &__input-icon { 20 | display: flex; 21 | padding-right: 7px; 22 | border: none; 23 | font: inherit; 24 | background: none; 25 | color: var(--g-color-text-hint); 26 | cursor: pointer; 27 | } 28 | 29 | &__search-suggest { 30 | display: flex; 31 | align-items: center; 32 | 33 | height: $searchHeight; 34 | 35 | background-color: var(--g-color-base-background); 36 | 37 | border-radius: var(--bc-text-input-border-radius); 38 | border: 1px solid var(--g-color-base-background); 39 | 40 | &:hover, 41 | &:focus { 42 | border: 1px solid var(--g-color-base-generic-hover); 43 | } 44 | } 45 | 46 | #{$block} &__search-suggest-control { 47 | padding-left: $indentXXS; 48 | padding-right: $indentM; 49 | } 50 | 51 | &_size_s { 52 | --bc-text-input-border-radius: var(--g-border-radius-l); 53 | 54 | $searchHeight: 36px; 55 | $searchWidth: 352px; 56 | 57 | height: $searchHeight; 58 | width: $searchWidth; 59 | max-width: 100%; 60 | } 61 | 62 | &_size_m { 63 | --bc-text-input-border-radius: var(--g-border-radius-xl); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Wrapper/Wrapper.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/mixins.scss'; 2 | @import '../../../styles/variables.scss'; 3 | 4 | $block: '.#{$namespace}wrapper'; 5 | 6 | #{$block} { 7 | @include paddings(); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Wrapper/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {DEFAULT_PADDINGS} from '../../constants'; 4 | import {ClassNameProps, QAProps} from '../../models/common'; 5 | import {Paddings} from '../../models/paddings'; 6 | import {block} from '../../utils/cn'; 7 | 8 | import './Wrapper.scss'; 9 | 10 | const b = block('wrapper'); 11 | 12 | type WrapperProps = ClassNameProps & 13 | QAProps & { 14 | paddings?: Paddings; 15 | children?: React.ReactNode; 16 | }; 17 | 18 | export const Wrapper: React.FunctionComponent = ({ 19 | children, 20 | paddings = DEFAULT_PADDINGS, 21 | className, 22 | qa, 23 | }) => ( 24 |
36 | {children} 37 |
38 | ); 39 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import {Paddings, PaddingsDirections} from './models/paddings'; 2 | 3 | export const BREAKPOINTS = { 4 | xs: 0, 5 | sm: 577, 6 | md: 769, 7 | lg: 1081, 8 | xl: 1185, 9 | }; 10 | 11 | export const DEFAULT_THEME = 'light'; 12 | 13 | export const UIKIT_ROOT_CLASS = 'g-root'; 14 | export const UIKIT_THEME_LIGHT_CLASS = `${UIKIT_ROOT_CLASS}_theme_${DEFAULT_THEME}`; 15 | 16 | export enum DefaultGoalIds { 17 | shareTop = 'SITE_BLOG_SHARE-TOP_CLICK', 18 | shareBottom = 'SITE_BLOG_SHARE-BOTTOM_CLICK', 19 | breadcrumbsTop = 'SITE_BLOG_BREADCRUMBS-TOP_CLICK', 20 | breadcrumbsBottom = 'SITE_BLOG_BREADCRUMBS-BOTTOM_CLICK', 21 | saveTop = 'SITE_BLOG_SAVE-TOP_CLICK', 22 | saveBottom = 'SITE_BLOG_SAVE-BOTTOM_CLICK', 23 | saveSuggest = 'SITE_BLOG_SAVE-SUGGEST_CLICK', 24 | suggest = 'SITE_BLOG_INTERESTING-CARD_CLICK', 25 | bannerCommon = 'SITE_BLOG_TEXT-BANNER_CLICK', 26 | cta = 'SITE_BLOG_CTA_CLICK', 27 | tag = 'SITE_BLOG_THEME-SELECTOR_CLCK', 28 | service = 'SITE_BLOG_SERVICE-SELECTOR_CLCK', 29 | showMore = 'SITE_BLOG-PAGINATION_SHOW-MORE_CLCK', 30 | next = 'SITE_BLOG-PAGINATION_NEXT_CLCK', 31 | home = 'SITE_BLOG-PAGINATION_HOME_CLCK', 32 | page = 'SITE_BLOG-PAGINATION_PAGE-NMBR_CLCK', 33 | } 34 | 35 | export const DEFAULT_PADDINGS: Paddings = { 36 | [PaddingsDirections.bottom]: 'l', 37 | [PaddingsDirections.top]: 'xs', 38 | }; 39 | -------------------------------------------------------------------------------- /src/constructor/blocksMap.ts: -------------------------------------------------------------------------------- 1 | import {Author} from '../blocks/Author/Author'; 2 | import {Banner} from '../blocks/Banner/Banner'; 3 | import {CTA} from '../blocks/CTA/CTA'; 4 | import {ColoredText} from '../blocks/ColoredText/ColoredText'; 5 | import {Feed} from '../blocks/Feed/Feed'; 6 | import {Form} from '../blocks/Form/Form'; 7 | import {Header} from '../blocks/Header/Header'; 8 | import {Layout} from '../blocks/Layout/Layout'; 9 | import {Media} from '../blocks/Media/Media'; 10 | import {Meta} from '../blocks/Meta/Meta'; 11 | import {Suggest} from '../blocks/Suggest/Suggest'; 12 | import {YFM} from '../blocks/YFM/YFM'; 13 | import {BlockType} from '../models/common'; 14 | 15 | const blocks = { 16 | [BlockType.YFM]: YFM, 17 | [BlockType.Layout]: Layout, 18 | [BlockType.Media]: Media, 19 | [BlockType.Banner]: Banner, 20 | [BlockType.CTA]: CTA, 21 | [BlockType.ColoredText]: ColoredText, 22 | [BlockType.Author]: Author, 23 | [BlockType.Suggest]: Suggest, 24 | [BlockType.Meta]: Meta, 25 | [BlockType.Feed]: Feed, 26 | [BlockType.Form]: Form, 27 | }; 28 | 29 | const headers = { 30 | [BlockType.Header]: Header, 31 | }; 32 | 33 | export default {blocks, headers}; 34 | -------------------------------------------------------------------------------- /src/containers/BlogPage/BlogPage.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/root'; 2 | -------------------------------------------------------------------------------- /src/containers/BlogPage/README.md: -------------------------------------------------------------------------------- 1 | Blog post page 2 | 3 | `TODO` 4 | -------------------------------------------------------------------------------- /src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-Default-with-opened-select-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-Default-with-opened-select-light-chromium-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-Default-with-opened-select-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-Default-with-opened-select-light-webkit-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-WithNavigation-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-WithNavigation-light-chromium-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-WithNavigation-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPage/__snapshots__/BlogPage.visual.test.tsx-snapshots/BlogPage-render-stories-WithNavigation-light-webkit-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPage/__tests__/BlogPage.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default, WithNavigation} from './helpers'; 4 | import {MobileProvider} from '@gravity-ui/uikit'; 5 | 6 | const BLOG_POST_DALAY = 10 * 1000; 7 | 8 | test.describe('BlogPage', () => { 9 | test('render stories ', async ({mount, expectScreenshot, delay}) => { 10 | await mount(); 11 | await delay(BLOG_POST_DALAY); 12 | await expectScreenshot({skipTheme: 'dark'}); 13 | }); 14 | 15 | test('render stories ', async ({mount, expectScreenshot, delay}) => { 16 | await mount(); 17 | await delay(BLOG_POST_DALAY); 18 | await expectScreenshot({skipTheme: 'dark'}); 19 | }); 20 | 21 | test('render stories with opened select', async ({ 22 | mount, 23 | expectScreenshot, 24 | delay, 25 | page, 26 | }) => { 27 | await mount( 28 | 29 | {' '} 30 | , 31 | ); 32 | await delay(BLOG_POST_DALAY); 33 | await page.click('[data-qa="service-select"]'); 34 | await expectScreenshot({skipTheme: 'dark'}); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/containers/BlogPage/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as BlogPageStories from '../__stories__/BlogPage.stories'; 4 | 5 | export const {Default, WithNavigation} = composeStories(BlogPageStories); 6 | -------------------------------------------------------------------------------- /src/containers/BlogPostPage/BlogPostPage.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/root'; 2 | @import '../../../styles/yfm.scss'; 3 | -------------------------------------------------------------------------------- /src/containers/BlogPostPage/README.md: -------------------------------------------------------------------------------- 1 | Blog page 2 | 3 | `TODO` 4 | -------------------------------------------------------------------------------- /src/containers/BlogPostPage/__snapshots__/BlogPostPage.visual.test.tsx-snapshots/BlogPostPage-render-stories-Default-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPostPage/__snapshots__/BlogPostPage.visual.test.tsx-snapshots/BlogPostPage-render-stories-Default-light-chromium-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPostPage/__snapshots__/BlogPostPage.visual.test.tsx-snapshots/BlogPostPage-render-stories-Default-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPostPage/__snapshots__/BlogPostPage.visual.test.tsx-snapshots/BlogPostPage-render-stories-Default-light-webkit-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPostPage/__snapshots__/BlogPostPage.visual.test.tsx-snapshots/BlogPostPage-render-stories-WithNavigation-light-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPostPage/__snapshots__/BlogPostPage.visual.test.tsx-snapshots/BlogPostPage-render-stories-WithNavigation-light-chromium-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPostPage/__snapshots__/BlogPostPage.visual.test.tsx-snapshots/BlogPostPage-render-stories-WithNavigation-light-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/src/containers/BlogPostPage/__snapshots__/BlogPostPage.visual.test.tsx-snapshots/BlogPostPage-render-stories-WithNavigation-light-webkit-linux.png -------------------------------------------------------------------------------- /src/containers/BlogPostPage/__stories__/BlogPostPage.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryFn} from '@storybook/react'; 2 | 3 | import {postPageMockData} from '../../../../.mocks/utils'; 4 | import {BlogPostPage, BlogPostPageProps} from '../BlogPostPage'; 5 | 6 | import navigation from '../../../../.mocks/navigation.json'; 7 | import {BlogConstructorProvider} from '../../../constructor/BlogConstructorProvider'; 8 | import {CircleInfo} from '@gravity-ui/icons'; 9 | import {Button, Icon} from '@gravity-ui/uikit'; 10 | import {CustomInfoItemComponent} from '../../../components/PostInfo/PostInfo'; 11 | 12 | export default { 13 | title: 'Containers/BlogPostPage', 14 | component: BlogPostPage, 15 | args: { 16 | theme: 'light', 17 | ...postPageMockData, 18 | }, 19 | } as Meta; 20 | 21 | const DefaultTemplate: StoryFn = (args) => ; 22 | 23 | const ExtraInfoItem: CustomInfoItemComponent = ({post}) => ( 24 | 33 | ); 34 | 35 | const ExtraItemsTemplate: StoryFn = (args) => ( 36 | 41 | 42 | 43 | ); 44 | 45 | export const Default = DefaultTemplate.bind({}); 46 | 47 | export const WithNavigation = DefaultTemplate.bind({}); 48 | WithNavigation.args = { 49 | navigation, 50 | } as unknown as BlogPostPageProps; 51 | 52 | export const WithExtraInfoItems = ExtraItemsTemplate.bind({}); 53 | -------------------------------------------------------------------------------- /src/containers/BlogPostPage/__tests__/BlogPostPage.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import {test} from '../../../../playwright/core/index'; 2 | 3 | import {Default, WithNavigation} from './helpers'; 4 | 5 | const BLOG_POST_PAGE_DALAY = 10 * 1000; 6 | 7 | test.describe('BlogPostPage', () => { 8 | test('render stories ', async ({mount, expectScreenshot, delay, page}) => { 9 | await mount(); 10 | await delay(BLOG_POST_PAGE_DALAY); 11 | await expectScreenshot({skipTheme: 'dark', mask: [page.locator('.pc-Media__youtube')]}); 12 | }); 13 | 14 | test('render stories ', async ({mount, expectScreenshot, delay, page}) => { 15 | await mount(); 16 | await delay(BLOG_POST_PAGE_DALAY); 17 | await expectScreenshot({skipTheme: 'dark', mask: [page.locator('.pc-Media__youtube')]}); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/containers/BlogPostPage/__tests__/helpers.tsx: -------------------------------------------------------------------------------- 1 | import {composeStories} from '@storybook/react'; 2 | 3 | import * as BlogPostPageStories from '../__stories__/BlogPostPage.stories'; 4 | 5 | export const {Default, WithNavigation} = composeStories(BlogPostPageStories); 6 | -------------------------------------------------------------------------------- /src/contexts/DeviceContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {WithDeviceProps} from '../models/common'; 4 | 5 | export type DeviceContextProps = Partial; 6 | 7 | export const DeviceContext = React.createContext({} as DeviceContextProps); 8 | -------------------------------------------------------------------------------- /src/contexts/FeedContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {GetPostsType, PostData, Service, Tag} from '../models/common'; 4 | 5 | export interface FeedContextProps { 6 | posts?: PostData[]; 7 | pinnedPost?: PostData; 8 | totalCount?: number; 9 | tags?: Tag[]; 10 | services?: Service[]; 11 | getPosts?: GetPostsType; 12 | pageCountForShowSupportButtons?: number; 13 | } 14 | 15 | export const FeedContext = React.createContext({} as FeedContextProps); 16 | -------------------------------------------------------------------------------- /src/contexts/LikesContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {ToggleLikeCallbackType} from '../models/common'; 4 | 5 | export interface LikesContextProps { 6 | toggleLike?: ToggleLikeCallbackType; 7 | hasLikes?: boolean; 8 | isSignedInUser?: boolean; 9 | requireSignIn?: React.MouseEventHandler; 10 | } 11 | 12 | export const LikesContext = React.createContext({} as LikesContextProps); 13 | -------------------------------------------------------------------------------- /src/contexts/LocaleContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {Locale} from '../models/locale'; 4 | import {Lang} from '@gravity-ui/uikit'; 5 | 6 | export type LocaleContextProps = { 7 | locale: Locale; 8 | }; 9 | 10 | export const LocaleContext = React.createContext({ 11 | locale: { 12 | code: 'en-En', 13 | lang: Lang.En, 14 | langName: 'English', 15 | pathPrefix: 'en', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/contexts/MobileContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const MobileContext = React.createContext(false); 4 | -------------------------------------------------------------------------------- /src/contexts/PostPageContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {ShareOptions} from '@gravity-ui/components'; 4 | 5 | import {PostData} from '../models/common'; 6 | import {HeaderBreadCrumbsProps} from '@gravity-ui/page-constructor'; 7 | 8 | export type LikesRoutineType = { 9 | handleUserLike: () => void; 10 | hasUserLike: boolean; 11 | likesCount: number; 12 | }; 13 | 14 | export interface PostPageContextProps { 15 | post: PostData; 16 | suggestedPosts: PostData[]; 17 | likes?: LikesRoutineType; 18 | shareOptions?: ShareOptions[]; 19 | breadcrumbs?: HeaderBreadCrumbsProps; 20 | } 21 | 22 | export const PostPageContext = React.createContext( 23 | {} as PostPageContextProps, 24 | ); 25 | -------------------------------------------------------------------------------- /src/contexts/RouterContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {Query} from '../models/common'; 4 | 5 | export interface RouterContextProps { 6 | pathname: string; 7 | as: string; 8 | hostname: string; 9 | query?: Query; 10 | updateQueryCallback: (query: Query) => void; 11 | } 12 | 13 | export const RouterContext = React.createContext({} as RouterContextProps); 14 | -------------------------------------------------------------------------------- /src/contexts/SettingsContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {CustomInfoItemComponent} from '../components/PostInfo/PostInfo'; 3 | 4 | export interface SettingsContextProps { 5 | addNavigationLinkForPages?: boolean; 6 | isAnimationEnabled?: boolean; 7 | 8 | getBlogPath?: (pathPrefix: string) => string; 9 | extraInfoItems?: CustomInfoItemComponent[]; 10 | } 11 | 12 | export const SettingsContext = React.createContext({}); 13 | -------------------------------------------------------------------------------- /src/contexts/theme/ThemeContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {DEFAULT_THEME} from '../../constants'; 4 | 5 | import {ThemeValueType} from './ThemeValueContext'; 6 | 7 | export interface ThemeContextProps { 8 | theme: ThemeValueType; 9 | setTheme: (newTheme: ThemeValueType) => void; 10 | } 11 | 12 | export const initialValue: ThemeContextProps = { 13 | theme: DEFAULT_THEME, 14 | setTheme: () => {}, 15 | }; 16 | 17 | export const ThemeContext = React.createContext(initialValue); 18 | -------------------------------------------------------------------------------- /src/contexts/theme/ThemeValueContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type ThemeValueType = 'light' | 'dark'; 4 | 5 | export interface ThemeValueContextProps { 6 | themeValue: ThemeValueType; 7 | } 8 | 9 | export const initialValue: ThemeValueContextProps = { 10 | themeValue: 'light', 11 | }; 12 | 13 | export const ThemeValueContext = React.createContext(initialValue); 14 | -------------------------------------------------------------------------------- /src/contexts/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ThemeContext'; 2 | export * from './ThemeProvider'; 3 | export * from './useTheme'; 4 | export * from './useThemeValue'; 5 | export * from './withTheme'; 6 | export * from './withThemeValue'; 7 | -------------------------------------------------------------------------------- /src/contexts/theme/useTheme.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {ThemeContext, ThemeContextProps} from './ThemeContext'; 4 | 5 | export function useTheme(): [ThemeContextProps['theme'], ThemeContextProps['setTheme']] { 6 | const {theme, setTheme} = React.useContext(ThemeContext); 7 | return [theme, setTheme]; 8 | } 9 | -------------------------------------------------------------------------------- /src/contexts/theme/useThemeValue.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {ThemeValueContext, ThemeValueContextProps} from './ThemeValueContext'; 4 | 5 | export function useThemeValue(): ThemeValueContextProps['themeValue'] { 6 | const {themeValue} = React.useContext(ThemeValueContext); 7 | return themeValue; 8 | } 9 | -------------------------------------------------------------------------------- /src/contexts/theme/withTheme.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {Subtract} from 'utility-types'; 4 | 5 | import {ThemeContext, ThemeContextProps} from './ThemeContext'; 6 | 7 | export interface WithThemeProps extends ThemeContextProps {} 8 | 9 | export function withTheme( 10 | WrappedComponent: React.ComponentType, 11 | ): React.ComponentType> { 12 | const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; 13 | 14 | return class WithThemeComponent extends React.Component> { 15 | static displayName = `withTheme(${componentName})`; 16 | static contextType = ThemeContext; 17 | context!: ThemeContextProps; 18 | 19 | render() { 20 | return ( 21 | 26 | ); 27 | } 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/contexts/theme/withThemeValue.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {Subtract} from 'utility-types'; 4 | 5 | import {ThemeValueContext, ThemeValueContextProps} from './ThemeValueContext'; 6 | 7 | export interface WithThemeValueProps extends ThemeValueContextProps {} 8 | 9 | export function withThemeValue( 10 | WrappedComponent: React.ComponentType, 11 | ): React.ComponentType> { 12 | const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; 13 | 14 | return class WithThemeValueComponent extends React.Component> { 15 | static displayName = `withThemeValue(${componentName})`; 16 | static contextType = ThemeValueContext; 17 | context!: ThemeValueContextProps; 18 | 19 | render() { 20 | return ; 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/counters/utils.ts: -------------------------------------------------------------------------------- 1 | export enum AnalyticsCounter { 2 | Main = 'main', 3 | CrossSite = 'cross-site', 4 | Scale = 'scale', 5 | } 6 | -------------------------------------------------------------------------------- /src/data/config.ts: -------------------------------------------------------------------------------- 1 | import {BlockType as PCBlockType} from '@gravity-ui/page-constructor'; 2 | import {yfmTransformer} from '@gravity-ui/page-constructor/server'; 3 | 4 | import {BlockType} from '../models/common'; 5 | 6 | const BLOCKS_FOR_TYPOGRAPHY_TRANSFORM = [BlockType.YFM, BlockType.ColoredText, BlockType.Media]; 7 | 8 | type GetConfigForCreateReadableContent = () => { 9 | [x in BlockType | PCBlockType]: { 10 | fields: string[]; 11 | }; 12 | }; 13 | 14 | /** 15 | * Func for create extended typography config for page-constructor 16 | * 17 | * @returns - { 18 | * [blockTypes.YFM]: [ 19 | * { 20 | * fields: ['text'], 21 | * transformer: yfmTransformer, 22 | * }, 23 | * ], 24 | * } 25 | */ 26 | export const getExtendTypographyConfig = () => 27 | BLOCKS_FOR_TYPOGRAPHY_TRANSFORM.reduce( 28 | (result, current) => ({ 29 | [current]: [ 30 | { 31 | fields: ['text'], 32 | transformer: yfmTransformer, 33 | }, 34 | ], 35 | ...result, 36 | }), 37 | {}, 38 | ); 39 | 40 | /** 41 | * Func for create readable content func 42 | * 43 | * @returns - { 44 | * [blockTypes.YFM]: { 45 | * fields: ['text'], 46 | * transformer: yfmTransformer, 47 | * }, 48 | * } 49 | */ 50 | export const getConfigForCreateReadableContent: GetConfigForCreateReadableContent = () => 51 | BLOCKS_FOR_TYPOGRAPHY_TRANSFORM.reduce( 52 | (result, current) => ({ 53 | [current]: { 54 | fields: ['text'], 55 | }, 56 | ...result, 57 | }), 58 | {} as ReturnType, 59 | ); 60 | -------------------------------------------------------------------------------- /src/data/createReadableContent.ts: -------------------------------------------------------------------------------- 1 | import {Block} from '../models/blocks'; 2 | import {BlockType} from '../models/common'; 3 | 4 | import {getConfigForCreateReadableContent} from './config'; 5 | 6 | type CreateReadableContentProps = { 7 | blocks: Block[]; 8 | content?: string; 9 | authors?: unknown[]; 10 | }; 11 | 12 | /** 13 | * Function for create readable content 14 | * 15 | * @param blocks - content blocks array 16 | * @param content - content data 17 | * @param authors - authors array 18 | * 19 | * @returns readable content 20 | */ 21 | export const createReadableContent = ({ 22 | content = '', 23 | blocks, 24 | authors = [], 25 | }: CreateReadableContentProps) => { 26 | try { 27 | const config = getConfigForCreateReadableContent(); 28 | 29 | const readableContent = blocks.reduce((resultContent: string, block) => { 30 | let innerContent = resultContent; 31 | 32 | if (config[block.type]) { 33 | innerContent += config[block.type].fields 34 | .map((field: string) => block[field]) 35 | .filter(Boolean) 36 | .join('\n'); 37 | innerContent += '\n'; 38 | } 39 | 40 | if (block.type === BlockType.Author) { 41 | authors.push(block.uid); 42 | } 43 | 44 | if (block.children && block.children.length) { 45 | innerContent = createReadableContent({ 46 | content: innerContent, 47 | blocks: block.children, 48 | authors, 49 | }); 50 | } 51 | 52 | return innerContent; 53 | }, content); 54 | 55 | return readableContent; 56 | } catch (err) { 57 | // eslint-disable-next-line no-console 58 | console.error('Page content transformation error', err); 59 | return ''; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/data/sanitizeMeta.ts: -------------------------------------------------------------------------------- 1 | import {sanitizeHtml} from '@gravity-ui/page-constructor/server'; 2 | 3 | import {PostMetaProps} from '../models/common'; 4 | 5 | /** 6 | * Function for sanitized meta-data fields 7 | * @param metaData PostMetaProps 8 | * @returns metaData 9 | */ 10 | export const sanitizeMeta = (metaData: PostMetaProps) => { 11 | const {title, description, date, image, canonicalUrl, organization} = metaData; 12 | 13 | // this func for resolve type conflicts in reduce method 14 | const stringObjectKeys = (obj: Record) => Object.keys(obj) as K[]; 15 | 16 | const sanitizedOrganization = stringObjectKeys(organization).reduce( 17 | (acc, current) => { 18 | acc[current] = sanitizeHtml(organization[current]); 19 | 20 | return acc; 21 | }, 22 | {} as PostMetaProps['organization'], 23 | ); 24 | 25 | return { 26 | ...metaData, 27 | title: sanitizeHtml(title), 28 | description: sanitizeHtml(description as string), 29 | date: sanitizeHtml(date), 30 | image: sanitizeHtml(image), 31 | canonicalUrl: sanitizeHtml(canonicalUrl), 32 | organization: sanitizedOrganization, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/data/transformPost.ts: -------------------------------------------------------------------------------- 1 | import {typografToHTML, typografToText, yfmTransformer} from '@gravity-ui/page-constructor/server'; 2 | 3 | import {PostData, TransformPostOptions} from '../models/common'; 4 | import {Lang} from '@gravity-ui/uikit'; 5 | 6 | export type TransformPostType = { 7 | postData: PostData; 8 | lang: Lang; 9 | options: TransformPostOptions; 10 | }; 11 | 12 | /** 13 | * Func for transform post data 14 | * 15 | * @param postData - post data 16 | * @param lang - runtime language 17 | * 18 | * @param plugins - YFM plugins list 19 | * @returns -prepared post 20 | */ 21 | export const transformPost = ({postData, lang, options: {plugins} = {}}: TransformPostType) => { 22 | if (!postData) { 23 | // eslint-disable-next-line no-console 24 | console.error('Post not found'); 25 | 26 | return {} as PostData; 27 | } 28 | 29 | const {tags, title, metaTitle, description, ...post} = postData; 30 | 31 | return { 32 | ...post, 33 | title, 34 | tags, 35 | textTitle: typografToText(title, lang), 36 | htmlTitle: typografToHTML(title, lang), 37 | metaTitle: metaTitle || title, 38 | description: yfmTransformer(lang, description as string, {plugins}), 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/demo/StoryTemplate.mdx: -------------------------------------------------------------------------------- 1 | import {Title, Subtitle, Description, Primary, Controls, Stories, Meta, Markdown } from '@storybook/blocks'; 2 | 3 | export const StoryTemplate = ({children}) => ( 4 | <> 5 | 6 | <Subtitle /> 7 | {children} 8 | <Primary /> 9 | <Controls /> 10 | <Stories /> 11 | </> 12 | ); 13 | -------------------------------------------------------------------------------- /src/demo/mocks.ts: -------------------------------------------------------------------------------- 1 | import {Query} from '../models/common'; 2 | 3 | export const routerData = { 4 | as: '/', 5 | pathname: '/', 6 | hostname: 'host', 7 | query: {}, 8 | updateQueryCallback: (params: Query) => { 9 | // eslint-disable-next-line no-console 10 | console.log('params', params); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/hooks/useAriaAttributes.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type Labels = string | number | boolean | undefined; 4 | type Description = string | number | boolean | undefined; 5 | interface UseAriaAttributesProps { 6 | labelIds?: Labels[]; 7 | descriptionIds: Description[]; 8 | } 9 | 10 | /** 11 | * Returns aria-attributes 12 | * @param labelIds - labels ids. Falsy values will be ignored 13 | * @param descriptionIds - descriptions ids. Falsy values will be ignored 14 | * @returns aria attributes for the element to be labelled 15 | */ 16 | export const useAriaAttributes = ({labelIds = [], descriptionIds = []}: UseAriaAttributesProps) => { 17 | const labelledBy = React.useMemo(() => labelIds.filter(Boolean).join(' '), [labelIds]); 18 | const describedBy = React.useMemo( 19 | () => descriptionIds.filter(Boolean).join(' '), 20 | [descriptionIds], 21 | ); 22 | 23 | return { 24 | 'aria-labelledby': labelledBy, 25 | 'aria-describedby': describedBy, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/hooks/useExtendedComponentMap.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {CustomConfig, getCustomItems} from '@gravity-ui/page-constructor'; 4 | 5 | import componentMap from '../constructor/blocksMap'; 6 | 7 | export const useExtendedComponentMap = (custom: CustomConfig | undefined) => 8 | React.useMemo( 9 | () => ({ 10 | ...custom, 11 | blocks: {...componentMap.blocks, ...getCustomItems(['blocks'], custom)}, 12 | headers: {...componentMap.headers, ...getCustomItems(['headers'], custom)}, 13 | }), 14 | [custom], 15 | ); 16 | -------------------------------------------------------------------------------- /src/hooks/useHover.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | /* 4 | * Source code copied from https://github.com/uidotdev/usehooks | MIT License 5 | * @see https://usehooks.com/usehover 6 | */ 7 | export function useHover<T extends HTMLElement = HTMLElement>(): [ 8 | ref: React.RefObject<T>, 9 | hovering: boolean, 10 | ] { 11 | const [hovering, setHovering] = React.useState(false); 12 | const ref = React.useRef(null); 13 | 14 | React.useEffect(() => { 15 | const node = ref.current as unknown as HTMLElement; 16 | 17 | if (!node) return; 18 | 19 | const handleMouseEnter = () => { 20 | setHovering(true); 21 | }; 22 | 23 | const handleMouseLeave = () => { 24 | setHovering(false); 25 | }; 26 | 27 | node.addEventListener('mouseenter', handleMouseEnter); 28 | node.addEventListener('mouseleave', handleMouseLeave); 29 | 30 | // eslint-disable-next-line consistent-return 31 | return () => { 32 | node.removeEventListener('mouseenter', handleMouseEnter); 33 | node.removeEventListener('mouseleave', handleMouseLeave); 34 | }; 35 | }, []); 36 | 37 | return [ref, hovering]; 38 | } 39 | -------------------------------------------------------------------------------- /src/hooks/useIsIPhone.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {DeviceContext} from '../contexts/DeviceContext'; 4 | 5 | export const useIsIPhone = () => { 6 | const {device} = React.useContext(DeviceContext); 7 | 8 | return device?.model === 'iPhone'; 9 | }; 10 | -------------------------------------------------------------------------------- /src/hooks/useLikes.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {ToggleLikeCallbackType} from '../models/common'; 4 | 5 | type UseLikesProps = { 6 | hasLike?: boolean; 7 | count?: number; 8 | postId?: number | string; 9 | toggleLikeCallback?: ToggleLikeCallbackType; 10 | }; 11 | 12 | type UseLikeData = { 13 | likesCount: number; 14 | hasUserLike: boolean; 15 | handleLike: () => void; 16 | }; 17 | 18 | type UseLikesType = (props: UseLikesProps) => UseLikeData; 19 | 20 | export const useLikes: UseLikesType = ({hasLike, count, toggleLikeCallback, postId}) => { 21 | const [hasUserLike, setHasUserLike] = React.useState(hasLike ?? false); 22 | const [likesCount, setLikesCount] = React.useState(count ?? 0); 23 | 24 | const handleLike = React.useCallback(() => { 25 | let newLikesCount = likesCount; 26 | 27 | if (hasUserLike && likesCount > 0) { 28 | newLikesCount--; 29 | } 30 | 31 | if (!hasUserLike) { 32 | newLikesCount++; 33 | } 34 | 35 | setHasUserLike(!hasUserLike); 36 | setLikesCount(newLikesCount); 37 | 38 | if (toggleLikeCallback) { 39 | toggleLikeCallback({ 40 | postId, 41 | hasLike: !hasUserLike, 42 | }); 43 | } 44 | }, [hasUserLike, likesCount, postId, toggleLikeCallback]); 45 | 46 | React.useEffect(() => { 47 | setHasUserLike(hasLike ?? false); 48 | setLikesCount(count ?? 0); 49 | }, [hasLike, count]); 50 | 51 | return { 52 | likesCount, 53 | hasUserLike, 54 | handleLike, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /src/hooks/useOpenCloseTimer.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | /** 4 | * Timer to automatically update `open` state after a given duration 5 | * @param {number} openTimestamp - UNIX timestamp in milliseconds 6 | * @param {number} openDuration - in milliseconds 7 | * @returns {{open: boolean}} {open} - whether the state is open 8 | */ 9 | export function useOpenCloseTimer(openTimestamp = Date.now(), openDuration = 4000) { 10 | const open = Date.now() - openTimestamp < openDuration; 11 | const [, reset] = React.useState(0); // time to reset `open` state 12 | 13 | React.useEffect(() => { 14 | const closeTime = openTimestamp + openDuration; 15 | const delay = closeTime - Date.now(); 16 | 17 | if (delay <= 0) { 18 | return; 19 | } 20 | 21 | const timer = setTimeout(() => { 22 | reset(Date.now); 23 | }, delay); 24 | 25 | // eslint-disable-next-line consistent-return 26 | return () => { 27 | clearTimeout(timer); 28 | }; 29 | }, [openTimestamp, openDuration]); 30 | 31 | return {open}; 32 | } 33 | -------------------------------------------------------------------------------- /src/icons/Close.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {a11yHiddenSvgProps} from '../utils/svg'; 4 | 5 | export const Close = (props: React.SVGProps<SVGSVGElement>) => ( 6 | <svg 7 | xmlns="http://www.w3.org/2000/svg" 8 | width="10" 9 | height="10" 10 | viewBox="0 0 10 10" 11 | fill="currentColor" 12 | {...a11yHiddenSvgProps} 13 | {...props} 14 | > 15 | <path 16 | d="M10 0.7L9.3 0L5 4.3L0.7 0L0 0.7L4.3 5L0 9.3L0.7 10L5 5.7L9.3 10L10 9.3L5.7 5L10 0.7Z" 17 | fill="currentColor" 18 | /> 19 | </svg> 20 | ); 21 | -------------------------------------------------------------------------------- /src/icons/DropdownArrow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {a11yHiddenSvgProps} from '../utils/svg'; 4 | 5 | export const DropdownArrow = (props: React.SVGProps<SVGSVGElement>) => ( 6 | <svg 7 | xmlns="http://www.w3.org/2000/svg" 8 | viewBox="0 0 16 16" 9 | width="16" 10 | height="16" 11 | fill="currentColor" 12 | {...a11yHiddenSvgProps} 13 | {...props} 14 | > 15 | <path d="M3.50172 5.44253C3.19384 5.16544 2.71962 5.19039 2.44253 5.49828C2.16544 5.80616 2.19039 6.28038 2.49828 6.55747L3.50172 5.44253ZM8 10.5L7.49828 11.0575C7.7835 11.3142 8.2165 11.3142 8.50172 11.0575L8 10.5ZM13.5017 6.55747C13.8096 6.28038 13.8346 5.80616 13.5575 5.49828C13.2804 5.19039 12.8062 5.16544 12.4983 5.44253L13.5017 6.55747ZM2.49828 6.55747L7.49828 11.0575L8.50172 9.94253L3.50172 5.44253L2.49828 6.55747ZM8.50172 11.0575L13.5017 6.55747L12.4983 5.44253L7.49828 9.94253L8.50172 11.0575Z" /> 16 | </svg> 17 | ); 18 | -------------------------------------------------------------------------------- /src/icons/Save.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {a11yHiddenSvgProps} from '../utils/svg'; 4 | 5 | export const Save = (props: React.SVGProps<SVGSVGElement>) => ( 6 | <svg 7 | xmlns="http://www.w3.org/2000/svg" 8 | width="16" 9 | height="16" 10 | viewBox="0 0 16 16" 11 | fill="currentColor" 12 | {...a11yHiddenSvgProps} 13 | {...props} 14 | > 15 | <path 16 | fillRule="evenodd" 17 | clipRule="evenodd" 18 | d="M13 3H3v9.6l3.445-2.94.028-.022a2.624 2.624 0 0 1 3.054 0l.028.021L13 12.6V3Zm1.357-1.614A1.593 1.593 0 0 1 15 2.655v10.547c0 .678-.416 1.218-.841 1.499-.417.275-1.24.517-1.898-.103L8.32 11.234a.64.64 0 0 0-.64 0l-3.942 3.364c-.58.546-1.348.461-1.838.18a1.808 1.808 0 0 1-.9-1.576V2.786C1 2.055 1.515 1 2.667 1H13.2c.405 0 .822.13 1.157.386Z" 19 | /> 20 | </svg> 21 | ); 22 | -------------------------------------------------------------------------------- /src/icons/SaveFilled.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {a11yHiddenSvgProps} from '../utils/svg'; 4 | 5 | export const SaveFilled = (props: React.SVGProps<SVGSVGElement>) => ( 6 | <svg 7 | xmlns="http://www.w3.org/2000/svg" 8 | width="16" 9 | height="16" 10 | viewBox="0 0 16 16" 11 | fill="currentColor" 12 | {...a11yHiddenSvgProps} 13 | {...props} 14 | > 15 | <path 16 | fillRule="evenodd" 17 | clipRule="evenodd" 18 | d="M14.357 1.386A1.593 1.593 0 0 1 15 2.655v10.547c0 .678-.416 1.218-.841 1.499-.417.275-1.24.517-1.898-.103L8.32 11.234a.64.64 0 0 0-.64 0l-3.942 3.364c-.58.546-1.348.461-1.838.18a1.808 1.808 0 0 1-.9-1.576V2.786C1 2.055 1.515 1 2.667 1H13.2c.405 0 .822.13 1.157.386Z" 19 | /> 20 | </svg> 21 | ); 22 | -------------------------------------------------------------------------------- /src/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {a11yHiddenSvgProps} from '../utils/svg'; 4 | 5 | export const SearchIcon = (props: React.SVGProps<SVGSVGElement>) => ( 6 | <svg 7 | xmlns="http://www.w3.org/2000/svg" 8 | width="16" 9 | height="16" 10 | viewBox="0 0 16 16" 11 | fill="none" 12 | {...a11yHiddenSvgProps} 13 | {...props} 14 | > 15 | <path 16 | d="M9.532 9.539A5 5 0 1 0 2.468 2.46a5 5 0 0 0 7.064 7.08zm0 0L15 15" 17 | stroke="currentColor" 18 | strokeWidth="2" 19 | strokeLinecap="round" 20 | vectorEffect="non-scaling-stroke" 21 | /> 22 | </svg> 23 | ); 24 | -------------------------------------------------------------------------------- /src/icons/ShareArrowUp.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {a11yHiddenSvgProps} from '../utils/svg'; 4 | 5 | export const ShareArrowUp = (props: React.SVGProps<SVGSVGElement>) => ( 6 | <svg 7 | xmlns="http://www.w3.org/2000/svg" 8 | width="16" 9 | height="16" 10 | viewBox="0 0 16 16" 11 | fill="currentColor" 12 | {...a11yHiddenSvgProps} 13 | {...props} 14 | > 15 | <path 16 | fillRule="evenodd" 17 | clipRule="evenodd" 18 | d="M4.798 3.16a.5.5 0 0 0 .363.842H7V9a1 1 0 0 0 2 0V4.002h1.839a.5.5 0 0 0 .363-.844L8.363.156a.5.5 0 0 0-.726 0l-2.84 3.002.001.001ZM13 7a1 1 0 0 1 2 0v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 13.5V7a1 1 0 0 1 2 0v6h10V7Z" 19 | /> 20 | </svg> 21 | ); 22 | -------------------------------------------------------------------------------- /src/icons/Time.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {a11yHiddenSvgProps} from '../utils/svg'; 4 | 5 | export const Time = (props: React.SVGProps<SVGSVGElement>) => ( 6 | <svg 7 | xmlns="http://www.w3.org/2000/svg" 8 | width="16" 9 | height="17" 10 | viewBox="0 0 16 17" 11 | fill="currentColor" 12 | {...a11yHiddenSvgProps} 13 | {...props} 14 | > 15 | <path 16 | fillRule="evenodd" 17 | clipRule="evenodd" 18 | d="M8 16.004a8 8 0 1 1 0-16 8 8 0 0 1 0 16Zm0-2a6 6 0 1 0 0-12 6 6 0 0 0 0 12Zm3.357-3.736a1 1 0 0 0-.342-1.372L9 7.688V5.004a1 1 0 0 0-2 0v3.25a1 1 0 0 0 .486.857l2.5 1.5a1 1 0 0 0 1.371-.343Z" 19 | /> 20 | </svg> 21 | ); 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {BlogConstructorProvider} from './constructor/BlogConstructorProvider'; 2 | 3 | export {BlogPostPage} from './containers/BlogPostPage/BlogPostPage'; 4 | export {BlogPage} from './containers/BlogPage/BlogPage'; 5 | 6 | export * from './models/common'; 7 | export * from './models/locale'; 8 | 9 | export * from './schema'; 10 | 11 | export {BREAKPOINTS} from './constants'; 12 | 13 | export * from './utils'; 14 | -------------------------------------------------------------------------------- /src/internal-typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const path: string; 3 | 4 | export default path; 5 | } 6 | -------------------------------------------------------------------------------- /src/models/locale.ts: -------------------------------------------------------------------------------- 1 | import {Lang} from '@gravity-ui/uikit'; 2 | 3 | export enum Currency { 4 | RUB = 'RUB', 5 | USD = 'USD', 6 | KZT = 'KZT', 7 | } 8 | 9 | export interface LangData { 10 | lang: Lang; 11 | langName: string; 12 | regions: Record<string, RegionData>; 13 | pathPrefix: string; 14 | } 15 | 16 | export interface RegionData { 17 | regionName: string; 18 | tld: string; 19 | currency: string; 20 | order: number; 21 | default: boolean; 22 | local: boolean; 23 | } 24 | 25 | export interface LocaleData extends Pick<LangData, 'lang'>, Omit<RegionData, 'regionName'> { 26 | code: string; 27 | region: string; 28 | } 29 | 30 | export interface Locale 31 | extends Partial<Pick<LangData, 'langName'>>, 32 | Pick<LangData, 'lang'>, 33 | Partial<Pick<LangData, 'pathPrefix'>>, 34 | Partial<Pick<LocaleData, 'code'>> {} 35 | -------------------------------------------------------------------------------- /src/models/paddings.ts: -------------------------------------------------------------------------------- 1 | export enum PaddingsDirections { 2 | top = 'top', 3 | bottom = 'bottom', 4 | left = 'left', 5 | right = 'right', 6 | } 7 | 8 | export type PaddingSize = 'xs' | 's' | 'm' | 'l' | 'xl'; 9 | 10 | export type Paddings = { 11 | [key in PaddingsDirections]?: PaddingSize; 12 | }; 13 | 14 | export type PaddingsYFMProps = { 15 | paddingTop?: PaddingSize; 16 | paddingBottom?: PaddingSize; 17 | paddingRight?: PaddingSize; 18 | paddingLeft?: PaddingSize; 19 | }; 20 | -------------------------------------------------------------------------------- /src/schema/README.md: -------------------------------------------------------------------------------- 1 | ## Shemas and validators 2 | 3 | #### We use [`Ajv JSON schema validator`](https://ajv.js.org/) for validation configs with blocks descriptions. 4 | 5 | #### You can use blocks schemas in your backoffice or everything else places where you check configs. 6 | -------------------------------------------------------------------------------- /src/schema/blocks.ts: -------------------------------------------------------------------------------- 1 | export * from '../blocks/Author/schema'; 2 | export * from '../blocks/Banner/schema'; 3 | export * from '../blocks/ColoredText/schema'; 4 | export * from '../blocks/CTA/schema'; 5 | export * from '../blocks/Feed/schema'; 6 | export * from '../blocks/Layout/schema'; 7 | export * from '../blocks/Media/schema'; 8 | export * from '../blocks/Meta/schema'; 9 | export * from '../blocks/Suggest/schema'; 10 | export * from '../blocks/YFM/schema'; 11 | -------------------------------------------------------------------------------- /src/schema/common.ts: -------------------------------------------------------------------------------- 1 | export interface ObjectSchema extends Record<string, unknown> { 2 | properties: object; 3 | } 4 | 5 | const sizeTypes = ['xxs', 'xs', 's', 'm', 'l', 'xl', 'xxl']; 6 | 7 | export const filteredItem = (itemsSchema: ObjectSchema) => ({ 8 | ...itemsSchema, 9 | type: 'object', 10 | properties: { 11 | when: { 12 | type: 'string', 13 | }, 14 | ...itemsSchema.properties, 15 | }, 16 | }); 17 | 18 | export const BlogBlockBase = { 19 | paddingTop: { 20 | type: 'string', 21 | enum: sizeTypes, 22 | }, 23 | paddingBottom: { 24 | type: 'string', 25 | enum: sizeTypes, 26 | }, 27 | fullWidth: { 28 | type: 'boolean', 29 | }, 30 | column: { 31 | type: 'string', 32 | enum: ['left', 'right'], 33 | }, 34 | qa: { 35 | type: 'string', 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/schema/headers.ts: -------------------------------------------------------------------------------- 1 | export * from '../blocks/Header/schema'; 2 | -------------------------------------------------------------------------------- /src/schema/index.ts: -------------------------------------------------------------------------------- 1 | import {BlockType} from '../models/common'; 2 | 3 | import * as blocks from './blocks'; 4 | import * as headers from './headers'; 5 | 6 | const {Author, Banner, ColoredText, CTA, Feed, Layout, Media, Meta, Suggest, YFM} = blocks; 7 | const {Header} = headers; 8 | 9 | export const validators = { 10 | blocks, 11 | headers, 12 | }; 13 | 14 | export const schemasForCustom = { 15 | headers: { 16 | [BlockType.Header]: Header, 17 | }, 18 | blocks: { 19 | [BlockType.Author]: Author, 20 | [BlockType.Banner]: Banner, 21 | [BlockType.ColoredText]: ColoredText, 22 | [BlockType.CTA]: CTA, 23 | [BlockType.Feed]: Feed, 24 | [BlockType.Layout]: Layout, 25 | [BlockType.Media]: Media, 26 | [BlockType.Meta]: Meta, 27 | [BlockType.Suggest]: Suggest, 28 | [BlockType.YFM]: YFM, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/schema/utils.ts: -------------------------------------------------------------------------------- 1 | import {BlockType} from '../models/common'; 2 | 3 | type BlockConfig = { 4 | [x in BlockType]: Object; 5 | }; 6 | 7 | type GenerateConfig = (config: BlockConfig) => {[x in BlockType]: BlockConfig}; 8 | 9 | export const generateConfig: GenerateConfig = (config) => { 10 | return Object.keys(config).reduce( 11 | (acc, blockKey) => ({ 12 | ...acc, 13 | blockKey: { 14 | blockKey: config[blockKey as BlockType], 15 | }, 16 | }), 17 | {} as {[x in BlockType]: BlockConfig}, 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | export {transformPageContent} from './data/transformPageContent'; 2 | export {createReadableContent} from './data/createReadableContent'; 3 | export {sanitizeMeta} from './data/sanitizeMeta'; 4 | export {transformPost, TransformPostType} from './data/transformPost'; 5 | export {BlockType} from './models/common'; 6 | -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import {withNaming} from '@bem-react/classname'; 2 | 3 | export const NAMESPACE = 'bc-'; 4 | 5 | export const cn = withNaming({e: '__', m: '_'}); 6 | export const block = withNaming({n: NAMESPACE, e: '__', m: '_'}); 7 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export {getBreadcrumbs} from './common'; 2 | -------------------------------------------------------------------------------- /src/utils/svg.ts: -------------------------------------------------------------------------------- 1 | export const a11yHiddenSvgProps = { 2 | // Hides element from a11y tree 3 | 'aria-hidden': true, 4 | }; 5 | -------------------------------------------------------------------------------- /styles/mixins.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import '@gravity-ui/page-constructor/styles/mixins.scss'; 3 | @import '@gravity-ui/uikit/styles/mixins.scss'; 4 | 5 | @mixin paddings() { 6 | &_padding-top { 7 | &_xs { 8 | padding-top: 0px; 9 | } 10 | 11 | &_s { 12 | padding-top: $indentXS; 13 | } 14 | 15 | &_m { 16 | padding-top: $indentM; 17 | } 18 | 19 | &_l { 20 | padding-top: $indentL; 21 | } 22 | 23 | &_xl { 24 | padding-top: $indentXL; 25 | } 26 | } 27 | 28 | &_padding-bottom { 29 | &_xs { 30 | padding-bottom: 0px; 31 | } 32 | 33 | &_s { 34 | padding-bottom: $indentXS; 35 | } 36 | 37 | &_m { 38 | padding-bottom: $indentM; 39 | } 40 | 41 | &_l { 42 | padding-bottom: $indentL; 43 | } 44 | 45 | &_xl { 46 | padding-bottom: $indentXL; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /styles/root.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --header-height: 64px; 3 | } 4 | 5 | .g-root { 6 | --g-text-accent-font-weight: 500; 7 | 8 | // cross-browser transparent color 9 | --bc-transparent: rgba(255, 255, 255, 0); 10 | --bc-image-padding: 4px; 11 | 12 | --bc-border-radius: var(--pc-border-radius, 24px); 13 | --bc-color-sfx-shadow: var(--g-color-base-simple-hover); 14 | --bc-color-line-generic-active-solid: #b3b3b3; 15 | --bc-color-base-float-hover: var(--g-color-base-float); 16 | --bc-monochrome-button: #262626; 17 | --bc-monochrome-button-hover: #393939; 18 | --bc-text-header-color: var(--g-color-text-primary); 19 | 20 | &.g-root_theme_dark { 21 | --bc-color-sfx-shadow: var(--g-color-sfx-shadow); 22 | --bc-color-line-generic-active-solid: #6c6c70; 23 | --bc-color-base-float-hover: var(--g-color-base-float-hover); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /styles/storybook/common.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins.scss'; 2 | 3 | .demo-container { 4 | min-height: calc(100vh - 1px); 5 | padding: 50px; 6 | } 7 | 8 | .demo-header { 9 | margin-bottom: 20px; 10 | font-size: var(--g-text-body-1-font-size); 11 | font-weight: bold; 12 | text-transform: uppercase; 13 | color: var(--g-color-text-primary); 14 | } 15 | 16 | .demo-description { 17 | margin-bottom: 25px; 18 | font-size: var(--g-text-body-2-font-size); 19 | line-height: var(--g-text-body-2-line-height); 20 | } 21 | 22 | .demo-markdown { 23 | .markdown-body { 24 | padding: 20px; 25 | color: var(--g-color-text-primary); 26 | 27 | a { 28 | color: var(--g-color-text-link); 29 | } 30 | } 31 | } 32 | 33 | .pc-page-constructor { 34 | --pc-first-block-mobile-indent: 32px; 35 | --pc-first-block-indent: 64px; 36 | margin-bottom: 120px; 37 | } 38 | 39 | .pc-layout { 40 | @include add-specificity(&) { 41 | min-height: auto; 42 | } 43 | } 44 | 45 | .g-root:not(.g-root_mobile) { 46 | scrollbar-width: var(--g-scrollbar-width); 47 | scrollbar-color: var(--g-color-scroll-handle) var(--g-color-scroll-track); 48 | 49 | @include scrollbar; 50 | } 51 | -------------------------------------------------------------------------------- /styles/storybook/index.scss: -------------------------------------------------------------------------------- 1 | // stylelint-disable declaration-no-important 2 | 3 | @import './common'; 4 | @import './palette'; 5 | @import './typography'; 6 | @import '~@diplodoc/transform/dist/css/yfm.css'; 7 | /** 8 | * storybook-host container style overrides 9 | * @see {@link https://github.com/philcockfield/storybook-host/issues/43} 10 | */ 11 | 12 | [data-css-18sl43g] { 13 | font-family: inherit !important; 14 | line-height: inherit !important; 15 | background: none !important; 16 | } 17 | 18 | .docs-story { 19 | // without this in docs objects with z-index < 0 dissapear 20 | z-index: 0 !important; 21 | } 22 | -------------------------------------------------------------------------------- /styles/storybook/palette.scss: -------------------------------------------------------------------------------- 1 | .palette { 2 | &__section { 3 | margin: auto auto 40px; 4 | position: relative; 5 | z-index: 1; 6 | } 7 | 8 | &__section-message { 9 | color: var(--g-color-text-brand); 10 | margin-bottom: 10px; 11 | margin-top: -10px; 12 | } 13 | 14 | &__section-content { 15 | margin-left: -20px; 16 | padding-left: 20px; 17 | padding-bottom: 35px; 18 | position: relative; 19 | display: inline-flex; 20 | } 21 | 22 | &__section-contrast-layer { 23 | z-index: -5; 24 | position: absolute; 25 | bottom: 0; 26 | left: 0; 27 | right: 0; 28 | height: 135px; 29 | } 30 | 31 | &__color { 32 | &-block { 33 | &:not(:last-child) { 34 | margin-right: 20px; 35 | } 36 | width: 118px; 37 | height: 158px; 38 | } 39 | 40 | &-description { 41 | margin-top: 8px; 42 | height: 40px; 43 | color: var(--g-color-text-primary); 44 | font-size: 13px; 45 | } 46 | &-caption { 47 | opacity: 0.5; 48 | } 49 | 50 | &-value { 51 | width: 118px; 52 | height: 118px; 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | 57 | &_border_yes { 58 | border: 1px solid var(--g-color-line-generic); 59 | } 60 | 61 | &_skip_yes { 62 | color: var(--g-color-text-info); 63 | } 64 | } 65 | } 66 | } 67 | 68 | .g-root_theme_light .palette__section-contrast-layer { 69 | background: #000; 70 | opacity: 0.02; 71 | } 72 | 73 | .g-root_theme_dark .palette__section-contrast-layer { 74 | background: #323138; 75 | } 76 | -------------------------------------------------------------------------------- /styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import '@gravity-ui/uikit/styles/fonts.css'; 2 | @import '~@gravity-ui/uikit/styles/styles.css'; 3 | @import './yfm.scss'; 4 | @import './fonts.scss'; 5 | -------------------------------------------------------------------------------- /styles/variables.scss: -------------------------------------------------------------------------------- 1 | @import '@gravity-ui/page-constructor/styles/variables.scss'; 2 | 3 | $namespace: 'bc-'; 4 | 5 | $borderRadius: var(--bc-border-radius); 6 | 7 | $headerSliderLargeBreakpoint: 1300px; 8 | -------------------------------------------------------------------------------- /svgo.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | multipass: true, 5 | js2svg: { 6 | pretty: true, 7 | }, 8 | plugins: [ 9 | { 10 | name: 'preset-default', 11 | params: { 12 | overrides: { 13 | removeViewBox: false, 14 | cleanupIDs: { 15 | minify: false, 16 | }, 17 | }, 18 | }, 19 | }, 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /test-utils/constants.ts: -------------------------------------------------------------------------------- 1 | import {PaddingSize} from '../src/models/paddings'; 2 | 3 | export const PADDING_SIZES: PaddingSize[] = ['xs', 's', 'm', 'l', 'xl']; 4 | export const PADDING_TYPES: string[] = ['paddingTop', 'paddingBottom']; 5 | 6 | export const PADDING_SIZES_BY_PADDING_TYPE: Record<string, PaddingSize>[] = PADDING_TYPES.reduce( 7 | (acc, optionKey) => { 8 | const mappedPaddingSizes = PADDING_SIZES.map((paddingSize) => { 9 | return {optionKey, paddingSize} as Record<string, PaddingSize>; 10 | }); 11 | 12 | return [...acc, ...mappedPaddingSizes]; 13 | }, 14 | [] as Record<string, PaddingSize>[], 15 | ); 16 | 17 | export const ERROR_INPUT_DATA_MESSAGE = 'There are errors in input test data'; 18 | -------------------------------------------------------------------------------- /test-utils/custom-environment.ts: -------------------------------------------------------------------------------- 1 | import {EnvironmentContext, JestEnvironmentConfig} from '@jest/environment'; 2 | import JsDomEnvironmetn from 'jest-environment-jsdom'; 3 | 4 | const matchMedia = (query: string) => 5 | ({ 6 | query, 7 | matches: false, 8 | addListener: function () {}, 9 | removeListener: function () {}, 10 | }) as unknown as MediaQueryList; 11 | 12 | class CustomEnvironment extends JsDomEnvironmetn { 13 | constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { 14 | super(config, context); 15 | this.global.matchMedia = matchMedia; 16 | } 17 | } 18 | 19 | export default CustomEnvironment; 20 | -------------------------------------------------------------------------------- /test-utils/setup-tests-after.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /test-utils/setup-tests.ts: -------------------------------------------------------------------------------- 1 | import {Lang, configure as uiKitConfigure} from '@gravity-ui/uikit'; 2 | import {configure} from '@testing-library/dom'; 3 | 4 | uiKitConfigure({ 5 | lang: Lang.En, 6 | }); 7 | 8 | configure({testIdAttribute: 'data-qa'}); 9 | -------------------------------------------------------------------------------- /test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gravity-ui/blog-constructor/bc4a7095e3d4a5474f3fb77f12cd41c6e5bdb59d/test.txt -------------------------------------------------------------------------------- /tsconfig.configure.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gravity-ui/tsconfig", 3 | "compilerOptions": { 4 | "allowJs": false, 5 | "declaration": true, 6 | "resolveJsonModule": true, 7 | "rootDirs": ["src"], 8 | "outDir": "configure", 9 | "baseUrl": ".", 10 | "paths": { 11 | "*": ["./node_modules/*"] 12 | } 13 | }, 14 | "include": ["src/configure.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gravity-ui/tsconfig/tsconfig", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "outDir": "build/esm", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "jsx": "react-jsx", 9 | "resolveJsonModule": true, 10 | "importHelpers": true 11 | }, 12 | "include": ["src/**/*.ts", "src/**/*.tsx"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "paths": {} 5 | }, 6 | "exclude": ["src/stories"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gravity-ui/tsconfig/tsconfig", 3 | "compilerOptions": { 4 | "allowJs": false, 5 | "declaration": true, 6 | "resolveJsonModule": true, 7 | "rootDirs": ["src"], 8 | "outDir": "server", 9 | "baseUrl": ".", 10 | "paths": { 11 | "*": ["./node_modules/*"] 12 | } 13 | }, 14 | "include": ["src/server.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gravity-ui/tsconfig", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "jsx": "react-jsx" 8 | } 9 | } 10 | --------------------------------------------------------------------------------