@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +70 -6
  3. package/dist/cjs/components/CardServiceLight/CardServiceLight.d.ts +82 -0
  4. package/dist/cjs/components/CardServiceLight/CardServiceLight.js +85 -0
  5. package/dist/cjs/components/CardServiceLight/constants.d.ts +10 -0
  6. package/dist/cjs/components/CardServiceLight/constants.js +13 -0
  7. package/dist/cjs/components/CardServiceLight/index.d.ts +3 -0
  8. package/dist/cjs/components/CardServiceLight/index.js +20 -0
  9. package/dist/cjs/components/CardServiceLight/styles.module.css +90 -0
  10. package/dist/cjs/components/CardServiceLight/types.d.ts +3 -0
  11. package/dist/cjs/components/CardServiceLight/types.js +2 -0
  12. package/dist/cjs/components/CardServiceSmall/CardServiceSmall.d.ts +1 -0
  13. package/dist/cjs/components/CardServiceSmall/CardServiceSmall.js +1 -0
  14. package/dist/cjs/components/index.d.ts +1 -0
  15. package/dist/cjs/components/index.js +1 -0
  16. package/dist/esm/components/CardServiceLight/CardServiceLight.d.ts +82 -0
  17. package/dist/esm/components/CardServiceLight/CardServiceLight.js +79 -0
  18. package/dist/esm/components/CardServiceLight/constants.d.ts +10 -0
  19. package/dist/esm/components/CardServiceLight/constants.js +10 -0
  20. package/dist/esm/components/CardServiceLight/index.d.ts +3 -0
  21. package/dist/esm/components/CardServiceLight/index.js +2 -0
  22. package/dist/esm/components/CardServiceLight/styles.module.css +90 -0
  23. package/dist/esm/components/CardServiceLight/types.d.ts +3 -0
  24. package/dist/esm/components/CardServiceLight/types.js +1 -0
  25. package/dist/esm/components/CardServiceSmall/CardServiceSmall.d.ts +1 -0
  26. package/dist/esm/components/CardServiceSmall/CardServiceSmall.js +1 -0
  27. package/dist/esm/components/index.d.ts +1 -0
  28. package/dist/esm/components/index.js +1 -0
  29. package/package.json +4 -3
  30. package/src/components/CardServiceLight/CardServiceLight.tsx +197 -0
  31. package/src/components/CardServiceLight/constants.ts +12 -0
  32. package/src/components/CardServiceLight/index.ts +3 -0
  33. package/src/components/CardServiceLight/styles.module.scss +108 -0
  34. package/src/components/CardServiceLight/types.ts +5 -0
  35. package/src/components/CardServiceSmall/CardServiceSmall.tsx +1 -0
  36. 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,3 @@
1
+ export * from './CardServiceLight';
2
+ export { VISIBILITY_STRATEGY } from './constants';
3
+ export type { VisibilityStrategy } from './types';
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { ValueOf } from '@snack-uikit/utils';
2
+ import { VISIBILITY_STRATEGY } from './constants';
3
+ export type VisibilityStrategy = ValueOf<typeof VISIBILITY_STRATEGY>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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);
@@ -2,3 +2,4 @@ export * from './CardBanner';
2
2
  export * from './CardSuggest';
3
3
  export * from './CardService';
4
4
  export * from './CardServiceSmall';
5
+ export * from './CardServiceLight';
@@ -18,3 +18,4 @@ __exportStar(require("./CardBanner"), exports);
18
18
  __exportStar(require("./CardSuggest"), exports);
19
19
  __exportStar(require("./CardService"), exports);
20
20
  __exportStar(require("./CardServiceSmall"), exports);
21
+ __exportStar(require("./CardServiceLight"), exports);
@@ -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,3 @@
1
+ export * from './CardServiceLight';
2
+ export { VISIBILITY_STRATEGY } from './constants';
3
+ export type { VisibilityStrategy } from './types';
@@ -0,0 +1,2 @@
1
+ export * from './CardServiceLight';
2
+ export { VISIBILITY_STRATEGY } from './constants';
@@ -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,3 @@
1
+ import { ValueOf } from '@snack-uikit/utils';
2
+ import { VISIBILITY_STRATEGY } from './constants';
3
+ export type VisibilityStrategy = ValueOf<typeof VISIBILITY_STRATEGY>;
@@ -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);
@@ -2,3 +2,4 @@ export * from './CardBanner';
2
2
  export * from './CardSuggest';
3
3
  export * from './CardService';
4
4
  export * from './CardServiceSmall';
5
+ export * from './CardServiceLight';
@@ -2,3 +2,4 @@ export * from './CardBanner';
2
2
  export * from './CardSuggest';
3
3
  export * from './CardService';
4
4
  export * from './CardServiceSmall';
5
+ export * from './CardServiceLight';
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.1.13-preview-926fd6b4.0",
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.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": "b3f5ee8a0353d2bdf60276fcd5326b4e809bbd7e"
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,3 @@
1
+ export * from './CardServiceLight';
2
+ export { VISIBILITY_STRATEGY } from './constants';
3
+ export type { VisibilityStrategy } from './types';
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import { ValueOf } from '@snack-uikit/utils';
2
+
3
+ import { VISIBILITY_STRATEGY } from './constants';
4
+
5
+ export type VisibilityStrategy = ValueOf<typeof VISIBILITY_STRATEGY>;
@@ -32,6 +32,7 @@ function isIconProps(
32
32
  return Boolean(props && 'icon' in props);
33
33
  }
34
34
 
35
+ /** @deprecated use CardServiceLight instead */
35
36
  export function CardServiceSmall({
36
37
  href,
37
38
  promoBadge,
@@ -2,3 +2,4 @@ export * from './CardBanner';
2
2
  export * from './CardSuggest';
3
3
  export * from './CardService';
4
4
  export * from './CardServiceSmall';
5
+ export * from './CardServiceLight';