From 1ebf8b08af698eb7bf402b7f12b94a37367108da Mon Sep 17 00:00:00 2001 From: ehigodwin Date: Fri, 6 Mar 2026 16:14:22 +0100 Subject: [PATCH 1/3] feat(mobile): add themed atomic ui components for design system --- mobile/src/components/Button.tsx | 98 ++++++++++++++++++++++++++++ mobile/src/components/Card.tsx | 28 ++++++++ mobile/src/components/Input.tsx | 49 ++++++++++++++ mobile/src/components/Typography.tsx | 16 +++++ mobile/src/components/index.ts | 4 ++ mobile/src/theme/tokens.ts | 44 +++++++++++++ 6 files changed, 239 insertions(+) create mode 100644 mobile/src/components/Button.tsx create mode 100644 mobile/src/components/Card.tsx create mode 100644 mobile/src/components/Input.tsx create mode 100644 mobile/src/components/Typography.tsx create mode 100644 mobile/src/components/index.ts diff --git a/mobile/src/components/Button.tsx b/mobile/src/components/Button.tsx new file mode 100644 index 0000000..78bcb43 --- /dev/null +++ b/mobile/src/components/Button.tsx @@ -0,0 +1,98 @@ +import type { ReactNode } from "react" +import { ActivityIndicator, Pressable, type ViewStyle } from "react-native" +import { colors, radius, spacing } from "../theme/tokens" +import { Typography } from "./Typography" + +type ButtonVariant = "primary" | "secondary" | "outlined" + +type ButtonProps = { + label: string + onPress?: () => void + variant?: ButtonVariant + loading?: boolean + disabled?: boolean + leftIcon?: ReactNode + style?: ViewStyle +} + +function getContainerStyle(variant: ButtonVariant, disabled: boolean): ViewStyle { + if (variant === "outlined") { + return { + backgroundColor: colors.surface, + borderColor: disabled ? colors.disabled : colors.crypto, + borderWidth: 1.5, + } + } + + if (variant === "secondary") { + return { + backgroundColor: disabled ? colors.disabled : colors.crypto, + } + } + + return { + backgroundColor: disabled ? colors.disabled : colors.primary, + } +} + +function getLabelColor(variant: ButtonVariant, disabled: boolean): string { + if (disabled) { + return colors.surface + } + + if (variant === "outlined") { + return colors.crypto + } + + if (variant === "secondary") { + return colors.onCrypto + } + + return colors.onPrimary +} + +export function Button({ + label, + onPress, + variant = "primary", + loading = false, + disabled = false, + leftIcon, + style, +}: ButtonProps) { + const isDisabled = disabled || loading + const labelColor = getLabelColor(variant, isDisabled) + + return ( + [ + { + minHeight: 52, + borderRadius: radius.md, + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + alignItems: "center", + justifyContent: "center", + flexDirection: "row", + gap: spacing.sm, + opacity: pressed ? 0.9 : 1, + }, + getContainerStyle(variant, isDisabled), + style, + ]} + > + {loading ? ( + + ) : ( + <> + {leftIcon} + + {label} + + + )} + + ) +} diff --git a/mobile/src/components/Card.tsx b/mobile/src/components/Card.tsx new file mode 100644 index 0000000..c6230a7 --- /dev/null +++ b/mobile/src/components/Card.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from "react" +import { View, type ViewStyle } from "react-native" +import { colors, radius, shadows, spacing } from "../theme/tokens" + +type CardProps = { + children: ReactNode + style?: ViewStyle +} + +export function Card({ children, style }: CardProps) { + return ( + + {children} + + ) +} diff --git a/mobile/src/components/Input.tsx b/mobile/src/components/Input.tsx new file mode 100644 index 0000000..84af0a9 --- /dev/null +++ b/mobile/src/components/Input.tsx @@ -0,0 +1,49 @@ +import { useState } from "react" +import { TextInput, View, type TextInputProps, type ViewStyle } from "react-native" +import { colors, radius, spacing } from "../theme/tokens" +import { Typography } from "./Typography" + +type InputProps = TextInputProps & { + label?: string + error?: string + forceFocused?: boolean + containerStyle?: ViewStyle +} + +export function Input({ label, error, forceFocused = false, containerStyle, onFocus, onBlur, ...props }: InputProps) { + const [isFocused, setIsFocused] = useState(false) + const focused = forceFocused || isFocused + + return ( + + {label ? {label} : null} + { + setIsFocused(true) + onFocus?.(event) + }} + onBlur={(event) => { + setIsFocused(false) + onBlur?.(event) + }} + style={{ + minHeight: 52, + borderRadius: radius.md, + borderWidth: 1.5, + borderColor: error ? colors.error : focused ? colors.crypto : colors.border, + backgroundColor: colors.surface, + color: colors.text, + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + }} + {...props} + /> + {error ? ( + + {error} + + ) : null} + + ) +} diff --git a/mobile/src/components/Typography.tsx b/mobile/src/components/Typography.tsx new file mode 100644 index 0000000..a1e69ac --- /dev/null +++ b/mobile/src/components/Typography.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from "react" +import { Text, type TextStyle } from "react-native" +import { colors, typography } from "../theme/tokens" + +type TypographyVariant = keyof typeof typography + +type TypographyProps = { + children: ReactNode + variant?: TypographyVariant + color?: string + style?: TextStyle +} + +export function Typography({ children, variant = "body", color = colors.text, style }: TypographyProps) { + return {children} +} diff --git a/mobile/src/components/index.ts b/mobile/src/components/index.ts new file mode 100644 index 0000000..eb577a9 --- /dev/null +++ b/mobile/src/components/index.ts @@ -0,0 +1,4 @@ +export * from "./Button" +export * from "./Card" +export * from "./Input" +export * from "./Typography" diff --git a/mobile/src/theme/tokens.ts b/mobile/src/theme/tokens.ts index 259cae3..afe6561 100644 --- a/mobile/src/theme/tokens.ts +++ b/mobile/src/theme/tokens.ts @@ -5,10 +5,16 @@ export const colors = { surface: "#FFFFFF", text: "#1D1D1F", muted: "#6B7280", + border: "#E5E7EB", + success: "#16A34A", error: "#D92D20", + disabled: "#C9CDD4", + onPrimary: "#FFFFFF", + onCrypto: "#FFFFFF", } export const radius = { + xs: 10, sm: 12, md: 16, lg: 24, @@ -21,3 +27,41 @@ export const spacing = { lg: 24, xl: 32, } + +export const typography = { + h1: { + fontSize: 34, + lineHeight: 40, + fontWeight: "700" as const, + }, + h2: { + fontSize: 28, + lineHeight: 34, + fontWeight: "700" as const, + }, + h3: { + fontSize: 22, + lineHeight: 28, + fontWeight: "600" as const, + }, + body: { + fontSize: 16, + lineHeight: 22, + fontWeight: "400" as const, + }, + caption: { + fontSize: 13, + lineHeight: 18, + fontWeight: "500" as const, + }, +} + +export const shadows = { + soft: { + shadowColor: "#101828", + shadowOpacity: 0.12, + shadowRadius: 12, + shadowOffset: { width: 0, height: 6 }, + elevation: 4, + }, +} From 3345c6778a3a9565203ab16e7ffc7ebca2c3c17c Mon Sep 17 00:00:00 2001 From: ehigodwin Date: Sat, 7 Mar 2026 12:08:39 +0100 Subject: [PATCH 2/3] feat(mobile): build hidden component sandbox with typography button and input states --- mobile/app/(tabs)/sandbox.tsx | 52 +++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/mobile/app/(tabs)/sandbox.tsx b/mobile/app/(tabs)/sandbox.tsx index fe34de6..218ac51 100644 --- a/mobile/app/(tabs)/sandbox.tsx +++ b/mobile/app/(tabs)/sandbox.tsx @@ -1,10 +1,52 @@ -import { Text, View } from "react-native" +import { ScrollView, View } from "react-native" +import { Button, Card, Input, Typography } from "../../src/components" +import { colors, spacing } from "../../src/theme/tokens" export default function SandboxScreen() { return ( - - Component Sandbox - Typography, Button, Input, and Card previews will live here. - + + Component Sandbox + + + + Typography + H1 Discoverly + H2 Taste the Match + H3 Fresh picks nearby + Body copy for content and supporting descriptions. + + Caption and helper content + + + + + + + Buttons +