From 5e9f7adfbc60f21bdbd8f848c18a9c80d359b413 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sat, 2 May 2026 22:21:09 -0300 Subject: [PATCH 1/7] feat(atoms): add Switch types and styles Co-Authored-By: Claude Sonnet 4.6 --- .../types/IMountReanimatedStylesProps.ts | 5 ++++ .../hooks/useReanimatedStyles/types/index.ts | 1 + src/components/Atoms/Switch/styles.ts | 29 +++++++++++++++++++ .../Atoms/Switch/types/ISwitchProps.ts | 5 ++++ src/components/Atoms/Switch/types/index.ts | 1 + 5 files changed, 41 insertions(+) create mode 100644 src/components/Atoms/Switch/hooks/useReanimatedStyles/types/IMountReanimatedStylesProps.ts create mode 100644 src/components/Atoms/Switch/hooks/useReanimatedStyles/types/index.ts create mode 100644 src/components/Atoms/Switch/styles.ts create mode 100644 src/components/Atoms/Switch/types/ISwitchProps.ts create mode 100644 src/components/Atoms/Switch/types/index.ts diff --git a/src/components/Atoms/Switch/hooks/useReanimatedStyles/types/IMountReanimatedStylesProps.ts b/src/components/Atoms/Switch/hooks/useReanimatedStyles/types/IMountReanimatedStylesProps.ts new file mode 100644 index 0000000..b776861 --- /dev/null +++ b/src/components/Atoms/Switch/hooks/useReanimatedStyles/types/IMountReanimatedStylesProps.ts @@ -0,0 +1,5 @@ +export type IMountReanimatedStylesProps = { + isOn: boolean; + accentColor: string; + borderDefaultColor: string; +}; diff --git a/src/components/Atoms/Switch/hooks/useReanimatedStyles/types/index.ts b/src/components/Atoms/Switch/hooks/useReanimatedStyles/types/index.ts new file mode 100644 index 0000000..eca5fcd --- /dev/null +++ b/src/components/Atoms/Switch/hooks/useReanimatedStyles/types/index.ts @@ -0,0 +1 @@ +export * from './IMountReanimatedStylesProps'; diff --git a/src/components/Atoms/Switch/styles.ts b/src/components/Atoms/Switch/styles.ts new file mode 100644 index 0000000..8855d04 --- /dev/null +++ b/src/components/Atoms/Switch/styles.ts @@ -0,0 +1,29 @@ +import { StyleSheet } from 'react-native'; +import { useRubberDuckStore } from '../../../store'; +import { Tokens } from '../../../tokens/Tokens.class'; + +export const TRACK_WIDTH = Tokens.spacer({ key: 'xxl' }); +export const TRACK_HEIGHT = Tokens.spacer({ key: 'xl' }); +export const THUMB_SIZE = Tokens.spacer({ key: 'lg' }); +export const TRACK_PADDING = Tokens.spacer({ key: 'xxs' }); + +export const useStyles = () => { + const colors = useRubberDuckStore((s) => s.colors); + + return StyleSheet.create({ + track: { + width: TRACK_WIDTH, + height: TRACK_HEIGHT, + borderRadius: Tokens.radii({ key: 'full' }), + padding: TRACK_PADDING, + justifyContent: 'center', + }, + + thumb: { + width: THUMB_SIZE, + height: THUMB_SIZE, + borderRadius: Tokens.radii({ key: 'full' }), + backgroundColor: colors.background, + }, + }); +}; diff --git a/src/components/Atoms/Switch/types/ISwitchProps.ts b/src/components/Atoms/Switch/types/ISwitchProps.ts new file mode 100644 index 0000000..7f3cfc0 --- /dev/null +++ b/src/components/Atoms/Switch/types/ISwitchProps.ts @@ -0,0 +1,5 @@ +export interface ISwitchProps { + isOn?: boolean; + onPress: () => void; + disabled?: boolean; +} diff --git a/src/components/Atoms/Switch/types/index.ts b/src/components/Atoms/Switch/types/index.ts new file mode 100644 index 0000000..19f62c9 --- /dev/null +++ b/src/components/Atoms/Switch/types/index.ts @@ -0,0 +1 @@ +export * from './ISwitchProps'; From 3a38386400b371801962a14fea8be2b2e7289795 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sat, 2 May 2026 22:21:18 -0300 Subject: [PATCH 2/7] feat(atoms): add Switch hooks Co-Authored-By: Claude Sonnet 4.6 --- src/components/Atoms/Switch/hooks/index.ts | 2 ++ .../Switch/hooks/useReanimatedStyles/index.ts | 32 +++++++++++++++++++ .../__tests__/useSwitchViewModel.test.ts | 26 +++++++++++++++ .../Switch/hooks/useSwitchViewModel/index.ts | 15 +++++++++ 4 files changed, 75 insertions(+) create mode 100644 src/components/Atoms/Switch/hooks/index.ts create mode 100644 src/components/Atoms/Switch/hooks/useReanimatedStyles/index.ts create mode 100644 src/components/Atoms/Switch/hooks/useSwitchViewModel/__tests__/useSwitchViewModel.test.ts create mode 100644 src/components/Atoms/Switch/hooks/useSwitchViewModel/index.ts diff --git a/src/components/Atoms/Switch/hooks/index.ts b/src/components/Atoms/Switch/hooks/index.ts new file mode 100644 index 0000000..416cc49 --- /dev/null +++ b/src/components/Atoms/Switch/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useSwitchViewModel'; +export * from './useReanimatedStyles'; diff --git a/src/components/Atoms/Switch/hooks/useReanimatedStyles/index.ts b/src/components/Atoms/Switch/hooks/useReanimatedStyles/index.ts new file mode 100644 index 0000000..56771c9 --- /dev/null +++ b/src/components/Atoms/Switch/hooks/useReanimatedStyles/index.ts @@ -0,0 +1,32 @@ +import type { IMountReanimatedStylesProps } from './types'; +import { useEffect } from 'react'; +import { + interpolateColor, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; +import { TRACK_WIDTH, THUMB_SIZE, TRACK_PADDING } from '../../styles'; + +export const useReanimatedStyles = (props: IMountReanimatedStylesProps) => { + const { isOn, accentColor, borderDefaultColor } = props; + + const progress = useSharedValue(isOn ? 1 : 0); + + useEffect(() => { + progress.value = withTiming(isOn ? 1 : 0, { duration: 200 }); + }, [isOn, progress]); + + const track = useAnimatedStyle(() => ({ + backgroundColor: interpolateColor(progress.value, [0, 1], [borderDefaultColor, accentColor]), + }), [borderDefaultColor, accentColor]); + + const thumb = useAnimatedStyle(() => ({ + transform: [{ translateX: progress.value * (TRACK_WIDTH - THUMB_SIZE - 2 * TRACK_PADDING) }], + }), []); + + return { + track, + thumb, + }; +}; diff --git a/src/components/Atoms/Switch/hooks/useSwitchViewModel/__tests__/useSwitchViewModel.test.ts b/src/components/Atoms/Switch/hooks/useSwitchViewModel/__tests__/useSwitchViewModel.test.ts new file mode 100644 index 0000000..3e9b7e9 --- /dev/null +++ b/src/components/Atoms/Switch/hooks/useSwitchViewModel/__tests__/useSwitchViewModel.test.ts @@ -0,0 +1,26 @@ +import { renderHook } from '@testing-library/react-native'; +import { useSwitchViewModel } from '../index'; + +describe('useSwitchViewModel', () => { + describe('onToggle', () => { + it('Calls onPress when not disabled', () => { + const onPress = jest.fn(); + const { result } = renderHook(() => useSwitchViewModel({ onPress })); + + result.current.onToggle(); + + expect(onPress).toHaveBeenCalledTimes(1); + }); + + it('Does NOT call onPress when disabled', () => { + const onPress = jest.fn(); + const { result } = renderHook(() => + useSwitchViewModel({ onPress, disabled: true }), + ); + + result.current.onToggle(); + + expect(onPress).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/Atoms/Switch/hooks/useSwitchViewModel/index.ts b/src/components/Atoms/Switch/hooks/useSwitchViewModel/index.ts new file mode 100644 index 0000000..e345eba --- /dev/null +++ b/src/components/Atoms/Switch/hooks/useSwitchViewModel/index.ts @@ -0,0 +1,15 @@ +import type { ISwitchProps } from '../../types'; + +export const useSwitchViewModel = (props: ISwitchProps) => { + const { onPress, disabled } = props; + + function onToggle() { + if (!disabled) { + onPress(); + } + } + + return { + onToggle, + }; +}; From 0221f228ab318c2cc9348b27cbb4434c6a0165f7 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sat, 2 May 2026 22:21:27 -0300 Subject: [PATCH 3/7] feat(atoms): add Switch view and tests Co-Authored-By: Claude Sonnet 4.6 --- .../Atoms/Switch/__tests__/Switch.test.tsx | 44 +++++++++++++++++++ src/components/Atoms/Switch/index.tsx | 33 ++++++++++++++ src/components/Atoms/index.ts | 1 + 3 files changed, 78 insertions(+) create mode 100644 src/components/Atoms/Switch/__tests__/Switch.test.tsx create mode 100644 src/components/Atoms/Switch/index.tsx diff --git a/src/components/Atoms/Switch/__tests__/Switch.test.tsx b/src/components/Atoms/Switch/__tests__/Switch.test.tsx new file mode 100644 index 0000000..e82cfed --- /dev/null +++ b/src/components/Atoms/Switch/__tests__/Switch.test.tsx @@ -0,0 +1,44 @@ +import { render, screen, fireEvent } from '@testing-library/react-native'; +import { Switch } from '../index'; + +const baseProps = { + onPress: jest.fn(), +}; + +describe('Switch', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Rendering', () => { + it('Renders correctly when isOn is true', () => { + const { toJSON } = render(); + expect(toJSON()).not.toBeNull(); + }); + + it('Renders correctly when isOn is false', () => { + const { toJSON } = render(); + expect(toJSON()).not.toBeNull(); + }); + }); + + describe('Press behavior', () => { + it('Calls onPress when pressed', () => { + const onPress = jest.fn(); + render(); + + fireEvent.press(screen.getByRole('switch')); + + expect(onPress).toHaveBeenCalledTimes(1); + }); + + it('Does NOT call onPress when disabled is true', () => { + const onPress = jest.fn(); + render(); + + fireEvent.press(screen.getByRole('switch')); + + expect(onPress).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/Atoms/Switch/index.tsx b/src/components/Atoms/Switch/index.tsx new file mode 100644 index 0000000..22b25c2 --- /dev/null +++ b/src/components/Atoms/Switch/index.tsx @@ -0,0 +1,33 @@ +import type { ISwitchProps } from './types'; +import React from 'react'; +import { TouchableOpacity } from 'react-native'; +import Animated from 'react-native-reanimated'; +import { useStyles } from './styles'; +import { useRubberDuckStore } from '../../../store'; +import { useSwitchViewModel, useReanimatedStyles } from './hooks'; + +export const Switch: React.FC = (props) => { + const { isOn, disabled } = props; + + const colors = useRubberDuckStore((s) => s.colors); + const styles = useStyles(); + const reanimatedStyles = useReanimatedStyles({ + isOn: !!isOn, + accentColor: colors.accent, + borderDefaultColor: colors.borderDefault, + }); + const { onToggle } = useSwitchViewModel(props); + + return ( + + + + + + ); +}; diff --git a/src/components/Atoms/index.ts b/src/components/Atoms/index.ts index af6e204..54b60f4 100644 --- a/src/components/Atoms/index.ts +++ b/src/components/Atoms/index.ts @@ -1,3 +1,4 @@ export * from './Text'; export * from './Icon'; export * from './Loading'; +export * from './Switch'; From 0b4c4ebdb58ec8214108069010e5ce783d19351f Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sat, 2 May 2026 22:26:19 -0300 Subject: [PATCH 4/7] docs(atoms): add Switch storybook story Co-Authored-By: Claude Sonnet 4.6 --- .../stories/Atoms/Switch/Switch.stories.tsx | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 example/.rnstorybook/stories/Atoms/Switch/Switch.stories.tsx diff --git a/example/.rnstorybook/stories/Atoms/Switch/Switch.stories.tsx b/example/.rnstorybook/stories/Atoms/Switch/Switch.stories.tsx new file mode 100644 index 0000000..01bafdc --- /dev/null +++ b/example/.rnstorybook/stories/Atoms/Switch/Switch.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from '@storybook/react-native'; +import React, { useState } from 'react'; +import { Switch } from 'rubber-duck-ui'; + +const meta = { + title: 'Atoms/Switch', + component: Switch, + args: { + isOn: false, + onPress: () => {}, + }, + argTypes: { + isOn: { control: 'boolean' }, + disabled: { control: 'boolean' }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const On: Story = { + args: { + isOn: true, + }, +}; + +export const Disabled: Story = { + args: { + disabled: true, + }, +}; + +export const DisabledOn: Story = { + args: { + isOn: true, + disabled: true, + }, +}; + +const InteractiveSwitch = (args: React.ComponentProps) => { + const [isOn, setIsOn] = useState(false); + + return ( + setIsOn((prev) => !prev)} + /> + ); +}; + +export const Interactive: Story = { + render: (args) => , +}; From 824be3b85217a128884a7cd74487e061137d4b18 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sat, 2 May 2026 22:33:38 -0300 Subject: [PATCH 5/7] style(atoms): add padding and hitSlop to Switch Co-Authored-By: Claude Sonnet 4.6 --- src/components/Atoms/Switch/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Atoms/Switch/index.tsx b/src/components/Atoms/Switch/index.tsx index 22b25c2..12007be 100644 --- a/src/components/Atoms/Switch/index.tsx +++ b/src/components/Atoms/Switch/index.tsx @@ -2,7 +2,7 @@ import type { ISwitchProps } from './types'; import React from 'react'; import { TouchableOpacity } from 'react-native'; import Animated from 'react-native-reanimated'; -import { useStyles } from './styles'; +import { useStyles, TRACK_PADDING } from './styles'; import { useRubberDuckStore } from '../../../store'; import { useSwitchViewModel, useReanimatedStyles } from './hooks'; @@ -23,6 +23,8 @@ export const Switch: React.FC = (props) => { activeOpacity={1} onPress={onToggle} disabled={!!disabled} + hitSlop={TRACK_PADDING} + style={{ padding: TRACK_PADDING }} accessibilityRole="switch" accessibilityState={{ checked: !!isOn, disabled: !!disabled }}> From bda11cfb7e3848a93ff7836d4532de2e30987462 Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sat, 2 May 2026 22:35:32 -0300 Subject: [PATCH 6/7] refactor(atoms): extract Switch constants to constants/ Co-Authored-By: Claude Sonnet 4.6 --- .../Atoms/Switch/constants/SWITCH_PADDING.ts | 3 +++ .../Atoms/Switch/constants/SWITCH_THUMB_SIZE.ts | 3 +++ .../Switch/constants/SWITCH_TRACK_HEIGHT.ts | 3 +++ .../Atoms/Switch/constants/SWITCH_TRACK_WIDTH.ts | 3 +++ src/components/Atoms/Switch/constants/index.ts | 4 ++++ .../Switch/hooks/useReanimatedStyles/index.ts | 4 ++-- src/components/Atoms/Switch/index.tsx | 7 ++++--- src/components/Atoms/Switch/styles.ts | 16 ++++++---------- 8 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 src/components/Atoms/Switch/constants/SWITCH_PADDING.ts create mode 100644 src/components/Atoms/Switch/constants/SWITCH_THUMB_SIZE.ts create mode 100644 src/components/Atoms/Switch/constants/SWITCH_TRACK_HEIGHT.ts create mode 100644 src/components/Atoms/Switch/constants/SWITCH_TRACK_WIDTH.ts create mode 100644 src/components/Atoms/Switch/constants/index.ts diff --git a/src/components/Atoms/Switch/constants/SWITCH_PADDING.ts b/src/components/Atoms/Switch/constants/SWITCH_PADDING.ts new file mode 100644 index 0000000..f66ddb7 --- /dev/null +++ b/src/components/Atoms/Switch/constants/SWITCH_PADDING.ts @@ -0,0 +1,3 @@ +import { Tokens } from '../../../../tokens/Tokens.class'; + +export const SWITCH_PADDING = Tokens.spacer({ key: 'xxs' }); diff --git a/src/components/Atoms/Switch/constants/SWITCH_THUMB_SIZE.ts b/src/components/Atoms/Switch/constants/SWITCH_THUMB_SIZE.ts new file mode 100644 index 0000000..576d1eb --- /dev/null +++ b/src/components/Atoms/Switch/constants/SWITCH_THUMB_SIZE.ts @@ -0,0 +1,3 @@ +import { Tokens } from '../../../../tokens/Tokens.class'; + +export const SWITCH_THUMB_SIZE = Tokens.spacer({ key: 'lg' }); diff --git a/src/components/Atoms/Switch/constants/SWITCH_TRACK_HEIGHT.ts b/src/components/Atoms/Switch/constants/SWITCH_TRACK_HEIGHT.ts new file mode 100644 index 0000000..d99fcbc --- /dev/null +++ b/src/components/Atoms/Switch/constants/SWITCH_TRACK_HEIGHT.ts @@ -0,0 +1,3 @@ +import { Tokens } from '../../../../tokens/Tokens.class'; + +export const SWITCH_TRACK_HEIGHT = Tokens.spacer({ key: 'xl' }); diff --git a/src/components/Atoms/Switch/constants/SWITCH_TRACK_WIDTH.ts b/src/components/Atoms/Switch/constants/SWITCH_TRACK_WIDTH.ts new file mode 100644 index 0000000..854ca50 --- /dev/null +++ b/src/components/Atoms/Switch/constants/SWITCH_TRACK_WIDTH.ts @@ -0,0 +1,3 @@ +import { Tokens } from '../../../../tokens/Tokens.class'; + +export const SWITCH_TRACK_WIDTH = Tokens.spacer({ key: 'xxl' }); diff --git a/src/components/Atoms/Switch/constants/index.ts b/src/components/Atoms/Switch/constants/index.ts new file mode 100644 index 0000000..37203d5 --- /dev/null +++ b/src/components/Atoms/Switch/constants/index.ts @@ -0,0 +1,4 @@ +export * from './SWITCH_TRACK_WIDTH'; +export * from './SWITCH_TRACK_HEIGHT'; +export * from './SWITCH_THUMB_SIZE'; +export * from './SWITCH_PADDING'; diff --git a/src/components/Atoms/Switch/hooks/useReanimatedStyles/index.ts b/src/components/Atoms/Switch/hooks/useReanimatedStyles/index.ts index 56771c9..bf46d64 100644 --- a/src/components/Atoms/Switch/hooks/useReanimatedStyles/index.ts +++ b/src/components/Atoms/Switch/hooks/useReanimatedStyles/index.ts @@ -6,7 +6,7 @@ import { useSharedValue, withTiming, } from 'react-native-reanimated'; -import { TRACK_WIDTH, THUMB_SIZE, TRACK_PADDING } from '../../styles'; +import { SWITCH_TRACK_WIDTH, SWITCH_THUMB_SIZE, SWITCH_PADDING } from '../../constants'; export const useReanimatedStyles = (props: IMountReanimatedStylesProps) => { const { isOn, accentColor, borderDefaultColor } = props; @@ -22,7 +22,7 @@ export const useReanimatedStyles = (props: IMountReanimatedStylesProps) => { }), [borderDefaultColor, accentColor]); const thumb = useAnimatedStyle(() => ({ - transform: [{ translateX: progress.value * (TRACK_WIDTH - THUMB_SIZE - 2 * TRACK_PADDING) }], + transform: [{ translateX: progress.value * (SWITCH_TRACK_WIDTH - SWITCH_THUMB_SIZE - 2 * SWITCH_PADDING) }], }), []); return { diff --git a/src/components/Atoms/Switch/index.tsx b/src/components/Atoms/Switch/index.tsx index 12007be..69a665d 100644 --- a/src/components/Atoms/Switch/index.tsx +++ b/src/components/Atoms/Switch/index.tsx @@ -2,7 +2,8 @@ import type { ISwitchProps } from './types'; import React from 'react'; import { TouchableOpacity } from 'react-native'; import Animated from 'react-native-reanimated'; -import { useStyles, TRACK_PADDING } from './styles'; +import { useStyles } from './styles'; +import { SWITCH_PADDING } from './constants'; import { useRubberDuckStore } from '../../../store'; import { useSwitchViewModel, useReanimatedStyles } from './hooks'; @@ -23,8 +24,8 @@ export const Switch: React.FC = (props) => { activeOpacity={1} onPress={onToggle} disabled={!!disabled} - hitSlop={TRACK_PADDING} - style={{ padding: TRACK_PADDING }} + hitSlop={SWITCH_PADDING} + style={{ padding: SWITCH_PADDING }} accessibilityRole="switch" accessibilityState={{ checked: !!isOn, disabled: !!disabled }}> diff --git a/src/components/Atoms/Switch/styles.ts b/src/components/Atoms/Switch/styles.ts index 8855d04..a09d9e9 100644 --- a/src/components/Atoms/Switch/styles.ts +++ b/src/components/Atoms/Switch/styles.ts @@ -1,27 +1,23 @@ import { StyleSheet } from 'react-native'; import { useRubberDuckStore } from '../../../store'; import { Tokens } from '../../../tokens/Tokens.class'; - -export const TRACK_WIDTH = Tokens.spacer({ key: 'xxl' }); -export const TRACK_HEIGHT = Tokens.spacer({ key: 'xl' }); -export const THUMB_SIZE = Tokens.spacer({ key: 'lg' }); -export const TRACK_PADDING = Tokens.spacer({ key: 'xxs' }); +import { SWITCH_TRACK_WIDTH, SWITCH_TRACK_HEIGHT, SWITCH_THUMB_SIZE, SWITCH_PADDING } from './constants'; export const useStyles = () => { const colors = useRubberDuckStore((s) => s.colors); return StyleSheet.create({ track: { - width: TRACK_WIDTH, - height: TRACK_HEIGHT, + width: SWITCH_TRACK_WIDTH, + height: SWITCH_TRACK_HEIGHT, borderRadius: Tokens.radii({ key: 'full' }), - padding: TRACK_PADDING, + padding: SWITCH_PADDING, justifyContent: 'center', }, thumb: { - width: THUMB_SIZE, - height: THUMB_SIZE, + width: SWITCH_THUMB_SIZE, + height: SWITCH_THUMB_SIZE, borderRadius: Tokens.radii({ key: 'full' }), backgroundColor: colors.background, }, From 41c351e7644ebaa13f5f4ff3c7d672b79f9812ac Mon Sep 17 00:00:00 2001 From: eumaninho54 Date: Sat, 2 May 2026 22:38:51 -0300 Subject: [PATCH 7/7] docs(claude): add component patterns rules Co-Authored-By: Claude Sonnet 4.6 --- .claude/CLAUDE.md | 1 + .claude/rules/component-patterns.md | 38 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .claude/rules/component-patterns.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index e7f7ae1..a815d2e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -2,3 +2,4 @@ @rules/aiworkers/skills-format.md @rules/aiworkers/jsx-style.md @rules/aiworkers/conventional-commits.md +@rules/component-patterns.md diff --git a/.claude/rules/component-patterns.md b/.claude/rules/component-patterns.md new file mode 100644 index 0000000..8a7ddf4 --- /dev/null +++ b/.claude/rules/component-patterns.md @@ -0,0 +1,38 @@ +# Component patterns + +## Before implementing a new component + +Always read an existing component of the same category before writing any code. Do not guess the structure — read the actual code. + +- New Atom → read another existing Atom +- New Molecule → read another existing Molecule +- Component uses Reanimated → read the `useReanimatedStyles` hook of a component that already uses it (e.g. `CheckBox`) + +This ensures hooks, file structure, test patterns, and import style are consistent with the rest of the library. + +## Fixed values + +No hardcoded numeric values in code. Always use design system tokens: + +- Spacing and sizes: `Tokens.spacer({ key: '...' })` +- Border radii: `Tokens.radii({ key: '...' })` + +Component-specific layout values (e.g. a Switch track width) belong in the component's `constants/` folder — one file per constant, all exported via `index.ts`: + +``` +constants/ +├── NAME_OF_CONSTANT.ts ← one constant per file +└── index.ts ← export * from each file +``` + +Never define layout constants directly in `styles.ts` or in the component file. + +## Storybook + +Every new component needs a story at `example/.rnstorybook/stories///`. + +Minimum variants to cover: +- Default state (off / false / empty) +- Active state (on / true / filled) +- Disabled (when applicable) +- `Interactive` — with `useState` to test the real animation in Storybook