@sima-land/moleculas 14.2.0 → 15.1.0

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.
@@ -14,9 +14,6 @@ export interface BadgeProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEl
14
14
  /**
15
15
  * Шильдик.
16
16
  * @param props Свойства.
17
- * @param props.color Цвет.
18
- * @param props.fields Содержимое.
19
- * @param props.href Ссылка.
20
17
  * @return Элемент.
21
18
  */
22
- export declare const Badge: ({ className, color, fields, href, style, "data-testid": testId, ...restProps }: BadgeProps) => JSX.Element;
19
+ export declare const Badge: React.ForwardRefExoticComponent<BadgeProps & React.RefAttributes<HTMLAnchorElement>>;
@@ -1,4 +1,23 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
2
21
  var __rest = (this && this.__rest) || function (s, e) {
3
22
  var t = {};
4
23
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -15,23 +34,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
34
  };
16
35
  Object.defineProperty(exports, "__esModule", { value: true });
17
36
  exports.Badge = void 0;
18
- const react_1 = __importDefault(require("react"));
19
- const bind_1 = __importDefault(require("classnames/bind"));
37
+ const react_1 = __importStar(require("react"));
20
38
  const timer_1 = require("@sima-land/ui-nucleons/timer");
39
+ const bind_1 = __importDefault(require("classnames/bind"));
21
40
  const badge_module_scss_1 = __importDefault(require("./badge.module.scss"));
22
41
  const cx = bind_1.default.bind(badge_module_scss_1.default);
23
42
  /**
24
43
  * Шильдик.
25
44
  * @param props Свойства.
26
- * @param props.color Цвет.
27
- * @param props.fields Содержимое.
28
- * @param props.href Ссылка.
29
45
  * @return Элемент.
30
46
  */
