@cloud-ru/uikit-product-modal-predefined 2.1.4 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## 2.2.1 (2026-04-03)
7
+
8
+ ### Only dependencies have been changed
9
+ * [@cloud-ru/uikit-product-copy-line@2.1.9]($PUBLIC_PROJECT_URL/blob/master/packages/copy-line/CHANGELOG.md)
10
+ * [@cloud-ru/uikit-product-icons@17.2.0]($PUBLIC_PROJECT_URL/blob/master/packages/icons/CHANGELOG.md)
11
+ * [@cloud-ru/uikit-product-mobile-modal@2.1.2]($PUBLIC_PROJECT_URL/blob/master/packages/mobile-modal/CHANGELOG.md)
12
+
13
+
14
+
15
+
16
+
17
+ # 2.2.0 (2026-03-31)
18
+
19
+
20
+ ### Features
21
+
22
+ * **FF-7462:** add video into release modal ([0f9bedd](https://github.com/cloud-ru-tech/uikit-product/commit/0f9bedd7c9989b660131a24be5df70a75cb4ba25))
23
+
24
+
25
+
26
+
27
+
6
28
  ## 2.1.4 (2026-03-31)
7
29
 
8
30
 
@@ -8,5 +8,7 @@ export type NoteItemProps = {
8
8
  src: string;
9
9
  alt: string;
10
10
  };
11
+ /** URL видео (при наличии показывается вместо статичного изображения) */
12
+ video?: string;
11
13
  };
12
- export declare function NoteItem({ title, description, image }: NoteItemProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function NoteItem({ title, description, image, video }: NoteItemProps): import("react/jsx-runtime").JSX.Element;
@@ -5,10 +5,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.NoteItem = NoteItem;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
8
9
  const uikit_product_markdown_1 = require("@cloud-ru/uikit-product-markdown");
9
10
  const scroll_1 = require("@snack-uikit/scroll");
11
+ const skeleton_1 = require("@snack-uikit/skeleton");
10
12
  const typography_1 = require("@snack-uikit/typography");
11
13
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
12
- function NoteItem({ title, description, image }) {
13
- return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.noteItemWrapper, children: [(0, jsx_runtime_1.jsx)(scroll_1.Scroll, { size: 's', barHideStrategy: 'never', children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.noteItemText, children: [(0, jsx_runtime_1.jsx)(typography_1.Typography.SansTitleL, { children: title }), (0, jsx_runtime_1.jsx)(uikit_product_markdown_1.Markdown, { value: description, className: styles_module_scss_1.default.noteItemMarkdownViewer })] }) }), (0, jsx_runtime_1.jsx)("img", { src: image.src, alt: image.alt, className: styles_module_scss_1.default.noteItemIllustration })] }));
14
+ function NoteItemMedia({ image, video }) {
15
+ const [videoReady, setVideoReady] = (0, react_1.useState)(false);
16
+ const [videoError, setVideoError] = (0, react_1.useState)(false);
17
+ (0, react_1.useEffect)(() => {
18
+ if (!video) {
19
+ return;
20
+ }
21
+ setVideoReady(false);
22
+ setVideoError(false);
23
+ }, [video]);
24
+ if (!video) {
25
+ return (0, jsx_runtime_1.jsx)("img", { src: image.src, alt: image.alt, className: styles_module_scss_1.default.noteItemIllustration });
26
+ }
27
+ return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.noteItemIllustrationSlot, children: videoError ? ((0, jsx_runtime_1.jsx)("img", { src: image.src, alt: image.alt, className: styles_module_scss_1.default.noteItemIllustration })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("video", { src: video, className: styles_module_scss_1.default.noteItemVideo, muted: true, loop: true, playsInline: true, autoPlay: true, onLoadedData: () => setVideoReady(true), onError: () => setVideoError(true), "aria-hidden": true }), !videoReady && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.noteItemMediaSkeletonOverlay, children: (0, jsx_runtime_1.jsx)(skeleton_1.Skeleton, { width: 380, height: 380, borderRadius: 8, loading: true }) }))] })) }));
28
+ }
29
+ function NoteItem({ title, description, image, video }) {
30
+ return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.noteItemWrapper, children: [(0, jsx_runtime_1.jsx)(scroll_1.Scroll, { size: 's', barHideStrategy: 'never', children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.noteItemText, children: [(0, jsx_runtime_1.jsx)(typography_1.Typography.SansTitleL, { children: title }), (0, jsx_runtime_1.jsx)(uikit_product_markdown_1.Markdown, { value: description, className: styles_module_scss_1.default.noteItemMarkdownViewer })] }) }), (0, jsx_runtime_1.jsx)(NoteItemMedia, { image: image, video: video })] }));
14
31
  }
