From ba8a533c5c4ccfc55fca5ff5b071a7cb5a379e63 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sun, 17 May 2026 20:42:53 -0300 Subject: [PATCH 1/7] feat(tokens): add isDark color utility Determine if a color is dark based on relative luminance using WCAG standard. Co-Authored-By: Claude Haiku 4.5 --- src/tokens/Tokens.class.ts | 6 +++++- src/tokens/colors/Colors.class.ts | 18 +++++++++++++++++- src/tokens/colors/types/index.ts | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/tokens/Tokens.class.ts b/src/tokens/Tokens.class.ts index 26cb762..03dd645 100644 --- a/src/tokens/Tokens.class.ts +++ b/src/tokens/Tokens.class.ts @@ -1,5 +1,5 @@ import type { TextStyle } from 'react-native'; -import type { IColorProps } from './colors'; +import type { IColorProps, IIsDarkProps } from './colors'; import type { IRadiiProps, ISpacerProps } from './spacer'; import type { FamilyKey, IFamilyProps, ISizeProps, IWeightProps } from './typography'; import { Typography } from './typography'; @@ -11,6 +11,10 @@ export class Tokens { return Colors.color(props); }; + static isDark = (props: IIsDarkProps) => { + return Colors.isDark(props); + } + static spacer = (props: ISpacerProps) => { return Spacer.spacer(props); }; diff --git a/src/tokens/colors/Colors.class.ts b/src/tokens/colors/Colors.class.ts index 453b9f1..c8da8d1 100644 --- a/src/tokens/colors/Colors.class.ts +++ b/src/tokens/colors/Colors.class.ts @@ -1,4 +1,4 @@ -import type { IColorProps } from './types'; +import type { IColorProps, IIsDarkProps } from './types'; import { useRubberDuckStore } from '../../store'; export class Colors { @@ -7,4 +7,20 @@ export class Colors { const currentColors = useRubberDuckStore.getState().colors; return currentColors[key]; }; + + static isDark = (props: IIsDarkProps): boolean => { + const { color } = props; + + const hex = color.replace('#', ''); + const r = parseInt(hex.substring(0, 2), 16) / 255; + const g = parseInt(hex.substring(2, 4), 16) / 255; + const b = parseInt(hex.substring(4, 6), 16) / 255; + + const toLinear = (c: number) => + c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); + + const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b); + + return luminance < 0.179; + } } diff --git a/src/tokens/colors/types/index.ts b/src/tokens/colors/types/index.ts index 9fd60cc..1a070e5 100644 --- a/src/tokens/colors/types/index.ts +++ b/src/tokens/colors/types/index.ts @@ -1,2 +1,3 @@ export type * from './IColors'; export type * from './IColorProps'; +export type * from './IIsDarkProps'; From 91137479bffdb3e9179da007f1c51b25e392b289 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sun, 17 May 2026 20:42:57 -0300 Subject: [PATCH 2/7] feat(ui): add PillsGroup molecule component Export new PillsGroup component from Molecules. Co-Authored-By: Claude Haiku 4.5 --- src/components/Molecules/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Molecules/index.ts b/src/components/Molecules/index.ts index 5e5613a..a3c222f 100644 --- a/src/components/Molecules/index.ts +++ b/src/components/Molecules/index.ts @@ -2,4 +2,5 @@ export * from './Button'; export * from './CheckBox'; export * from './RadioButton'; export * from './Avatar'; +export * from './PillsGroup'; export * from './Inputs'; From ffe056349b83f359b509104aee63a338bcfa2421 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sun, 17 May 2026 20:43:01 -0300 Subject: [PATCH 3/7] build(deps): add @legendapp/list dependency Co-Authored-By: Claude Haiku 4.5 --- package.json | 1 + yarn.lock | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/package.json b/package.json index e907485..372b69d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "author": "eumaninho54 (https://github.com/eumaninho54)", "license": "MIT", "dependencies": { + "@legendapp/list": "^2.0.19", "lucide-react-native": "^0.577.0", "zustand": "^5.0.11" }, diff --git a/yarn.lock b/yarn.lock index 828b6eb..04d9a01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2983,6 +2983,18 @@ __metadata: languageName: node linkType: hard +"@legendapp/list@npm:^2.0.19": + version: 2.0.19 + resolution: "@legendapp/list@npm:2.0.19" + dependencies: + use-sync-external-store: "npm:^1.5.0" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/8201cd3ebf59bb885b8289e0e65df3134a3d59f9b7fce9b30884a50fab8b268ed601dec742bfa48beeb4fec45fdba8d69af56bf1e16e5afbdce0e1db7a1c56d8 + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -14153,6 +14165,7 @@ __metadata: "@eslint/eslintrc": "npm:^3.3.1" "@eslint/js": "npm:^9.35.0" "@gorhom/bottom-sheet": "npm:^5.1.0" + "@legendapp/list": "npm:^2.0.19" "@react-native/babel-preset": "npm:0.83.0" "@react-native/eslint-config": "npm:0.83.0" "@semantic-release/changelog": "npm:^6.0.3" From ed4a4d7bcf1960fcdd128e586dc48f3375f8c55a Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sun, 17 May 2026 20:43:36 -0300 Subject: [PATCH 4/7] feat(tokens): add IIsDarkProps type Co-Authored-By: Claude Haiku 4.5 --- src/tokens/colors/types/IIsDarkProps.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/tokens/colors/types/IIsDarkProps.ts diff --git a/src/tokens/colors/types/IIsDarkProps.ts b/src/tokens/colors/types/IIsDarkProps.ts new file mode 100644 index 0000000..19233b2 --- /dev/null +++ b/src/tokens/colors/types/IIsDarkProps.ts @@ -0,0 +1,3 @@ +export interface IIsDarkProps { + color: string; +} From d2658bbbd6c3dd5b0796d2ef3223615041061cb7 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sun, 17 May 2026 20:43:42 -0300 Subject: [PATCH 5/7] feat(ui): add PillsGroup component implementation Co-Authored-By: Claude Haiku 4.5 --- .../PillsGroup/components/Pill/hooks/index.ts | 2 + .../Pill/hooks/usePillViewModel/index.ts | 20 +++++++++ .../Pill/hooks/useReanimatedStyles/index.ts | 27 ++++++++++++ .../types/IUseReanimatedStylesProps.ts | 3 ++ .../hooks/useReanimatedStyles/types/index.ts | 1 + .../PillsGroup/components/Pill/index.tsx | 30 +++++++++++++ .../PillsGroup/components/Pill/styles.ts | 14 ++++++ .../PillsGroup/components/Pill/types/IPill.ts | 4 ++ .../components/Pill/types/IPillProps.ts | 7 +++ .../PillsGroup/components/Pill/types/index.ts | 2 + .../Molecules/PillsGroup/components/index.ts | 1 + .../Molecules/PillsGroup/hooks/index.ts | 1 + .../hooks/usePillsGroupViewModel/index.ts | 10 +++++ src/components/Molecules/PillsGroup/index.tsx | 43 +++++++++++++++++++ src/components/Molecules/PillsGroup/styles.ts | 15 +++++++ .../PillsGroup/types/IPillsGroupProps.ts | 7 +++ .../Molecules/PillsGroup/types/index.ts | 1 + 17 files changed, 188 insertions(+) create mode 100644 src/components/Molecules/PillsGroup/components/Pill/hooks/index.ts create mode 100644 src/components/Molecules/PillsGroup/components/Pill/hooks/usePillViewModel/index.ts create mode 100644 src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/index.ts create mode 100644 src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/types/IUseReanimatedStylesProps.ts create mode 100644 src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/types/index.ts create mode 100644 src/components/Molecules/PillsGroup/components/Pill/index.tsx create mode 100644 src/components/Molecules/PillsGroup/components/Pill/styles.ts create mode 100644 src/components/Molecules/PillsGroup/components/Pill/types/IPill.ts create mode 100644 src/components/Molecules/PillsGroup/components/Pill/types/IPillProps.ts create mode 100644 src/components/Molecules/PillsGroup/components/Pill/types/index.ts create mode 100644 src/components/Molecules/PillsGroup/components/index.ts create mode 100644 src/components/Molecules/PillsGroup/hooks/index.ts create mode 100644 src/components/Molecules/PillsGroup/hooks/usePillsGroupViewModel/index.ts create mode 100644 src/components/Molecules/PillsGroup/index.tsx create mode 100644 src/components/Molecules/PillsGroup/styles.ts create mode 100644 src/components/Molecules/PillsGroup/types/IPillsGroupProps.ts create mode 100644 src/components/Molecules/PillsGroup/types/index.ts diff --git a/src/components/Molecules/PillsGroup/components/Pill/hooks/index.ts b/src/components/Molecules/PillsGroup/components/Pill/hooks/index.ts new file mode 100644 index 0000000..71aecba --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/hooks/index.ts @@ -0,0 +1,2 @@ +export { useReanimatedStyles } from './useReanimatedStyles'; +export { usePillViewModel } from './usePillViewModel'; diff --git a/src/components/Molecules/PillsGroup/components/Pill/hooks/usePillViewModel/index.ts b/src/components/Molecules/PillsGroup/components/Pill/hooks/usePillViewModel/index.ts new file mode 100644 index 0000000..9e834d1 --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/hooks/usePillViewModel/index.ts @@ -0,0 +1,20 @@ +import type { IPillProps } from '../../types'; +import type { IColors } from '../../../../../../../tokens'; +import { useRubberDuckStore } from '../../../../../../../store'; +import { Tokens } from '../../../../../../../tokens/Tokens.class'; + +export const usePillViewModel = (props: IPillProps) => { + const { isSelected, pill, onPress } = props; + + const colors = useRubberDuckStore((s) => s.colors); + + const textColor: keyof IColors = isSelected + ? Tokens.isDark({ color: colors.accent }) ? 'white' : 'black' + : 'textPrimary'; + + return { + textColor, + title: pill.title, + onPress: () => onPress(pill.id), + }; +}; diff --git a/src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/index.ts b/src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/index.ts new file mode 100644 index 0000000..ecd00ba --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/index.ts @@ -0,0 +1,27 @@ +import type { IUseReanimatedStylesProps } from './types'; +import { useEffect } from 'react'; +import { + interpolateColor, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; +import { useRubberDuckStore } from '../../../../../../../store'; + +export const useReanimatedStyles = (props: IUseReanimatedStylesProps) => { + const { isSelected } = props; + + const colors = useRubberDuckStore((s) => s.colors); + const progress = useSharedValue(isSelected ? 1 : 0); + + useEffect(() => { + progress.value = withTiming(isSelected ? 1 : 0, { duration: 200 }); + }, [isSelected, progress]); + + const wrapper = useAnimatedStyle(() => ({ + backgroundColor: interpolateColor(progress.value, [0, 1], [colors.surface, colors.accent]), + borderColor: interpolateColor(progress.value, [0, 1], [colors.surfaceOverlay, colors.accentMuted]), + }), [colors]); + + return { wrapper }; +}; diff --git a/src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/types/IUseReanimatedStylesProps.ts b/src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/types/IUseReanimatedStylesProps.ts new file mode 100644 index 0000000..39e4f85 --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/types/IUseReanimatedStylesProps.ts @@ -0,0 +1,3 @@ +export type IUseReanimatedStylesProps = { + isSelected: boolean; +}; diff --git a/src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/types/index.ts b/src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/types/index.ts new file mode 100644 index 0000000..436b465 --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/hooks/useReanimatedStyles/types/index.ts @@ -0,0 +1 @@ +export type { IUseReanimatedStylesProps } from './IUseReanimatedStylesProps'; diff --git a/src/components/Molecules/PillsGroup/components/Pill/index.tsx b/src/components/Molecules/PillsGroup/components/Pill/index.tsx new file mode 100644 index 0000000..9935d96 --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/index.tsx @@ -0,0 +1,30 @@ +import type { IPillProps } from "./types"; +import Animated from "react-native-reanimated"; +import { TouchableOpacity } from "react-native"; +import { Text } from "../../../../Atoms"; +import { useStyles } from "./styles"; +import { useReanimatedStyles, usePillViewModel } from "./hooks"; + +const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity); + +export const Pill: React.FC = (props) => { + const { isSelected } = props; + + const { textColor, title, onPress } = usePillViewModel(props); + + const styles = useStyles(); + const reanimatedStyles = useReanimatedStyles({ isSelected }); + + return ( + + + {title} + + + ) +} diff --git a/src/components/Molecules/PillsGroup/components/Pill/styles.ts b/src/components/Molecules/PillsGroup/components/Pill/styles.ts new file mode 100644 index 0000000..1fb7da6 --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/styles.ts @@ -0,0 +1,14 @@ +import { StyleSheet } from 'react-native'; +import { Tokens } from '../../../../../tokens/Tokens.class'; + +export const useStyles = () => { + return StyleSheet.create({ + wrapper: { + borderRadius: Tokens.radii({ key: 'full' }), + borderWidth: 1, + padding: Tokens.spacer({ key: 'md' }), + alignItems: 'center', + justifyContent: 'center', + }, + }); +}; diff --git a/src/components/Molecules/PillsGroup/components/Pill/types/IPill.ts b/src/components/Molecules/PillsGroup/components/Pill/types/IPill.ts new file mode 100644 index 0000000..a1db870 --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/types/IPill.ts @@ -0,0 +1,4 @@ +export interface IPill { + id: string; + title: string; +} \ No newline at end of file diff --git a/src/components/Molecules/PillsGroup/components/Pill/types/IPillProps.ts b/src/components/Molecules/PillsGroup/components/Pill/types/IPillProps.ts new file mode 100644 index 0000000..8300bea --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/types/IPillProps.ts @@ -0,0 +1,7 @@ +import type { IPill } from "./IPill"; + +export interface IPillProps { + pill: IPill; + isSelected: boolean; + onPress: (id: string) => void; +} diff --git a/src/components/Molecules/PillsGroup/components/Pill/types/index.ts b/src/components/Molecules/PillsGroup/components/Pill/types/index.ts new file mode 100644 index 0000000..65678d8 --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/types/index.ts @@ -0,0 +1,2 @@ +export type * from './IPillProps'; +export type * from './IPill'; diff --git a/src/components/Molecules/PillsGroup/components/index.ts b/src/components/Molecules/PillsGroup/components/index.ts new file mode 100644 index 0000000..c32b085 --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/index.ts @@ -0,0 +1 @@ +export * from './Pill'; diff --git a/src/components/Molecules/PillsGroup/hooks/index.ts b/src/components/Molecules/PillsGroup/hooks/index.ts new file mode 100644 index 0000000..e80eb5a --- /dev/null +++ b/src/components/Molecules/PillsGroup/hooks/index.ts @@ -0,0 +1 @@ +export * from './usePillsGroupViewModel'; diff --git a/src/components/Molecules/PillsGroup/hooks/usePillsGroupViewModel/index.ts b/src/components/Molecules/PillsGroup/hooks/usePillsGroupViewModel/index.ts new file mode 100644 index 0000000..8e77c2c --- /dev/null +++ b/src/components/Molecules/PillsGroup/hooks/usePillsGroupViewModel/index.ts @@ -0,0 +1,10 @@ +import type { IPillsGroupProps } from '../../types'; + +export const usePillsGroupViewModel = (props: IPillsGroupProps) => { + const { pills, idSelected } = props; + + const selectedIndex = pills.findIndex((pill) => pill.id === idSelected); + const initialScrollIndex = selectedIndex > 0 ? selectedIndex : undefined; + + return { initialScrollIndex }; +}; diff --git a/src/components/Molecules/PillsGroup/index.tsx b/src/components/Molecules/PillsGroup/index.tsx new file mode 100644 index 0000000..74d270c --- /dev/null +++ b/src/components/Molecules/PillsGroup/index.tsx @@ -0,0 +1,43 @@ +import type { IPillsGroupProps } from "./types"; +import type { IPill } from "./components/Pill/types"; +import { View } from "react-native"; +import { LegendList, type LegendListRenderItemProps } from "@legendapp/list"; +import { useStyles } from "./styles"; +import { Pill } from "./components"; +import { usePillsGroupViewModel } from "./hooks"; + +export const PillsGroup: React.FC = (props) => { + const { pills, idSelected, onPress } = props; + + const styles = useStyles(); + const { initialScrollIndex } = usePillsGroupViewModel(props); + + const renderItem = ({ item }: LegendListRenderItemProps) => { + return ( + + ) + } + + const itemSeparator = () => { + return + } + + return ( + item.id} + extraData={idSelected} + initialScrollIndex={initialScrollIndex && initialScrollIndex > 0 ? initialScrollIndex : undefined} + showsHorizontalScrollIndicator={false} + horizontal + recycleItems + /> + ) +} diff --git a/src/components/Molecules/PillsGroup/styles.ts b/src/components/Molecules/PillsGroup/styles.ts new file mode 100644 index 0000000..fb52e3b --- /dev/null +++ b/src/components/Molecules/PillsGroup/styles.ts @@ -0,0 +1,15 @@ +import { StyleSheet } from 'react-native'; +import { Tokens } from '../../../tokens/Tokens.class'; + +export const useStyles = () => { + return StyleSheet.create({ + separator: { + width: Tokens.spacer({ key: 'xs' }), + }, + + contentContainer: { + paddingVertical: Tokens.spacer({ key: 'md' }), + paddingHorizontal: Tokens.spacer({ key: 'md' }), + }, + }); +}; diff --git a/src/components/Molecules/PillsGroup/types/IPillsGroupProps.ts b/src/components/Molecules/PillsGroup/types/IPillsGroupProps.ts new file mode 100644 index 0000000..bdd8223 --- /dev/null +++ b/src/components/Molecules/PillsGroup/types/IPillsGroupProps.ts @@ -0,0 +1,7 @@ +import type { IPill } from "../components/Pill/types"; + +export interface IPillsGroupProps { + pills: IPill[]; + idSelected: string; + onPress: (id: string) => void; +} diff --git a/src/components/Molecules/PillsGroup/types/index.ts b/src/components/Molecules/PillsGroup/types/index.ts new file mode 100644 index 0000000..dbf00c2 --- /dev/null +++ b/src/components/Molecules/PillsGroup/types/index.ts @@ -0,0 +1 @@ +export type * from './IPillsGroupProps'; From 0032718b8af80ba57b5d774354d0ffe1a68c3275 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sun, 17 May 2026 20:43:47 -0300 Subject: [PATCH 6/7] feat(storybook): add PillsGroup stories Co-Authored-By: Claude Haiku 4.5 --- .../PillsGroup/PillsGroup.stories.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 example/.rnstorybook/stories/Molecules/PillsGroup/PillsGroup.stories.tsx diff --git a/example/.rnstorybook/stories/Molecules/PillsGroup/PillsGroup.stories.tsx b/example/.rnstorybook/stories/Molecules/PillsGroup/PillsGroup.stories.tsx new file mode 100644 index 0000000..a04210b --- /dev/null +++ b/example/.rnstorybook/stories/Molecules/PillsGroup/PillsGroup.stories.tsx @@ -0,0 +1,69 @@ +import type { Meta, StoryObj } from '@storybook/react-native'; +import React, { useState } from 'react'; +import { PillsGroup } from 'rubber-duck-ui'; + +const pills = [ + { id: '1', title: 'Todos' }, + { id: '2', title: 'Pendentes' }, + { id: '3', title: 'Aprovados' }, + { id: '4', title: 'Recusados' }, +]; + +const meta = { + title: 'Molecules/PillsGroup', + component: PillsGroup, + args: { + pills, + idSelected: '1', + onPress: () => {}, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const NoneSelected: Story = { + args: { + idSelected: '', + }, +}; + +const ManyPills = Array.from({ length: 12 }, (_, i) => ({ + id: String(i + 1), + title: `Opção ${i + 1}`, +})); + +export const Scrollable: Story = { + args: { + pills: ManyPills, + idSelected: '1', + }, +}; + +export const ScrollableLastSelected: Story = { + args: { + pills: ManyPills, + idSelected: String(ManyPills.length), + }, +}; + +const InteractivePillsGroup = ( + args: React.ComponentProps, +) => { + const [selected, setSelected] = useState(args.idSelected); + + return ( + + ); +}; + +export const Interactive: Story = { + render: (args) => , +}; From 221ef681ec9a4154d2cca9bfc766169f1a63114d Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sun, 17 May 2026 20:47:59 -0300 Subject: [PATCH 7/7] test(ui): add unit tests for PillsGroup and Colors.isDark Co-Authored-By: Claude Haiku 4.5 --- .../PillsGroup/__tests__/PillsGroup.test.tsx | 58 +++++++++++++++++++ .../components/Pill/__tests__/Pill.test.tsx | 52 +++++++++++++++++ .../__tests__/usePillViewModel.test.ts | 48 +++++++++++++++ .../__tests__/usePillsGroupViewModel.test.ts | 40 +++++++++++++ .../colors/__tests__/Colors.isDark.test.ts | 31 ++++++++++ 5 files changed, 229 insertions(+) create mode 100644 src/components/Molecules/PillsGroup/__tests__/PillsGroup.test.tsx create mode 100644 src/components/Molecules/PillsGroup/components/Pill/__tests__/Pill.test.tsx create mode 100644 src/components/Molecules/PillsGroup/components/Pill/hooks/usePillViewModel/__tests__/usePillViewModel.test.ts create mode 100644 src/components/Molecules/PillsGroup/hooks/usePillsGroupViewModel/__tests__/usePillsGroupViewModel.test.ts create mode 100644 src/tokens/colors/__tests__/Colors.isDark.test.ts diff --git a/src/components/Molecules/PillsGroup/__tests__/PillsGroup.test.tsx b/src/components/Molecules/PillsGroup/__tests__/PillsGroup.test.tsx new file mode 100644 index 0000000..cf45abb --- /dev/null +++ b/src/components/Molecules/PillsGroup/__tests__/PillsGroup.test.tsx @@ -0,0 +1,58 @@ +import { render, screen, fireEvent } from '@testing-library/react-native'; +import { PillsGroup } from '../index'; + +jest.mock('@legendapp/list', () => { + const React = require('react'); + const { View } = require('react-native'); + return { + LegendList: ({ data, renderItem, extraData }: any) => + React.createElement( + View, + null, + data.map((item: any, index: number) => + React.cloneElement(renderItem({ item, index, data, extraData }), { key: item.id ?? index }), + ), + ), + }; +}); + +const pills = [ + { id: '1', title: 'Todos' }, + { id: '2', title: 'Pendentes' }, + { id: '3', title: 'Aprovados' }, +]; + +describe('PillsGroup', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Render', () => { + it('Renders all pill titles', () => { + render( + , + ); + expect(screen.getByText('Todos')).toBeTruthy(); + expect(screen.getByText('Pendentes')).toBeTruthy(); + expect(screen.getByText('Aprovados')).toBeTruthy(); + }); + + it('Renders without crashing with an empty list', () => { + const { toJSON } = render( + , + ); + expect(toJSON()).not.toBeNull(); + }); + }); + + describe('Press behavior', () => { + it('Calls onPress with the id of the pressed pill', () => { + const onPress = jest.fn(); + render(); + + fireEvent.press(screen.getByText('Pendentes')); + + expect(onPress).toHaveBeenCalledWith('2'); + }); + }); +}); diff --git a/src/components/Molecules/PillsGroup/components/Pill/__tests__/Pill.test.tsx b/src/components/Molecules/PillsGroup/components/Pill/__tests__/Pill.test.tsx new file mode 100644 index 0000000..5b7e912 --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/__tests__/Pill.test.tsx @@ -0,0 +1,52 @@ +import { render, screen, fireEvent } from '@testing-library/react-native'; +import { Pill } from '../index'; + +const pill = { id: '42', title: 'Aprovados' }; + +describe('Pill', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Render', () => { + it('Renders the pill title', () => { + render(); + expect(screen.getByText('Aprovados')).toBeTruthy(); + }); + + it('Renders without crashing when selected', () => { + const { toJSON } = render( + , + ); + expect(toJSON()).not.toBeNull(); + }); + + it('Renders without crashing when not selected', () => { + const { toJSON } = render( + , + ); + expect(toJSON()).not.toBeNull(); + }); + }); + + describe('Press behavior', () => { + it('Calls onPress with the pill id when pressed', () => { + const onPress = jest.fn(); + render(); + + fireEvent.press(screen.getByText('Aprovados')); + + expect(onPress).toHaveBeenCalledWith('42'); + }); + + it('Calls onPress every time the pill is pressed', () => { + const onPress = jest.fn(); + render(); + + fireEvent.press(screen.getByText('Aprovados')); + fireEvent.press(screen.getByText('Aprovados')); + + expect(onPress).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/src/components/Molecules/PillsGroup/components/Pill/hooks/usePillViewModel/__tests__/usePillViewModel.test.ts b/src/components/Molecules/PillsGroup/components/Pill/hooks/usePillViewModel/__tests__/usePillViewModel.test.ts new file mode 100644 index 0000000..7ce8b1c --- /dev/null +++ b/src/components/Molecules/PillsGroup/components/Pill/hooks/usePillViewModel/__tests__/usePillViewModel.test.ts @@ -0,0 +1,48 @@ +import { renderHook } from '@testing-library/react-native'; +import { usePillViewModel } from '../index'; + +const pill = { id: '1', title: 'Todos' }; +const onPress = jest.fn(); + +describe('usePillViewModel', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('textColor', () => { + it('Returns black when selected and accent is a light color', () => { + const { result } = renderHook(() => + usePillViewModel({ pill, isSelected: true, onPress }), + ); + expect(result.current.textColor).toBe('black'); + }); + + it('Returns textPrimary when not selected', () => { + const { result } = renderHook(() => + usePillViewModel({ pill, isSelected: false, onPress }), + ); + expect(result.current.textColor).toBe('textPrimary'); + }); + }); + + describe('title', () => { + it('Returns the pill title', () => { + const { result } = renderHook(() => + usePillViewModel({ pill, isSelected: false, onPress }), + ); + expect(result.current.title).toBe('Todos'); + }); + }); + + describe('onPress', () => { + it('Calls onPress with the pill id when invoked', () => { + const { result } = renderHook(() => + usePillViewModel({ pill, isSelected: false, onPress }), + ); + + result.current.onPress(); + + expect(onPress).toHaveBeenCalledWith('1'); + }); + }); +}); diff --git a/src/components/Molecules/PillsGroup/hooks/usePillsGroupViewModel/__tests__/usePillsGroupViewModel.test.ts b/src/components/Molecules/PillsGroup/hooks/usePillsGroupViewModel/__tests__/usePillsGroupViewModel.test.ts new file mode 100644 index 0000000..9c6c5c1 --- /dev/null +++ b/src/components/Molecules/PillsGroup/hooks/usePillsGroupViewModel/__tests__/usePillsGroupViewModel.test.ts @@ -0,0 +1,40 @@ +import { renderHook } from '@testing-library/react-native'; +import { usePillsGroupViewModel } from '../index'; + +const pills = [ + { id: '1', title: 'Todos' }, + { id: '2', title: 'Pendentes' }, + { id: '3', title: 'Aprovados' }, +]; + +describe('usePillsGroupViewModel', () => { + describe('initialScrollIndex', () => { + it('Is undefined when the first pill is selected', () => { + const { result } = renderHook(() => + usePillsGroupViewModel({ pills, idSelected: '1', onPress: jest.fn() }), + ); + expect(result.current.initialScrollIndex).toBeUndefined(); + }); + + it('Is undefined when idSelected does not match any pill', () => { + const { result } = renderHook(() => + usePillsGroupViewModel({ pills, idSelected: 'none', onPress: jest.fn() }), + ); + expect(result.current.initialScrollIndex).toBeUndefined(); + }); + + it('Returns the correct index when a middle pill is selected', () => { + const { result } = renderHook(() => + usePillsGroupViewModel({ pills, idSelected: '2', onPress: jest.fn() }), + ); + expect(result.current.initialScrollIndex).toBe(1); + }); + + it('Returns the correct index when the last pill is selected', () => { + const { result } = renderHook(() => + usePillsGroupViewModel({ pills, idSelected: '3', onPress: jest.fn() }), + ); + expect(result.current.initialScrollIndex).toBe(2); + }); + }); +}); diff --git a/src/tokens/colors/__tests__/Colors.isDark.test.ts b/src/tokens/colors/__tests__/Colors.isDark.test.ts new file mode 100644 index 0000000..929cba3 --- /dev/null +++ b/src/tokens/colors/__tests__/Colors.isDark.test.ts @@ -0,0 +1,31 @@ +import { Colors } from '../Colors.class'; + +describe('Colors.isDark', () => { + describe('Dark colors', () => { + it('Returns true for black', () => { + expect(Colors.isDark({ color: '#000000' })).toBe(true); + }); + + it('Returns true for near-black', () => { + expect(Colors.isDark({ color: '#0A0A0A' })).toBe(true); + }); + + it('Returns true for dark navy', () => { + expect(Colors.isDark({ color: '#1C1C1E' })).toBe(true); + }); + }); + + describe('Light colors', () => { + it('Returns false for white', () => { + expect(Colors.isDark({ color: '#FFFFFF' })).toBe(false); + }); + + it('Returns false for near-white', () => { + expect(Colors.isDark({ color: '#F5F5F5' })).toBe(false); + }); + + it('Returns false for the accent yellow (#FFD600)', () => { + expect(Colors.isDark({ color: '#FFD600' })).toBe(false); + }); + }); +});