diff --git a/AGENTS.md b/AGENTS.md index e1dd105d..7c8d51a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,9 @@ Do not load every skill preemptively; use the one matching the request. controlling prop changes. - In new source and test files, use `UpperPascalCase` `testID` values. Existing IDs are compatibility-sensitive; do not flag or rename them only for format. +- Story files containing JSX must use the `.stories.tsx` extension and JSX. Do + not use `createElement` to keep them as `.stories.ts`; reserve `.stories.ts` + for stories without JSX. - A new public component or material variant normally needs typed source, a barrel export, focused tests, and representative Storybook states. diff --git a/design-tokens/input/tokens.json b/design-tokens/input/tokens.json index 4f26c240..0b157a6d 100644 --- a/design-tokens/input/tokens.json +++ b/design-tokens/input/tokens.json @@ -1331,14 +1331,14 @@ }, "avatar": { "extend": { - "borderColor": "{color.border.neutral.inverse}", + "borderColor": "{color.border.neutral.strong}", "circle": { "borderRadius": "{dimension.borderRadius.max}" } }, "root": { "width": "{dimension.size.900}", "height": "{dimension.size.900}", "fontSize": "{fonts.fontSize.300}", - "color": "{color.fg.default}", + "color": "{color.fg.on.brand.default}", "background": "{color.bg.brand.strong.default}", "borderRadius": "{dimension.borderRadius.400}" }, @@ -1427,7 +1427,7 @@ "color": "{color.fg.default}" }, "success": { - "background": "{color.bg.brand.strong.active}", + "background": "{color.bg.status.success.strong.default}", "color": "{color.fg.on.status.success.default}" }, "warn": { @@ -2146,7 +2146,7 @@ "icon": { "size": "{dimension.size.450}", "color": "{color.fg.active}", - "checkedColor": "{color.fg.inverse.default}", + "checkedColor": "{color.fg.on.fill.default}", "checkedHoverColor": "{color.fg.on.fill.hover}", "disabledColor": "{color.fg.muted}", "sm": { "size": "{dimension.size.350}" }, @@ -2679,10 +2679,10 @@ "backdrop": "{color.bg.surface.backdrop}" }, "root": { - "background": "{color.bg.surface.default.default}", + "background": "{color.bg.surface.raised.default}", "borderColor": "{color.border.neutral.default}", "color": "{color.fg.default}", - "borderRadius": "{dimension.borderRadius.500}", + "borderRadius": "{dimension.borderRadius.max}", "shadow": "{effects.elevation.400}" }, "header": { @@ -2716,7 +2716,7 @@ }, "content": { "background": "{color.bg.surface.default.default}", - "color": "{color.fg.muted}" + "color": "{color.fg.default}" }, "root": { "borderColor": "{color.border.neutral.default}" } }, @@ -4444,7 +4444,7 @@ }, "root": { "transitionDuration": "{effects.transition.duration.200}" }, "tablist": { - "borderWidth": "{dimension.borderWidth.none} {dimension.borderWidth.none} {dimension.borderWidth.200} {dimension.borderWidth.none}", + "borderWidth": "{dimension.borderWidth.none} {dimension.borderWidth.none} {dimension.borderWidth.100} {dimension.borderWidth.none}", "background": "transparent", "borderColor": "{color.border.neutral.default}" }, @@ -5077,11 +5077,11 @@ "checkedHoverBackground": "{color.bg.neutral.strong.hover}" }, "handle": { - "background": "{color.fg.on.fill.default}", - "hoverBackground": "{color.fg.on.fill.default}", + "background": "{color.bg.surface.raised.default}", + "hoverBackground": "{color.bg.surface.raised.default}", "disabledBackground": "{color.bg.neutral.medium.strong}", - "checkedBackground": "{color.fg.inverse.active}", - "checkedHoverBackground": "{color.fg.inverse.active}", + "checkedBackground": "{color.bg.surface.raised.default}", + "checkedHoverBackground": "{color.bg.surface.raised.default}", "color": "{color.fg.default}", "hoverColor": "{color.fg.default}", "checkedColor": "{color.fg.default}", @@ -5090,22 +5090,22 @@ }, "dark": { "handle": { - "background": "{color.fg.on.fill.default}", - "checkedBackground": "{color.fg.inverse.active}", + "background": "{color.bg.surface.raised.default}", + "checkedBackground": "{color.bg.surface.raised.default}", "checkedColor": "{color.fg.inverse.focus}", - "checkedHoverBackground": "{color.fg.inverse.active}", + "checkedHoverBackground": "{color.bg.surface.raised.default}", "checkedHoverColor": "{color.fg.on.brand.hover}", "color": "{color.fg.inverse.default}", - "disabledBackground": "{color.bg.neutral.medium.strong}", - "hoverBackground": "{color.fg.on.fill.default}", + "disabledBackground": "{color.bg.surface.raised.active}", + "hoverBackground": "{color.bg.surface.raised.default}", "hoverColor": "{color.fg.on.brand.hover}" }, "root": { "background": "{color.bg.neutral.medium.active}", - "checkedBackground": "{color.bg.neutral.strong.default}", - "checkedHoverBackground": "{color.bg.neutral.strong.hover}", - "disabledBackground": "{color.bg.surface.canvas.hover}", - "hoverBackground": "{color.bg.neutral.medium.strong}" + "checkedBackground": "{color.bg.surface.canvas.default}", + "checkedHoverBackground": "{color.bg.surface.canvas.hover}", + "disabledBackground": "{color.bg.neutral.strong.hover}", + "hoverBackground": "{color.bg.surface.raised.active}" } } }, diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx index e04b763f..64b220bf 100644 --- a/src/components/Accordion/Accordion.tsx +++ b/src/components/Accordion/Accordion.tsx @@ -39,6 +39,7 @@ export interface AccordionProps extends ViewProps { * @param withSeparator - Наличие разделитель. Когда true, верхняя граница заголовка становится разделителем * @param disabled - Отключенное состояние. Когда true компонент рендерится в соответствии со значением isInitiallyExpanded и не реагирует на нажатия * @link https://www.figma.com/design/4TYeki0MDLhfPGJstbIicf/UI-kit-PrimeFace--DS-?node-id=1207-1852&m=dev + * @link https://www.figma.com/design/Q1BWgZ7zoV5UzlBOnjW0cM/UI-Kit--DS--v2.1?node-id=24043-1592 */ export const Accordion: React.FC = ({ Icon, diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index 2c479799..9f04c386 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -122,6 +122,7 @@ const ICON_MULTIPLIER = 0.43 * @param badge - Компонент бейджа * @param showBadge - Показывать бейдж или нет * @link https://www.figma.com/design/4TYeki0MDLhfPGJstbIicf/UI-kit-PrimeFace-(DS)?node-id=484-4972&m=dev + * @link https://www.figma.com/design/Q1BWgZ7zoV5UzlBOnjW0cM/UI-Kit--DS--v2.1?node-id=24043-14161 */ export const Avatar = memo( ({ diff --git a/src/components/Badge/Badge.stories.ts b/src/components/Badge/Badge.stories.ts deleted file mode 100644 index c5b7857c..00000000 --- a/src/components/Badge/Badge.stories.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react' - -import { Badge } from './Badge' - -const meta: Meta = { - title: 'Misc/Badge', - component: Badge, - args: { severity: 'basic', dot: false, children: 'Test' }, - argTypes: { - severity: { - control: 'radio', - options: ['basic', 'info', 'success', 'warning', 'danger'], - }, - }, -} - -export default meta - -type Story = StoryObj - -const BadgeStory: Story = { args: { children: 'Test' }, argTypes: {} } - -export { BadgeStory as Badge } diff --git a/src/components/Badge/Badge.stories.tsx b/src/components/Badge/Badge.stories.tsx new file mode 100644 index 00000000..e6111955 --- /dev/null +++ b/src/components/Badge/Badge.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { StyleSheet, View } from 'react-native' + +import { Body } from '../Typography' + +import { Badge } from './Badge' + +const styles = StyleSheet.create({ + container: { gap: 16 }, + example: { gap: 8 }, +}) + +const meta: Meta = { + title: 'Misc/Badge', + component: Badge, + args: { children: 'Badge', severity: 'basic', size: 'base' }, + argTypes: { + children: { control: 'text' }, + severity: { + control: 'radio', + options: ['basic', 'info', 'success', 'warning', 'danger'], + }, + size: { control: 'radio', options: ['base', 'large', 'xlarge'] }, + }, + parameters: { controls: { exclude: ['dot'] } }, + render: ({ children = 'Badge', severity, size }) => ( + + + Severity + + {children} + + + + Dot + + + + ), +} + +export default meta + +type Story = StoryObj + +const BadgeStory: Story = {} + +export { BadgeStory as Badge } diff --git a/src/components/Badge/Badge.tsx b/src/components/Badge/Badge.tsx index 2cdf0052..eac73773 100644 --- a/src/components/Badge/Badge.tsx +++ b/src/components/Badge/Badge.tsx @@ -22,6 +22,11 @@ export interface BadgeBase * @default 'basic' */ severity?: BadgeSeverity + /** + * Выбор размера компонента + * @default 'base' + */ + size?: 'base' | 'large' | 'xlarge' /** Дополнительная стилизация для контейнера компонента */ style?: StyleProp } @@ -46,13 +51,22 @@ export type BadgeProps = BadgeText | BadgeDot * Компонент Badge * @param children - Текст внутри бейджа * @param dot - Отображать бейдж в форме точки - * @param severiy - Выбор варианта стиля компонента + * @param severity - Выбор варианта стиля компонента + * @param size - Выбор размера компонента * @param style - Дополнительная стилизация для контейнера компонента - * @link https://www.figma.com/design/4TYeki0MDLhfPGJstbIicf/UI-kit-PrimeFace-(DS)?node-id=484-4871&m=dev + * @link https://www.figma.com/design/Q1BWgZ7zoV5UzlBOnjW0cM/UI-Kit--DS--v2.1?node-id=24043-13668 */ export const Badge = memo( - ({ children, dot, severity = 'basic', style, testID, ...rest }) => { - badgeStyles.useVariants({ severity }) + ({ + children, + dot, + severity = 'basic', + size = 'base', + style, + testID, + ...rest + }) => { + badgeStyles.useVariants({ severity, size }) const [textLayout, setTextLayout] = useState() const onTextLayout = useCallback((e: LayoutChangeEvent) => { @@ -68,10 +82,7 @@ export const Badge = memo( /> ) : ( <> - + ({ dotShape: { width: components.badge.dot.size, height: components.badge.dot.size, - borderRadius: components.badge.root.borderRadius, + borderRadius: semantic.dimension.borderRadius.max, + borderWidth: components.overlaybadge.root.outline.width, + borderColor: components.overlaybadge.root.outline.color, + variants: { + size: { + base: {}, + large: { + width: components.badge.extend.extDot.lg.size, + height: components.badge.extend.extDot.lg.size, + }, + xlarge: { + width: components.badge.extend.extDot.xlg.size, + height: components.badge.extend.extDot.xlg.size, + }, + }, + }, }, textBadgeContainer: { height: components.badge.root.height, + minWidth: components.badge.root.minWidth, paddingHorizontal: components.badge.root.padding, justifyContent: 'center', - borderRadius: semantic.dimension.borderRadius.max, + borderRadius: components.badge.root.borderRadius, + borderWidth: components.overlaybadge.root.outline.width, + borderColor: components.overlaybadge.root.outline.color, + variants: { + severity: { + basic: { + backgroundColor: components.badge.colorScheme.primary.background, + }, + info: { backgroundColor: components.badge.colorScheme.info.background }, + success: { + backgroundColor: components.badge.colorScheme.success.background, + }, + warning: { + backgroundColor: components.badge.colorScheme.warn.background, + }, + danger: { + backgroundColor: components.badge.colorScheme.danger.background, + }, + }, + size: { + base: {}, + large: { + height: components.badge.lg.height, + minWidth: components.badge.lg.minWidth, + }, + xlarge: { + height: components.badge.xl.height, + minWidth: components.badge.xl.minWidth, + }, + }, + }, }, textBadge: { color: components.badge.colorScheme.primary.color, fontSize: components.badge.root.fontSize, + fontWeight: fonts.fontWeight.regular, + lineHeight: fonts.lineHeight[100], + letterSpacing: fonts.letterSpacing[500], includeFontPadding: false, verticalAlign: 'middle', fontFamily: fonts.fontFamily.heading, + variants: { + severity: { + basic: { color: components.badge.colorScheme.primary.color }, + info: { color: components.badge.colorScheme.info.color }, + success: { color: components.badge.colorScheme.success.color }, + warning: { color: components.badge.colorScheme.warn.color }, + danger: { color: components.badge.colorScheme.danger.color }, + }, + size: { + base: {}, + large: {}, + xlarge: { lineHeight: fonts.lineHeight[350] }, + }, + }, }, hiddenContainer: { width: Dimensions.get('window').width, diff --git a/src/components/Badge/__tests__/Badge.test.tsx b/src/components/Badge/__tests__/Badge.test.tsx index 0fcac425..8f3e605a 100644 --- a/src/components/Badge/__tests__/Badge.test.tsx +++ b/src/components/Badge/__tests__/Badge.test.tsx @@ -1,29 +1,53 @@ import { render } from '@testing-library/react-native' -import { Badge, type BadgeProps } from '../Badge' - -describe('Badge component tests', () => { - const snapshotCases: Array<[string, BadgeProps]> = [ - ['dot, severity: basic', { dot: true, severity: 'basic' }], - ['dot, severity: info', { dot: true, severity: 'info' }], - ['dot, severity: success', { dot: true, severity: 'success' }], - ['dot, severity: warning', { dot: true, severity: 'warning' }], - ['dot, severity: danger', { dot: true, severity: 'danger' }], - ['severity: basic', { children: '12', severity: 'basic' }], - ['severity: basic', { children: '12', severity: 'info' }], - ['severity: basic', { children: '12', severity: 'success' }], - ['severity: basic', { children: '12', severity: 'warning' }], - ['severity: basic', { children: '12', severity: 'danger' }], - ['severity: default', { children: '12' }], - [ - 'with custom style', - { dot: true, severity: 'basic', style: { margin: 10 } }, - ], +import { Badge, type BadgeSeverity } from '../Badge' + +describe('Badge', () => { + const severities: BadgeSeverity[] = [ + 'basic', + 'info', + 'success', + 'warning', + 'danger', ] - test.each(snapshotCases)('%s', (_, props) => { - const renderedBadge = render() + test.each(severities)( + 'отображает переданный текст для severity %s', + (severity) => { + const { getByText } = render(12) + + expect(getByText('12')).toBeOnTheScreen() + } + ) - expect(renderedBadge.toJSON()).toMatchSnapshot() + test.each(severities)('отображает dot для severity %s', (severity) => { + const { getByTestId, queryByText } = render( + + ) + + expect(getByTestId('Badge')).toBeOnTheScreen() + expect(queryByText('12')).not.toBeOnTheScreen() }) + + test.each(['large', 'xlarge'] as const)( + 'отображает переданный текст для размера %s', + (size) => { + const { getByText } = render( + + 12 + + ) + + expect(getByText('12')).toBeOnTheScreen() + } + ) + + test.each(['large', 'xlarge'] as const)( + 'отображает dot-вариант для размера %s', + (size) => { + const { getByTestId } = render() + + expect(getByTestId('Badge')).toBeOnTheScreen() + } + ) }) diff --git a/src/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap b/src/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap deleted file mode 100644 index c728269d..00000000 --- a/src/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap +++ /dev/null @@ -1,657 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Badge component tests dot, severity: basic 1`] = ` - - - -`; - -exports[`Badge component tests dot, severity: danger 1`] = ` - - - -`; - -exports[`Badge component tests dot, severity: info 1`] = ` - - - -`; - -exports[`Badge component tests dot, severity: success 1`] = ` - - - -`; - -exports[`Badge component tests dot, severity: warning 1`] = ` - - - -`; - -exports[`Badge component tests severity: basic 1`] = ` - - - - 12 - - - - - - 12 - - - - -`; - -exports[`Badge component tests severity: basic 2`] = ` - - - - 12 - - - - - - 12 - - - - -`; - -exports[`Badge component tests severity: basic 3`] = ` - - - - 12 - - - - - - 12 - - - - -`; - -exports[`Badge component tests severity: basic 4`] = ` - - - - 12 - - - - - - 12 - - - - -`; - -exports[`Badge component tests severity: basic 5`] = ` - - - - 12 - - - - - - 12 - - - - -`; - -exports[`Badge component tests severity: default 1`] = ` - - - - 12 - - - - - - 12 - - - - -`; - -exports[`Badge component tests with custom style 1`] = ` - - - -`; diff --git a/src/components/Button/Button.stories.ts b/src/components/Button/Button.stories.tsx similarity index 53% rename from src/components/Button/Button.stories.ts rename to src/components/Button/Button.stories.tsx index 7c3d9298..24dac296 100644 --- a/src/components/Button/Button.stories.ts +++ b/src/components/Button/Button.stories.tsx @@ -3,10 +3,18 @@ import { IconArrowDownRight, IconArrowDownLeft, } from '@tabler/icons-react-native' +import { StyleSheet, View } from 'react-native' + +import { Body } from '../Typography' import { Button } from './Button' +import type { ButtonBaseVariant, ButtonProps } from './types' const Icons = { IconArrowDownRight, IconArrowDownLeft, undefined } +const styles = StyleSheet.create({ + container: { gap: 16 }, + example: { gap: 8 }, +}) const meta: Meta = { title: 'Button', @@ -31,13 +39,41 @@ const meta: Meta = { disabled: { control: 'boolean' }, iconPosition: { control: 'radio', options: ['prefix', 'postfix'] }, onPress: { action: 'OnPress' }, - iconOnly: { - control: 'radio', - options: ['IconOnly', 'Not IconOnly'], - mapping: { IconOnly: true, 'Not IconOnly': undefined }, - }, Icon: { control: 'select', options: Object.keys(Icons), mapping: Icons }, }, + parameters: { controls: { exclude: ['iconOnly'] } }, + render: ({ + iconOnly: _iconOnly, + Icon, + iconPosition, + label = 'Button', + ...args + }) => { + const buttonProps: ButtonProps = { + ...args, + Icon, + iconPosition, + label, + } + const iconOnlyButtonProps: ButtonProps = { + ...args, + iconOnly: true, + Icon: Icon ?? IconArrowDownRight, + } + + return ( + + + С текстом +