@@ -12,6 +12,15 @@
12
12
  gap:var(--dimension-2m, 16px);
13
13
  }
14
14
 
15
+ .noteItemIllustrationSlot{
16
+ position:relative;
17
+ flex-shrink:0;
18
+ width:380px;
19
+ height:380px;
20
+ border-radius:var(--dimension-1m, 8px);
21
+ overflow:hidden;
22
+ }
23
+
15
24
  .noteItemIllustration{
16
25
  border-radius:var(--dimension-1m, 8px);
17
26
  width:380px;
@@ -19,6 +28,31 @@
19
28
  -o-object-fit:cover;
20
29
  object-fit:cover;
21
30
  pointer-events:none;
31
+ display:block;
32
+ }
33
+
34
+ .noteItemIllustrationSlot .noteItemIllustration,
35
+ .noteItemVideo{
36
+ width:100%;
37
+ height:100%;
38
+ border-radius:0;
39
+ }
40
+
41
+ .noteItemVideo{
42
+ -o-object-fit:cover;
43
+ object-fit:cover;
44
+ pointer-events:none;
45
+ display:block;
46
+ }
47
+
48
+ .noteItemMediaSkeletonOverlay{
49
+ position:absolute;
50
+ inset:0;
51
+ display:flex;
52
+ align-items:center;
53
+ justify-content:center;
54
+ border-radius:inherit;
55
+ pointer-events:none;
22
56
  }
23
57
 
