@cloud-ru/uikit-product-card-predefined 1.1.13-preview-926fd6b4.0 → 1.2.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.
- package/CHANGELOG.md +11 -0
- package/README.md +70 -6
- package/dist/cjs/components/CardServiceLight/CardServiceLight.d.ts +82 -0
- package/dist/cjs/components/CardServiceLight/CardServiceLight.js +85 -0
- package/dist/cjs/components/CardServiceLight/constants.d.ts +10 -0
- package/dist/cjs/components/CardServiceLight/constants.js +13 -0
- package/dist/cjs/components/CardServiceLight/index.d.ts +3 -0
- package/dist/cjs/components/CardServiceLight/index.js +20 -0
- package/dist/cjs/components/CardServiceLight/styles.module.css +90 -0
- package/dist/cjs/components/CardServiceLight/types.d.ts +3 -0
- package/dist/cjs/components/CardServiceLight/types.js +2 -0
- package/dist/cjs/components/CardServiceSmall/CardServiceSmall.d.ts +1 -0
- package/dist/cjs/components/CardServiceSmall/CardServiceSmall.js +1 -0
- package/dist/cjs/components/index.d.ts +1 -0
- package/dist/cjs/components/index.js +1 -0
- package/dist/esm/components/CardServiceLight/CardServiceLight.d.ts +82 -0
- package/dist/esm/components/CardServiceLight/CardServiceLight.js +79 -0
- package/dist/esm/components/CardServiceLight/constants.d.ts +10 -0
- package/dist/esm/components/CardServiceLight/constants.js +10 -0
- package/dist/esm/components/CardServiceLight/index.d.ts +3 -0
- package/dist/esm/components/CardServiceLight/index.js +2 -0
- package/dist/esm/components/CardServiceLight/styles.module.css +90 -0
- package/dist/esm/components/CardServiceLight/types.d.ts +3 -0
- package/dist/esm/components/CardServiceLight/types.js +1 -0
- package/dist/esm/components/CardServiceSmall/CardServiceSmall.d.ts +1 -0
- package/dist/esm/components/CardServiceSmall/CardServiceSmall.js +1 -0
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/components/index.js +1 -0
- package/package.json +4 -3
- package/src/components/CardServiceLight/CardServiceLight.tsx +197 -0
- package/src/components/CardServiceLight/constants.ts +12 -0
- package/src/components/CardServiceLight/index.ts +3 -0
- package/src/components/CardServiceLight/styles.module.scss +108 -0
- package/src/components/CardServiceLight/types.ts +5 -0
- package/src/components/CardServiceSmall/CardServiceSmall.tsx +1 -0
- package/src/components/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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
|
+
# 1.2.0 (2026-05-22)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **FF-8339:** new CardServiceLight, deprecated CardServiceSmall ([2a0fdbf](https://github.com/cloud-ru-tech/uikit-product/commit/2a0fdbf21e02a5569b2545e5f67cf583843af202))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
## 1.1.12 (2026-04-27)
|
|
7
18
|
|
|
8
19
|
### Only dependencies have been changed
|
package/README.md
CHANGED
|
@@ -3,10 +3,80 @@
|
|
|
3
3
|
## Installation
|
|
4
4
|
`npm i @cloud-ru/uikit-product-card-predefined`
|
|
5
5
|
|
|
6
|
+
## CardServiceLight
|
|
7
|
+
|
|
8
|
+
Компактная карточка сервиса для списков и навигации. Не использует обёртку `@snack-uikit/card`.
|
|
9
|
+
|
|
10
|
+
Корневой элемент — нативный `<a>` при передаче `href`, иначе `<button>`. Содержимое карточки (иконка, заголовок, promo tag) доступно для скринридеров. Кнопка «Избранное» отображается при наведении и фокусе на карточке; клик по ней не активирует переход по ссылке.
|
|
11
|
+
|
|
12
|
+
Тултип promo tag всегда открывается по `hover` (задаётся внутри компонента).
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { CardServiceLight } from '@cloud-ru/uikit-product-card-predefined';
|
|
16
|
+
|
|
17
|
+
<CardServiceLight
|
|
18
|
+
href="/services/compute"
|
|
19
|
+
title="Compute"
|
|
20
|
+
icon={<ComputeSVG />}
|
|
21
|
+
promoTag={{ variant: 'preview', context: 'service', tooltip: { placement: 'top' } }}
|
|
22
|
+
favorite={{ enabled: true, checked, onChange: setChecked }}
|
|
23
|
+
/>;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## CardServiceSmall
|
|
27
|
+
|
|
28
|
+
> **Deprecated.** Используйте [`CardServiceLight`](#cardservicelight).
|
|
29
|
+
|
|
30
|
+
Предыдущая реализация на базе `@snack-uikit/card` с overlay-ссылкой. Сохранена для обратной совместимости.
|
|
6
31
|
|
|
7
32
|
## Props
|
|
8
33
|
|
|
9
34
|
```ts
|
|
35
|
+
type CardServiceLightAnchorProps = {
|
|
36
|
+
href: string;
|
|
37
|
+
type?: never;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type CardServiceLightButtonProps = {
|
|
41
|
+
href?: never;
|
|
42
|
+
type?: 'button' | 'submit' | 'reset';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type CardServiceLightProps = WithSupportProps<
|
|
46
|
+
(CardServiceLightAnchorProps | CardServiceLightButtonProps) & {
|
|
47
|
+
icon: ReactElement;
|
|
48
|
+
title: string;
|
|
49
|
+
/** При отсутствии promo tag не отображается. Тултип всегда с trigger: 'hover' */
|
|
50
|
+
promoTag?: Omit<PromoTagPredefinedProps, 'trigger'>;
|
|
51
|
+
onClick?(event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>): void;
|
|
52
|
+
onKeyDown?: KeyboardEventHandler<HTMLAnchorElement | HTMLButtonElement>;
|
|
53
|
+
truncate?: {
|
|
54
|
+
title?: number;
|
|
55
|
+
};
|
|
56
|
+
favorite?: {
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
checked?: boolean;
|
|
59
|
+
onChange?(value: boolean): void;
|
|
60
|
+
};
|
|
61
|
+
className?: string;
|
|
62
|
+
}
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
/** @deprecated use CardServiceLight */
|
|
66
|
+
type CardServiceSmallProps = WithSupportProps<
|
|
67
|
+
Pick<CardProps, 'onClick' | 'className' | 'disabled' | 'outline' | 'href' | 'checked'> &
|
|
68
|
+
Required<Pick<Card.HeaderProps, 'title' | 'emblem'>> & {
|
|
69
|
+
truncate?: Pick<NonNullable<Card.HeaderProps['truncate']>, 'title'>;
|
|
70
|
+
favorite?: {
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
visibilityStrategy: 'always' | 'hover';
|
|
73
|
+
checked?: boolean;
|
|
74
|
+
onChange?(value: boolean): void;
|
|
75
|
+
};
|
|
76
|
+
promoBadge?: PromoTagPredefinedProps | CardProps['promoBadge'];
|
|
77
|
+
}
|
|
78
|
+
>;
|
|
79
|
+
|
|
10
80
|
type CardBannerProps = WithSupportProps<
|
|
11
81
|
Pick<CardProps, 'onClick' | 'className'> &
|
|
12
82
|
Required<Pick<Card.HeaderProps, 'title' | 'emblem'>> & {
|
|
@@ -27,10 +97,6 @@ type CardServiceProps = WithSupportProps<
|
|
|
27
97
|
}
|
|
28
98
|
>;
|
|
29
99
|
|
|
30
|
-
type CardServiceSmallProps = WithSupportProps<
|
|
31
|
-
Pick<CardProps, 'promoBadge' | 'onClick' | 'className'> & Required<Pick<Card.HeaderProps, 'title' | 'emblem'>>
|
|
32
|
-
>;
|
|
33
|
-
|
|
34
100
|
export type CardSuggestProps = WithSupportProps<
|
|
35
101
|
Pick<CardProps, 'promoBadge' | 'onClick' | 'className'> & {
|
|
36
102
|
title: string;
|
|
@@ -45,5 +111,3 @@ export type CardSuggestProps = WithSupportProps<
|
|
|
45
111
|
```
|
|
46
112
|
|
|
47
113
|
[Changelog](./CHANGELOG.md)
|
|
48
|
-
|
|
49
|
-
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { KeyboardEventHandler, MouseEvent, ReactElement } from 'react';
|
|
2
|
+
import { PromoTagPredefinedProps } from '@cloud-ru/uikit-product-promo-tag-predefined';
|
|
3
|
+
import { WithSupportProps } from '@snack-uikit/utils';
|
|
4
|
+
import { VisibilityStrategy } from './types';
|
|
5
|
+
type CardElement = HTMLAnchorElement | HTMLButtonElement;
|
|
6
|
+
export type CardServiceLightProps = WithSupportProps<{
|
|
7
|
+
/** Иконка сервиса */
|
|
8
|
+
icon: ReactElement;
|
|
9
|
+
/** Заголовок карточки */
|
|
10
|
+
title: string;
|
|
11
|
+
/** Ссылка, открываемая при клике по карточке */
|
|
12
|
+
href?: string;
|
|
13
|
+
/** Тип кнопки */
|
|
14
|
+
type?: 'button' | 'submit' | 'reset';
|
|
15
|
+
/** Настройки promo tag. При отсутствии не отображается */
|
|
16
|
+
promoTag?: Omit<PromoTagPredefinedProps, 'trigger'>;
|
|
17
|
+
/** Колбек на клик по карточке */
|
|
18
|
+
onClick?(event: MouseEvent<CardElement>): void;
|
|
19
|
+
/** Колбек нажатия клавиши клавиатуры на карточке */
|
|
20
|
+
onKeyDown?: KeyboardEventHandler<CardElement>;
|
|
21
|
+
/** Настройки обрезки текста заголовка */
|
|
22
|
+
truncate?: {
|
|
23
|
+
/** Максимальное количество строк заголовка */
|
|
24
|
+
title?: number;
|
|
25
|
+
};
|
|
26
|
+
/** Настройки кнопки «Избранное» */
|
|
27
|
+
favorite?: {
|
|
28
|
+
/** Включить отображение кнопки избранного */
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Формат отображения: всегда или при наведении и фокусе
|
|
32
|
+
* @default 'hover'
|
|
33
|
+
*/
|
|
34
|
+
visibilityStrategy?: VisibilityStrategy;
|
|
35
|
+
/** Состояние избранного. При передаче вместе с `onChange` компонент работает в controlled-режиме */
|
|
36
|
+
checked?: boolean;
|
|
37
|
+
/** Колбек изменения состояния избранного. При передаче вместе с `checked` компонент работает в controlled-режиме */
|
|
38
|
+
onChange?(value: boolean): void;
|
|
39
|
+
};
|
|
40
|
+
/** CSS-класс корневого элемента */
|
|
41
|
+
className?: string;
|
|
42
|
+
}>;
|
|
43
|
+
export declare const CardServiceLight: import("react").ForwardRefExoticComponent<{
|
|
44
|
+
'data-test-id'?: string;
|
|
45
|
+
} & import("react").AriaAttributes & {
|
|
46
|
+
/** Иконка сервиса */
|
|
47
|
+
icon: ReactElement;
|
|
48
|
+
/** Заголовок карточки */
|
|
49
|
+
title: string;
|
|
50
|
+
/** Ссылка, открываемая при клике по карточке */
|
|
51
|
+
href?: string;
|
|
52
|
+
/** Тип кнопки */
|
|
53
|
+
type?: "button" | "submit" | "reset";
|
|
54
|
+
/** Настройки promo tag. При отсутствии не отображается */
|
|
55
|
+
promoTag?: Omit<PromoTagPredefinedProps, "trigger">;
|
|
56
|
+
/** Колбек на клик по карточке */
|
|
57
|
+
onClick?(event: MouseEvent<CardElement>): void;
|
|
58
|
+
/** Колбек нажатия клавиши клавиатуры на карточке */
|
|
59
|
+
onKeyDown?: KeyboardEventHandler<CardElement>;
|
|
60
|
+
/** Настройки обрезки текста заголовка */
|
|
61
|
+
truncate?: {
|
|
62
|
+
/** Максимальное количество строк заголовка */
|
|
63
|
+
title?: number;
|
|
64
|
+
};
|
|
65
|
+
/** Настройки кнопки «Избранное» */
|
|
66
|
+
favorite?: {
|
|
67
|
+
/** Включить отображение кнопки избранного */
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Формат отображения: всегда или при наведении и фокусе
|
|
71
|
+
* @default 'hover'
|
|
72
|
+
*/
|
|
73
|
+
visibilityStrategy?: VisibilityStrategy;
|
|
74
|
+
/** Состояние избранного. При передаче вместе с `onChange` компонент работает в controlled-режиме */
|
|
75
|
+
checked?: boolean;
|
|
76
|
+
/** Колбек изменения состояния избранного. При передаче вместе с `checked` компонент работает в controlled-режиме */
|
|
77
|
+
onChange?(value: boolean): void;
|
|
78
|
+
};
|
|
79
|
+
/** CSS-класс корневого элемента */
|
|
80
|
+
className?: string;
|
|
81
|
+
} & import("react").RefAttributes<CardElement>>;
|
|
82
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.CardServiceLight = void 0;
|
|
18
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
19
|
+
const classnames_1 = __importDefault(require("classnames"));
|
|
20
|
+
const merge_refs_1 = __importDefault(require("merge-refs"));
|
|
21
|
+
const react_1 = require("react");
|
|
22
|
+
const uncontrollable_1 = require("uncontrollable");
|
|
23
|
+
const uikit_product_promo_tag_predefined_1 = require("@cloud-ru/uikit-product-promo-tag-predefined");
|
|
24
|
+
const toggles_1 = require("@snack-uikit/toggles");
|
|
25
|
+
const truncate_string_1 = require("@snack-uikit/truncate-string");
|
|
26
|
+
const typography_1 = require("@snack-uikit/typography");
|
|
27
|
+
const utils_1 = require("@snack-uikit/utils");
|
|
28
|
+
const constants_1 = require("./constants");
|
|
29
|
+
const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
|
|
30
|
+
exports.CardServiceLight = (0, react_1.forwardRef)((_a, ref) => {
|
|
31
|
+
var { href, type: typeProp, promoTag, title, icon, onClick, onKeyDown: onKeyDownProp, className, truncate, favorite } = _a, rest = __rest(_a, ["href", "type", "promoTag", "title", "icon", "onClick", "onKeyDown", "className", "truncate", "favorite"]);
|
|
32
|
+
const [isFavorite, setIsFavorite] = (0, uncontrollable_1.useUncontrolledProp)(favorite === null || favorite === void 0 ? void 0 : favorite.checked, false, favorite === null || favorite === void 0 ? void 0 : favorite.onChange);
|
|
33
|
+
const componentRef = (0, react_1.useRef)(null);
|
|
34
|
+
const favoriteRef = (0, react_1.useRef)(null);
|
|
35
|
+
const Component = href ? 'a' : 'button';
|
|
36
|
+
const onKeyDown = (e) => {
|
|
37
|
+
var _a, _b, _c;
|
|
38
|
+
if (e.target !== componentRef.current) {
|
|
39
|
+
onKeyDownProp === null || onKeyDownProp === void 0 ? void 0 : onKeyDownProp(e);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (e.code === 'ArrowRight' && (favorite === null || favorite === void 0 ? void 0 : favorite.enabled)) {
|
|
43
|
+
(_a = favoriteRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
44
|
+
}
|
|
45
|
+
if (constants_1.TRIGGER_CLICK_KEY_CODES.includes(e.code)) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
href ? (_b = componentRef.current) === null || _b === void 0 ? void 0 : _b.click() : (_c = componentRef.current) === null || _c === void 0 ? void 0 : _c.click();
|
|
48
|
+
}
|
|
49
|
+
onKeyDownProp === null || onKeyDownProp === void 0 ? void 0 : onKeyDownProp(e);
|
|
50
|
+
};
|
|
51
|
+
const onFavoriteKeyUp = event => {
|
|
52
|
+
var _a;
|
|
53
|
+
if (event.code === 'ArrowLeft') {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
(_a = componentRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
56
|
+
}
|
|
57
|
+
if (event.code === 'Space' || event.code === 'Enter') {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
setIsFavorite(!isFavorite);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const onFavoriteClick = event => {
|
|
63
|
+
var _a;
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
(_a = favoriteRef.current) === null || _a === void 0 ? void 0 : _a.blur();
|
|
66
|
+
};
|
|
67
|
+
const attributes = (0, react_1.useMemo)(() => {
|
|
68
|
+
if (href) {
|
|
69
|
+
return {
|
|
70
|
+
href,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
type: typeProp !== null && typeProp !== void 0 ? typeProp : 'button',
|
|
75
|
+
};
|
|
76
|
+
}, [href, typeProp]);
|
|
77
|
+
const promoTagProps = (0, react_1.useMemo)(() => {
|
|
78
|
+
if (!promoTag) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const tooltipProps = Object.assign(Object.assign({}, ((promoTag === null || promoTag === void 0 ? void 0 : promoTag.tooltip) || {})), { trigger: 'hover' });
|
|
82
|
+
return Object.assign(Object.assign({}, promoTag), { tooltip: tooltipProps });
|
|
83
|
+
}, [promoTag]);
|
|
84
|
+
return ((0, jsx_runtime_1.jsx)(Component, Object.assign({ ref: (0, merge_refs_1.default)(ref, componentRef), className: (0, classnames_1.default)(styles_module_scss_1.default.card, className) }, (0, utils_1.extractSupportProps)(rest), attributes, { onClick: onClick, onKeyDown: onKeyDown, children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.container, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.icon, children: icon }), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.content, children: [(0, jsx_runtime_1.jsx)(typography_1.Typography.SansBodyM, { className: styles_module_scss_1.default.title, "data-test-id": constants_1.TEST_IDS.title, children: (0, jsx_runtime_1.jsx)(truncate_string_1.TruncateString, { text: title, maxLines: truncate === null || truncate === void 0 ? void 0 : truncate.title, variant: 'end' }) }), promoTagProps && (0, jsx_runtime_1.jsx)(uikit_product_promo_tag_predefined_1.PromoTagPredefined, Object.assign({}, promoTagProps, { "data-test-id": constants_1.TEST_IDS.promoTag }))] }), (favorite === null || favorite === void 0 ? void 0 : favorite.enabled) && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.favoriteWrapper, "data-visibility-strategy": favorite.visibilityStrategy || constants_1.VISIBILITY_STRATEGY.hover, "data-checked": isFavorite || undefined, children: (0, jsx_runtime_1.jsx)(toggles_1.Favorite, { size: 'm', checked: isFavorite, inputRef: favoriteRef, onChange: setIsFavorite, tabIndex: -1, onKeyUp: onFavoriteKeyUp, onClick: onFavoriteClick, "data-test-id": constants_1.TEST_IDS.favorite, icon: 'star' }) }))] }) })));
|
|
85
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const TEST_IDS: {
|
|
2
|
+
readonly title: "card-service-light__title";
|
|
3
|
+
readonly promoTag: "card-service-light__promo-tag";
|
|
4
|
+
readonly favorite: "card-service-light__favorite";
|
|
5
|
+
};
|
|
6
|
+
export declare const TRIGGER_CLICK_KEY_CODES: string[];
|
|
7
|
+
export declare const VISIBILITY_STRATEGY: {
|
|
8
|
+
readonly hover: "hover";
|
|
9
|
+
readonly always: "always";
|
|
10
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VISIBILITY_STRATEGY = exports.TRIGGER_CLICK_KEY_CODES = exports.TEST_IDS = void 0;
|
|
4
|
+
exports.TEST_IDS = {
|
|
5
|
+
title: 'card-service-light__title',
|
|
6
|
+
promoTag: 'card-service-light__promo-tag',
|
|
7
|
+
favorite: 'card-service-light__favorite',
|
|
8
|
+
};
|
|
9
|
+
exports.TRIGGER_CLICK_KEY_CODES = ['Enter', 'Space'];
|
|
10
|
+
exports.VISIBILITY_STRATEGY = {
|
|
11
|
+
hover: 'hover',
|
|
12
|
+
always: 'always',
|
|
13
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.VISIBILITY_STRATEGY = void 0;
|
|
18
|
+
__exportStar(require("./CardServiceLight"), exports);
|
|
19
|
+
var constants_1 = require("./constants");
|
|
20
|
+
Object.defineProperty(exports, "VISIBILITY_STRATEGY", { enumerable: true, get: function () { return constants_1.VISIBILITY_STRATEGY; } });
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
.favoriteWrapper{
|
|
2
|
+
position:relative;
|
|
3
|
+
flex-shrink:0;
|
|
4
|
+
height:var(--size-icon-container-s, 24px);
|
|
5
|
+
width:var(--size-icon-container-s, 24px);
|
|
6
|
+
border-radius:2px;
|
|
7
|
+
background-color:var(--sys-neutral-background1-level, #fdfdfd);
|
|
8
|
+
box-shadow:var(--box-shadow-elevation-level2, 0px 0px 4px 0px rgba(0, 0, 0, 0.0392156863), 0px 4px 8px 0px rgba(0, 0, 0, 0.0392156863));
|
|
9
|
+
}
|
|
10
|
+
.favoriteWrapper[data-visibility-strategy=hover]{
|
|
11
|
+
visibility:hidden;
|
|
12
|
+
}
|
|
13
|
+
.favoriteWrapper[data-visibility-strategy=hover]:not(.favoriteWrapper[data-visibility-strategy=hover][data-checked]){
|
|
14
|
+
position:absolute;
|
|
15
|
+
right:4px;
|
|
16
|
+
}
|
|
17
|
+
.favoriteWrapper[data-visibility-strategy=always]{
|
|
18
|
+
visibility:visible;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.card{
|
|
22
|
+
display:flex;
|
|
23
|
+
flex:1 0 0;
|
|
24
|
+
min-width:228px;
|
|
25
|
+
width:100%;
|
|
26
|
+
align-items:center;
|
|
27
|
+
position:relative;
|
|
28
|
+
padding:0;
|
|
29
|
+
border:none;
|
|
30
|
+
font:inherit;
|
|
31
|
+
color:inherit;
|
|
32
|
+
text-decoration:none;
|
|
33
|
+
background:transparent;
|
|
34
|
+
cursor:pointer;
|
|
35
|
+
}
|
|
36
|
+
.card:focus-visible{
|
|
37
|
+
outline-width:var(--border-state-focus-s-border-width, 2px);
|
|
38
|
+
outline-style:var(--border-state-focus-s-border-style, solid);
|
|
39
|
+
outline-color:var(--border-state-focus-s-border-color, );
|
|
40
|
+
border-color:var(--sys-available-complementary, #1c1c24);
|
|
41
|
+
border-radius:4px;
|
|
42
|
+
}
|
|
43
|
+
.card:hover:before, .card:focus-within:before{
|
|
44
|
+
display:flex;
|
|
45
|
+
position:absolute;
|
|
46
|
+
width:100%;
|
|
47
|
+
height:100%;
|
|
48
|
+
content:"";
|
|
49
|
+
border-radius:4px;
|
|
50
|
+
opacity:0.08;
|
|
51
|
+
background-color:var(--sys-neutral-accent-default, #787b8a);
|
|
52
|
+
}
|
|
53
|
+
.card:hover .favoriteWrapper, .card:focus-within .favoriteWrapper{
|
|
54
|
+
visibility:visible;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.container{
|
|
58
|
+
position:relative;
|
|
59
|
+
display:flex;
|
|
60
|
+
padding:4px;
|
|
61
|
+
align-items:center;
|
|
62
|
+
gap:8px;
|
|
63
|
+
flex:1;
|
|
64
|
+
width:100%;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.content{
|
|
68
|
+
display:flex;
|
|
69
|
+
min-height:24px;
|
|
70
|
+
align-items:center;
|
|
71
|
+
gap:4px;
|
|
72
|
+
max-width:100%;
|
|
73
|
+
min-width:0;
|
|
74
|
+
flex-grow:1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.icon{
|
|
78
|
+
height:var(--size-icon-container-s, 24px);
|
|
79
|
+
width:var(--size-icon-container-s, 24px);
|
|
80
|
+
flex-shrink:0;
|
|
81
|
+
}
|
|
82
|
+
.icon > svg{
|
|
83
|
+
height:var(--size-icon-container-s, 24px);
|
|
84
|
+
width:var(--size-icon-container-s, 24px);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.title{
|
|
88
|
+
color:var(--sys-neutral-text-main, #41424e);
|
|
89
|
+
min-width:0;
|
|
90
|
+
}
|
|
@@ -11,4 +11,5 @@ export type CardServiceSmallProps = WithSupportProps<Pick<CardProps, 'onClick' |
|
|
|
11
11
|
};
|
|
12
12
|
promoBadge?: PromoTagPredefinedProps | CardProps['promoBadge'];
|
|
13
13
|
}>;
|
|
14
|
+
/** @deprecated use CardServiceLight instead */
|
|
14
15
|
export declare function CardServiceSmall({ href, promoBadge, title, emblem, onClick, className, disabled, truncate, outline, checked, favorite, ...rest }: CardServiceSmallProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -30,6 +30,7 @@ const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
|
|
|
30
30
|
function isIconProps(props) {
|
|
31
31
|
return Boolean(props && 'icon' in props);
|
|
32
32
|
}
|
|
33
|
+
/** @deprecated use CardServiceLight instead */
|
|
33
34
|
function CardServiceSmall(_a) {
|
|
34
35
|
var { href, promoBadge, title, emblem, onClick, className, disabled, truncate, outline, checked, favorite } = _a, rest = __rest(_a, ["href", "promoBadge", "title", "emblem", "onClick", "className", "disabled", "truncate", "outline", "checked", "favorite"]);
|
|
35
36
|
const [isFavourite, setIsFavorite] = (0, uncontrollable_1.useUncontrolledProp)(favorite === null || favorite === void 0 ? void 0 : favorite.checked, false, favorite === null || favorite === void 0 ? void 0 : favorite.onChange);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { KeyboardEventHandler, MouseEvent, ReactElement } from 'react';
|
|
2
|
+
import { PromoTagPredefinedProps } from '@cloud-ru/uikit-product-promo-tag-predefined';
|
|
3
|
+
import { WithSupportProps } from '@snack-uikit/utils';
|
|
4
|
+
import { VisibilityStrategy } from './types';
|
|
5
|
+
type CardElement = HTMLAnchorElement | HTMLButtonElement;
|
|
6
|
+
export type CardServiceLightProps = WithSupportProps<{
|
|
7
|
+
/** Иконка сервиса */
|
|
8
|
+
icon: ReactElement;
|
|
9
|
+
/** Заголовок карточки */
|
|
10
|
+
title: string;
|
|
11
|
+
/** Ссылка, открываемая при клике по карточке */
|
|
12
|
+
href?: string;
|
|
13
|
+
/** Тип кнопки */
|
|
14
|
+
type?: 'button' | 'submit' | 'reset';
|
|
15
|
+
/** Настройки promo tag. При отсутствии не отображается */
|
|
16
|
+
promoTag?: Omit<PromoTagPredefinedProps, 'trigger'>;
|
|
17
|
+
/** Колбек на клик по карточке */
|
|
18
|
+
onClick?(event: MouseEvent<CardElement>): void;
|
|
19
|
+
/** Колбек нажатия клавиши клавиатуры на карточке */
|
|
20
|
+
onKeyDown?: KeyboardEventHandler<CardElement>;
|
|
21
|
+
/** Настройки обрезки текста заголовка */
|
|
22
|
+
truncate?: {
|
|
23
|
+
/** Максимальное количество строк заголовка */
|
|
24
|
+
title?: number;
|
|
25
|
+
};
|
|
26
|
+
/** Настройки кнопки «Избранное» */
|
|
27
|
+
favorite?: {
|
|
28
|
+
/** Включить отображение кнопки избранного */
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Формат отображения: всегда или при наведении и фокусе
|
|
32
|
+
* @default 'hover'
|
|
33
|
+
*/
|
|
34
|
+
visibilityStrategy?: VisibilityStrategy;
|
|
35
|
+
/** Состояние избранного. При передаче вместе с `onChange` компонент работает в controlled-режиме */
|
|
36
|
+
checked?: boolean;
|
|
37
|
+
/** Колбек изменения состояния избранного. При передаче вместе с `checked` компонент работает в controlled-режиме */
|
|
38
|
+
onChange?(value: boolean): void;
|
|
39
|
+
};
|
|
40
|
+
/** CSS-класс корневого элемента */
|
|
41
|
+
className?: string;
|
|
42
|
+
}>;
|
|
43
|
+
export declare const CardServiceLight: import("react").ForwardRefExoticComponent<{
|
|
44
|
+
'data-test-id'?: string;
|
|
45
|
+
} & import("react").AriaAttributes & {
|
|
46
|
+
/** Иконка сервиса */
|
|
47
|
+
icon: ReactElement;
|
|
48
|
+
/** Заголовок карточки */
|
|
49
|
+
title: string;
|
|
50
|
+
/** Ссылка, открываемая при клике по карточке */
|
|
51
|
+
href?: string;
|
|
52
|
+
/** Тип кнопки */
|
|
53
|
+
type?: "button" | "submit" | "reset";
|
|
54
|
+
/** Настройки promo tag. При отсутствии не отображается */
|
|
55
|
+
promoTag?: Omit<PromoTagPredefinedProps, "trigger">;
|
|
56
|
+
/** Колбек на клик по карточке */
|
|
57
|
+
onClick?(event: MouseEvent<CardElement>): void;
|
|
58
|
+
/** Колбек нажатия клавиши клавиатуры на карточке */
|
|
59
|
+
onKeyDown?: KeyboardEventHandler<CardElement>;
|
|
60
|
+
/** Настройки обрезки текста заголовка */
|
|
61
|
+
truncate?: {
|
|
62
|
+
/** Максимальное количество строк заголовка */
|
|
63
|
+
title?: number;
|
|
64
|
+
};
|
|
65
|
+
/** Настройки кнопки «Избранное» */
|
|
66
|
+
favorite?: {
|
|
67
|
+
/** Включить отображение кнопки избранного */
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Формат отображения: всегда или при наведении и фокусе
|
|
71
|
+
* @default 'hover'
|
|
72
|
+
*/
|
|
73
|
+
visibilityStrategy?: VisibilityStrategy;
|
|
74
|
+
/** Состояние избранного. При передаче вместе с `onChange` компонент работает в controlled-режиме */
|
|
75
|
+
checked?: boolean;
|
|
76
|
+
/** Колбек изменения состояния избранного. При передаче вместе с `checked` компонент работает в controlled-режиме */
|
|
77
|
+
onChange?(value: boolean): void;
|
|
78
|
+
};
|
|
79
|
+
/** CSS-класс корневого элемента */
|
|
80
|
+
className?: string;
|
|
81
|
+
} & import("react").RefAttributes<CardElement>>;
|
|
82
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import cn from 'classnames';
|
|
14
|
+
import mergeRefs from 'merge-refs';
|
|
15
|
+
import { forwardRef, useMemo, useRef, } from 'react';
|
|
16
|
+
import { useUncontrolledProp } from 'uncontrollable';
|
|
17
|
+
import { PromoTagPredefined } from '@cloud-ru/uikit-product-promo-tag-predefined';
|
|
18
|
+
import { Favorite } from '@snack-uikit/toggles';
|
|
19
|
+
import { TruncateString } from '@snack-uikit/truncate-string';
|
|
20
|
+
import { Typography } from '@snack-uikit/typography';
|
|
21
|
+
import { extractSupportProps } from '@snack-uikit/utils';
|
|
22
|
+
import { TEST_IDS, TRIGGER_CLICK_KEY_CODES, VISIBILITY_STRATEGY } from './constants';
|
|
23
|
+
import styles from './styles.module.css';
|
|
24
|
+
export const CardServiceLight = forwardRef((_a, ref) => {
|
|
25
|
+
var { href, type: typeProp, promoTag, title, icon, onClick, onKeyDown: onKeyDownProp, className, truncate, favorite } = _a, rest = __rest(_a, ["href", "type", "promoTag", "title", "icon", "onClick", "onKeyDown", "className", "truncate", "favorite"]);
|
|
26
|
+
const [isFavorite, setIsFavorite] = useUncontrolledProp(favorite === null || favorite === void 0 ? void 0 : favorite.checked, false, favorite === null || favorite === void 0 ? void 0 : favorite.onChange);
|
|
27
|
+
const componentRef = useRef(null);
|
|
28
|
+
const favoriteRef = useRef(null);
|
|
29
|
+
const Component = href ? 'a' : 'button';
|
|
30
|
+
const onKeyDown = (e) => {
|
|
31
|
+
var _a, _b, _c;
|
|
32
|
+
if (e.target !== componentRef.current) {
|
|
33
|
+
onKeyDownProp === null || onKeyDownProp === void 0 ? void 0 : onKeyDownProp(e);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (e.code === 'ArrowRight' && (favorite === null || favorite === void 0 ? void 0 : favorite.enabled)) {
|
|
37
|
+
(_a = favoriteRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
38
|
+
}
|
|
39
|
+
if (TRIGGER_CLICK_KEY_CODES.includes(e.code)) {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
href ? (_b = componentRef.current) === null || _b === void 0 ? void 0 : _b.click() : (_c = componentRef.current) === null || _c === void 0 ? void 0 : _c.click();
|
|
42
|
+
}
|
|
43
|
+
onKeyDownProp === null || onKeyDownProp === void 0 ? void 0 : onKeyDownProp(e);
|
|
44
|
+
};
|
|
45
|
+
const onFavoriteKeyUp = event => {
|
|
46
|
+
var _a;
|
|
47
|
+
if (event.code === 'ArrowLeft') {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
(_a = componentRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
50
|
+
}
|
|
51
|
+
if (event.code === 'Space' || event.code === 'Enter') {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
setIsFavorite(!isFavorite);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const onFavoriteClick = event => {
|
|
57
|
+
var _a;
|
|
58
|
+
event.stopPropagation();
|
|
59
|
+
(_a = favoriteRef.current) === null || _a === void 0 ? void 0 : _a.blur();
|
|
60
|
+
};
|
|
61
|
+
const attributes = useMemo(() => {
|
|
62
|
+
if (href) {
|
|
63
|
+
return {
|
|
64
|
+
href,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
type: typeProp !== null && typeProp !== void 0 ? typeProp : 'button',
|
|
69
|
+
};
|
|
70
|
+
}, [href, typeProp]);
|
|
71
|
+
const promoTagProps = useMemo(() => {
|
|
72
|
+
if (!promoTag) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const tooltipProps = Object.assign(Object.assign({}, ((promoTag === null || promoTag === void 0 ? void 0 : promoTag.tooltip) || {})), { trigger: 'hover' });
|
|
76
|
+
return Object.assign(Object.assign({}, promoTag), { tooltip: tooltipProps });
|
|
77
|
+
}, [promoTag]);
|
|
78
|
+
return (_jsx(Component, Object.assign({ ref: mergeRefs(ref, componentRef), className: cn(styles.card, className) }, extractSupportProps(rest), attributes, { onClick: onClick, onKeyDown: onKeyDown, children: _jsxs("div", { className: styles.container, children: [_jsx("div", { className: styles.icon, children: icon }), _jsxs("div", { className: styles.content, children: [_jsx(Typography.SansBodyM, { className: styles.title, "data-test-id": TEST_IDS.title, children: _jsx(TruncateString, { text: title, maxLines: truncate === null || truncate === void 0 ? void 0 : truncate.title, variant: 'end' }) }), promoTagProps && _jsx(PromoTagPredefined, Object.assign({}, promoTagProps, { "data-test-id": TEST_IDS.promoTag }))] }), (favorite === null || favorite === void 0 ? void 0 : favorite.enabled) && (_jsx("div", { className: styles.favoriteWrapper, "data-visibility-strategy": favorite.visibilityStrategy || VISIBILITY_STRATEGY.hover, "data-checked": isFavorite || undefined, children: _jsx(Favorite, { size: 'm', checked: isFavorite, inputRef: favoriteRef, onChange: setIsFavorite, tabIndex: -1, onKeyUp: onFavoriteKeyUp, onClick: onFavoriteClick, "data-test-id": TEST_IDS.favorite, icon: 'star' }) }))] }) })));
|
|
79
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const TEST_IDS: {
|
|
2
|
+
readonly title: "card-service-light__title";
|
|
3
|
+
readonly promoTag: "card-service-light__promo-tag";
|
|
4
|
+
readonly favorite: "card-service-light__favorite";
|
|
5
|
+
};
|
|
6
|
+
export declare const TRIGGER_CLICK_KEY_CODES: string[];
|
|
7
|
+
export declare const VISIBILITY_STRATEGY: {
|
|
8
|
+
readonly hover: "hover";
|
|
9
|
+
readonly always: "always";
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const TEST_IDS = {
|
|
2
|
+
title: 'card-service-light__title',
|
|
3
|
+
promoTag: 'card-service-light__promo-tag',
|
|
4
|
+
favorite: 'card-service-light__favorite',
|
|
5
|
+
};
|
|
6
|
+
export const TRIGGER_CLICK_KEY_CODES = ['Enter', 'Space'];
|
|
7
|
+
export const VISIBILITY_STRATEGY = {
|
|
8
|
+
hover: 'hover',
|
|
9
|
+
always: 'always',
|
|
10
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
.favoriteWrapper{
|
|
2
|
+
position:relative;
|
|
3
|
+
flex-shrink:0;
|
|
4
|
+
height:var(--size-icon-container-s, 24px);
|
|
5
|
+
width:var(--size-icon-container-s, 24px);
|
|
6
|
+
border-radius:2px;
|
|
7
|
+
background-color:var(--sys-neutral-background1-level, #fdfdfd);
|
|
8
|
+
box-shadow:var(--box-shadow-elevation-level2, 0px 0px 4px 0px rgba(0, 0, 0, 0.0392156863), 0px 4px 8px 0px rgba(0, 0, 0, 0.0392156863));
|
|
9
|
+
}
|
|
10
|
+
.favoriteWrapper[data-visibility-strategy=hover]{
|
|
11
|
+
visibility:hidden;
|
|
12
|
+
}
|
|
13
|
+
.favoriteWrapper[data-visibility-strategy=hover]:not(.favoriteWrapper[data-visibility-strategy=hover][data-checked]){
|
|
14
|
+
position:absolute;
|
|
15
|
+
right:4px;
|
|
16
|
+
}
|
|
17
|
+
.favoriteWrapper[data-visibility-strategy=always]{
|
|
18
|
+
visibility:visible;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.card{
|
|
22
|
+
display:flex;
|
|
23
|
+
flex:1 0 0;
|
|
24
|
+
min-width:228px;
|
|
25
|
+
width:100%;
|
|
26
|
+
align-items:center;
|
|
27
|
+
position:relative;
|
|
28
|
+
padding:0;
|
|
29
|
+
border:none;
|
|
30
|
+
font:inherit;
|
|
31
|
+
color:inherit;
|
|
32
|
+
text-decoration:none;
|
|
33
|
+
background:transparent;
|
|
34
|
+
cursor:pointer;
|
|
35
|
+
}
|
|
36
|
+
.card:focus-visible{
|
|
37
|
+
outline-width:var(--border-state-focus-s-border-width, 2px);
|
|
38
|
+
outline-style:var(--border-state-focus-s-border-style, solid);
|
|
39
|
+
outline-color:var(--border-state-focus-s-border-color, );
|
|
40
|
+
border-color:var(--sys-available-complementary, #1c1c24);
|
|
41
|
+
border-radius:4px;
|
|
42
|
+
}
|
|
43
|
+
.card:hover:before, .card:focus-within:before{
|
|
44
|
+
display:flex;
|
|
45
|
+
position:absolute;
|
|
46
|
+
width:100%;
|
|
47
|
+
height:100%;
|
|
48
|
+
content:"";
|
|
49
|
+
border-radius:4px;
|
|
50
|
+
opacity:0.08;
|
|
51
|
+
background-color:var(--sys-neutral-accent-default, #787b8a);
|
|
52
|
+
}
|
|
53
|
+
.card:hover .favoriteWrapper, .card:focus-within .favoriteWrapper{
|
|
54
|
+
visibility:visible;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.container{
|
|
58
|
+
position:relative;
|
|
59
|
+
display:flex;
|
|
60
|
+
padding:4px;
|
|
61
|
+
align-items:center;
|
|
62
|
+
gap:8px;
|
|
63
|
+
flex:1;
|
|
64
|
+
width:100%;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.content{
|
|
68
|
+
display:flex;
|
|
69
|
+
min-height:24px;
|
|
70
|
+
align-items:center;
|
|
71
|
+
gap:4px;
|
|
72
|
+
max-width:100%;
|
|
73
|
+
min-width:0;
|
|
74
|
+
flex-grow:1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.icon{
|
|
78
|
+
height:var(--size-icon-container-s, 24px);
|
|
79
|
+
width:var(--size-icon-container-s, 24px);
|
|
80
|
+
flex-shrink:0;
|
|
81
|
+
}
|
|
82
|
+
.icon > svg{
|
|
83
|
+
height:var(--size-icon-container-s, 24px);
|
|
84
|
+
width:var(--size-icon-container-s, 24px);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.title{
|
|
88
|
+
color:var(--sys-neutral-text-main, #41424e);
|
|
89
|
+
min-width:0;
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -11,4 +11,5 @@ export type CardServiceSmallProps = WithSupportProps<Pick<CardProps, 'onClick' |
|
|
|
11
11
|
};
|
|
12
12
|
promoBadge?: PromoTagPredefinedProps | CardProps['promoBadge'];
|
|
13
13
|
}>;
|
|
14
|
+
/** @deprecated use CardServiceLight instead */
|
|
14
15
|
export declare function CardServiceSmall({ href, promoBadge, title, emblem, onClick, className, disabled, truncate, outline, checked, favorite, ...rest }: CardServiceSmallProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -24,6 +24,7 @@ import styles from './styles.module.css';
|
|
|
24
24
|
function isIconProps(props) {
|
|
25
25
|
return Boolean(props && 'icon' in props);
|
|
26
26
|
}
|
|
27
|
+
/** @deprecated use CardServiceLight instead */
|
|
27
28
|
export function CardServiceSmall(_a) {
|
|
28
29
|
var { href, promoBadge, title, emblem, onClick, className, disabled, truncate, outline, checked, favorite } = _a, rest = __rest(_a, ["href", "promoBadge", "title", "emblem", "onClick", "className", "disabled", "truncate", "outline", "checked", "favorite"]);
|
|
29
30
|
const [isFavourite, setIsFavorite] = useUncontrolledProp(favorite === null || favorite === void 0 ? void 0 : favorite.checked, false, favorite === null || favorite === void 0 ? void 0 : favorite.onChange);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloud-ru/uikit-product-card-predefined",
|
|
3
3
|
"title": "Card Predefined",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"*.css",
|
|
7
7
|
"*.woff",
|
|
@@ -39,14 +39,15 @@
|
|
|
39
39
|
"@cloud-ru/uikit-product-icons": "17.3.0",
|
|
40
40
|
"@cloud-ru/uikit-product-promo-tag-predefined": "2.0.5",
|
|
41
41
|
"@snack-uikit/button": "0.19.16",
|
|
42
|
-
"@snack-uikit/card": "0.20.
|
|
42
|
+
"@snack-uikit/card": "0.20.18",
|
|
43
43
|
"@snack-uikit/promo-tag": "0.7.1",
|
|
44
44
|
"@snack-uikit/toggles": "0.13.26",
|
|
45
45
|
"@snack-uikit/truncate-string": "0.7.11",
|
|
46
46
|
"@snack-uikit/typography": "0.8.4",
|
|
47
47
|
"@snack-uikit/utils": "3.7.0",
|
|
48
48
|
"classnames": "2.5.1",
|
|
49
|
+
"merge-refs": "1.3.0",
|
|
49
50
|
"uncontrollable": "8.0.4"
|
|
50
51
|
},
|
|
51
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "af75ed24ad92cfdc4c7a84b21f930c610f445d79"
|
|
52
53
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import mergeRefs from 'merge-refs';
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
KeyboardEvent,
|
|
6
|
+
KeyboardEventHandler,
|
|
7
|
+
MouseEvent,
|
|
8
|
+
MouseEventHandler,
|
|
9
|
+
ReactElement,
|
|
10
|
+
Ref,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
} from 'react';
|
|
14
|
+
import { useUncontrolledProp } from 'uncontrollable';
|
|
15
|
+
|
|
16
|
+
import { PromoTagPredefined, PromoTagPredefinedProps } from '@cloud-ru/uikit-product-promo-tag-predefined';
|
|
17
|
+
import { Favorite } from '@snack-uikit/toggles';
|
|
18
|
+
import { TruncateString } from '@snack-uikit/truncate-string';
|
|
19
|
+
import { Typography } from '@snack-uikit/typography';
|
|
20
|
+
import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
|
|
21
|
+
|
|
22
|
+
import { TEST_IDS, TRIGGER_CLICK_KEY_CODES, VISIBILITY_STRATEGY } from './constants';
|
|
23
|
+
import styles from './styles.module.scss';
|
|
24
|
+
import { VisibilityStrategy } from './types';
|
|
25
|
+
|
|
26
|
+
type CardElement = HTMLAnchorElement | HTMLButtonElement;
|
|
27
|
+
|
|
28
|
+
export type CardServiceLightProps = WithSupportProps<{
|
|
29
|
+
/** Иконка сервиса */
|
|
30
|
+
icon: ReactElement;
|
|
31
|
+
/** Заголовок карточки */
|
|
32
|
+
title: string;
|
|
33
|
+
/** Ссылка, открываемая при клике по карточке */
|
|
34
|
+
href?: string;
|
|
35
|
+
/** Тип кнопки */
|
|
36
|
+
type?: 'button' | 'submit' | 'reset';
|
|
37
|
+
/** Настройки promo tag. При отсутствии не отображается */
|
|
38
|
+
promoTag?: Omit<PromoTagPredefinedProps, 'trigger'>;
|
|
39
|
+
/** Колбек на клик по карточке */
|
|
40
|
+
onClick?(event: MouseEvent<CardElement>): void;
|
|
41
|
+
/** Колбек нажатия клавиши клавиатуры на карточке */
|
|
42
|
+
onKeyDown?: KeyboardEventHandler<CardElement>;
|
|
43
|
+
/** Настройки обрезки текста заголовка */
|
|
44
|
+
truncate?: {
|
|
45
|
+
/** Максимальное количество строк заголовка */
|
|
46
|
+
title?: number;
|
|
47
|
+
};
|
|
48
|
+
/** Настройки кнопки «Избранное» */
|
|
49
|
+
favorite?: {
|
|
50
|
+
/** Включить отображение кнопки избранного */
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Формат отображения: всегда или при наведении и фокусе
|
|
54
|
+
* @default 'hover'
|
|
55
|
+
*/
|
|
56
|
+
visibilityStrategy?: VisibilityStrategy;
|
|
57
|
+
/** Состояние избранного. При передаче вместе с `onChange` компонент работает в controlled-режиме */
|
|
58
|
+
checked?: boolean;
|
|
59
|
+
/** Колбек изменения состояния избранного. При передаче вместе с `checked` компонент работает в controlled-режиме */
|
|
60
|
+
onChange?(value: boolean): void;
|
|
61
|
+
};
|
|
62
|
+
/** CSS-класс корневого элемента */
|
|
63
|
+
className?: string;
|
|
64
|
+
}>;
|
|
65
|
+
|
|
66
|
+
export const CardServiceLight = forwardRef<CardElement, CardServiceLightProps>(
|
|
67
|
+
(
|
|
68
|
+
{
|
|
69
|
+
href,
|
|
70
|
+
type: typeProp,
|
|
71
|
+
promoTag,
|
|
72
|
+
title,
|
|
73
|
+
icon,
|
|
74
|
+
onClick,
|
|
75
|
+
onKeyDown: onKeyDownProp,
|
|
76
|
+
className,
|
|
77
|
+
truncate,
|
|
78
|
+
favorite,
|
|
79
|
+
...rest
|
|
80
|
+
},
|
|
81
|
+
ref,
|
|
82
|
+
) => {
|
|
83
|
+
const [isFavorite, setIsFavorite] = useUncontrolledProp(favorite?.checked, false, favorite?.onChange);
|
|
84
|
+
const componentRef = useRef<CardElement>(null);
|
|
85
|
+
const favoriteRef = useRef<HTMLInputElement>(null);
|
|
86
|
+
|
|
87
|
+
const Component = href ? 'a' : 'button';
|
|
88
|
+
|
|
89
|
+
const onKeyDown = (e: KeyboardEvent<CardElement>) => {
|
|
90
|
+
if (e.target !== componentRef.current) {
|
|
91
|
+
onKeyDownProp?.(e);
|
|
92
|
+
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (e.code === 'ArrowRight' && favorite?.enabled) {
|
|
97
|
+
favoriteRef.current?.focus();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (TRIGGER_CLICK_KEY_CODES.includes(e.code)) {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
href ? componentRef.current?.click() : componentRef.current?.click();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onKeyDownProp?.(e);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const onFavoriteKeyUp: KeyboardEventHandler = event => {
|
|
109
|
+
if (event.code === 'ArrowLeft') {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
componentRef.current?.focus();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (event.code === 'Space' || event.code === 'Enter') {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
setIsFavorite(!isFavorite);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const onFavoriteClick: MouseEventHandler = event => {
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
|
|
123
|
+
favoriteRef.current?.blur();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const attributes = useMemo(() => {
|
|
127
|
+
if (href) {
|
|
128
|
+
return {
|
|
129
|
+
href,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
type: typeProp ?? 'button',
|
|
135
|
+
};
|
|
136
|
+
}, [href, typeProp]);
|
|
137
|
+
|
|
138
|
+
const promoTagProps: PromoTagPredefinedProps | null = useMemo(() => {
|
|
139
|
+
if (!promoTag) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const tooltipProps: PromoTagPredefinedProps['tooltip'] = {
|
|
144
|
+
...(promoTag?.tooltip || {}),
|
|
145
|
+
trigger: 'hover',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
...promoTag,
|
|
150
|
+
tooltip: tooltipProps,
|
|
151
|
+
};
|
|
152
|
+
}, [promoTag]);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Component
|
|
156
|
+
ref={mergeRefs(ref, componentRef) as Ref<HTMLAnchorElement> & Ref<HTMLButtonElement>}
|
|
157
|
+
className={cn(styles.card, className)}
|
|
158
|
+
{...extractSupportProps(rest)}
|
|
159
|
+
{...attributes}
|
|
160
|
+
onClick={onClick}
|
|
161
|
+
onKeyDown={onKeyDown}
|
|
162
|
+
>
|
|
163
|
+
<div className={styles.container}>
|
|
164
|
+
<div className={styles.icon}>{icon}</div>
|
|
165
|
+
|
|
166
|
+
<div className={styles.content}>
|
|
167
|
+
<Typography.SansBodyM className={styles.title} data-test-id={TEST_IDS.title}>
|
|
168
|
+
<TruncateString text={title} maxLines={truncate?.title} variant='end' />
|
|
169
|
+
</Typography.SansBodyM>
|
|
170
|
+
|
|
171
|
+
{promoTagProps && <PromoTagPredefined {...promoTagProps} data-test-id={TEST_IDS.promoTag} />}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{favorite?.enabled && (
|
|
175
|
+
<div
|
|
176
|
+
className={styles.favoriteWrapper}
|
|
177
|
+
data-visibility-strategy={favorite.visibilityStrategy || VISIBILITY_STRATEGY.hover}
|
|
178
|
+
data-checked={isFavorite || undefined}
|
|
179
|
+
>
|
|
180
|
+
<Favorite
|
|
181
|
+
size='m'
|
|
182
|
+
checked={isFavorite}
|
|
183
|
+
inputRef={favoriteRef}
|
|
184
|
+
onChange={setIsFavorite}
|
|
185
|
+
tabIndex={-1}
|
|
186
|
+
onKeyUp={onFavoriteKeyUp}
|
|
187
|
+
onClick={onFavoriteClick}
|
|
188
|
+
data-test-id={TEST_IDS.favorite}
|
|
189
|
+
icon='star'
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
</Component>
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const TEST_IDS = {
|
|
2
|
+
title: 'card-service-light__title',
|
|
3
|
+
promoTag: 'card-service-light__promo-tag',
|
|
4
|
+
favorite: 'card-service-light__favorite',
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
export const TRIGGER_CLICK_KEY_CODES = ['Enter', 'Space'];
|
|
8
|
+
|
|
9
|
+
export const VISIBILITY_STRATEGY = {
|
|
10
|
+
hover: 'hover',
|
|
11
|
+
always: 'always',
|
|
12
|
+
} as const;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
@use '@cloud-ru/figma-tokens-cloud-platform/build/scss/components/styles-tokens-element' as ste;
|
|
2
|
+
|
|
3
|
+
$side: ste.$icon-s;
|
|
4
|
+
|
|
5
|
+
.favoriteWrapper {
|
|
6
|
+
position: relative;
|
|
7
|
+
flex-shrink: 0;
|
|
8
|
+
height: $side;
|
|
9
|
+
width: $side;
|
|
10
|
+
border-radius: 2px;
|
|
11
|
+
background-color: ste.$sys-neutral-background1-level;
|
|
12
|
+
box-shadow: ste.$box-shadow-elevation-level2;
|
|
13
|
+
|
|
14
|
+
&[data-visibility-strategy='hover'] {
|
|
15
|
+
visibility: hidden;
|
|
16
|
+
|
|
17
|
+
&:not(&[data-checked]) {
|
|
18
|
+
position: absolute;
|
|
19
|
+
right: 4px;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&[data-visibility-strategy='always'] {
|
|
24
|
+
visibility: visible;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.card {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex: 1 0 0;
|
|
31
|
+
min-width: 228px;
|
|
32
|
+
width: 100%;
|
|
33
|
+
align-items: center;
|
|
34
|
+
position: relative;
|
|
35
|
+
|
|
36
|
+
padding: 0;
|
|
37
|
+
border: none;
|
|
38
|
+
|
|
39
|
+
font: inherit;
|
|
40
|
+
color: inherit;
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
|
|
43
|
+
background: transparent;
|
|
44
|
+
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
|
|
47
|
+
&:focus-visible {
|
|
48
|
+
@include ste.outline-var(ste.$container-focused-s);
|
|
49
|
+
|
|
50
|
+
border-color: ste.$sys-available-complementary;
|
|
51
|
+
border-radius: 4px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&:hover,
|
|
55
|
+
&:focus-within {
|
|
56
|
+
&:before {
|
|
57
|
+
display: flex;
|
|
58
|
+
position: absolute;
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: 100%;
|
|
61
|
+
content: '';
|
|
62
|
+
border-radius: 4px;
|
|
63
|
+
opacity: 0.08;
|
|
64
|
+
background-color: ste.$sys-neutral-accent-default;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.favoriteWrapper {
|
|
68
|
+
visibility: visible;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.container {
|
|
74
|
+
position: relative;
|
|
75
|
+
display: flex;
|
|
76
|
+
padding: 4px;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: 8px;
|
|
79
|
+
flex: 1;
|
|
80
|
+
width: 100%;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.content {
|
|
84
|
+
display: flex;
|
|
85
|
+
min-height: 24px;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 4px;
|
|
88
|
+
max-width: 100%;
|
|
89
|
+
min-width: 0;
|
|
90
|
+
flex-grow: 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.icon {
|
|
94
|
+
height: $side;
|
|
95
|
+
width: $side;
|
|
96
|
+
|
|
97
|
+
flex-shrink: 0;
|
|
98
|
+
|
|
99
|
+
& > svg {
|
|
100
|
+
height: $side;
|
|
101
|
+
width: $side;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.title {
|
|
106
|
+
color: ste.$sys-neutral-text-main;
|
|
107
|
+
min-width: 0; // Fixes sluggish truncate behavior
|
|
108
|
+
}
|
package/src/components/index.ts
CHANGED