Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
52 changes: 47 additions & 5 deletions mobile/app/(tabs)/sandbox.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: 24 }}>
<Text style={{ fontSize: 24, fontWeight: "700" }}>Component Sandbox</Text>
<Text style={{ marginTop: 8 }}>Typography, Button, Input, and Card previews will live here.</Text>
</View>
<ScrollView
contentContainerStyle={{
padding: spacing.lg,
paddingBottom: spacing.xl,
backgroundColor: colors.background,
gap: spacing.lg,
}}
>
<Typography variant="h1">Component Sandbox</Typography>

<Card>
<View style={{ gap: spacing.sm }}>
<Typography variant="h2">Typography</Typography>
<Typography variant="h1">H1 Discoverly</Typography>
<Typography variant="h2">H2 Taste the Match</Typography>
<Typography variant="h3">H3 Fresh picks nearby</Typography>
<Typography variant="body">Body copy for content and supporting descriptions.</Typography>
<Typography variant="caption" color={colors.muted}>
Caption and helper content
</Typography>
</View>
</Card>

<Card>
<View style={{ gap: spacing.sm }}>
<Typography variant="h2">Buttons</Typography>
<Button label="Primary Button" />
<Button label="Secondary Button" variant="secondary" />
<Button label="Outlined Button" variant="outlined" />
<Button label="Loading Button" loading />
<Button label="Disabled Button" disabled />
</View>
</Card>

<Card>
<View style={{ gap: spacing.sm }}>
<Typography variant="h2">Inputs</Typography>
<Input label="Empty Input" placeholder="Enter email" />
<Input label="Focused Input" placeholder="Focused state" forceFocused />
<Input label="Filled Input" value="hello@discoverly.app" editable={false} />
<Input label="Error Input" placeholder="name@domain.com" error="Invalid email address" />
</View>
</Card>
</ScrollView>
)
}
98 changes: 98 additions & 0 deletions mobile/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -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 variant === "outlined" ? colors.muted : 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 (
<Pressable
onPress={onPress}
disabled={isDisabled}
style={({ pressed }) => [
{
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 ? (
<ActivityIndicator color={labelColor} />
) : (
<>
{leftIcon}
<Typography variant="body" color={labelColor} style={{ fontWeight: "700" }}>
{label}
</Typography>
</>
)}
</Pressable>
)
}
28 changes: 28 additions & 0 deletions mobile/src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View
style={[
{
borderRadius: radius.md,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
padding: spacing.md,
...shadows.soft,
},
style,
]}
>
{children}
</View>
)
}
49 changes: 49 additions & 0 deletions mobile/src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View style={[{ width: "100%", gap: spacing.xs }, containerStyle]}>
{label ? <Typography variant="caption">{label}</Typography> : null}
<TextInput
placeholderTextColor={colors.muted}
onFocus={(event) => {
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 ? (
<Typography variant="caption" color={colors.error}>
{error}
</Typography>
) : null}
</View>
)
}
16 changes: 16 additions & 0 deletions mobile/src/components/Typography.tsx
Original file line number Diff line number Diff line change
@@ -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 <Text style={[typography[variant], { color }, style]}>{children}</Text>
}
4 changes: 4 additions & 0 deletions mobile/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./Button"
export * from "./Card"
export * from "./Input"
export * from "./Typography"
44 changes: 44 additions & 0 deletions mobile/src/theme/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
},
}
Loading