24
58
  .noteItemMarkdownViewer{
@@ -5,5 +5,5 @@ type NoteItemMobileProps = NoteItemProps & {
5
5
  childrenScrollRefs: RefObject<HTMLElement[]>;
6
6
  onScrollRefInitialized(): void;
7
7
  };
8
- export declare function NoteItemMobile({ title, description, image, childrenScrollRefs, index, onScrollRefInitialized, }: NoteItemMobileProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function NoteItemMobile({ title, description, image, video, childrenScrollRefs, index, onScrollRefInitialized, }: NoteItemMobileProps): import("react/jsx-runtime").JSX.Element;
9
9
  export {};
@@ -5,15 +5,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.NoteItemMobile = NoteItemMobile;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
8
9
  const uikit_product_markdown_1 = require("@cloud-ru/uikit-product-markdown");
9
10
  const scroll_1 = require("@snack-uikit/scroll");
11
+ const skeleton_1 = require("@snack-uikit/skeleton");
10
12
  const typography_1 = require("@snack-uikit/typography");
11
13
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
12
- function NoteItemMobile({ title, description, image, childrenScrollRefs, index, onScrollRefInitialized, }) {
14
+ function NoteItemMobileMedia({ image, video }) {
15
+ const [videoReady, setVideoReady] = (0, react_1.useState)(false);
16
+ const [videoError, setVideoError] = (0, react_1.useState)(false);
17
+ (0, react_1.useEffect)(() => {
18
+ if (!video) {
19
+ return;
20
+ }
21
+ setVideoReady(false);
22
+ setVideoError(false);
23
+ }, [video]);
24
+ if (!video) {
25
+ return (0, jsx_runtime_1.jsx)("img", { src: image.src, alt: image.alt, className: styles_module_scss_1.default.noteItemIllustration });
26
+ }
27
+ return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.noteItemIllustrationSlot, children: videoError ? ((0, jsx_runtime_1.jsx)("img", { src: image.src, alt: image.alt, className: styles_module_scss_1.default.noteItemIllustration })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("video", { src: video, className: styles_module_scss_1.default.noteItemVideo, muted: true, loop: true, playsInline: true, autoPlay: true, onLoadedData: () => setVideoReady(true), onError: () => setVideoError(true), "aria-hidden": true }), !videoReady && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.noteItemMediaSkeletonOverlay, children: (0, jsx_runtime_1.jsx)(skeleton_1.Skeleton, { width: '100%', height: '100%', borderRadius: 8 }) }))] })) }));
28
+ }
29
+ function NoteItemMobile({ title, description, image, video, childrenScrollRefs, index, onScrollRefInitialized, }) {
13
30
  return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.noteItemWrapper, children: (0, jsx_runtime_1.jsx)(scroll_1.Scroll, { size: 'm', barHideStrategy: 'never', ref: el => {
14
31
  if (childrenScrollRefs.current && el) {
15
32
  childrenScrollRefs.current[index] = el;
16
33
  onScrollRefInitialized();
17
34
  }
18
- }, children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.noteItemContent, children: [(0, jsx_runtime_1.jsx)("img", { src: image.src, alt: image.alt, className: styles_module_scss_1.default.noteItemIllustration }), (0, jsx_runtime_1.jsx)(typography_1.Typography.SansTitleL, { children: title }), (0, jsx_runtime_1.jsx)(uikit_product_markdown_1.Markdown, { value: description, className: styles_module_scss_1.default.noteItemMarkdownViewer })] }) }) }));
35
+ }, children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.noteItemContent, children: [(0, jsx_runtime_1.jsx)(NoteItemMobileMedia, { image: image, video: video }), (0, jsx_runtime_1.jsx)(typography_1.Typography.SansTitleL, { children: title }), (0, jsx_runtime_1.jsx)(uikit_product_markdown_1.Markdown, { value: description, className: styles_module_scss_1.default.noteItemMarkdownViewer })] }) }) }));
19
36
  }
@@ -10,11 +10,50 @@
10
10
  padding-right:var(--dimension-2m, 16px);
11
11
  }
12
12
 
13
+ .noteItemIllustrationSlot{
14
+ position:relative;
15
+ width:100%;
16
+ max-width:380px;
17
+ align-self:center;
18
+ aspect-ratio:1/1;
19
+ border-radius:var(--dimension-1m, 8px);
20
+ overflow:hidden;
21
+ }
22
+
13
23
  .noteItemIllustration{
14
24
  width:100%;
25
+ height:auto;
15
26
  pointer-events:none;
16
27
  max-width:380px;
17
28
  align-self:center;
29
+ display:block;
30
+ }
31
+
32
+ .noteItemIllustrationSlot .noteItemIllustration{
33
+ width:100%;
34
+ height:100%;
35
+ max-width:none;
36
+ -o-object-fit:cover;
37
+ object-fit:cover;
38
+ }
39
+
40
+ .noteItemVideo{
41
+ width:100%;
42
+ height:100%;
43
+ -o-object-fit:cover;
44
+ object-fit:cover;
45
+ pointer-events:none;
46
+ display:block;
47
+ }
48
+
49
+ .noteItemMediaSkeletonOverlay{
50
+ position:absolute;
51
+ inset:0;
52
+ display:flex;
53
+ align-items:stretch;
54
+ justify-content:stretch;
55
+ border-radius:inherit;
56
+ pointer-events:none;
18
57
  }
19
58
 
20
59
  .noteItemSkeletonWrapper{
@@ -8,5 +8,7 @@ export type NoteItemProps = {
8
8
  src: string;
9
9
  alt: string;
10
10
  };
11
+ /** URL видео (при наличии показывается вместо статичного изображения) */
12
+ video?: string;
11
13
  };
