@cloud-ru/uikit-product-claudia 1.6.2

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +201 -0
  3. package/README.md +586 -0
  4. package/package.json +60 -0
  5. package/src/components/ButtonClaudia/ButtonClaudia.tsx +33 -0
  6. package/src/components/ButtonClaudia/constants.ts +29 -0
  7. package/src/components/ButtonClaudia/helperComponents/ButtonPrivate/ButtonPrivate.tsx +99 -0
  8. package/src/components/ButtonClaudia/helperComponents/ButtonPrivate/constants.ts +13 -0
  9. package/src/components/ButtonClaudia/helperComponents/ButtonPrivate/index.ts +1 -0
  10. package/src/components/ButtonClaudia/helperComponents/ButtonPrivate/styles.module.scss +46 -0
  11. package/src/components/ButtonClaudia/helperComponents/ButtonPrivate/utils.tsx +92 -0
  12. package/src/components/ButtonClaudia/helperComponents/index.ts +1 -0
  13. package/src/components/ButtonClaudia/index.ts +1 -0
  14. package/src/components/ButtonClaudia/styles.module.scss +141 -0
  15. package/src/components/ButtonClaudia/types.ts +63 -0
  16. package/src/components/ButtonClaudia/utils.ts +15 -0
  17. package/src/components/ButtonGiga/ButtonGigaFunction/ButtonGigaFunction.tsx +43 -0
  18. package/src/components/ButtonGiga/ButtonGigaFunction/index.ts +1 -0
  19. package/src/components/ButtonGiga/ButtonGigaFunction/styles.module.scss +179 -0
  20. package/src/components/ButtonGiga/ButtonGigaFunction/types.ts +43 -0
  21. package/src/components/ButtonGiga/ButtonGigaFunction/utils.ts +16 -0
  22. package/src/components/ButtonGiga/ButtonGigaMama/ButtonGigaMama.tsx +29 -0
  23. package/src/components/ButtonGiga/ButtonGigaMama/index.ts +1 -0
  24. package/src/components/ButtonGiga/ButtonGigaMama/styles.module.scss +180 -0
  25. package/src/components/ButtonGiga/ButtonGigaMama/utils.ts +15 -0
  26. package/src/components/ButtonGiga/ButtonGigaOutline/ButtonGigaOutline.tsx +43 -0
  27. package/src/components/ButtonGiga/ButtonGigaOutline/index.ts +1 -0
  28. package/src/components/ButtonGiga/ButtonGigaOutline/styles.module.scss +223 -0
  29. package/src/components/ButtonGiga/ButtonGigaOutline/types.ts +63 -0
  30. package/src/components/ButtonGiga/ButtonGigaOutline/utils.ts +16 -0
  31. package/src/components/ButtonGiga/constants.ts +29 -0
  32. package/src/components/ButtonGiga/helperComponents/ButtonPrivate/ButtonPrivate.tsx +99 -0
  33. package/src/components/ButtonGiga/helperComponents/ButtonPrivate/constants.ts +15 -0
  34. package/src/components/ButtonGiga/helperComponents/ButtonPrivate/index.ts +1 -0
  35. package/src/components/ButtonGiga/helperComponents/ButtonPrivate/styles.module.scss +46 -0
  36. package/src/components/ButtonGiga/helperComponents/ButtonPrivate/types.ts +63 -0
  37. package/src/components/ButtonGiga/helperComponents/ButtonPrivate/utils.tsx +92 -0
  38. package/src/components/ButtonGiga/helperComponents/index.ts +1 -0
  39. package/src/components/ButtonGiga/index.ts +3 -0
  40. package/src/components/ButtonGiga/types.ts +43 -0
  41. package/src/components/ChatStatusAnnouncement/ChatStatusAnnouncement.tsx +109 -0
  42. package/src/components/ChatStatusAnnouncement/constants.ts +1 -0
  43. package/src/components/ChatStatusAnnouncement/helperComponents/AlertButton/AlertButton.tsx +24 -0
  44. package/src/components/ChatStatusAnnouncement/helperComponents/AlertButton/index.ts +1 -0
  45. package/src/components/ChatStatusAnnouncement/helperComponents/AlertButton/styles.module.scss +27 -0
  46. package/src/components/ChatStatusAnnouncement/helperComponents/TextContent/TextContent.tsx +18 -0
  47. package/src/components/ChatStatusAnnouncement/helperComponents/TextContent/index.ts +1 -0
  48. package/src/components/ChatStatusAnnouncement/index.ts +1 -0
  49. package/src/components/ChatStatusAnnouncement/styled.module.scss +65 -0
  50. package/src/components/ChatStatusAnnouncement/types.ts +17 -0
  51. package/src/components/ChatStatusAnnouncement/utils/index.ts +52 -0
  52. package/src/components/IconGiga/IconGiga.tsx +64 -0
  53. package/src/components/IconGiga/constants.ts +23 -0
  54. package/src/components/IconGiga/index.ts +1 -0
  55. package/src/components/RecommendPannel/RecommendPanel.tsx +131 -0
  56. package/src/components/RecommendPannel/helperComponents/Chip/Chip.tsx +47 -0
  57. package/src/components/RecommendPannel/helperComponents/Chip/index.ts +1 -0
  58. package/src/components/RecommendPannel/helperComponents/Chip/styles.module.scss +45 -0
  59. package/src/components/RecommendPannel/helperComponents/ClaudiaChip/ClaudiaChip.tsx +40 -0
  60. package/src/components/RecommendPannel/helperComponents/ClaudiaChip/index.ts +1 -0
  61. package/src/components/RecommendPannel/helperComponents/CloseChip/CloseChip.tsx +106 -0
  62. package/src/components/RecommendPannel/helperComponents/CloseChip/index.ts +1 -0
  63. package/src/components/RecommendPannel/helperComponents/CloseChip/styles.module.scss +73 -0
  64. package/src/components/RecommendPannel/helperComponents/DropdownChip/DropdownChip.tsx +112 -0
  65. package/src/components/RecommendPannel/helperComponents/DropdownChip/index.ts +1 -0
  66. package/src/components/RecommendPannel/helperComponents/DropdownChip/styles.module.scss +56 -0
  67. package/src/components/RecommendPannel/hooks/index.ts +15 -0
  68. package/src/components/RecommendPannel/index.ts +1 -0
  69. package/src/components/RecommendPannel/styles.module.scss +4 -0
  70. package/src/components/RecommendPannel/types.ts +21 -0
  71. package/src/components/RecommendPannel/utils/gitVisibleChipsCount.ts +57 -0
  72. package/src/components/SshField/SshField.tsx +222 -0
  73. package/src/components/SshField/components/MobileFieldAi/MobileFieldAi.tsx +71 -0
  74. package/src/components/SshField/components/MobileFieldAi/index.ts +1 -0
  75. package/src/components/SshField/components/MobileFieldAi/styles.module.scss +80 -0
  76. package/src/components/SshField/components/TextArea/TextArea.tsx +113 -0
  77. package/src/components/SshField/components/TextArea/index.ts +1 -0
  78. package/src/components/SshField/components/TextArea/styles.module.scss +35 -0
  79. package/src/components/SshField/helperComponents/DropZoneContent/DropZoneContent.tsx +15 -0
  80. package/src/components/SshField/helperComponents/DropZoneContent/index.ts +1 -0
  81. package/src/components/SshField/helperComponents/DropZoneContent/styles.module.scss +17 -0
  82. package/src/components/SshField/helperComponents/FieldSubmitButton/FieldSubmitButton.tsx +45 -0
  83. package/src/components/SshField/helperComponents/FieldSubmitButton/index.ts +1 -0
  84. package/src/components/SshField/helperComponents/TextAreaActionsFooter/TextAreaActionsFooter.tsx +18 -0
  85. package/src/components/SshField/helperComponents/TextAreaActionsFooter/index.ts +1 -0
  86. package/src/components/SshField/helperComponents/TextAreaActionsFooter/styles.module.scss +23 -0
  87. package/src/components/SshField/index.ts +1 -0
  88. package/src/components/SshField/styles.module.scss +54 -0
  89. package/src/components/SshField/utils/handleFileError.ts +41 -0
  90. package/src/components/SshField/utils/isTouchDevice.ts +5 -0
  91. package/src/components/SshField/utils/readFileContent.ts +23 -0
  92. package/src/components/SshField/utils/validateSSHKey.ts +84 -0
  93. package/src/components/index.ts +6 -0
  94. package/src/index.ts +1 -0