31
- const Badge = (_a) => {
47
+ exports.Badge = (0, react_1.forwardRef)(function Badge(_a, ref) {
32
48
  var { className, color, fields, href, style, 'data-testid': testId = 'badge' } = _a, restProps = __rest(_a, ["className", "color", "fields", "href", "style", 'data-testid']);
33
49
  const iconOnly = fields.length === 1 && fields[0].type === 'svg-url';
34
- return (react_1.default.createElement("a", Object.assign({}, restProps, { "data-testid": testId, href: href, style: Object.assign(Object.assign({}, style), { '--badge-color': color }), className: cx('root', iconOnly && 'icon-only', href && 'interactive', className), children: iconOnly ? (react_1.default.createElement("img", { className: cx('icon'), src: fields[0].value })) : (
50
+ return (react_1.default.createElement("a", Object.assign({ ref: ref }, restProps, { "data-testid": testId, href: href, style: Object.assign(Object.assign({}, style), { '--badge-color': color }), className: cx('root', iconOnly && 'icon-only', href && 'interactive', className), children: iconOnly ? (react_1.default.createElement("img", { className: cx('icon'), src: fields[0].value })) : (
35
51
  // вложенный span нужен для того чтобы объединить `display: inline-flex` и `text-overflow: ellipsis`
36
52
  react_1.default.createElement("span", { className: cx('content') }, fields.reduce((acc, item, i) => {
37
53
  let result = null;
@@ -48,8 +64,7 @@ const Badge = (_a) => {
48
64
  i < fields.length - 1 && acc.push(' ');
49
65
  return acc;
50
66
  }, []))) })));
51
- };
52
- exports.Badge = Badge;
67
+ });
53
68
  /**
54
69
  * Форматирует оставшееся время.
55
70
  * @param distance Оставшееся время.
@@ -11,6 +11,7 @@
11
11
  }
12
12
 
13
13
  .item {
14
+ max-width: 100%;
14
15
  margin-bottom: 4px;
15
16
  &:not(:last-child) {
16
17
  margin-right: 4px;
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { ReactNode } from 'react';
2
2
  export interface BadgeListProps {
3
3
  /** Список данных для шильдиков. */
4
4
  children?: React.ReactNode;
@@ -14,4 +14,11 @@ export interface BadgeListProps {
14
14
  * @param props Свойства компонента.
15
15
  * @return Элемент.
16
16
  */
17
- export declare const BadgeList: ({ children, className, lineLimit, style }: BadgeListProps) => JSX.Element;
17
+ export declare const BadgeList: {
18
+ ({ children, className, lineLimit, style }: BadgeListProps): JSX.Element;
19
+ Slot: typeof BadgeListSlot;
20
+ };
21
+ declare function BadgeListSlot({ children }: {
22
+ children?: ReactNode;
23
+ }): JSX.Element;
24
+ export {};
@@ -33,10 +33,18 @@ const cx = bind_1.default.bind(badge_list_module_scss_1.default);
33
33
  * @param props Свойства компонента.
34
34
  * @return Элемент.
35
35
  */
36
- const BadgeList = ({ children, className, lineLimit, style }) => (react_1.default.createElement("div", { className: cx('root', className, lineLimit && 'line-limit'), style: lineLimit ? Object.assign(Object.assign({}, style), { '--line-limit': lineLimit }) : style }, react_1.Children.toArray(children).reduce((list, item) => {
37
- (0, react_1.isValidElement)(item) &&
38
- item.type === badge_1.Badge &&
39
- list.push((0, react_1.cloneElement)(item, { className: cx('item', item.props.className) }));
40
- return list;
41
- }, [])));
36
+ const BadgeList = ({ children, className, lineLimit, style }) => (react_1.default.createElement("div", { className: cx('root', className, lineLimit && 'line-limit'), style: lineLimit ? Object.assign(Object.assign({}, style), { '--line-limit': lineLimit }) : style }, react_1.Children.toArray(children).map(child => {
37
+ switch (true) {
38
+ case (0, react_1.isValidElement)(child) && child.type === badge_1.Badge:
39
+ return react_1.default.createElement(BadgeListSlot, null, child);
40
+ case (0, react_1.isValidElement)(child) && child.type === BadgeListSlot:
41
+ return child;
42
+ default:
43
+ return null;
44
+ }
45
+ })));
42
46
  exports.BadgeList = BadgeList;
47
+ function BadgeListSlot({ children }) {
48
+ return react_1.default.createElement("div", { className: cx('item') }, children);
49
+ }
50
+ exports.BadgeList.Slot = BadgeListSlot;
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { MediaData } from '../types';
3
3
  import { ModalProps } from '@sima-land/ui-nucleons/modal';
4
+ import { ReviewInfoProps } from './review-info';
4
5
  export interface GalleryModalProps extends Pick<ModalProps, 'withScrollDisable' | 'scrollDisableOptions'> {
5
6
  /** Список медиа. */
6
7
  media: MediaData[];
@@ -9,10 +10,7 @@ export interface GalleryModalProps extends Pick<ModalProps, 'withScrollDisable'
9
10
  /** Будет вызвана при переключении элемента из списка медиа. */
10
11
  onMediaChange?: (data: MediaData, index: number) => void;
11
12
  /** Данные отзыва, выводятся внизу окна. */
12
- review?: {
13
- rating: number;
14
- author: string;
15
- };
13
+ review?: Pick<ReviewInfoProps, 'rating' | 'author' | 'content' | 'loading' | 'notAffectRating'>;
16
14
  /** Будет вызвана при попытке перейти к отзыву. */
17
15
  onGoToReview?: () => void;
18
16
  /** Будет вызвана при закрытии. */
@@ -50,38 +50,37 @@ const gallery_modal_module_scss_1 = __importDefault(require("./gallery-modal.mod
50
50
  const cx = bind_1.default.bind(gallery_modal_module_scss_1.default);
51
51
  const GalleryModal = ({ media, defaultMediaIndex = 0, review, onClose, onGoToReview, onMediaChange, withScrollDisable, scrollDisableOptions, onVideoEvent, }) => {
52
52
  const [currentIndex, setCurrent] = (0, react_1.useState)(defaultMediaIndex);
53
- const thumbnailsRef = (0, react_1.useRef)(null);
54
- const areaRef = (0, react_1.useRef)(null);
55
- const footerGutter = 40;
56
- const footerHeight = 20;
57
- const correction = review ? footerGutter + footerHeight : 0;
58
- const size = (0, utils_1.useRectFit)(areaRef, { correction });
59
- const innerStyles = Object.assign({}, (size !== null && {
60
- '--area-height': `${size + correction}px`,
61
- '--all-round-view-size': `${size}px`,
62
- }));
53
+ const rootRef = (0, react_1.useRef)(null);
63
54
  const handlerRef = (0, identity_1.useIdentityRef)(onMediaChange);
55
+ const thumbnailsRef = (0, react_1.useRef)(null);
64
56
  (0, react_1.useEffect)(() => {
65
57
  var _a;
66
58
  (_a = handlerRef.current) === null || _a === void 0 ? void 0 : _a.call(handlerRef, media[currentIndex], currentIndex);
67
59
  thumbnailsRef.current &&
68
60
  (0, scroll_to_child_1.scrollToChild)(thumbnailsRef.current, thumbnailsRef.current.children[currentIndex]);
69
61
  }, [currentIndex]);
62
+ const size = (0, utils_1.useSquareFit)(rootRef, { hasReview: Boolean(review) });
63
+ const style = size
64
+ ? {
65
+ '--square-size': `${size}px`,
66
+ '--all-round-view-size': `${size}px`,
67
+ }
68
+ : {};
70
69
  return (react_1.default.createElement(modal_1.Modal, Object.assign({ size: 'fullscreen', onClose: onClose }, { withScrollDisable, scrollDisableOptions }),
71
70
  react_1.default.createElement(modal_1.Modal.Header, { onClose: onClose }),
72
71
  react_1.default.createElement(modal_1.Modal.Body, null,
73
- react_1.default.createElement(layout_1.DesktopLayout, { className: cx('root'), style: innerStyles },
74
- size !== null && media.length > 1 && (react_1.default.createElement("div", { className: cx('thumbnails'), ref: thumbnailsRef }, media.map((item, index) => (react_1.default.createElement(thumbnail_1.Thumbnail, Object.assign({ key: index, className: cx('thumbnail'), type: item.type, checked: index === currentIndex, onClick: () => setCurrent(index) }, (item.type === 'image' && {
75
- src: item.data.thumbnailSrc,
76
- alt: item.data.alt || '',
77
- }))))))),
78
- react_1.default.createElement("div", { ref: areaRef, className: cx('area') }, size !== null && (react_1.default.createElement("div", { className: cx('inner'), style: { width: `${size}px` } },
72
+ react_1.default.createElement(layout_1.DesktopLayout, { className: cx('root'), ref: rootRef, style: style },
73
+ react_1.default.createElement("div", { className: cx('main') },
74
+ react_1.default.createElement("div", { className: cx('thumbnails'), ref: thumbnailsRef }, media.map((item, index) => (react_1.default.createElement(thumbnail_1.Thumbnail, Object.assign({ key: index, className: cx('thumbnail'), type: item.type, checked: index === currentIndex, onClick: () => setCurrent(index) }, (item.type === 'image' && {
75
+ src: item.data.thumbnailSrc,
76
+ alt: item.data.alt || '',
77
+ })))))),
79
78
  react_1.default.createElement("div", { className: cx('square') },
80
79
  react_1.default.createElement(Media, Object.assign({}, media[currentIndex], { onVideoEvent: onVideoEvent })),
81
80
  media.length > 1 && (react_1.default.createElement(react_1.default.Fragment, null,
82
81
  react_1.default.createElement(arrow_button_1.ArrowButton, { direction: 'left', className: cx('button', 'left'), onClick: () => setCurrent((media.length + currentIndex - 1) % media.length), "data-testid": 'gallery-modal:prev-media-button' }),
83
- react_1.default.createElement(arrow_button_1.ArrowButton, { direction: 'right', className: cx('button', 'right'), onClick: () => setCurrent((currentIndex + 1) % media.length), "data-testid": 'gallery-modal:next-media-button' })))),
84
- review && (react_1.default.createElement(review_info_1.ReviewInfo, { rating: review.rating, author: review.author, onGoToReview: onGoToReview, className: cx('footer') })))))))));
82
+ react_1.default.createElement(arrow_button_1.ArrowButton, { direction: 'right', className: cx('button', 'right'), onClick: () => setCurrent((currentIndex + 1) % media.length), "data-testid": 'gallery-modal:next-media-button' })))))),
83
+ size !== null && (react_1.default.createElement("div", { className: cx('footer'), style: style }, review && react_1.default.createElement(review_info_1.ReviewInfo, Object.assign({}, review, { onGoToReview: onGoToReview })))))));
85
84
  };
86
85
  exports.GalleryModal = GalleryModal;
87
86
  const Media = (_a) => {
@@ -1,47 +1,52 @@
1
1
  @use 'node_modules/@sima-land/ui-nucleons/colors';
2
2
 
3
3
  .root {
4
- height: calc(100vh - var(--header-height) - 40px);
5
4
  position: relative;
5
+ height: calc(100vh - var(--header-height));
6
+ display: flex;
7
+ flex-direction: column;
8
+ // box-shadow: inset 0 0 0 6px #f00;
6
9
  }
7
10
 
8
- .thumbnails {
11
+ .main {
12
+ position: relative;
13
+ // box-shadow: inset 0 0 0 6px #0f0;
14
+ width: 100%;
9
15
  height: var(--area-height);
16
+ }
17
+
18
+ .thumbnails {
19
+ width: 104px;
20
+ height: 100%;
10
21
  overflow-x: hidden;
11
22
  overflow-y: auto;
12
23
  position: absolute;
13
- width: 104px;
14
24
  .thumbnail + .thumbnail {
15
25
  margin-top: 8px;
16
26
  }
17
27
  }
18
28
 
19
- .area {
20
- height: 100%;
21
- margin: 0 auto;
22
- width: calc(100% - (2 * (120px + 56px + 24px)));
23
- .inner {
24
- height: 100%;
25
- margin: 0 auto;
29
+ .square {
30
+ width: var(--square-size);
31
+ height: var(--square-size);
32
+ position: relative;
33
+ left: 50%;
34
+ transform: translateX(-50%);
35
+ &::after {
36
+ content: '';
37
+ display: block;
38
+ padding-top: 100%;
39
+ width: 100%;
26
40
  }
27
- .square {
28
- position: relative;
29
- &::after {
30
- content: '';
31
- display: block;
32
- padding-top: 100%;
33
- width: 100%;
41
+ .button {
42
+ position: absolute;
43
+ top: 50%;
44
+ transform: translateY(-50%);
45
+ &.left {
46
+ right: calc(100% + 24px);
34
47
  }
35
- .button {
36
- position: absolute;
37
- top: 50%;
38
- transform: translateY(-50%);
39
- &.left {
40
- right: calc(100% + 24px);
41
- }
42
- &.right {
43
- left: calc(100% + 24px);
44
- }
48
+ &.right {
49
+ left: calc(100% + 24px);
45
50
  }
46
51
  }
47
52
  }
@@ -71,5 +76,10 @@
71
76
  }
72
77
 
73
78
  .footer {
74
- margin-top: 40px;
79
+ position: absolute;
80
+ bottom: 0;
81
+ left: 0;
82
+ right: 0;
83
+ margin-top: auto;
84
+ // background-color: #ff000024;
75
85
  }
@@ -1,8 +1,11 @@
1
- /// <reference types="react" />
1
+ import { ReactNode } from 'react';
2
2
  export interface ReviewInfoProps {
3
3
  rating: number;
4
4
  author: string;
5
+ content?: ReactNode;
5
6
  onGoToReview?: () => void;
7
+ notAffectRating?: boolean;
6
8
  className?: string;
9
+ loading?: boolean;
7
10
  }
8
- export declare const ReviewInfo: ({ rating, author, onGoToReview, className }: ReviewInfoProps) => JSX.Element;
11
+ export declare const ReviewInfo: ({ rating, author, onGoToReview, content, className, notAffectRating, loading, }: ReviewInfoProps) => JSX.Element;
@@ -1,16 +1,75 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
2
21
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
23
  };
5
24
  Object.defineProperty(exports, "__esModule", { value: true });
6
25
  exports.ReviewInfo = void 0;
7
- const react_1 = __importDefault(require("react"));
26
+ const react_1 = __importStar(require("react"));
8
27
  const link_1 = require("@sima-land/ui-nucleons/link");
9
28
  const rating_1 = require("@sima-land/ui-nucleons/rating");
10
- const classnames_1 = __importDefault(require("classnames"));
29
+ const bind_1 = __importDefault(require("classnames/bind"));
11
30
  const review_info_module_scss_1 = __importDefault(require("./review-info.module.scss"));
12
- const ReviewInfo = ({ rating, author, onGoToReview, className }) => (react_1.default.createElement("div", { className: (0, classnames_1.default)(review_info_module_scss_1.default.root, className) },
13
- react_1.default.createElement(rating_1.Rating, { value: rating }),
14
- react_1.default.createElement("div", { className: review_info_module_scss_1.default.author }, author),
15
- onGoToReview && (react_1.default.createElement(link_1.Link, { className: review_info_module_scss_1.default.link, pseudo: true, onClick: onGoToReview, "data-testid": 'gallery-modal:go-to-review-link' }, "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u0442\u0437\u044B\u0432\u0443"))));
31
+ const on_1 = __importDefault(require("@sima-land/ui-nucleons/helpers/on"));
32
+ const shadows_1 = require("@sima-land/ui-nucleons/styling/shadows");
33
+ const custom_scrollbar_1 = require("@sima-land/ui-nucleons/_internal/custom-scrollbar");
34
+ const cx = bind_1.default.bind(review_info_module_scss_1.default);
35
+ const ReviewInfo = ({ rating, author, onGoToReview, content, className, notAffectRating, loading, }) => {
36
+ const [clamped, setClamped] = (0, react_1.useState)(false);
37
+ const [opened, setOpened] = (0, react_1.useState)(false);
38
+ const contentRef = (0, react_1.useRef)(null);
39
+ const checkClamp = (0, react_1.useCallback)(() => {
40
+ const contentEl = contentRef.current;
41
+ if (!opened && contentEl) {
42
+ setClamped(contentEl.scrollHeight > contentEl.clientHeight);
43
+ }
44
+ }, [opened]);
45
+ (0, react_1.useEffect)(checkClamp, [content, opened]);
46
+ (0, react_1.useEffect)(() => (0, on_1.default)(window, 'resize', checkClamp), [content, checkClamp]);
47
+ return (react_1.default.createElement("div", { className: cx('root', className, opened && shadows_1.BoxShadow.z2straight) },
48
+ loading && (react_1.default.createElement(Layout, null,
49
+ react_1.default.createElement(LoadingView, null))),
50
+ !loading && (react_1.default.createElement(react_1.default.Fragment, null,
51
+ react_1.default.createElement(Layout, { className: cx('header') },
52
+ react_1.default.createElement("div", { className: cx('rating') }, notAffectRating ? 'Не влияет на рейтинг' : react_1.default.createElement(rating_1.Rating, { value: rating })),
53
+ react_1.default.createElement("div", { className: cx('author') }, author),
54
+ onGoToReview && (react_1.default.createElement(link_1.Link, { className: cx('link'), pseudo: true, onClick: onGoToReview, "data-testid": 'gallery-modal:go-to-review-link' }, "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u0442\u0437\u044B\u0432\u0443"))),
55
+ content && !opened && (react_1.default.createElement(Layout, null,
56
+ react_1.default.createElement("div", { ref: contentRef, className: cx('content'), "data-testid": 'review-info:collapsed-content' }, content))),
57
+ content && opened && (react_1.default.createElement(custom_scrollbar_1.CustomScrollbar, { className: cx('content', 'opened') },
58
+ react_1.default.createElement(Layout, null,
59
+ react_1.default.createElement("div", { "data-testid": 'review-info:expanded-content' }, content),
60
+ react_1.default.createElement(link_1.Link, { pseudo: true, onClick: () => setOpened(false), "data-testid": 'review-info:collapse' }, "\u0421\u043A\u0440\u044B\u0442\u044C \u043E\u0442\u0437\u044B\u0432")))),
61
+ content && !opened && clamped && (react_1.default.createElement(Layout, null,
62
+ react_1.default.createElement(link_1.Link, { pseudo: true, onClick: () => setOpened(true), "data-testid": 'review-info:expand' }, "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E")))))));
63
+ };
16
64
  exports.ReviewInfo = ReviewInfo;
65
+ function Layout({ className, children }) {
66
+ return react_1.default.createElement("div", { className: cx(className, 'layout') }, children);
67
+ }
68
+ function LoadingView() {
69
+ return (react_1.default.createElement("div", { className: cx('loading') },
70
+ react_1.default.createElement("div", { className: cx('head') },
71
+ react_1.default.createElement("div", { className: cx('text', 'name') }),
72
+ react_1.default.createElement(link_1.Link, { pseudo: true, disabled: true, className: cx('button') }, "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u0442\u0437\u044B\u0432\u0443")),
73
+ react_1.default.createElement("div", { className: cx('text') }),
74
+ react_1.default.createElement("div", { className: cx('text') })));
75
+ }
@@ -1,15 +1,35 @@
1
1
  @use 'node_modules/@sima-land/ui-nucleons/colors';
2
2
 
3
3
  .root {
4
- height: 20px;
5
- display: flex;
4
+ min-height: 140px;
5
+ max-height: 314px;
6
6
  font-size: 14px;
7
7
  line-height: 20px;
8
+ padding-bottom: 24px;
9
+ background: #fff;
10
+ }
11
+
12
+ .layout {
13
+ width: var(--square-size);
14
+ margin: 0 auto;
15
+ }
16
+
17
+ .header {
18
+ height: 56px;
19
+ display: flex;
20
+ align-items: center;
21
+ padding: 26px 0 14px;
8
22
  white-space: nowrap;
9
23
  }
10
24
 
25
+ .rating {
26
+ flex-shrink: 0;
27
+ font-weight: 600;
28
+ color: colors.$basic-gray38;
29
+ }
30
+
11
31
  .author {
12
- flex: 1;
32
+ flex-grow: 1;
13
33
  margin-left: 16px;
14
34
  overflow: hidden;
15
35
  text-overflow: ellipsis;
@@ -18,5 +38,50 @@
18
38
 
19
39
  .link {
20
40
  margin-left: 16px;
21
- font-weight: 500;
41
+ font-weight: 600;
42
+ &:hover {
43
+ font-weight: 600;
44
+ }
45
+ }
46
+
47
+ .content {
48
+ &.opened {
49
+ max-height: calc(314px - 56px - 24px);
50
+ }
51
+ &:not(.opened) {
52
+ display: -webkit-box;
53
+ -webkit-box-orient: vertical;
54
+ -webkit-line-clamp: 2;
55
+ overflow: hidden;
56
+ }
57
+ }
58
+
59
+ .loading {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ padding-top: 26px;
63
+ .head {
64
+ width: 100%;
65
+ display: flex;
66
+ align-items: center;
67
+ margin-bottom: 8px;
68
+ }
69
+ .text {
70
+ width: 100%;
71
+ border-radius: 4px;
72
+ background: colors.$basic-gray4;
73
+ height: 16px;
74
+ line-height: 20px;
75
+ vertical-align: middle;
76
+ margin: 2px 0;
77
+ }
78
+ .name {
79
+ max-width: 240px;
80
+ margin-right: 16px;
81
+ }
82
+ .button {
83
+ margin-left: auto;
84
+ white-space: nowrap;
85
+ font-weight: 600;
86
+ }
22
87
  }
@@ -6,11 +6,11 @@ import React from 'react';
6
6
  */
7
7
  export declare const useImagesLoad: (srcList: string[]) => boolean;
8
8
  /**
9
- * Хук, вернет размер квадрата вписанного в прямоугольную область, определенную заданным элементом.
9
+ * Хук, вернет размер квадрата вписанного в область окна галереи фото по макетам.
10
10
  * @param areaRef Реф с элементом.
11
11
  * @param options Опции.
12
12
  * @return Размер или null.
13
13
  */
14
- export declare const useRectFit: (areaRef: React.RefObject<HTMLDivElement | null>, { correction }: {
15
- correction: number;
14
+ export declare const useSquareFit: (areaRef: React.RefObject<HTMLDivElement | null>, { hasReview }: {
15
+ hasReview: boolean;
16
16
  }) => number | null;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.useRectFit = exports.useImagesLoad = void 0;
6
+ exports.useSquareFit = exports.useImagesLoad = void 0;
7
7
  const react_1 = require("react");
8
8
  const on_1 = __importDefault(require("@sima-land/ui-nucleons/helpers/on"));
9
9
  /**
@@ -25,29 +25,26 @@ const useImagesLoad = (srcList) => {
25
25
  };
26
26
  exports.useImagesLoad = useImagesLoad;
27
27
  /**
28
- * Хук, вернет размер квадрата вписанного в прямоугольную область, определенную заданным элементом.
28
+ * Хук, вернет размер квадрата вписанного в область окна галереи фото по макетам.
29
29
  * @param areaRef Реф с элементом.
30
30
  * @param options Опции.
31
31
  * @return Размер или null.
32
32
  */
33
- const useRectFit = (areaRef, { correction }) => {
33
+ const useSquareFit = (areaRef, { hasReview }) => {
34
34
  const [size, setSize] = (0, react_1.useState)(null);
35
35
  (0, react_1.useEffect)(() => {
36
- const calcSquareSize = () => {
36
+ function calc() {
37
37
  if (areaRef.current) {
38
- const availWidth = areaRef.current.clientWidth;
39
- const availHeight = areaRef.current.clientHeight;
40
- if (availWidth + correction <= availHeight) {
41
- setSize(availWidth);
42
- }
43
- else {
44
- setSize(availHeight - correction);
45
- }
38
+ const correction = 2 * (104 + 16 + 56 + 24); // ширина превью + отступ + ширина кнопки + отступ
39
+ // доступная область в которую можно вписать **квадрат**
40
+ const availWidth = areaRef.current.clientWidth - correction;
41
+ const availHeight = areaRef.current.clientHeight - (hasReview ? 140 : 40); // вычитаем высоту футера
42
+ setSize(Math.min(availWidth, availHeight));
46
43
  }
47
- };
48
- calcSquareSize();
49
- return (0, on_1.default)(window, 'resize', () => window.requestAnimationFrame(calcSquareSize));
50
- }, [areaRef]);
44
+ }
45
+ calc();
46
+ return (0, on_1.default)(window, 'resize', () => window.requestAnimationFrame(calc));
47
+ }, [areaRef, hasReview]);
51
48
  return size;
52
49
  };
53
- exports.useRectFit = useRectFit;
50
+ exports.useSquareFit = useSquareFit;
@@ -47,9 +47,11 @@ exports.HoverCard = (0, react_1.forwardRef)((props, ref) => {
47
47
  const ProductCard = (_a) => {
48
48
  var { children, className } = _a, props = __rest(_a, ["children", "className"]);
49
49
  const [hovered, toggle] = (0, react_1.useState)(false);
50
- return (react_1.default.createElement("div", Object.assign({}, props, { "data-testid": 'product-card', className: (0, classnames_1.default)(product_card_module_scss_1.default.root, className) }),
51
- react_1.default.createElement("div", { "data-testid": 'product-card:info', onMouseEnter: () => toggle(true) }, (0, exports.reduceBaseInfo)(children)),
52
- hovered && (react_1.default.createElement(exports.HoverCard, { onMouseLeave: () => toggle(false) }, (0, exports.reduceHoverInfo)(children)))));
50
+ return (react_1.default.createElement("div", Object.assign({}, props, { "data-testid": 'product-card', className: (0, classnames_1.default)(product_card_module_scss_1.default.root, className), onMouseEnter: () => toggle(true),
51
+ // вешаем onMouseLeave на корневой элемент так как в Chrome проблемы если вешать на "product-card:info"
52
+ onMouseLeave: () => toggle(false) }),
53
+ react_1.default.createElement("div", { "data-testid": 'product-card:info' }, (0, exports.reduceBaseInfo)(children)),
54
+ hovered && react_1.default.createElement(exports.HoverCard, null, (0, exports.reduceHoverInfo)(children))));
53
55
  };
54
56
  exports.ProductCard = ProductCard;
55
57
  /**
@@ -27,6 +27,7 @@ const react_1 = __importStar(require("react"));
27
27
  const carousel_1 = require("@sima-land/ui-nucleons/carousel");
28
28
  const hover_card_1 = require("./hover-card");
29
29
  const utils_1 = require("./utils");
30
+ const intersection_1 = require("@sima-land/ui-nucleons/hooks/intersection");
30
31
  const media_1 = require("@sima-land/ui-nucleons/hooks/media");
31
32
  const product_info_1 = require("../../../common/components/product-info");
32
33
  const bind_1 = __importDefault(require("classnames/bind"));
@@ -62,12 +63,15 @@ const ProductCarousel = ({ className, itemSize, onInViewport, onNeedRequest, wit
62
63
  // вычисляем ширину элемента карусели для позиционирования стрелок
63
64
  const itemWidth = (0, utils_1.useChildWidth)(rootRef, `.${cx('item')}`, [items.length]);
64
65
  // инициируем загрузку данных, когда компонент почти попал в зону видимости
65
- (0, utils_1.useViewport)(rootRef, onNeedRequest, {
66
- rootMargin: '200px 0px 200px 0px',
67
- });
66
+ const options = (0, react_1.useMemo)(() => ({ rootMargin: '200px 0px 200px 0px' }), []);
67
+ (0, intersection_1.useIntersection)(rootRef, entry => {
68
+ entry.isIntersecting && onNeedRequest && onNeedRequest();
69
+ }, options);
68
70
  // отправляем статистку, когда компонент попадает в зону видимости
69
- (0, utils_1.useViewport)(rootRef, onInViewport);
70
- return (react_1.default.createElement("div", { ref: rootRef, className: cx('root', className) },
71
+ (0, intersection_1.useIntersection)(rootRef, entry => {
72
+ entry.isIntersecting && onInViewport && onInViewport();
73
+ });
74
+ return (react_1.default.createElement("div", { ref: rootRef, className: cx('root', className), "data-testid": 'product-carousel:root' },
71
75
  items.length > 0 && (react_1.default.createElement(carousel_1.Carousel, Object.assign({ step: 3, draggable: false,
72
76
  // докидываем индекс чтобы позже брать актуальные данные из списка по нему
73
77
  items: items.map((item, index) => [item, index]) }, (itemWidth !== null && {
@@ -1,11 +1,4 @@
1
1
  import React from 'react';
2
- /**
3
- * Вызывает callback когда заданный в ref элемент попал во viewport.
4
- * @param ref Ref отслеживаемого элемента.
5
- * @param callback Функция обратного вызова.
6
- * @param options Опции IntersectionObserver.
7
- */
8
- export declare const useViewport: (ref: React.MutableRefObject<HTMLElement | null>, callback?: (() => void) | undefined, options?: IntersectionObserverInit | undefined) => void;
9
2
  /**
10
3
  * Возвращает объект работы с флагом, который автоматически возвращается в true через заданный промежуток времени.
11
4
  * @return Объект работы с флагом.
@@ -3,34 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.useChildWidth = exports.useAllowFlag = exports.useViewport = void 0;
6
+ exports.useChildWidth = exports.useAllowFlag = void 0;
7
7
  const react_1 = require("react");
8
8
  const on_1 = __importDefault(require("@sima-land/ui-nucleons/helpers/on"));
9
- const identity_1 = require("@sima-land/ui-nucleons/hooks/identity");
10
- /**
11
- * Вызывает callback когда заданный в ref элемент попал во viewport.
12
- * @param ref Ref отслеживаемого элемента.
13
- * @param callback Функция обратного вызова.
14
- * @param options Опции IntersectionObserver.
15
- */
16
- const useViewport = (ref, callback, options) => {
17
- const callbackRef = (0, identity_1.useIdentityRef)(callback);
18
- (0, react_1.useEffect)(() => {
19
- const element = ref.current;
20
- if (element) {
21
- const observer = new IntersectionObserver(entries => {
22
- const fn = callbackRef.current;
23
- for (const entry of entries) {
24
- entry.target === element && entry.isIntersecting && fn && fn();
25
- break;
26
- }
27
- }, options);
28
- observer.observe(element);
29
- return () => observer.disconnect();
30
- }
31
- }, []);
32
- };
33
- exports.useViewport = useViewport;
34
9
  /**
35
10
  * Возвращает объект работы с флагом, который автоматически возвращается в true через заданный промежуток времени.
36
11
  * @return Объект работы с флагом.
@@ -24,7 +24,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.ProductSlider = void 0;
26
26
  const react_1 = __importStar(require("react"));
27
- const utils_1 = require("../../../desktop/components/product-carousel/utils");
27
+ const intersection_1 = require("@sima-land/ui-nucleons/hooks/intersection");
28
28
  const touch_slider_1 = require("@sima-land/ui-nucleons/touch-slider");
29
29
  const product_info_1 = require("../../../common/components/product-info");
30
30
  const product_slider_module_scss_1 = __importDefault(require("./product-slider.module.scss"));
@@ -36,16 +36,19 @@ const product_slider_module_scss_1 = __importDefault(require("./product-slider.m
36
36
  const ProductSlider = ({ children, onInViewport, onNeedRequest }) => {
37
37
  const rootRef = (0, react_1.useRef)(null);
38
38
  // инициируем загрузку данных, когда компонент почти попал в зону видимости
39
- (0, utils_1.useViewport)(rootRef, onNeedRequest, {
40
- rootMargin: '200px 0px 200px 0px',
41
- });
39
+ const options = (0, react_1.useMemo)(() => ({ rootMargin: '200px 0px 200px 0px' }), []);
40
+ (0, intersection_1.useIntersection)(rootRef, entry => {
41
+ entry.isIntersecting && (onNeedRequest === null || onNeedRequest === void 0 ? void 0 : onNeedRequest());
42
+ }, options);
42
43
  // сообщаем, когда компонент попадет в зону видимости
43
- (0, utils_1.useViewport)(rootRef, onInViewport);
44
- return (react_1.default.createElement("div", { ref: rootRef },
44
+ (0, intersection_1.useIntersection)(rootRef, entry => {
45
+ entry.isIntersecting && (onInViewport === null || onInViewport === void 0 ? void 0 : onInViewport());
46
+ });
47
+ return (react_1.default.createElement("div", { ref: rootRef, "data-testid": 'product-slider:root' },
45
48
  react_1.default.createElement(touch_slider_1.TouchSlider, null, react_1.Children.toArray(children).reduce((list, item) => {
46
49
  (0, react_1.isValidElement)(item) &&
47
50
  item.type === product_info_1.ProductInfo &&
48
- list.push(react_1.default.createElement("div", { key: item.key, className: product_slider_module_scss_1.default.item }, item));
51
+ list.push(react_1.default.createElement("div", { key: item.key, className: product_slider_module_scss_1.default.item, "data-testid": 'product-slider:item' }, item));
49
52
  return list;
50
53
  }, []))));
51
54
  };
@@ -24,10 +24,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.SearchBar = exports.cx = void 0;
26
26
  const react_1 = __importStar(require("react"));
27
- const bind_1 = __importDefault(require("classnames/bind"));
28
- const search_bar_module_scss_1 = __importDefault(require("./search-bar.module.scss"));
29
27
  const lodash_1 = require("lodash");
30
- const link_1 = require("@sima-land/ui-nucleons/link");
31
28
  const colors_1 = require("@sima-land/ui-nucleons/colors");
32
29
  const dropdown_1 = require("@sima-land/ui-nucleons/dropdown");
33
30
  const dropdown_item_1 = require("@sima-land/ui-nucleons/dropdown-item");
@@ -36,6 +33,8 @@ const on_1 = __importDefault(require("@sima-land/ui-nucleons/helpers/on"));
36
33
  const cross_1 = __importDefault(require("@sima-land/ui-quarks/icons/24x24/Filled/cross"));
37
34
  const search_1 = __importDefault(require("@sima-land/ui-quarks/icons/24x24/Stroked/search"));
38
35
  const more_vert_1 = __importDefault(require("@sima-land/ui-quarks/icons/24x24/Filled/more-vert"));
36
+ const bind_1 = __importDefault(require("classnames/bind"));
37
+ const search_bar_module_scss_1 = __importDefault(require("./search-bar.module.scss"));
39
38
  exports.cx = bind_1.default.bind(search_bar_module_scss_1.default);
40
39
  /**
41
40
  * Компонент поисковой строки.
@@ -56,20 +55,25 @@ const SearchBar = ({ value, onChange, inputMode, placeholder = 'Поиск', onC
56
55
  (0, react_1.useEffect)(() => needHideEndButtons
57
56
  ? (0, on_1.default)(window, 'resize', (0, lodash_1.throttle)(() => toggleDropdown(false), 500))
58
57
  : undefined, [needHideEndButtons]);
59
- return (react_1.default.createElement("div", { className: (0, exports.cx)('wrapper') },
58
+ const preventFieldBlur = e => {
59
+ if (inputRef.current &&
60
+ inputRef.current !== e.target && // не предотвращаем события на самом поле (например выделение текста)
61
+ document.activeElement === inputRef.current) {
62
+ e.preventDefault();
63
+ }
64
+ };
65
+ return (react_1.default.createElement("div", { className: (0, exports.cx)('root') },
60
66
  Boolean(startButtons.length) && (react_1.default.createElement("div", { className: (0, exports.cx)('before-field') },
61
67
  react_1.default.createElement("div", { className: (0, exports.cx)('buttons-container') }, startButtons.map((props, index) => (react_1.default.createElement(Button, Object.assign({}, props, { key: index }))))))),
62
- react_1.default.createElement("label", { className: (0, exports.cx)('input-wrapper') },
68
+ react_1.default.createElement("label", { className: (0, exports.cx)('input-wrapper'), onMouseDown: preventFieldBlur, "data-testid": 'search-bar:input-wrapper' },
63
69
  withSearchIcon && (react_1.default.createElement("span", { className: (0, exports.cx)('search-icon') },
64
70
  react_1.default.createElement(search_1.default, { fill: colors_1.COLORS.get('basic-gray38'), "aria-hidden": true }))),
65
71
  react_1.default.createElement("div", { className: (0, exports.cx)('input-container') },
66
72
  react_1.default.createElement("input", { ref: inputRef, value: value, onChange: onChange, className: (0, exports.cx)('search-field'), placeholder: placeholder, autoFocus: autoFocus, "data-testid": 'search-bar-input', type: 'text', inputMode: inputMode, tabIndex: 0 }),
67
73
  description && (react_1.default.createElement("div", { className: (0, exports.cx)('description-container') },
68
74
  react_1.default.createElement("span", { className: (0, exports.cx)('description') }, description)))),
69
- (0, lodash_1.isFunction)(onClear) && value && (react_1.default.createElement(link_1.Link, { onClick: event => {
70
- onClear(event);
71
- inputRef.current && inputRef.current.focus();
72
- }, pseudo: true, "aria-label": '\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C', "data-testid": 'search-bar:clear-icon', className: (0, exports.cx)('clear-icon'), children: react_1.default.createElement(cross_1.default, { fill: colors_1.COLORS.get('basic-gray38'), "aria-hidden": true }) }))),
75
+ value && onClear && (react_1.default.createElement("button", { onClick: onClear, "aria-label": '\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C', "data-testid": 'search-bar:clear-icon', className: (0, exports.cx)('clear-button') },
76
+ react_1.default.createElement(cross_1.default, { fill: colors_1.COLORS.get('basic-gray38'), "aria-hidden": true })))),
73
77
  Boolean(endButtons.length) && (react_1.default.createElement(react_1.default.Fragment, null,
74
78
  react_1.default.createElement("div", { className: (0, exports.cx)('after-field', needHideEndButtons && 'wide-screen') },
75
79
  react_1.default.createElement("div", { className: (0, exports.cx)('buttons-container') }, endButtons.map((props, index) => (react_1.default.createElement(Button, Object.assign({}, props, { key: index })))))),
@@ -1,13 +1,9 @@
1
1
  @import 'node_modules/@sima-land/ui-nucleons/colors';
2
2
 
3
- .wrapper {
3
+ .root {
4
4
  display: flex;
5
5
  }
6
6
 
7
- .search-cancel {
8
- margin-left: 16px;
9
- }
10
-
11
7
  .input-wrapper {
12
8
  display: inline-flex;
13
9
  position: relative;
@@ -16,14 +12,11 @@
16
12
  width: 100%;
17
13
  background-color: $basic-gray4;
18
14
  border-radius: 8px;
19
- padding: 0 0 0 12px;
15
+ padding: 0 12px;
20
16
  &:focus-within {
21
- .clear-icon {
17
+ .clear-button {
22
18
  display: flex;
23
19
  }
24
- .input-container {
25
- padding-right: 0;
26
- }
27
20
  .description-container {
28
21
  display: none;
29
22
  }
@@ -42,12 +35,12 @@
42
35
  display: flex;
43
36
  justify-content: space-around;
44
37
  align-items: center;
45
- padding-right: 12px;
46
38
  }
47
39
 
48
40
  .search-field {
49
41
  background-color: transparent;
50
42
  margin: 0;
43
+ padding: 0;
51
44
  border: 0;
52
45
  outline: 0;
53
46
  line-height: 24px;
@@ -76,6 +69,7 @@
76
69
  max-width: 50%;
77
70
  display: flex;
78
71
  justify-content: flex-end;
72
+ margin-left: 12px;
79
73
  }
80
74
 
81
75
  .description {
@@ -84,13 +78,16 @@
84
78
  text-overflow: ellipsis;
85
79
  }
86
80
 
87
- .clear-icon {
81
+ .clear-button {
88
82
  position: relative;
89
83
  align-items: center;
90
84
  justify-content: center;
91
- padding: 0 12px;
85
+ padding: 0;
86
+ margin-left: 12px;
92
87
  display: none;
93
88
  cursor: pointer;
89
+ background: transparent;
90
+ border: 0;
94
91
  }
95
92
 
96
93
  .hidden {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sima-land/moleculas",
3
- "version": "14.2.0",
3
+ "version": "15.1.0",
4
4
  "repository": "ssh://git@github.com:sima-land/moleculas.git",
5
5
  "author": "www.sima-land.ru team",
6
6
  "license": "Apache-2.0",
@@ -25,7 +25,7 @@
25
25
  "prettier:check": "prettier --check \"./src/**/*.{js,jsx,ts,tsx,css,scss}\" --loglevel error"
26
26
  },
27
27
  "dependencies": {
28
- "@sima-land/ui-nucleons": "^43.1.0",
28
+ "@sima-land/ui-nucleons": "^44.0.1",
29
29
  "@sima-land/ui-quarks": "^2.2.0",
30
30
  "classnames": "^2.3.1",
31
31
  "date-fns": "^2.26.0",