12
- export declare function NoteItem({ title, description, image }: NoteItemProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function NoteItem({ title, description, image, video }: NoteItemProps): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,25 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
2
3
  import { Markdown } from '@cloud-ru/uikit-product-markdown';
3
4
  import { Scroll } from '@snack-uikit/scroll';
5
+ import { Skeleton } from '@snack-uikit/skeleton';
4
6
  import { Typography } from '@snack-uikit/typography';
5
7
  import styles from './styles.module.css';
6
- export function NoteItem({ title, description, image }) {
7
- return (_jsxs("div", { className: styles.noteItemWrapper, children: [_jsx(Scroll, { size: 's', barHideStrategy: 'never', children: _jsxs("div", { className: styles.noteItemText, children: [_jsx(Typography.SansTitleL, { children: title }), _jsx(Markdown, { value: description, className: styles.noteItemMarkdownViewer })] }) }), _jsx("img", { src: image.src, alt: image.alt, className: styles.noteItemIllustration })] }));
8
+ function NoteItemMedia({ image, video }) {
9
+ const [videoReady, setVideoReady] = useState(false);
10
+ const [videoError, setVideoError] = useState(false);
11
+ useEffect(() => {
12
+ if (!video) {
13
+ return;
14
+ }
15
+ setVideoReady(false);
16
+ setVideoError(false);
17
+ }, [video]);
18
+ if (!video) {
19
+ return _jsx("img", { src: image.src, alt: image.alt, className: styles.noteItemIllustration });
20
+ }
21
+ return (_jsx("div", { className: styles.noteItemIllustrationSlot, children: videoError ? (_jsx("img", { src: image.src, alt: image.alt, className: styles.noteItemIllustration })) : (_jsxs(_Fragment, { children: [_jsx("video", { src: video, className: styles.noteItemVideo, muted: true, loop: true, playsInline: true, autoPlay: true, onLoadedData: () => setVideoReady(true), onError: () => setVideoError(true), "aria-hidden": true }), !videoReady && (_jsx("div", { className: styles.noteItemMediaSkeletonOverlay, children: _jsx(Skeleton, { width: 380, height: 380, borderRadius: 8, loading: true }) }))] })) }));
22
+ }
23
+ export function NoteItem({ title, description, image, video }) {
24
+ return (_jsxs("div", { className: styles.noteItemWrapper, children: [_jsx(Scroll, { size: 's', barHideStrategy: 'never', children: _jsxs("div", { className: styles.noteItemText, children: [_jsx(Typography.SansTitleL, { children: title }), _jsx(Markdown, { value: description, className: styles.noteItemMarkdownViewer })] }) }), _jsx(NoteItemMedia, { image: image, video: video })] }));
8
25
  }
@@ -12,6 +12,15 @@
12
12
  gap:var(--dimension-2m, 16px);
13
13
  }
14
14
 
15
+ .noteItemIllustrationSlot{
16
+ position:relative;
17
+ flex-shrink:0;
18
+ width:380px;
19
+ height:380px;
20
+ border-radius:var(--dimension-1m, 8px);
21
+ overflow:hidden;
22
+ }
23
+
15
24
  .noteItemIllustration{
16
25
  border-radius:var(--dimension-1m, 8px);
17
26
  width:380px;
@@ -19,6 +28,31 @@
19
28
  -o-object-fit:cover;
20
29
  object-fit:cover;
21
30
  pointer-events:none;
31
+ display:block;
32
+ }
33
+
34
+ .noteItemIllustrationSlot .noteItemIllustration,
35
+ .noteItemVideo{
36
+ width:100%;
37
+ height:100%;
38
+ border-radius:0;
39
+ }
40
+
41
+ .noteItemVideo{
42
+ -o-object-fit:cover;
43
+ object-fit:cover;
44
+ pointer-events:none;
45
+ display:block;
46
+ }
47
+
48
+ .noteItemMediaSkeletonOverlay{
49
+ position:absolute;
50
+ inset:0;
51
+ display:flex;
52
+ align-items:center;
53
+ justify-content:center;
54
+ border-radius:inherit;
55
+ pointer-events:none;
22
56
  }
23
57
 
24
58
  .noteItemMarkdownViewer{
@@ -5,5 +5,5 @@ type NoteItemMobileProps = NoteItemProps & {
5
5
  childrenScrollRefs: RefObject<HTMLElement[]>;
6
6
  onScrollRefInitialized(): void;
7
7
  };
8
- export declare function NoteItemMobile({ title, description, image, childrenScrollRefs, index, onScrollRefInitialized, }: NoteItemMobileProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function NoteItemMobile({ title, description, image, video, childrenScrollRefs, index, onScrollRefInitialized, }: NoteItemMobileProps): import("react/jsx-runtime").JSX.Element;
9
9
  export {};
@@ -1,13 +1,30 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
2
3
  import { Markdown } from '@cloud-ru/uikit-product-markdown';
3
4
  import { Scroll } from '@snack-uikit/scroll';
5
+ import { Skeleton } from '@snack-uikit/skeleton';
4
6
  import { Typography } from '@snack-uikit/typography';
5
7
  import styles from './styles.module.css';
6
- export function NoteItemMobile({ title, description, image, childrenScrollRefs, index, onScrollRefInitialized, }) {
8
+ function NoteItemMobileMedia({ image, video }) {
9
+ const [videoReady, setVideoReady] = useState(false);
10
+ const [videoError, setVideoError] = useState(false);
11
+ useEffect(() => {
12
+ if (!video) {
13
+ return;
14
+ }
15
+ setVideoReady(false);
16
+ setVideoError(false);
17
+ }, [video]);
18
+ if (!video) {
19
+ return _jsx("img", { src: image.src, alt: image.alt, className: styles.noteItemIllustration });
20
+ }
21
+ return (_jsx("div", { className: styles.noteItemIllustrationSlot, children: videoError ? (_jsx("img", { src: image.src, alt: image.alt, className: styles.noteItemIllustration })) : (_jsxs(_Fragment, { children: [_jsx("video", { src: video, className: styles.noteItemVideo, muted: true, loop: true, playsInline: true, autoPlay: true, onLoadedData: () => setVideoReady(true), onError: () => setVideoError(true), "aria-hidden": true }), !videoReady && (_jsx("div", { className: styles.noteItemMediaSkeletonOverlay, children: _jsx(Skeleton, { width: '100%', height: '100%', borderRadius: 8 }) }))] })) }));
22
+ }
23
+ export function NoteItemMobile({ title, description, image, video, childrenScrollRefs, index, onScrollRefInitialized, }) {
7
24
  return (_jsx("div", { className: styles.noteItemWrapper, children: _jsx(Scroll, { size: 'm', barHideStrategy: 'never', ref: el => {
8
25
  if (childrenScrollRefs.current && el) {
9
26
  childrenScrollRefs.current[index] = el;
10
27
  onScrollRefInitialized();
11
28
  }
12
- }, children: _jsxs("div", { className: styles.noteItemContent, children: [_jsx("img", { src: image.src, alt: image.alt, className: styles.noteItemIllustration }), _jsx(Typography.SansTitleL, { children: title }), _jsx(Markdown, { value: description, className: styles.noteItemMarkdownViewer })] }) }) }));
29
+ }, children: _jsxs("div", { className: styles.noteItemContent, children: [_jsx(NoteItemMobileMedia, { image: image, video: video }), _jsx(Typography.SansTitleL, { children: title }), _jsx(Markdown, { value: description, className: styles.noteItemMarkdownViewer })] }) }) }));
13
30
  }
