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 diff --git a/__mocks__/react-native-gesture-handler.ts b/__mocks__/react-native-gesture-handler.ts new file mode 100644 index 0000000..a101230 --- /dev/null +++ b/__mocks__/react-native-gesture-handler.ts @@ -0,0 +1,29 @@ +import React from 'react'; +import { View } from 'react-native'; + +const makePanGesture = () => { + const gesture = { + onUpdate: () => gesture, + onEnd: () => gesture, + onStart: () => gesture, + onBegin: () => gesture, + onFinalize: () => gesture, + enabled: () => gesture, + minDistance: () => gesture, + activeOffsetX: () => gesture, + activeOffsetY: () => gesture, + }; + return gesture; +}; + +const Gesture = { + Pan: makePanGesture, +}; + +const GestureDetector = ({ children }: { children: React.ReactNode }) => + React.createElement(View, null, children); + +module.exports = { + Gesture, + GestureDetector, +}; diff --git a/__mocks__/react-native-reanimated.ts b/__mocks__/react-native-reanimated.ts index 5c916ae..d8ff5f3 100644 --- a/__mocks__/react-native-reanimated.ts +++ b/__mocks__/react-native-reanimated.ts @@ -5,6 +5,8 @@ const useSharedValue = (init: unknown) => ({ value: init }); const useAnimatedStyle = (fn: () => object) => fn(); const withTiming = (value: unknown) => value; const withSpring = (value: unknown) => value; +const interpolate = (_value: unknown, _input: unknown, output: number[]) => + output[0]; const interpolateColor = (_value: unknown, _range: unknown, colors: string[]) => colors[0]; const runOnJS = (fn: (...args: unknown[]) => unknown) => fn; @@ -42,6 +44,7 @@ module.exports = { useAnimatedGestureHandler, withTiming, withSpring, + interpolate, interpolateColor, runOnJS, runOnUI, diff --git a/docs/migration-font-sizes-v0.3.0.md b/docs/migration-font-sizes-v0.3.0.md new file mode 100644 index 0000000..3680375 --- /dev/null +++ b/docs/migration-font-sizes-v0.3.0.md @@ -0,0 +1,72 @@ +# Migration: Font Size Scale — v0.3.0 + +## O que mudou + +A escala `FONT_SIZES` foi rebalanceada seguindo a regra dos múltiplos de 4/8, com `md` agora valendo **16px** (era 14px). + +### Escala anterior → nova + +| Key | Antes | Depois | +|-----|-------|--------| +| `xs` | 10 | 12 | +| `sm` | 12 | 14 | +| `md` | 14 | **16** | +| `lg` | 16 | 20 | +| `xl` | 20 | 24 | +| `xxl` | 24 | 32 | +| `xxxl` | 32 | 40 | +| `display` | 40 | 48 | + +--- + +## Impacto no seu app + +Se você usa o componente `` do DS **sem passar `size`**, o tamanho padrão era `md=14px` e agora passa a ser `md=16px`. Seu texto vai aparecer **maior** do que antes. + +### Como corrigir + +Passe `size` explicitamente onde quiser manter o visual anterior: + +```tsx +// Antes — recebia 14px via default +Meu texto + +// Depois — para manter 14px, passe size="sm" explicitamente +Meu texto +``` + +### Tabela de remapeamento + +Use esta tabela para converter os valores que você já passava explicitamente: + +| Você passava | Valor visual | Passe agora | +|---|---|---| +| `size="sm"` | 12px | `size="xs"` | +| `size="md"` ou nada | 14px | `size="sm"` | +| `size="lg"` | 16px | `size="md"` | +| `size="xl"` | 20px | `size="lg"` | +| `size="xxl"` | 24px | `size="xl"` | +| `size="xxxl"` | 32px | `size="xxl"` | +| `size="display"` | 40px | `size="xxxl"` | + +--- + +## O que já foi ajustado internamente no DS + +Os componentes internos do DS já foram atualizados para manter seus tamanhos visuais. Você não precisa fazer nada em relação a: + +- `Button` (todas as variantes: `sm`, `md`, `lg`) +- `CheckBox` +- `RadioButton` +- `SimpleText`, `NumberField`, `MultilineText`, `MoneyField` +- `Toaster` +- `Avatar` +- `BottomListModal` + +Esses componentes continuam com a mesma aparência de antes. + +--- + +## Se você quer aproveitar a nova escala + +Se preferir que seus textos cresçam proporcionalmente com a nova escala (ao invés de preservar o tamanho antigo), basta **não passar `size`** e deixar o novo default `md=16px` agir. 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) => , +}; diff --git a/example/.rnstorybook/stories/Atoms/Text/Text.stories.tsx b/example/.rnstorybook/stories/Atoms/Text/Text.stories.tsx index 538d7d3..418f94e 100644 --- a/example/.rnstorybook/stories/Atoms/Text/Text.stories.tsx +++ b/example/.rnstorybook/stories/Atoms/Text/Text.stories.tsx @@ -139,3 +139,20 @@ export const Truncated: Story = { 'This is a very long text that should be truncated after one line because numberOfLines is set to 1', }, }; + +export const InlineBold: Story = { + args: { + children: 'I agree to the **Terms & Privacy Policy**', + }, +}; + +export const InlineBoldVariants: Story = { + render: () => ( + + {'I agree to the **Terms & Privacy Policy**'} + {'**Bold** at the start of the sentence'} + {'Mix of **bold** and regular text here'} + {'**Multiple** bold **segments** in one text'} + + ), +}; diff --git a/example/.rnstorybook/stories/Molecules/Button/Button.stories.tsx b/example/.rnstorybook/stories/Molecules/Button/Button.stories.tsx new file mode 100644 index 0000000..838095c --- /dev/null +++ b/example/.rnstorybook/stories/Molecules/Button/Button.stories.tsx @@ -0,0 +1,116 @@ +import type { Meta, StoryObj } from '@storybook/react-native'; +import React, { useState } from 'react'; +import { View } from 'react-native'; +import { Button } from 'rubber-duck-ui'; +import { styles } from './Button.styles'; + +const meta = { + title: 'Molecules/Button', + component: Button, + args: { + label: 'Button', + variant: 'primary', + size: 'md', + disabled: false, + isLoading: false, + onPress: () => {}, + }, + argTypes: { + label: { control: 'text' }, + variant: { + control: 'select', + options: ['primary', 'secondary', 'outline', 'ghost', 'destructive'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + disabled: { control: 'boolean' }, + isLoading: { control: 'boolean' }, + leftIcon: { + control: 'select', + options: [undefined, 'ArrowLeft', 'Plus', 'Check', 'Trash2'], + }, + rightIcon: { + control: 'select', + options: [undefined, 'ArrowRight', 'Plus', 'Check', 'Trash2'], + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Variants: Story = { + render: (args) => ( + +