From 5edea0a2baf0c0d81127deb06803edc0b50a3aa7 Mon Sep 17 00:00:00 2001 From: krutoo Date: Thu, 29 Aug 2024 16:08:42 +0500 Subject: [PATCH] #285 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - badge: теперь умеет выводить градиенты если они заданы через css-переменные - deps: обновлен пакет @sima-land/ui-nucleons --- .../badge/10-gradient.stories.meta.json | 7 + .../components/badge/10-gradient.stories.tsx | 33 ++++ .../common/components/badge/gradient.m.scss | 8 + package-lock.json | 14 +- package.json | 2 +- .../components/badge/__test__/utils.test.ts | 28 ++++ src/common/components/badge/badge.tsx | 112 +------------- src/common/components/badge/utils.tsx | 144 ++++++++++++++++++ 8 files changed, 232 insertions(+), 116 deletions(-) create mode 100644 docs/stories/common/components/badge/10-gradient.stories.meta.json create mode 100644 docs/stories/common/components/badge/10-gradient.stories.tsx create mode 100644 docs/stories/common/components/badge/gradient.m.scss create mode 100644 src/common/components/badge/__test__/utils.test.ts create mode 100644 src/common/components/badge/utils.tsx diff --git a/docs/stories/common/components/badge/10-gradient.stories.meta.json b/docs/stories/common/components/badge/10-gradient.stories.meta.json new file mode 100644 index 0000000..eca5448 --- /dev/null +++ b/docs/stories/common/components/badge/10-gradient.stories.meta.json @@ -0,0 +1,7 @@ +{ + "parameters": { + "sources": { + "extraSources": ["./gradient.m.scss"] + } + } +} diff --git a/docs/stories/common/components/badge/10-gradient.stories.tsx b/docs/stories/common/components/badge/10-gradient.stories.tsx new file mode 100644 index 0000000..5b722cf --- /dev/null +++ b/docs/stories/common/components/badge/10-gradient.stories.tsx @@ -0,0 +1,33 @@ +import { Badge } from '@sima-land/moleculas/common/components/badge'; +import styles from './gradient.m.scss'; + +export const meta = { + category: 'common/Badge', + title: 'Заливка градиентом', + parameters: { + layout: 'padded', + }, +}; + +export default function DifferentStates() { + return ( + // ВАЖНО: для использования градиентов необходимо наличие соответствующих css-переменных выше в DOM +
+ +
+ ); +} diff --git a/docs/stories/common/components/badge/gradient.m.scss b/docs/stories/common/components/badge/gradient.m.scss new file mode 100644 index 0000000..f4f1b93 --- /dev/null +++ b/docs/stories/common/components/badge/gradient.m.scss @@ -0,0 +1,8 @@ +@use 'pkg:@sima-land/ui-nucleons/colors.scss'; +@use 'pkg:@sima-land/ui-nucleons/gradients.scss'; + +// предоставляем переменные цветов и градиентов (для использования градиентов в Badge это необходимо) +.container { + @include colors.properties; + @include gradients.properties; +} diff --git a/package-lock.json b/package-lock.json index f1d80da..14b563d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "node": ">=16.15.1" }, "peerDependencies": { - "@sima-land/ui-nucleons": "^45.0.0-alpha.19", + "@sima-land/ui-nucleons": "^45.0.0-alpha.20", "@sima-land/ui-quarks": "^5.0.0", "react": "^17.0 || ^18.0" } @@ -3125,9 +3125,9 @@ } }, "node_modules/@sima-land/ui-nucleons": { - "version": "45.0.0-alpha.19", - "resolved": "https://registry.npmjs.org/@sima-land/ui-nucleons/-/ui-nucleons-45.0.0-alpha.19.tgz", - "integrity": "sha512-+8Xj3oFS1d1s5ecUPY0SExrEH8F+abPdmmtkC2HnzKHqFjQs30VRHJWnfhabKgtnX8wBd2gVna+ZgDOsO6/W3g==", + "version": "45.0.0-alpha.20", + "resolved": "https://registry.npmjs.org/@sima-land/ui-nucleons/-/ui-nucleons-45.0.0-alpha.20.tgz", + "integrity": "sha512-uD3941oF7KpTgoPXsCRuhInk8TyjeuVD2TXDZzjogyLPsBpxMkg+O4WBu7SGyFYyurZVQ5N6GGeHzaeLSke8Kg==", "peer": true, "dependencies": { "@floating-ui/react": "^0.26.13", @@ -15879,9 +15879,9 @@ } }, "@sima-land/ui-nucleons": { - "version": "45.0.0-alpha.19", - "resolved": "https://registry.npmjs.org/@sima-land/ui-nucleons/-/ui-nucleons-45.0.0-alpha.19.tgz", - "integrity": "sha512-+8Xj3oFS1d1s5ecUPY0SExrEH8F+abPdmmtkC2HnzKHqFjQs30VRHJWnfhabKgtnX8wBd2gVna+ZgDOsO6/W3g==", + "version": "45.0.0-alpha.20", + "resolved": "https://registry.npmjs.org/@sima-land/ui-nucleons/-/ui-nucleons-45.0.0-alpha.20.tgz", + "integrity": "sha512-uD3941oF7KpTgoPXsCRuhInk8TyjeuVD2TXDZzjogyLPsBpxMkg+O4WBu7SGyFYyurZVQ5N6GGeHzaeLSke8Kg==", "peer": true, "requires": { "@floating-ui/react": "^0.26.13", diff --git a/package.json b/package.json index ea8c10b..8895f6b 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "typescript": "^5.3.3" }, "peerDependencies": { - "@sima-land/ui-nucleons": "^45.0.0-alpha.19", + "@sima-land/ui-nucleons": "^45.0.0-alpha.20", "@sima-land/ui-quarks": "^5.0.0", "react": "^17.0 || ^18.0" } diff --git a/src/common/components/badge/__test__/utils.test.ts b/src/common/components/badge/__test__/utils.test.ts new file mode 100644 index 0000000..94db19a --- /dev/null +++ b/src/common/components/badge/__test__/utils.test.ts @@ -0,0 +1,28 @@ +import { tokenToCustomProperty } from '../utils'; + +describe('tokenToCustomProperty', () => { + it('should return value if it is not a string', () => { + expect(tokenToCustomProperty(undefined)).toBe(undefined); + }); + + it('should return value if it is not a token', () => { + expect(tokenToCustomProperty('something')).toBe('something'); + expect(tokenToCustomProperty('#000')).toBe('#000'); + }); + + it('should return value if it is color token but color not exist', () => { + expect(tokenToCustomProperty('color/foo')).toBe('color/foo'); + }); + + it('should return css property if it is valid color token', () => { + expect(tokenToCustomProperty('color/basic-blue')).toBe('var(--color-basic-blue)'); + }); + + it('should return value if it is gradient token but gradient not exist', () => { + expect(tokenToCustomProperty('gradient/hello')).toBe('gradient/hello'); + }); + + it('should return css property if it is valid gradient token', () => { + expect(tokenToCustomProperty('gradient/gold')).toBe('var(--gradient-gold)'); + }); +}); diff --git a/src/common/components/badge/badge.tsx b/src/common/components/badge/badge.tsx index 921417b..0fe1053 100644 --- a/src/common/components/badge/badge.tsx +++ b/src/common/components/badge/badge.tsx @@ -1,6 +1,6 @@ -import { type ReactNode, forwardRef } from 'react'; -import type { BadgeField, BadgeProps, BadgeStyle } from './types'; -import { Timer } from '@sima-land/ui-nucleons/timer'; +import type { BadgeProps, BadgeStyle } from './types'; +import { forwardRef } from 'react'; +import { renderFields, tokenToCustomProperty } from './utils'; import classnames from 'classnames/bind'; import styles from './badge.m.scss'; @@ -43,7 +43,7 @@ export const Badge = forwardRef(function Badge( ); const rootStyle: BadgeStyle = { - '--badge-color': color, + '--badge-color': tokenToCustomProperty(color), ...style, }; @@ -60,107 +60,3 @@ export const Badge = forwardRef(function Badge( ); }); - -/** - * Сформирует содержимое шильдика на основе списка полей. - * Текстовые поля (type === text | timer) идущие подряд, будут объединены в один span. - * Между текстовыми полями в span будет вставлен 1 пробел. - * @param fields Поля. - * @return СОдержимое шильдика. - */ -function renderFields(fields: BadgeField[]): ReactNode[] { - const result: ReactNode[] = []; - - let item: null | Array = null; - - for (const field of fields) { - if (!item) { - switch (field.type) { - case 'text': { - item = [field.value]; - break; - } - - case 'timer': { - item = []; - break; - } - - case 'svg-url': { - result.push(); - break; - } - } - } else { - switch (field.type) { - case 'text': { - item.push(' '); - item.push(field.value); - break; - } - - case 'timer': { - item.push(' '); - item.push( - , - ); - break; - } - - case 'svg-url': { - // вложенный span нужен для того чтобы объединить `display: inline-flex` и `text-overflow: ellipsis` - result.push( - - {item} - , - ); - item = null; - result.push(); - break; - } - } - } - } - - if (item) { - result.push( - - {item} - , - ); - item = null; - } - - return result; -} - -/** - * Форматирует оставшееся время. - * @param distance Оставшееся время. - * @return Отформатированное время. - */ -function formatDistance({ - days, - hours, - minutes, -}: { - days: number; - hours: number; - minutes: number; -}) { - return `${toTimePart(days)}:${toTimePart(hours % 24)}:${toTimePart(minutes % 60)}`; -} - -/** - * Форматирует число добавляя нули при необходимости. - * @param n Число. - * @return Отформатированное число. - */ -function toTimePart(n: number) { - return `${n}`.padStart(2, '0'); -} diff --git a/src/common/components/badge/utils.tsx b/src/common/components/badge/utils.tsx new file mode 100644 index 0000000..80842c2 --- /dev/null +++ b/src/common/components/badge/utils.tsx @@ -0,0 +1,144 @@ +import type { ReactNode } from 'react'; +import type { BadgeField } from './types'; +import { COLORS } from '@sima-land/ui-nucleons/colors'; +import { GRADIENTS } from '@sima-land/ui-nucleons/gradients'; +import { Timer } from '@sima-land/ui-nucleons/timer'; +import styles from './badge.m.scss'; + +/** + * Если переданное значение является токеном цвета/градиента, вернёт CSS-значение с соответствующей CSS-переменной. + * Иначе вернет само значение. + * @param value Значение. + * @return Значение или undefined. + */ +export function tokenToCustomProperty(value: string | undefined): string | undefined { + if (typeof value !== 'string') { + return value; + } + + if (value.startsWith('color/')) { + const colorName = value.replace('color/', ''); + + if (!COLORS.has(colorName as any)) { + return value; + } + + return `var(--color-${colorName})`; + } + + if (value.startsWith('gradient/')) { + const gradientName = value.replace('gradient/', ''); + + if (!GRADIENTS.has(gradientName as any)) { + return value; + } + + return `var(--gradient-${gradientName})`; + } + + return value; +} + +/** + * Сформирует содержимое шильдика на основе списка полей. + * Текстовые поля (type === text | timer) идущие подряд, будут объединены в один span. + * Между текстовыми полями в span будет вставлен 1 пробел. + * @param fields Поля. + * @return СОдержимое шильдика. + */ +export function renderFields(fields: BadgeField[]): ReactNode[] { + const result: ReactNode[] = []; + + let item: null | Array = null; + + for (const field of fields) { + if (!item) { + switch (field.type) { + case 'text': { + item = [field.value]; + break; + } + + case 'timer': { + item = []; + break; + } + + case 'svg-url': { + result.push(); + break; + } + } + } else { + switch (field.type) { + case 'text': { + item.push(' '); + item.push(field.value); + break; + } + + case 'timer': { + item.push(' '); + item.push( + , + ); + break; + } + + case 'svg-url': { + // вложенный span нужен для того чтобы объединить `display: inline-flex` и `text-overflow: ellipsis` + result.push( + + {item} + , + ); + item = null; + result.push(); + break; + } + } + } + } + + if (item) { + result.push( + + {item} + , + ); + item = null; + } + + return result; +} + +/** + * Форматирует оставшееся время. + * @param distance Оставшееся время. + * @return Отформатированное время. + */ +export function formatDistance({ + days, + hours, + minutes, +}: { + days: number; + hours: number; + minutes: number; +}) { + return `${toTimePart(days)}:${toTimePart(hours % 24)}:${toTimePart(minutes % 60)}`; +} + +/** + * Форматирует число добавляя нули при необходимости. + * @param n Число. + * @return Отформатированное число. + */ +export function toTimePart(n: number) { + return `${n}`.padStart(2, '0'); +}