@@ -10,11 +10,50 @@
10
10
  padding-right:var(--dimension-2m, 16px);
11
11
  }
12
12
 
13
+ .noteItemIllustrationSlot{
14
+ position:relative;
15
+ width:100%;
16
+ max-width:380px;
17
+ align-self:center;
18
+ aspect-ratio:1/1;
19
+ border-radius:var(--dimension-1m, 8px);
20
+ overflow:hidden;
21
+ }
22
+
13
23
  .noteItemIllustration{
14
24
  width:100%;
25
+ height:auto;
15
26
  pointer-events:none;
16
27
  max-width:380px;
17
28
  align-self:center;
29
+ display:block;
30
+ }
31
+
32
+ .noteItemIllustrationSlot .noteItemIllustration{
33
+ width:100%;
34
+ height:100%;
35
+ max-width:none;
36
+ -o-object-fit:cover;
37
+ object-fit:cover;
38
+ }
39
+
40
+ .noteItemVideo{
41
+ width:100%;
42
+ height:100%;
43
+ -o-object-fit:cover;
44
+ object-fit:cover;
45
+ pointer-events:none;
46
+ display:block;
47
+ }
48
+
49
+ .noteItemMediaSkeletonOverlay{
50
+ position:absolute;
51
+ inset:0;
52
+ display:flex;
53
+ align-items:stretch;
54
+ justify-content:stretch;
55
+ border-radius:inherit;
56
+ pointer-events:none;
18
57
  }