@@ -0,0 +1,43 @@
1
+ import {
2
+ AnchorHTMLAttributes,
3
+ ButtonHTMLAttributes,
4
+ FocusEventHandler,
5
+ KeyboardEventHandler,
6
+ MouseEventHandler,
7
+ ReactElement,
8
+ } from 'react';
9
+
10
+ import { ValueOf } from '@snack-uikit/utils';
11
+
12
+ import { APPEARANCE, ICON_POSITION, SIZE } from './constants';
13
+
14
+ export type Appearance = ValueOf<typeof APPEARANCE>;
15
+
16
+ export type IconPosition = ValueOf<typeof ICON_POSITION>;
17
+
18
+ export type Size = ValueOf<typeof SIZE>;
19
+
20
+ export type BaseButtonProps = {
21
+ className?: string;
22
+ disabled?: boolean;
23
+ icon?: ReactElement;
24
+ iconPosition?: IconPosition;
25
+ label?: string;
26
+ loading?: boolean;
27
+ onClick?: MouseEventHandler<HTMLElement>;
28
+ onKeyDown?: KeyboardEventHandler<HTMLElement>;
29
+ onFocus?: FocusEventHandler<HTMLAnchorElement | HTMLButtonElement>;
30
+ onBlur?: FocusEventHandler<HTMLAnchorElement | HTMLButtonElement>;
31
+ size?: Size;
32
+ appearance?: Appearance;
33
+ type?: ButtonHTMLAttributes<HTMLButtonElement>['type'];
34
+ tabIndex?: number;
35
+ fullWidth?: boolean;
36
+ };
37
+
38
+ export type AnchorButtonProps = {
39
+ href?: string;
40
+ target?: AnchorHTMLAttributes<HTMLAnchorElement>['target'];
41
+ };
42
+
43
+ export type CommonButtonProps = AnchorButtonProps & BaseButtonProps;
@@ -0,0 +1,109 @@
1
+ import cn from 'classnames';
2
+ import { useEffect, useRef, useState } from 'react';
3
+
4
+ import { PlaceholderSVG } from '@cloud-ru/uikit-product-icons';
5
+ import { LAYOUT_TYPE } from '@cloud-ru/uikit-product-utils';
6
+
7
+ import { ANIMATION_INTERVAL } from './constants';
8
+ import { AlertButton } from './helperComponents/AlertButton';
9
+ import { TextContent } from './helperComponents/TextContent';
10
+ import styles from './styled.module.scss';
11
+ import { ChatStatusAnnouncementProps } from './types';
12
+ import { getContent, isReactNode } from './utils';
13
+
14
+ export function ChatStatusAnnouncement({
15
+ content,
16
+ onActionClick,
17
+ actionLabel,
18
+ icon,
19
+ layoutType,
20
+ className,
21
+ }: ChatStatusAnnouncementProps) {
22
+ const [currentIndex, setCurrentIndex] = useState(0);
23
+ const [isAnimationEnded, setAnimationEnded] = useState(false);
24
+ const [mouseEntered, setMouseEntered] = useState(false);
25
+ const hoverRef = useRef<NodeJS.Timeout>();
26
+ const totalTextItems = getContent(content);
27
+ const isMobile = layoutType === LAYOUT_TYPE.Mobile;
28
+
29
+ useEffect(() => () => clearTimeout(hoverRef.current));
30
+
31
+ // запуск основной анимации
32
+ useEffect(() => {
33
+ if (isReactNode(content) || isAnimationEnded || content.length <= 1) return;
34
+
35
+ let interval: NodeJS.Timeout;
36
+
37
+ if (currentIndex === content.length - 1) {
38
+ interval = setInterval(() => {
39
+ setAnimationEnded(true);
40
+ }, ANIMATION_INTERVAL);
41
+ return;
42
+ }
43
+
44
+ interval = setInterval(() => {
45
+ setCurrentIndex(prevIndex => (prevIndex + 1) % content.length);
46
+ }, ANIMATION_INTERVAL);
47
+
48
+ return () => clearInterval(interval);
49
+ }, [currentIndex, content, isAnimationEnded]);
50
+
51
+ // запуск анимации по ховеру
52
+ useEffect(() => {
53
+ if (!isAnimationEnded || totalTextItems.length === 1) return;
54
+
55
+ const runForwardAnimation = () => {
56
+ if (hoverRef.current) clearTimeout(hoverRef.current);
57
+
58
+ if (currentIndex === totalTextItems.length - 1) return;
59
+
60
+ hoverRef.current = setTimeout(() => setCurrentIndex(prev => prev + 1), 300);
61
+ };
62
+
63
+ const runBackwardAnimation = () => {
64
+ if (hoverRef.current) clearTimeout(hoverRef.current);
65
+
66
+ if (currentIndex <= totalTextItems.length - 2) return;
67
+
68
+ hoverRef.current = setTimeout(() => setCurrentIndex(prev => prev - 1), 300);
69
+ };
70
+
71
+ if (mouseEntered) {
72
+ runForwardAnimation();
73
+ return;
74
+ }
75
+ runBackwardAnimation();
76
+ }, [currentIndex, isAnimationEnded, mouseEntered, totalTextItems.length]);
77
+
78
+ const onMouseEnter = () => setMouseEntered(true);
79
+
80
+ const onMouseLeave = () => setMouseEntered(false);
81
+
82
+ return (
83
+ <div
84
+ className={cn(styles.fieldAdvice, className)}
85
+ data-mobile={isMobile || undefined}
86
+ onMouseEnter={onMouseEnter}
87
+ onMouseLeave={onMouseLeave}
88
+ >
89
+ <div className={styles.descriptionContainer}>
90
+ {icon ?? <PlaceholderSVG size={16} />}
91
+ <div className={styles.animationContainer}>
92
+ {totalTextItems.map((item, index) => {
93
+ const currentTextNextOrPreviousStyle =
94
+ index === (currentIndex - 1 + totalTextItems.length) % totalTextItems.length
95
+ ? styles.textBlockPrevious
96
+ : styles.textBlockNext;
97
+
98
+ const currentTextStyle = index === currentIndex ? styles.textBlockCurrent : currentTextNextOrPreviousStyle;
99
+
100
+ return (
101
+ <TextContent key={index} content={item.content} className={cn(styles.textBlock, currentTextStyle)} />
102
+ );
103
+ })}
104
+ </div>
105
+ </div>
106
+ <AlertButton onClick={onActionClick} text={actionLabel} layoutType={layoutType} />
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1 @@
1
+ export const ANIMATION_INTERVAL = 1900;
@@ -0,0 +1,24 @@
1
+ import { MouseEvent } from 'react';
2
+
3
+ import { LAYOUT_TYPE, LayoutType } from '@cloud-ru/uikit-product-utils';
4
+ import { Typography } from '@snack-uikit/typography';
5
+
6
+ import styles from './styles.module.scss';
7
+
8
+ type PrivateAlertButtonProps = {
9
+ text?: string;
10
+ onClick?(e: MouseEvent<HTMLButtonElement>): void;
11
+ layoutType: LayoutType;
12
+ };
13
+
14
+ export type AlertButtonProps = Omit<PrivateAlertButtonProps, 'appearance' | 'variant'>;
15
+
16
+ export function AlertButton({ text, onClick, layoutType }: PrivateAlertButtonProps) {
17
+ const TypographyText = layoutType === LAYOUT_TYPE.Mobile ? Typography.SansLabelL : Typography.SansLabelM;
18
+
19
+ return (
20
+ <button type='button' onClick={onClick} className={styles.alertButton}>
21
+ {text && <TypographyText className={styles.text}>{text}</TypographyText>}
22
+ </button>
23
+ );
24
+ }
@@ -0,0 +1 @@
1
+ export * from './AlertButton';
@@ -0,0 +1,27 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables' as stv;
2
+
3
+ .alertButton {
4
+ cursor: pointer;
5
+
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ height: 16px;
10
+
11
+ box-sizing: border-box;
12
+ margin: 0;
13
+ padding: 0;
14
+
15
+ white-space: nowrap;
16
+
17
+ background: none;
18
+ border: 0;
19
+ }
20
+
21
+ .text {
22
+ color: stv.$sys-blue-text-support;
23
+
24
+ &:hover {
25
+ color: stv.$sys-blue-text-main;
26
+ }
27
+ }
@@ -0,0 +1,18 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import { Typography } from '@snack-uikit/typography';
4
+
5
+ import { isReactNode } from '../../utils';
6
+
7
+ type TextContent = {
8
+ content: ReactNode;
9
+ className?: string;
10
+ };
11
+
12
+ export function TextContent({ content, className }: TextContent) {
13
+ if (isReactNode(content)) {
14
+ return content;
15
+ }
16
+
17
+ return <Typography.SansBodyS className={className}>{content}</Typography.SansBodyS>;
18
+ }
@@ -0,0 +1 @@
1
+ export * from './TextContent';
@@ -0,0 +1 @@
1
+ export * from './ChatStatusAnnouncement';
@@ -0,0 +1,65 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables' as stv;
2
+
3
+ .fieldAdvice {
4
+ display: flex;
5
+ justify-content: space-between;
6
+ align-items: flex-start;
7
+ padding: 4px 8px 0px 8px;
8
+ height: 38px;
9
+ max-height: 38px;
10
+ border-radius: 10px 10px 0 0;
11
+ border: 1px solid stv.$sys-blue-decor-activated;
12
+ background-color: stv.$sys-blue-decor-default;
13
+ box-sizing: border-box;
14
+
15
+ &[data-mobile] {
16
+ height: 44px;
17
+ max-height: 44px;
18
+ border-radius: 0;
19
+ padding: 8px 16px 0px 16px;
20
+ }
21
+ }
22
+
23
+ .descriptionContainer {
24
+ display: flex;
25
+ justify-content: center;
26
+ align-items: center;
27
+ gap: 8px;
28
+ width: 100%;
29
+ }
30
+
31
+ .animationContainer {
32
+ position: relative;
33
+ width: 100%;
34
+ height: 16px;
35
+ padding: 0 8px 0 0;
36
+ box-sizing: border-box;
37
+ display: flex;
38
+ flex-direction: column;
39
+ align-items: flex-start;
40
+ justify-content: center;
41
+ overflow: hidden;
42
+ }
43
+
44
+ .textBlock {
45
+ position: absolute;
46
+ padding: 0 8px 0 0;
47
+ box-sizing: border-box;
48
+ transition: all 0.7s ease-in-out;
49
+ will-change: transform, opacity;
50
+ }
51
+
52
+ .textBlockCurrent {
53
+ opacity: 1;
54
+ transform: translateY(0);
55
+ }
56
+
57
+ .textBlockPrevious {
58
+ opacity: 0;
59
+ transform: translateY(-50px);
60
+ }
61
+
62
+ .textBlockNext {
63
+ opacity: 0;
64
+ transform: translateY(50px);
65
+ }
@@ -0,0 +1,17 @@
1
+ import { ReactElement, ReactNode } from 'react';
2
+
3
+ import { LayoutType } from '@cloud-ru/uikit-product-utils';
4
+
5
+ export type TextItem = {
6
+ content: ReactNode;
7
+ shouldFocusOnHover?: boolean;
8
+ };
9
+
10
+ export type ChatStatusAnnouncementProps = {
11
+ icon?: ReactElement;
12
+ content: ReactNode | TextItem[];
13
+ actionLabel: string;
14
+ onActionClick?: () => void;
15
+ layoutType: LayoutType;
16
+ className?: string;
17
+ };
@@ -0,0 +1,52 @@
1
+ import { isValidElement, ReactElement, ReactNode } from 'react';
2
+
3
+ import { ChatStatusAnnouncementProps, TextItem } from '../types';
4
+
5
+ export function isString(value: unknown): value is string {
6
+ return typeof value === 'string';
7
+ }
8
+
9
+ export function isReactNode(value: unknown): value is ReactNode {
10
+ if (
11
+ value === null ||
12
+ value === undefined ||
13
+ typeof value === 'boolean' ||
14
+ typeof value === 'string' ||
15
+ typeof value === 'number'
16
+ ) {
17
+ return true;
18
+ }
19
+
20
+ if (isValidElement(value)) {
21
+ return true;
22
+ }
23
+
24
+ if (Array.isArray(value)) {
25
+ return value.every(item => isReactNode(item));
26
+ }
27
+
28
+ if (value && typeof value === 'object' && 'key' in value && 'props' in value && 'type' in value) {
29
+ return isValidElement(value as ReactElement);
30
+ }
31
+
32
+ return false;
33
+ }
34
+
35
+ export const getContent = (textContent: ChatStatusAnnouncementProps['content']): TextItem[] => {
36
+ if (isReactNode(textContent))
37
+ return [
38
+ {
39
+ content: textContent,
40
+ },
41
+ ];
42
+
43
+ if (textContent.length === 1) return textContent;
44
+
45
+ const alertTextElement = textContent.find(item => item.shouldFocusOnHover);
46
+
47
+ if (!alertTextElement) return textContent;
48
+
49
+ const totalTextItems = [...textContent, { content: alertTextElement.content }];
50
+
51
+ return totalTextItems;
52
+ };
@@ -0,0 +1,64 @@
1
+ import { CSSProperties, forwardRef, SVGProps } from 'react';
2
+
3
+ import { GIGA_ICON_GRADIENT_ID, GRADIENT_PARAMS, GRADIENT_STOPS, ICON_PATH } from './constants';
4
+
5
+ export type IconGigaProps = {
6
+ className?: string;
7
+ size?: number;
8
+ style?: CSSProperties;
9
+ withBranding?: boolean;
10
+ } & SVGProps<SVGSVGElement>;
11
+
12
+ export const IconGiga = forwardRef<SVGSVGElement, IconGigaProps>(
13
+ ({ size = 24, className, style, withBranding, ...props }, ref) => {
14
+ const customStyle: CSSProperties = {
15
+ ...style,
16
+ };
17
+
18
+ if (typeof size === 'number') {
19
+ customStyle.width = `${size}px`;
20
+ customStyle.height = `${size}px`;
21
+ }
22
+
23
+ if (withBranding) {
24
+ return (
25
+ <svg
26
+ ref={ref}
27
+ width={24}
28
+ height={24}
29
+ viewBox='0 0 24 24'
30
+ fill='none'
31
+ xmlns='http://www.w3.org/2000/svg'
32
+ style={customStyle}
33
+ className={className}
34
+ {...props}
35
+ >
36
+ <path d={ICON_PATH} fill={`url(#${GIGA_ICON_GRADIENT_ID})`} />
37
+ <defs>
38
+ <radialGradient id={GIGA_ICON_GRADIENT_ID} {...GRADIENT_PARAMS}>
39
+ {GRADIENT_STOPS.map(({ offset, stopColor }) => (
40
+ <stop key={offset} offset={offset} stopColor={stopColor} />
41
+ ))}
42
+ </radialGradient>
43
+ </defs>
44
+ </svg>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <svg
50
+ ref={ref}
51
+ width={24}
52
+ height={24}
53
+ viewBox='0 0 24 24'
54
+ fill='none'
55
+ xmlns='http://www.w3.org/2000/svg'
56
+ style={customStyle}
57
+ className={className}
58
+ {...props}
59
+ >
60
+ <path d={ICON_PATH} fill='currentColor' />
61
+ </svg>
62
+ );
63
+ },
64
+ );
@@ -0,0 +1,23 @@
1
+ export const ICON_PATH =
2
+ 'M12 3C7.02902 3 3 7.0297 3 12C3 16.9703 7.0297 21 12 21C16.9703 21 21 16.9703 21 12C21 7.0297 16.9703 3 12 3ZM5.54642 10.3679C5.4259 9.98549 5.40974 9.54784 5.49794 9.0698C5.65146 8.28473 6.04938 7.48418 6.64861 6.75499L6.65737 6.74422C8.04234 4.91352 10.4521 3.82075 13.1035 3.82075C13.6503 3.82075 14.1882 3.86721 14.7121 3.95878C12.7581 4.1399 10.7436 5.00038 8.71901 6.51934C8.63283 6.58398 8.57762 6.6816 8.56752 6.78866C8.55742 6.89571 8.59243 7.00277 8.66515 7.08289C9.32566 7.8114 9.98684 8.60791 10.7443 9.59026C10.8702 9.75387 11.1011 9.78821 11.2708 9.66769C12.7979 8.57358 14.1229 7.76292 15.4062 7.14147C15.349 7.79928 14.469 8.81866 13.5493 9.58959C12.0788 10.7934 10.308 12.0451 8.32176 12.2054L8.24434 12.2121C7.95213 12.2256 7.63769 12.1912 7.31114 12.1104C6.5254 11.9091 5.83257 11.223 5.54777 10.3686L5.54642 10.3679ZM11.9939 19.7827C10.1908 19.7827 8.53183 19.1673 7.21419 18.1358C7.466 18.1587 7.73128 18.1701 8.02012 18.1701C8.26184 18.1701 8.52039 18.162 8.79846 18.1466C10.4413 18.0267 12.1394 17.5372 13.7095 16.7326C15.2971 15.9186 16.6808 14.8178 17.713 13.5459C18.3822 12.6989 19.1841 11.3974 19.4117 9.81985C19.4258 9.83938 19.44 9.85958 19.4541 9.87978C19.6494 10.5605 19.7544 11.2796 19.7544 12.0236C19.7544 16.3091 16.2802 19.7833 11.9946 19.7833L11.9939 19.7827Z';
3
+
4
+ export const GIGA_ICON_GRADIENT_ID = 'GIGA_ICON_GRADIENT_ID';
5
+
6
+ export const GRADIENT_STOPS = [
7
+ { offset: '0.08', stopColor: '#7CB5F2' },
8
+ { offset: '0.17', stopColor: '#78B9EC' },
9
+ { offset: '0.28', stopColor: '#70C6DD' },
10
+ { offset: '0.38', stopColor: '#64D8C7' },
11
+ { offset: '0.44', stopColor: '#5FD7C2' },
12
+ { offset: '0.51', stopColor: '#54D5B3' },
13
+ { offset: '0.58', stopColor: '#40D39C' },
14
+ { offset: '0.66', stopColor: '#26D07C' },
15
+ ];
16
+
17
+ export const GRADIENT_PARAMS = {
18
+ cx: '0',
19
+ cy: '0',
20
+ r: '1',
21
+ gradientUnits: 'userSpaceOnUse',
22
+ gradientTransform: 'translate(14.8575 6.90447) scale(16.6554)',
23
+ };
@@ -0,0 +1 @@
1
+ export * from './IconGiga';
@@ -0,0 +1,131 @@
1
+ import { ReactNode, useEffect, useRef, useState } from 'react';
2
+
3
+ import { LAYOUT_TYPE, LayoutType } from '@cloud-ru/uikit-product-utils';
4
+
5
+ import { Chip } from './helperComponents/Chip';
6
+ import { ClaudiaChip } from './helperComponents/ClaudiaChip';
7
+ import { CloseChip } from './helperComponents/CloseChip';
8
+ import { DropdownChip } from './helperComponents/DropdownChip';
9
+ import styles from './styles.module.scss';
10
+ import { CHIP_TYPE, ChipProps, ChipType, SIZE, Size } from './types';
11
+ import { getVisibleChipsCount } from './utils/gitVisibleChipsCount';
12
+
13
+ export type RecommendPanelProps = {
14
+ chips: ChipProps[];
15
+ size: Size;
16
+ type?: ChipType;
17
+ layoutType?: LayoutType;
18
+ onCloseClick?: () => void;
19
+ onCloseChipLabel?: ReactNode;
20
+ tooltip?: ReactNode;
21
+ onClaudiaClick?: () => void;
22
+ };
23
+
24
+ export function RecommendPanel({
25
+ chips,
26
+ type = CHIP_TYPE.Default,
27
+ size = SIZE.S,
28
+ layoutType,
29
+ onCloseClick,
30
+ onCloseChipLabel,
31
+ tooltip,
32
+ onClaudiaClick,
33
+ }: RecommendPanelProps) {
34
+ const [containerWidth, setContainerWidth] = useState(0);
35
+ const [isCloseIconVisible, setCloseIconVisible] = useState(false);
36
+ const [chipWidths, setChipWidths] = useState<number[]>([]);
37
+ const containerRef = useRef<HTMLDivElement>(null);
38
+ const allChipsRefs = useRef<(HTMLButtonElement | HTMLElement | null)[]>([]);
39
+ const isSmall = size === SIZE.S;
40
+
41
+ useEffect(() => {
42
+ if (layoutType === LAYOUT_TYPE.Mobile) {
43
+ setCloseIconVisible(true);
44
+ return;
45
+ }
46
+
47
+ setCloseIconVisible(false);
48
+ }, [layoutType]);
49
+
50
+ useEffect(() => {
51
+ const widths = allChipsRefs.current.map(ref => {
52
+ if (!ref) return 0;
53
+ const rect = ref.getBoundingClientRect();
54
+
55
+ return rect.width;
56
+ });
57
+
58
+ setChipWidths(widths);
59
+ }, [chips, allChipsRefs]);
60
+
61
+ useEffect(() => {
62
+ if (!containerRef.current) return;
63
+
64
+ const resizeObserver = new ResizeObserver(entries => {
65
+ for (const entry of entries) {
66
+ setContainerWidth(entry.contentRect.width);
67
+ }
68
+ });
69
+
70
+ resizeObserver.observe(containerRef.current);
71
+
72
+ return () => {
73
+ resizeObserver.disconnect();
74
+ };
75
+ }, []);
76
+
77
+ const visibleChipsCount = getVisibleChipsCount({
78
+ containerWidth,
79
+ chips,
80
+ chipWidths,
81
+ isSmall,
82
+ isCloseChipExist: Boolean(onCloseClick) && Boolean(onCloseChipLabel),
83
+ });
84
+ const visibleChips = chips.slice(0, visibleChipsCount);
85
+ const hiddenChips = chips.slice(visibleChipsCount);
86
+ const hasHiddenChips = hiddenChips.length > 0;
87
+
88
+ const showCloseIcon = () => {
89
+ if (layoutType === LAYOUT_TYPE.Mobile) return;
90
+ setCloseIconVisible(true);
91
+ };
92
+ const hideCloseIcon = () => {
93
+ if (layoutType === LAYOUT_TYPE.Mobile) return;
94
+ setCloseIconVisible(false);
95
+ };
96
+
97
+ return (
98
+ <div className={styles.container} ref={containerRef} onMouseEnter={showCloseIcon} onMouseLeave={hideCloseIcon}>
99
+ <ClaudiaChip layoutType={layoutType} onClick={onClaudiaClick} size={size} tooltip={tooltip} />
100
+ {visibleChips.map((chip, index) => (
101
+ <Chip
102
+ ref={chipElement => (allChipsRefs.current[index] = chipElement)}
103
+ key={chip.id}
104
+ label={chip.label}
105
+ size={size}
106
+ type={type}
107
+ layoutType={layoutType}
108
+ onClick={chip.onClick}
109
+ />
110
+ ))}
111
+ {hasHiddenChips && (
112
+ <DropdownChip
113
+ layoutType={layoutType}
114
+ type={type}
115
+ size={size}
116
+ label={`+${hiddenChips.length}`}
117
+ dropdownItems={hiddenChips}
118
+ />
119
+ )}
120
+ {onCloseClick && onCloseChipLabel && (
121
+ <CloseChip
122
+ size={size}
123
+ layoutType={layoutType}
124
+ content={onCloseChipLabel}
125
+ onClick={onCloseClick}
126
+ isVisible={isCloseIconVisible}
127
+ />
128
+ )}
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,47 @@
1
+ import cn from 'classnames';
2
+ import { forwardRef, MouseEventHandler } from 'react';
3
+
4
+ import { LAYOUT_TYPE, LayoutType } from '@cloud-ru/uikit-product-utils';
5
+ import { TruncateString } from '@snack-uikit/truncate-string';
6
+ import { Typography } from '@snack-uikit/typography';
7
+
8
+ import { CHIP_TYPE, ChipType, SIZE, Size } from '../../types';
9
+ import styles from './styles.module.scss';
10
+
11
+ type ChipProps = {
12
+ type: ChipType;
13
+ label: string;
14
+ onClick?: MouseEventHandler<HTMLElement>;
15
+ className?: string;
16
+ isVisible?: boolean;
17
+ size: Size;
18
+ layoutType?: LayoutType;
19
+ };
20
+
21
+ export const Chip = forwardRef<HTMLButtonElement, ChipProps>(
22
+ ({ layoutType, type, label, onClick, size, className }, ref) => {
23
+ const isDefaultType = type === CHIP_TYPE.Default;
24
+ const isMobile = layoutType === LAYOUT_TYPE.Mobile;
25
+ const isMobileChipSize = isMobile || size === SIZE.M;
26
+
27
+ return (
28
+ <button
29
+ ref={ref}
30
+ data-mobile={isMobileChipSize || undefined}
31
+ className={cn(
32
+ styles.chip,
33
+ {
34
+ [styles.chipOutline]: !isDefaultType,
35
+ [styles.chipDefault]: isDefaultType,
36
+ },
37
+ className,
38
+ )}
39
+ onClick={onClick}
40
+ >
41
+ <Typography.SansBodyS className={styles.text}>
42
+ <TruncateString variant='end' placement='top' text={label} maxLines={1} />
43
+ </Typography.SansBodyS>
44
+ </button>
45
+ );
46
+ },
47
+ );
@@ -0,0 +1 @@
1
+ export * from './Chip';