Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
- badge: теперь умеет выводить градиенты если они заданы через css-переменные
- deps: обновлен пакет @sima-land/ui-nucleons
  • Loading branch information
krutoo committed Aug 29, 2024
1 parent 7db8e4b commit 5edea0a
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 116 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"parameters": {
"sources": {
"extraSources": ["./gradient.m.scss"]
}
}
}
33 changes: 33 additions & 0 deletions docs/stories/common/components/badge/10-gradient.stories.tsx
Original file line number Diff line number Diff line change
@@ -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
<div className={styles.container}>
<Badge
shape='round'
coloring='fill'
color='gradient/gold'
fields={[
{
type: 'svg-url',
value: 'public/images/logo_white.svg',
},
{
type: 'text',
value: 'Сделано в Сима-ленд',
},
]}
/>
</div>
);
}
8 changes: 8 additions & 0 deletions docs/stories/common/components/badge/gradient.m.scss
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
28 changes: 28 additions & 0 deletions src/common/components/badge/__test__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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)');
});
});
112 changes: 4 additions & 108 deletions src/common/components/badge/badge.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -43,7 +43,7 @@ export const Badge = forwardRef<HTMLAnchorElement, BadgeProps>(function Badge(
);

const rootStyle: BadgeStyle = {
'--badge-color': color,
'--badge-color': tokenToCustomProperty(color),
...style,
};

Expand All @@ -60,107 +60,3 @@ export const Badge = forwardRef<HTMLAnchorElement, BadgeProps>(function Badge(
</a>
);
});

/**
* Сформирует содержимое шильдика на основе списка полей.
* Текстовые поля (type === text | timer) идущие подряд, будут объединены в один span.
* Между текстовыми полями в span будет вставлен 1 пробел.
* @param fields Поля.
* @return СОдержимое шильдика.
*/
function renderFields(fields: BadgeField[]): ReactNode[] {
const result: ReactNode[] = [];

let item: null | Array<ReactNode> = null;

for (const field of fields) {
if (!item) {
switch (field.type) {
case 'text': {
item = [field.value];
break;
}

case 'timer': {
item = [<Timer key={0} date={field.value} timeout={1000 * 60} format={formatDistance} />];
break;
}

case 'svg-url': {
result.push(<img key={result.length} className={cx('icon')} src={field.value} alt='' />);
break;
}
}
} else {
switch (field.type) {
case 'text': {
item.push(' ');
item.push(field.value);
break;
}

case 'timer': {
item.push(' ');
item.push(
<Timer
key={item.length}
date={field.value}
timeout={1000 * 60}
format={formatDistance}
/>,
);
break;
}

case 'svg-url': {
// вложенный span нужен для того чтобы объединить `display: inline-flex` и `text-overflow: ellipsis`
result.push(
<span key={result.length} className={cx('content')}>
{item}
</span>,
);
item = null;
result.push(<img key={result.length} className={cx('icon')} src={field.value} alt='' />);
break;
}
}
}
}

if (item) {
result.push(
<span key={result.length} className={cx('content')}>
{item}
</span>,
);
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');
}
144 changes: 144 additions & 0 deletions src/common/components/badge/utils.tsx
Original file line number Diff line number Diff line change
@@ -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<ReactNode> = null;

for (const field of fields) {
if (!item) {
switch (field.type) {
case 'text': {
item = [field.value];
break;
}

case 'timer': {
item = [<Timer key={0} date={field.value} timeout={1000 * 60} format={formatDistance} />];
break;
}

case 'svg-url': {
result.push(<img key={result.length} className={styles.icon} src={field.value} alt='' />);
break;
}
}
} else {
switch (field.type) {
case 'text': {
item.push(' ');
item.push(field.value);
break;
}

case 'timer': {
item.push(' ');
item.push(
<Timer
key={item.length}
date={field.value}
timeout={1000 * 60}
format={formatDistance}
/>,
);
break;
}

case 'svg-url': {
// вложенный span нужен для того чтобы объединить `display: inline-flex` и `text-overflow: ellipsis`
result.push(
<span key={result.length} className={styles.content}>
{item}
</span>,
);
item = null;
result.push(<img key={result.length} className={styles.icon} src={field.value} alt='' />);
break;
}
}
}
}

if (item) {
result.push(
<span key={result.length} className={styles.content}>
{item}
</span>,
);
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');
}

0 comments on commit 5edea0a

Please sign in to comment.