19
58
 
20
59
  .noteItemSkeletonWrapper{
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloud-ru/uikit-product-modal-predefined",
3
3
  "title": "Modal Predefined",
4
- "version": "2.1.4",
4
+ "version": "2.2.1",
5
5
  "sideEffects": [
6
6
  "*.css",
7
7
  "*.woff",
@@ -36,10 +36,10 @@
36
36
  },
37
37
  "scripts": {},
38
38
  "dependencies": {
39
- "@cloud-ru/uikit-product-copy-line": "2.1.8",
40
- "@cloud-ru/uikit-product-icons": "17.1.1",
39
+ "@cloud-ru/uikit-product-copy-line": "2.1.9",
40
+ "@cloud-ru/uikit-product-icons": "17.2.0",
41
41
  "@cloud-ru/uikit-product-markdown": "1.1.4",
42
- "@cloud-ru/uikit-product-mobile-modal": "2.1.1",
42
+ "@cloud-ru/uikit-product-mobile-modal": "2.1.2",
43
43
  "@cloud-ru/uikit-product-switch-row": "1.1.5",
44
44
  "@cloud-ru/uikit-product-toggles-predefined": "2.0.5",
45
45
  "@cloud-ru/uikit-product-utils": "9.1.0",
@@ -57,5 +57,5 @@
57
57
  "peerDependencies": {
58
58
  "@cloud-ru/uikit-product-locale": "*"
59
59
  },
60
- "gitHead": "c5d44ec0fe45548b84559257798a1d2bdf7124ab"
60
+ "gitHead": "7ab3fb759acc8f2f742b85ac3fefdc02540c58f6"
61
61
  }
@@ -1,5 +1,8 @@
1
+ import { useEffect, useState } from 'react';
2
+
1
3
  import { Markdown } from '@cloud-ru/uikit-product-markdown';
2
4
  import { Scroll } from '@snack-uikit/scroll';
5
+ import { Skeleton } from '@snack-uikit/skeleton';
3
6
  import { Typography } from '@snack-uikit/typography';
4
7
 
5
8
  import styles from './styles.module.scss';
@@ -11,9 +14,55 @@ export type NoteItemProps = {
11
14
  description: string;
12
15
  /** Иллюстрация (изображение) */
13
16
  image: { src: string; alt: string };
17
+ /** URL видео (при наличии показывается вместо статичного изображения) */
18
+ video?: string;
14
19
  };
15
20
 
16
- export function NoteItem({ title, description, image }: NoteItemProps) {
21
+ function NoteItemMedia({ image, video }: Pick<NoteItemProps, 'image' | 'video'>) {
22
+ const [videoReady, setVideoReady] = useState(false);
23
+ const [videoError, setVideoError] = useState(false);
24
+
25
+ useEffect(() => {
26
+ if (!video) {
27
+ return;
28
+ }
29
+ setVideoReady(false);
30
+ setVideoError(false);
31
+ }, [video]);
32
+
33
+ if (!video) {
34
+ return <img src={image.src} alt={image.alt} className={styles.noteItemIllustration} />;
35
+ }
36
+
37
+ return (
38
+ <div className={styles.noteItemIllustrationSlot}>
39
+ {videoError ? (
40
+ <img src={image.src} alt={image.alt} className={styles.noteItemIllustration} />
41
+ ) : (
42
+ <>
43
+ <video
44
+ src={video}
45
+ className={styles.noteItemVideo}
46
+ muted
47
+ loop
48
+ playsInline
49
+ autoPlay
50
+ onLoadedData={() => setVideoReady(true)}
51
+ onError={() => setVideoError(true)}
52
+ aria-hidden
53
+ />
54
+ {!videoReady && (
55
+ <div className={styles.noteItemMediaSkeletonOverlay}>
56
+ <Skeleton width={380} height={380} borderRadius={8} loading />
57
+ </div>
58
+ )}
59
+ </>
60
+ )}
61
+ </div>
62
+ );
63
+ }
64
+
65
+ export function NoteItem({ title, description, image, video }: NoteItemProps) {
17
66
  return (
18
67
  <div className={styles.noteItemWrapper}>
19
68
  <Scroll size='s' barHideStrategy='never'>
@@ -22,7 +71,7 @@ export function NoteItem({ title, description, image }: NoteItemProps) {
22
71
  <Markdown value={description} className={styles.noteItemMarkdownViewer} />
23
72
  </div>
24
73
  </Scroll>
25
- <img src={image.src} alt={image.alt} className={styles.noteItemIllustration} />
74
+ <NoteItemMedia image={image} video={video} />
26
75
  </div>
27
76
  );
28
77
  }
@@ -14,12 +14,46 @@
14
14
  gap: styles-theme-variables.$dimension-2m;
15
15
  }
16
16
 
17
+ .noteItemIllustrationSlot {
18
+ position: relative;
19
+ flex-shrink: 0;
20
+ width: 380px;
21
+ height: 380px;
22
+ border-radius: styles-theme-variables.$dimension-1m;
23
+ overflow: hidden;
24
+ }
25
+
17
26
  .noteItemIllustration {
18
27
  border-radius: styles-theme-variables.$dimension-1m;
19
28
  width: 380px;
20
29
  height: 380px;
21
30
  object-fit: cover;
22
31
  pointer-events: none;
32
+ display: block;
33
+ }
34
+
35
+ .noteItemIllustrationSlot .noteItemIllustration,
36
+ .noteItemVideo {
37
+ width: 100%;
38
+ height: 100%;
39
+ border-radius: 0;
40
+ }
41
+
42
+ .noteItemVideo {
43
+ object-fit: cover;
44
+ pointer-events: none;
45
+ display: block;
46
+ }
47
+
48
+ .noteItemMediaSkeletonOverlay {
49
+ position: absolute;
50
+ inset: 0;
51
+
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ border-radius: inherit;
56
+ pointer-events: none;
23
57
  }
24
58
 
25
59
  .noteItemMarkdownViewer {
@@ -1,7 +1,8 @@
1
- import { RefObject } from 'react';
1
+ import { RefObject, useEffect, useState } from 'react';
2
2
 
3
3
  import { Markdown } from '@cloud-ru/uikit-product-markdown';
4
4
  import { Scroll } from '@snack-uikit/scroll';
5
+ import { Skeleton } from '@snack-uikit/skeleton';
5
6
  import { Typography } from '@snack-uikit/typography';
6
7
 
7
8
  import { NoteItemProps } from '../NoteItem';
@@ -13,10 +14,55 @@ type NoteItemMobileProps = NoteItemProps & {
13
14
  onScrollRefInitialized(): void;
14
15
  };
15
16
 
17
+ function NoteItemMobileMedia({ image, video }: Pick<NoteItemProps, 'image' | 'video'>) {
18
+ const [videoReady, setVideoReady] = useState(false);
19
+ const [videoError, setVideoError] = useState(false);
20
+
21
+ useEffect(() => {
22
+ if (!video) {
23
+ return;
24
+ }
25
+ setVideoReady(false);
26
+ setVideoError(false);
27
+ }, [video]);
28
+
29
+ if (!video) {
30
+ return <img src={image.src} alt={image.alt} className={styles.noteItemIllustration} />;
31
+ }
32
+
33
+ return (
34
+ <div className={styles.noteItemIllustrationSlot}>
35
+ {videoError ? (
36
+ <img src={image.src} alt={image.alt} className={styles.noteItemIllustration} />
37
+ ) : (
38
+ <>
39
+ <video
40
+ src={video}
41
+ className={styles.noteItemVideo}
42
+ muted
43
+ loop
44
+ playsInline
45
+ autoPlay
46
+ onLoadedData={() => setVideoReady(true)}
47
+ onError={() => setVideoError(true)}
48
+ aria-hidden
49
+ />
50
+ {!videoReady && (
51
+ <div className={styles.noteItemMediaSkeletonOverlay}>
52
+ <Skeleton width='100%' height='100%' borderRadius={8} />
53
+ </div>
54
+ )}
55
+ </>
56
+ )}
57
+ </div>
58
+ );
59
+ }
60
+
16
61
  export function NoteItemMobile({
17
62
  title,
18
63
  description,
19
64
  image,
65
+ video,
20
66
  childrenScrollRefs,
21
67
  index,
22
68
  onScrollRefInitialized,
@@ -34,7 +80,7 @@ export function NoteItemMobile({
34
80
  }}
35
81
  >
36
82
  <div className={styles.noteItemContent}>
37
- <img src={image.src} alt={image.alt} className={styles.noteItemIllustration} />
83
+ <NoteItemMobileMedia image={image} video={video} />
38
84
  <Typography.SansTitleL>{title}</Typography.SansTitleL>
39
85
  <Markdown value={description} className={styles.noteItemMarkdownViewer} />
40
86
  </div>
@@ -13,11 +13,49 @@
13
13
  padding-right: styles-tokens-drawer.$dimension-2m;
14
14
  }
15
15
 
16
+ .noteItemIllustrationSlot {
17
+ position: relative;
18
+ width: 100%;
19
+ max-width: 380px;
20
+ align-self: center;
21
+ aspect-ratio: 1 / 1;
22
+ border-radius: styles-theme-variables.$dimension-1m;
23
+ overflow: hidden;
24
+ }
25
+
16
26
  .noteItemIllustration {
17
27
  width: 100%;
28
+ height: auto;
18
29
  pointer-events: none;
19
30
  max-width: 380px;
20
31
  align-self: center;
32
+ display: block;
33
+ }
34
+
35
+ .noteItemIllustrationSlot .noteItemIllustration {
36
+ width: 100%;
37
+ height: 100%;
38
+ max-width: none;
39
+ object-fit: cover;
40
+ }
41
+
42
+ .noteItemVideo {
43
+ width: 100%;
44
+ height: 100%;
45
+ object-fit: cover;
46
+ pointer-events: none;
47
+ display: block;
48
+ }
49
+
50
+ .noteItemMediaSkeletonOverlay {
51
+ position: absolute;
52
+ inset: 0;
53
+
54
+ display: flex;
55
+ align-items: stretch;
56
+ justify-content: stretch;
57
+ border-radius: inherit;
58
+ pointer-events: none;
21
59
  }
22
60
 
23
61
  .noteItemSkeletonWrapper {