diff --git a/.storybook/stories/documentation/Blocks.mdx b/.storybook/stories/documentation/Blocks.mdx
index b70ea7b28..f9b4c4a39 100644
--- a/.storybook/stories/documentation/Blocks.mdx
+++ b/.storybook/stories/documentation/Blocks.mdx
@@ -56,3 +56,5 @@ _[Common field types](?id=documentation-types&viewMode=docs)_
## [Tabs](?path=/story/blocks-tabs--docs&viewMode=docs)
## [Form](?path=/story/blocks-form--docs&viewMode=docs)
+
+## [MarqueeLinks](?path=/story/blocks-marqueelinks--docs&viewMode=docs)
diff --git a/package-lock.json b/package-lock.json
index df7af68c3..79cc53869 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
"js-yaml-source-map": "^0.2.2",
"lodash": "^4.17.21",
"monaco-editor": "^0.38.0",
+ "react-fast-marquee": "^1.6.5",
"react-final-form": "^6.5.9",
"react-monaco-editor": "^0.53.0",
"react-player": "^2.9.0",
@@ -24832,6 +24833,15 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
+ "node_modules/react-fast-marquee": {
+ "version": "1.6.5",
+ "resolved": "https://registry.npmjs.org/react-fast-marquee/-/react-fast-marquee-1.6.5.tgz",
+ "integrity": "sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg==",
+ "peerDependencies": {
+ "react": ">= 16.8.0 || ^18.0.0",
+ "react-dom": ">= 16.8.0 || ^18.0.0"
+ }
+ },
"node_modules/react-final-form": {
"version": "6.5.9",
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz",
diff --git a/package.json b/package.json
index 14e0be484..5f028beb2 100644
--- a/package.json
+++ b/package.json
@@ -102,6 +102,7 @@
"js-yaml-source-map": "^0.2.2",
"lodash": "^4.17.21",
"monaco-editor": "^0.38.0",
+ "react-fast-marquee": "^1.6.5",
"react-final-form": "^6.5.9",
"react-monaco-editor": "^0.53.0",
"react-player": "^2.9.0",
diff --git a/src/blocks/MarqueeLinks/MarqueeLinks.scss b/src/blocks/MarqueeLinks/MarqueeLinks.scss
new file mode 100644
index 000000000..2a94a1f3f
--- /dev/null
+++ b/src/blocks/MarqueeLinks/MarqueeLinks.scss
@@ -0,0 +1,35 @@
+@import '../../../styles/mixins';
+
+$block: '.#{$ns}marquee-links-block';
+
+#{$block} {
+ &_left {
+ text-align: left;
+ }
+
+ &_right {
+ text-align: right;
+ }
+
+ &_center {
+ text-align: center;
+ }
+
+ &__items {
+ .rfm-child img {
+ margin: 0 57px;
+ }
+ .rfm-overlay::after,
+ .rfm-overlay::before {
+ --gradient-color: var(--g-color-base-background);
+ }
+ }
+
+ &__header {
+ margin-bottom: $indentXS;
+ }
+
+ &__description {
+ margin-bottom: $indentSM;
+ }
+}
diff --git a/src/blocks/MarqueeLinks/MarqueeLinks.tsx b/src/blocks/MarqueeLinks/MarqueeLinks.tsx
new file mode 100644
index 000000000..680a5590d
--- /dev/null
+++ b/src/blocks/MarqueeLinks/MarqueeLinks.tsx
@@ -0,0 +1,56 @@
+import React, {useCallback} from 'react';
+
+import {Link, Text} from '@gravity-ui/uikit';
+import Marquee from 'react-fast-marquee';
+
+import {HTML, Image} from '../../components';
+import {MarqueeLinksBlockProps, MarqueeLinksItem} from '../../models';
+import {block} from '../../utils';
+
+import './MarqueeLinks.scss';
+
+const b = block('marquee-links-block');
+
+export const MarqueeLinksBlock = ({
+ title,
+ description,
+ textAlign = 'left',
+ speed = 10,
+ items,
+}: MarqueeLinksBlockProps) => {
+ const renderItem = useCallback((item: MarqueeLinksItem) => {
+ const imageComponent = ;
+ if (item.url) {
+ return (
+
+ {imageComponent}
+
+ );
+ }
+ return imageComponent;
+ }, []);
+
+ if (!items.length) return null;
+
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+ {description && (
+
+
+ {description}
+
+
+ )}
+
+
+ );
+};
+
+export default MarqueeLinksBlock;
diff --git a/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.mdx b/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.mdx
new file mode 100644
index 000000000..1276a68ba
--- /dev/null
+++ b/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.mdx
@@ -0,0 +1,26 @@
+import {Meta} from '@storybook/blocks';
+
+import {StoryTemplate} from '../../../demo/StoryTemplate.mdx';
+import * as MarqueeLinksStories from './MarqueeLinks.stories.tsx';
+
+
+
+## Parameters
+
+`type: 'marquee-links-block'`
+
+`title?: string`
+
+`description?: string`
+
+`textAlign?: 'left' | 'right' | 'center'`
+
+`speed?: number` — default value 10.
+
+`items:` — Items, the block displays.
+
+- `src: string` - Link image.
+
+- `url: string` - Link
+
+
diff --git a/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.stories.tsx b/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.stories.tsx
new file mode 100644
index 000000000..1b1ef8fe4
--- /dev/null
+++ b/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.stories.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+import {Meta, StoryFn} from '@storybook/react';
+
+import {PageConstructor} from '../../../containers/PageConstructor';
+import {MarqueeLinksBlockModel, MarqueeLinksBlockProps} from '../../../models';
+import MarqueeLinksBlock from '../MarqueeLinks';
+
+import data from './data.json';
+
+export default {
+ title: 'Blocks/MarqueeLinks',
+ component: MarqueeLinksBlock,
+} as Meta;
+
+const DefaultTemplate: StoryFn = (args) => (
+
+);
+
+export const Default = DefaultTemplate.bind({});
+
+Default.args = data.default.content as MarqueeLinksBlockProps;
diff --git a/src/blocks/MarqueeLinks/__stories__/data.json b/src/blocks/MarqueeLinks/__stories__/data.json
new file mode 100644
index 000000000..159a80f1d
--- /dev/null
+++ b/src/blocks/MarqueeLinks/__stories__/data.json
@@ -0,0 +1,25 @@
+{
+ "default": {
+ "content": {
+ "type": "marquee-links-block",
+ "title": "Title",
+ "textAlign": "left",
+ "description": "description with a link",
+ "speed": 10,
+ "items": [
+ {
+ "src": "/story-assets/icons-link_1_64.svg",
+ "url": "https://example.com"
+ },
+ {
+ "src": "/story-assets/icons-link_1_64.svg",
+ "url": "https://example.com"
+ },
+ {
+ "src": "/story-assets/icons-link_1_64.svg",
+ "url": "https://example.com"
+ }
+ ]
+ }
+ }
+}
diff --git a/src/blocks/MarqueeLinks/__tests__/MarqueeLinks.visual.test.tsx b/src/blocks/MarqueeLinks/__tests__/MarqueeLinks.visual.test.tsx
new file mode 100644
index 000000000..d78c3d3d7
--- /dev/null
+++ b/src/blocks/MarqueeLinks/__tests__/MarqueeLinks.visual.test.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import {test} from '../../../../playwright/core/index';
+
+import {Default} from './helpers';
+
+test.describe('MarqueeLinks', () => {
+ test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => {
+ await mount();
+ await defaultDelay();
+ await expectScreenshot({skipTheme: 'dark'});
+ });
+});
diff --git a/src/blocks/MarqueeLinks/__tests__/helpers.tsx b/src/blocks/MarqueeLinks/__tests__/helpers.tsx
new file mode 100644
index 000000000..b8edf0961
--- /dev/null
+++ b/src/blocks/MarqueeLinks/__tests__/helpers.tsx
@@ -0,0 +1,5 @@
+import {composeStories} from '@storybook/react';
+
+import * as MarqueeLinksStories from '../__stories__/MarqueeLinks.stories';
+
+export const {Default} = composeStories(MarqueeLinksStories);
diff --git a/src/blocks/MarqueeLinks/schema.ts b/src/blocks/MarqueeLinks/schema.ts
new file mode 100644
index 000000000..ab7b44f9a
--- /dev/null
+++ b/src/blocks/MarqueeLinks/schema.ts
@@ -0,0 +1,42 @@
+import {BaseProps} from '../../schema/validators/common';
+
+export const MarqueeLink = {
+ type: 'object',
+ additionalProperties: false,
+ required: ['src'],
+ properties: {
+ src: {
+ type: 'string',
+ },
+ url: {
+ type: 'string',
+ },
+ },
+};
+
+export const MarqueeLinks = {
+ 'marquee-links-block': {
+ additionalProperties: false,
+ required: ['items'],
+ properties: {
+ ...BaseProps,
+ title: {
+ type: 'string',
+ },
+ description: {
+ type: 'string',
+ },
+ textAlign: {
+ type: 'string',
+ enum: ['left', 'right', 'center'],
+ },
+ speed: {
+ type: 'number',
+ },
+ items: {
+ type: 'array',
+ items: MarqueeLink,
+ },
+ },
+ },
+};
diff --git a/src/blocks/index.ts b/src/blocks/index.ts
index 6ce472095..1d4c927a6 100644
--- a/src/blocks/index.ts
+++ b/src/blocks/index.ts
@@ -17,3 +17,4 @@ export {default as ContentLayoutBlock} from './ContentLayout/ContentLayout';
export {default as ShareBlock} from './Share/Share';
export {default as FilterBlock} from './FilterBlock/FilterBlock';
export {default as FormBlock} from './Form/Form';
+export {default as MarqueeLinks} from './MarqueeLinks/MarqueeLinks';
diff --git a/src/blocks/validators.ts b/src/blocks/validators.ts
index a1a536817..8a0145895 100644
--- a/src/blocks/validators.ts
+++ b/src/blocks/validators.ts
@@ -13,3 +13,4 @@ export * from './Questions/schema';
export * from './Slider/schema';
export * from './Table/schema';
export * from './Share/schema';
+export * from './MarqueeLinks/schema';
diff --git a/src/constructor-items.ts b/src/constructor-items.ts
index 3bb5c4a7d..aa56a49b7 100644
--- a/src/constructor-items.ts
+++ b/src/constructor-items.ts
@@ -11,6 +11,7 @@ import {
IconsBlock,
InfoBlock,
MapBlock,
+ MarqueeLinks,
MediaBlock,
PromoFeaturesBlock,
QuestionsBlock,
@@ -62,6 +63,7 @@ export const blockMap = {
[BlockType.MapBlock]: MapBlock,
[BlockType.FilterBlock]: FilterBlock,
[BlockType.FormBlock]: FormBlock,
+ [BlockType.MarqueeLinks]: MarqueeLinks,
// unstable
[BlockType.SliderNewBlock]: SliderNewBlock,
};
diff --git a/src/models/constructor-items/blocks.ts b/src/models/constructor-items/blocks.ts
index 715d0f235..1bc3fa23f 100644
--- a/src/models/constructor-items/blocks.ts
+++ b/src/models/constructor-items/blocks.ts
@@ -61,6 +61,7 @@ export enum BlockType {
MapBlock = 'map-block',
FilterBlock = 'filter-block',
FormBlock = 'form-block',
+ MarqueeLinks = 'marquee-links-block',
// unstable
SliderNewBlock = 'slider-new-block',
}
@@ -280,6 +281,19 @@ export interface InfoBlockProps {
rightContent?: Omit;
}
+export type MarqueeLinksItem = {
+ src: string;
+ url?: string;
+};
+
+export interface MarqueeLinksBlockProps {
+ title?: string;
+ description?: string;
+ textAlign?: 'left' | 'right' | 'center';
+ speed?: number;
+ items: MarqueeLinksItem[];
+}
+
export interface TableProps {
content: string[][];
legend?: string[];
@@ -529,6 +543,10 @@ export type FormBlockModel = {
type: BlockType.FormBlock;
} & FormBlockProps;
+export type MarqueeLinksBlockModel = {
+ type: BlockType.MarqueeLinks;
+} & MarqueeLinksBlockProps;
+
// unstable block models
export type SliderNewBlockModel = {
type: BlockType.SliderNewBlock;
@@ -553,7 +571,8 @@ type BlockModels =
| ContentLayoutBlockModel
| ShareBLockModel
| FilterBlockModel
- | FormBlockModel;
+ | FormBlockModel
+ | MarqueeLinksBlockModel;
type UnstableBlockModels = SliderNewBlockModel;
diff --git a/src/schema/constants.ts b/src/schema/constants.ts
index 63d4b62a5..1278d9225 100644
--- a/src/schema/constants.ts
+++ b/src/schema/constants.ts
@@ -14,6 +14,7 @@ import {
IconsBlock,
InfoBlock,
MapBlock,
+ MarqueeLinks,
MediaBlock,
PromoFeaturesBlock,
QuestionsBlock,
@@ -56,6 +57,7 @@ export const blockSchemas: Record = {
...FilterBlock,
...FormBlock,
...SliderNewBlock,
+ ...MarqueeLinks,
};
export const cardSchemas = {
@@ -86,6 +88,7 @@ export const constructorBlockSchemaNames = [
'info-block',
'table-block',
'tabs-block',
+ 'marquee-links-block',
/** @deprecated */
'price-detailed',
'header-slider-block',
diff --git a/src/schema/validators/blocks.ts b/src/schema/validators/blocks.ts
index ca30f4929..684547b10 100644
--- a/src/schema/validators/blocks.ts
+++ b/src/schema/validators/blocks.ts
@@ -18,3 +18,4 @@ export * from '../../blocks/Share/schema';
export * from '../../blocks/FilterBlock/schema';
export * from '../../blocks/Form/schema';
export * from '../../blocks/SliderNew/schema';
+export * from '../../blocks/MarqueeLinks/schema';