Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5e9f7ad
feat(atoms): add Switch types and styles
eumaninho54 May 3, 2026
3a38386
feat(atoms): add Switch hooks
eumaninho54 May 3, 2026
0221f22
feat(atoms): add Switch view and tests
eumaninho54 May 3, 2026
0b4c4eb
docs(atoms): add Switch storybook story
eumaninho54 May 3, 2026
824be3b
style(atoms): add padding and hitSlop to Switch
eumaninho54 May 3, 2026
bda11cf
refactor(atoms): extract Switch constants to constants/
eumaninho54 May 3, 2026
41c351e
docs(claude): add component patterns rules
eumaninho54 May 3, 2026
5aa7f56
Merge pull request #24 from Salve-Software/feat/switch-atom
eumaninho54 May 3, 2026
2f8a8e1
feat(atoms): add inline bold parsing to Text
eumaninho54 May 3, 2026
17ba9df
refactor(atoms): move parseInlineBold to Text library
eumaninho54 May 3, 2026
892e38f
refactor(atoms): extract Text ViewModel with memoization
eumaninho54 May 3, 2026
5ae38a6
feat(example): load Geist fonts via expo-font
eumaninho54 May 3, 2026
1bea480
feat(atoms): use semibold for Text inline bold
eumaninho54 May 3, 2026
629fd7a
docs(storybook): add Text inline bold stories
eumaninho54 May 3, 2026
eb014f5
Merge pull request #25 from Salve-Software/feat/text-inline-semibold
eumaninho54 May 3, 2026
ac7f874
feat(molecules): add Button molecule
eumaninho54 May 3, 2026
a515609
refactor(molecules): move builds to Button ViewModel
eumaninho54 May 3, 2026
da4341f
style(molecules): polish Button formatting and naming
eumaninho54 May 3, 2026
b93aa04
refactor(molecules): remove fullWidth prop from Button
eumaninho54 May 3, 2026
3261628
style(molecules): set Button alignSelf flex-start by default
eumaninho54 May 3, 2026
21751e3
docs(storybook): add Button FullWidth story
eumaninho54 May 3, 2026
ae489dc
docs(storybook): add Button Inline story and fix default width
eumaninho54 May 3, 2026
0218802
test(molecules): add useButtonViewModel tests
eumaninho54 May 3, 2026
8014ce9
Merge pull request #26 from Salve-Software/feat/molecules-button
eumaninho54 May 3, 2026
5d804be
Merge branch 'feat/molecules-button' into release/v0.3.0
eumaninho54 May 3, 2026
114725a
style(molecules): increase border radius to lg
eumaninho54 May 3, 2026
9f53ad0
feat(toaster): add ToasterApi controller and types
eumaninho54 May 4, 2026
08c333e
feat(toaster): add useToasterViewModel and reanimated styles
eumaninho54 May 4, 2026
4592b5c
feat(toaster): add Toaster component and styles
eumaninho54 May 4, 2026
a5745e3
test(toaster): add ToasterApi, ViewModel and component tests
eumaninho54 May 4, 2026
6886256
docs(storybook): add Toaster stories
eumaninho54 May 4, 2026
0d18ba9
fix(toaster): extract duration and slide offset constants
eumaninho54 May 4, 2026
88b6bdd
fix(toaster): resolve review issues in component and hooks
eumaninho54 May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
@rules/aiworkers/skills-format.md
@rules/aiworkers/jsx-style.md
@rules/aiworkers/conventional-commits.md
@rules/component-patterns.md
38 changes: 38 additions & 0 deletions .claude/rules/component-patterns.md
Original file line number Diff line number Diff line change
@@ -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/<Category>/<Component>/`.

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
3 changes: 3 additions & 0 deletions __mocks__/react-native-reanimated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +44,7 @@ module.exports = {
useAnimatedGestureHandler,
withTiming,
withSpring,
interpolate,
interpolateColor,
runOnJS,
runOnUI,
Expand Down
57 changes: 57 additions & 0 deletions example/.rnstorybook/stories/Atoms/Switch/Switch.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Switch>;

export default meta;

type Story = StoryObj<typeof meta>;

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<typeof Switch>) => {
const [isOn, setIsOn] = useState(false);

return (
<Switch
{...args}
isOn={isOn}
onPress={() => setIsOn((prev) => !prev)}
/>
);
};

export const Interactive: Story = {
render: (args) => <InteractiveSwitch {...args} />,
};
17 changes: 17 additions & 0 deletions example/.rnstorybook/stories/Atoms/Text/Text.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => (
<View style={styles.container}>
<Text>{'I agree to the **Terms & Privacy Policy**'}</Text>
<Text size="lg">{'**Bold** at the start of the sentence'}</Text>
<Text size="sm" color="textSecondary">{'Mix of **bold** and regular text here'}</Text>
<Text>{'**Multiple** bold **segments** in one text'}</Text>
</View>
),
};
116 changes: 116 additions & 0 deletions example/.rnstorybook/stories/Molecules/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Button>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const Variants: Story = {
render: (args) => (
<View style={styles.container}>
<Button {...args} variant="primary" label="Primary" />
<Button {...args} variant="secondary" label="Secondary" />
<Button {...args} variant="outline" label="Outline" />
<Button {...args} variant="ghost" label="Ghost" />
<Button {...args} variant="destructive" label="Destructive" />
</View>
),
};

export const Sizes: Story = {
render: (args) => (
<View style={styles.container}>
<Button {...args} size="sm" label="Small" />
<Button {...args} size="md" label="Medium" />
<Button {...args} size="lg" label="Large" />
</View>
),
};

export const Loading: Story = {
args: {
isLoading: true,
},
};

export const Disabled: Story = {
args: {
disabled: true,
},
};

export const WithIcons: Story = {
render: (args) => (
<View style={styles.container}>
<Button {...args} leftIcon="ArrowLeft" label="Back" />
<Button {...args} rightIcon="ArrowRight" label="Next" />
<Button {...args} leftIcon="Plus" rightIcon="ArrowRight" label="Add item" />
</View>
),
};

export const Inline: Story = {
render: (args) => (
<View style={styles.inline}>
<Button {...args} />
</View>
),
};

const InteractiveButton = (args: React.ComponentProps<typeof Button>) => {
const [loading, setLoading] = useState(false);

return (
<Button
{...args}
isLoading={loading}
onPress={() => {
setLoading(true);
setTimeout(() => setLoading(false), 2000);
}}
/>
);
};

export const Interactive: Story = {
render: (args) => <InteractiveButton {...args} />,
};
11 changes: 11 additions & 0 deletions example/.rnstorybook/stories/Molecules/Button/Button.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
gap: 16,
},

inline: {
alignSelf: 'flex-start',
},
});
99 changes: 99 additions & 0 deletions example/.rnstorybook/stories/Organisms/Toaster/Toaster.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import React, { useState } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { Toaster, ToasterApi } from 'rubber-duck-ui';
import { styles } from './Toaster.styles';

const Trigger = ({ label, onPress }: { label: string; onPress: () => void }) => (
<TouchableOpacity style={styles.trigger} onPress={onPress}>
<Text style={styles.triggerText}>{label}</Text>
</TouchableOpacity>
);

const DefaultDemo = () => (
<View style={styles.container}>
<Trigger
label="Show Success"
onPress={() =>
ToasterApi.show({
type: 'success',
title: 'Success!',
description: 'Your action was completed.',
})
}
/>
<Trigger
label="Show Error"
onPress={() =>
ToasterApi.show({
type: 'error',
title: 'Error!',
description: 'Something went wrong.',
})
}
/>
<Trigger
label="Show Warning"
onPress={() =>
ToasterApi.show({
type: 'warning',
title: 'Warning!',
description: 'Please check your input.',
})
}
/>
<Trigger
label="Show Info"
onPress={() =>
ToasterApi.show({
type: 'info',
title: 'Info',
description: 'Here is some information.',
})
}
/>
<Toaster />
</View>
);

type ToasterType = 'success' | 'error' | 'warning' | 'info';

const TYPES: ToasterType[] = ['success', 'error', 'warning', 'info'];

const InteractiveDemo = () => {
const [currentIndex, setCurrentIndex] = useState(0);

const showNext = () => {
const type = TYPES[currentIndex % TYPES.length];
ToasterApi.show({
type,
title: `${type.charAt(0).toUpperCase()}${type.slice(1)} toast`,
description: `This is a ${type} notification.`,
});
setCurrentIndex((prev) => (prev + 1) % TYPES.length);
};

return (
<View style={styles.container}>
<Trigger label="Cycle through types" onPress={showNext} />
<Toaster />
</View>
);
};

const meta = {
title: 'Organisms/Toaster',
component: View,
} satisfies Meta<typeof View>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: () => <DefaultDemo />,
};

export const Interactive: Story = {
render: () => <InteractiveDemo />,
};
24 changes: 24 additions & 0 deletions example/.rnstorybook/stories/Organisms/Toaster/Toaster.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
padding: 24,
},
trigger: {
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 8,
backgroundColor: '#FFD600',
alignItems: 'center',
width: '100%',
},
triggerText: {
color: '#000',
fontWeight: '600',
fontSize: 14,
},
});
Loading
Loading