summaryrefslogtreecommitdiff
path: root/app/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/components')
-rw-r--r--app/components/auto-image/auto-image.story.tsx31
-rw-r--r--app/components/auto-image/auto-image.tsx46
-rw-r--r--app/components/bullet-item/bullet-item.tsx41
-rw-r--r--app/components/button/button.presets.ts54
-rw-r--r--app/components/button/button.props.ts35
-rw-r--r--app/components/button/button.story.tsx33
-rw-r--r--app/components/button/button.tsx36
-rw-r--r--app/components/checkbox/checkbox.props.ts44
-rw-r--r--app/components/checkbox/checkbox.story.tsx121
-rw-r--r--app/components/checkbox/checkbox.tsx53
-rw-r--r--app/components/form-row/form-row.presets.ts71
-rw-r--r--app/components/form-row/form-row.props.tsx23
-rw-r--r--app/components/form-row/form-row.story.tsx107
-rw-r--r--app/components/form-row/form-row.tsx13
-rw-r--r--app/components/header/header.props.ts45
-rw-r--r--app/components/header/header.story.tsx43
-rw-r--r--app/components/header/header.tsx61
-rw-r--r--app/components/icon/icon.props.ts21
-rw-r--r--app/components/icon/icon.story.tsx19
-rw-r--r--app/components/icon/icon.tsx19
-rw-r--r--app/components/icon/icons/arrow-left.pngbin0 -> 329 bytes
-rw-r--r--app/components/icon/icons/[email protected]bin0 -> 329 bytes
-rw-r--r--app/components/icon/icons/bullet.pngbin0 -> 204 bytes
-rw-r--r--app/components/icon/icons/[email protected]bin0 -> 204 bytes
-rw-r--r--app/components/icon/icons/index.ts6
-rw-r--r--app/components/index.ts12
-rw-r--r--app/components/screen/screen.presets.ts66
-rw-r--r--app/components/screen/screen.props.ts46
-rw-r--r--app/components/screen/screen.tsx66
-rw-r--r--app/components/switch/switch.props.ts39
-rw-r--r--app/components/switch/switch.story.tsx110
-rw-r--r--app/components/switch/switch.tsx114
-rw-r--r--app/components/text-field/text-field.story.tsx159
-rw-r--r--app/components/text-field/text-field.tsx98
-rw-r--r--app/components/text/text.presets.ts48
-rw-r--r--app/components/text/text.props.ts37
-rw-r--r--app/components/text/text.story.tsx92
-rw-r--r--app/components/text/text.tsx28
-rw-r--r--app/components/wallpaper/bg.pngbin0 -> 56176 bytes
-rw-r--r--app/components/wallpaper/[email protected]bin0 -> 203224 bytes
-rw-r--r--app/components/wallpaper/wallpaper.presets.ts34
-rw-r--r--app/components/wallpaper/wallpaper.props.ts19
-rw-r--r--app/components/wallpaper/wallpaper.story.tsx16
-rw-r--r--app/components/wallpaper/wallpaper.tsx25
44 files changed, 1931 insertions, 0 deletions
diff --git a/app/components/auto-image/auto-image.story.tsx b/app/components/auto-image/auto-image.story.tsx
new file mode 100644
index 0000000..f7ecc86
--- /dev/null
+++ b/app/components/auto-image/auto-image.story.tsx
@@ -0,0 +1,31 @@
+/* eslint-disable */
+import * as React from "react"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { AutoImage } from "./auto-image"
+
+declare let module
+
+const bowser = require("../../screens/welcome/bowser.png")
+const morty = { uri: "https://rickandmortyapi.com/api/character/avatar/2.jpeg" }
+
+storiesOf("AutoImage", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Style Presets", () => (
+ <Story>
+ <UseCase text="With require()">
+ <AutoImage source={bowser} />
+ <AutoImage source={bowser} style={{ width: 150 }} />
+ <AutoImage source={bowser} style={{ width: 150, height: 150 }} />
+ <AutoImage source={bowser} style={{ height: 150 }} />
+ <AutoImage source={bowser} style={{ height: 150, resizeMode: "contain" }} />
+ </UseCase>
+ <UseCase text="With URL">
+ <AutoImage source={morty} />
+ <AutoImage source={morty} style={{ width: 150 }} />
+ <AutoImage source={morty} style={{ width: 150, height: 150 }} />
+ <AutoImage source={morty} style={{ height: 150 }} />
+ <AutoImage source={morty} style={{ height: 150, resizeMode: "contain" }} />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/auto-image/auto-image.tsx b/app/components/auto-image/auto-image.tsx
new file mode 100644
index 0000000..39d71ca
--- /dev/null
+++ b/app/components/auto-image/auto-image.tsx
@@ -0,0 +1,46 @@
+import React, { useLayoutEffect, useState } from "react"
+import {
+ Image as RNImage,
+ ImageProps as DefaultImageProps,
+ ImageURISource,
+ Platform,
+} from "react-native"
+
+type ImageProps = DefaultImageProps & {
+ source: ImageURISource
+}
+
+/**
+ * An Image wrapper component that autosizes itself to the size of the actual image.
+ * You can always override by passing a width and height in the style.
+ * If passing only one of width/height this image component will use the actual
+ * size of the other dimension.
+ *
+ * This component isn't required, but is provided as a convenience so that
+ * we don't have to remember to explicitly set image sizes on every image instance.
+ *
+ * To use as a stand-in replacement import { AutoImage as Image } and remove the
+ * Image import from react-native. Now all images in that file are handled by this
+ * component and are web-ready if not explicitly sized in the style property.
+ */
+export function AutoImage(props: ImageProps) {
+ const [imageSize, setImageSize] = useState({ width: 0, height: 0 })
+
+ useLayoutEffect(() => {
+ if (props.source?.uri) {
+ RNImage.getSize(props.source.uri as any, (width, height) => {
+ setImageSize({ width, height })
+ })
+ } else if (Platform.OS === "web") {
+ // web requires a different method to get it's size
+ RNImage.getSize(props.source as any, (width, height) => {
+ setImageSize({ width, height })
+ })
+ } else {
+ const { width, height } = RNImage.resolveAssetSource(props.source)
+ setImageSize({ width, height })
+ }
+ }, [])
+
+ return <RNImage {...props} style={[imageSize, props.style]} />
+}
diff --git a/app/components/bullet-item/bullet-item.tsx b/app/components/bullet-item/bullet-item.tsx
new file mode 100644
index 0000000..d999e10
--- /dev/null
+++ b/app/components/bullet-item/bullet-item.tsx
@@ -0,0 +1,41 @@
+import * as React from "react"
+import { View, ViewStyle, ImageStyle, TextStyle } from "react-native"
+import { Text } from "../text/text"
+import { Icon } from "../icon/icon"
+import { spacing, typography } from "../../theme"
+
+const BULLET_ITEM: ViewStyle = {
+ flexDirection: "row",
+ marginTop: spacing[4],
+ paddingBottom: spacing[4],
+ borderBottomWidth: 1,
+ borderBottomColor: "#3A3048",
+}
+const BULLET_CONTAINER: ViewStyle = {
+ marginRight: spacing[4] - 1,
+ marginTop: spacing[2],
+}
+const BULLET: ImageStyle = {
+ width: 8,
+ height: 8,
+}
+const BULLET_TEXT: TextStyle = {
+ flex: 1,
+ fontFamily: typography.primary,
+ color: "#BAB6C8",
+ fontSize: 15,
+ lineHeight: 22,
+}
+
+export interface BulletItemProps {
+ text: string
+}
+
+export function BulletItem(props: BulletItemProps) {
+ return (
+ <View style={BULLET_ITEM}>
+ <Icon icon="bullet" containerStyle={BULLET_CONTAINER} style={BULLET} />
+ <Text style={BULLET_TEXT} text={props.text} />
+ </View>
+ )
+}
diff --git a/app/components/button/button.presets.ts b/app/components/button/button.presets.ts
new file mode 100644
index 0000000..b140fd2
--- /dev/null
+++ b/app/components/button/button.presets.ts
@@ -0,0 +1,54 @@
+import { ViewStyle, TextStyle } from "react-native"
+import { color, spacing } from "../../theme"
+
+/**
+ * All text will start off looking like this.
+ */
+const BASE_VIEW: ViewStyle = {
+ paddingVertical: spacing[2],
+ paddingHorizontal: spacing[2],
+ borderRadius: 4,
+ justifyContent: "center",
+ alignItems: "center",
+}
+
+const BASE_TEXT: TextStyle = {
+ paddingHorizontal: spacing[3],
+}
+
+/**
+ * All the variations of text styling within the app.
+ *
+ * You want to customize these to whatever you need in your app.
+ */
+export const viewPresets: Record<string, ViewStyle> = {
+ /**
+ * A smaller piece of secondard information.
+ */
+ primary: { ...BASE_VIEW, backgroundColor: color.palette.orange } as ViewStyle,
+
+ /**
+ * A button without extras.
+ */
+ link: {
+ ...BASE_VIEW,
+ paddingHorizontal: 0,
+ paddingVertical: 0,
+ alignItems: "flex-start",
+ } as ViewStyle,
+}
+
+export const textPresets: Record<ButtonPresetNames, TextStyle> = {
+ primary: { ...BASE_TEXT, fontSize: 9, color: color.palette.white } as TextStyle,
+ link: {
+ ...BASE_TEXT,
+ color: color.text,
+ paddingHorizontal: 0,
+ paddingVertical: 0,
+ } as TextStyle,
+}
+
+/**
+ * A list of preset names.
+ */
+export type ButtonPresetNames = keyof typeof viewPresets
diff --git a/app/components/button/button.props.ts b/app/components/button/button.props.ts
new file mode 100644
index 0000000..1377a7e
--- /dev/null
+++ b/app/components/button/button.props.ts
@@ -0,0 +1,35 @@
+import { StyleProp, TextStyle, TouchableOpacityProps, ViewStyle } from "react-native"
+import { ButtonPresetNames } from "./button.presets"
+import { TxKeyPath } from "../../i18n"
+
+export interface ButtonProps extends TouchableOpacityProps {
+ /**
+ * Text which is looked up via i18n.
+ */
+ tx?: TxKeyPath
+
+ /**
+ * The text to display if not using `tx` or nested components.
+ */
+ text?: string
+
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<ViewStyle>
+
+ /**
+ * An optional style override useful for the button text.
+ */
+ textStyle?: StyleProp<TextStyle>
+
+ /**
+ * One of the different types of text presets.
+ */
+ preset?: ButtonPresetNames
+
+ /**
+ * One of the different types of text presets.
+ */
+ children?: React.ReactNode
+}
diff --git a/app/components/button/button.story.tsx b/app/components/button/button.story.tsx
new file mode 100644
index 0000000..4861772
--- /dev/null
+++ b/app/components/button/button.story.tsx
@@ -0,0 +1,33 @@
+import * as React from "react"
+import { ViewStyle, TextStyle, Alert } from "react-native"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { Button } from "./button"
+
+declare let module
+
+const buttonStyleArray: ViewStyle[] = [{ paddingVertical: 100 }, { borderRadius: 0 }]
+
+const buttonTextStyleArray: TextStyle[] = [{ fontSize: 20 }, { color: "#a511dc" }]
+
+storiesOf("Button", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Style Presets", () => (
+ <Story>
+ <UseCase text="Primary" usage="The primary button.">
+ <Button text="Click It" preset="primary" onPress={() => Alert.alert("pressed")} />
+ </UseCase>
+ <UseCase text="Disabled" usage="The disabled behaviour of the primary button.">
+ <Button text="Click It" preset="primary" onPress={() => Alert.alert("pressed")} disabled />
+ </UseCase>
+ <UseCase text="Array Style" usage="Button with array style">
+ <Button
+ text="Click It"
+ preset="primary"
+ onPress={() => Alert.alert("pressed")}
+ style={buttonStyleArray}
+ textStyle={buttonTextStyleArray}
+ />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/button/button.tsx b/app/components/button/button.tsx
new file mode 100644
index 0000000..a5662ff
--- /dev/null
+++ b/app/components/button/button.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { TouchableOpacity } from "react-native"
+import { Text } from "../text/text"
+import { viewPresets, textPresets } from "./button.presets"
+import { ButtonProps } from "./button.props"
+
+/**
+ * For your text displaying needs.
+ *
+ * This component is a HOC over the built-in React Native one.
+ */
+export function Button(props: ButtonProps) {
+ // grab the props
+ const {
+ preset = "primary",
+ tx,
+ text,
+ style: styleOverride,
+ textStyle: textStyleOverride,
+ children,
+ ...rest
+ } = props
+
+ const viewStyle = viewPresets[preset] || viewPresets.primary
+ const viewStyles = [viewStyle, styleOverride]
+ const textStyle = textPresets[preset] || textPresets.primary
+ const textStyles = [textStyle, textStyleOverride]
+
+ const content = children || <Text tx={tx} text={text} style={textStyles} />
+
+ return (
+ <TouchableOpacity style={viewStyles} {...rest}>
+ {content}
+ </TouchableOpacity>
+ )
+}
diff --git a/app/components/checkbox/checkbox.props.ts b/app/components/checkbox/checkbox.props.ts
new file mode 100644
index 0000000..01a13d0
--- /dev/null
+++ b/app/components/checkbox/checkbox.props.ts
@@ -0,0 +1,44 @@
+import { StyleProp, ViewStyle } from "react-native"
+import { TxKeyPath } from "../../i18n"
+
+export interface CheckboxProps {
+ /**
+ * Additional container style. Useful for margins.
+ */
+ style?: StyleProp<ViewStyle>
+
+ /**
+ * Additional outline style.
+ */
+ outlineStyle?: StyleProp<ViewStyle>
+
+ /**
+ * Additional fill style. Only visible when checked.
+ */
+ fillStyle?: StyleProp<ViewStyle>
+
+ /**
+ * Is the checkbox checked?
+ */
+ value?: boolean
+
+ /**
+ * The text to display if there isn't a tx.
+ */
+ text?: string
+
+ /**
+ * The i18n lookup key.
+ */
+ tx?: TxKeyPath
+
+ /**
+ * Multiline or clipped single line?
+ */
+ multiline?: boolean
+
+ /**
+ * Fires when the user tabs to change the value.
+ */
+ onToggle?: (newValue: boolean) => void
+}
diff --git a/app/components/checkbox/checkbox.story.tsx b/app/components/checkbox/checkbox.story.tsx
new file mode 100644
index 0000000..f8d80d8
--- /dev/null
+++ b/app/components/checkbox/checkbox.story.tsx
@@ -0,0 +1,121 @@
+/* eslint-disable react-native/no-inline-styles */
+/* eslint-disable react-native/no-color-literals */
+
+import * as React from "react"
+import { View, ViewStyle } from "react-native"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { Checkbox } from "./checkbox"
+import { Toggle } from "react-powerplug"
+
+declare let module
+
+const arrayStyle: ViewStyle[] = [{ paddingVertical: 40 }, { alignSelf: "flex-end" }]
+const arrayOutlineStyle: ViewStyle[] = [{ borderColor: "#b443c9" }, { borderWidth: 25 }]
+const arrayFillStyle: ViewStyle[] = [{ backgroundColor: "#55e0ff" }]
+
+storiesOf("Checkbox", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Behaviour", () => (
+ <Story>
+ <UseCase text="The Checkbox" usage="Use the checkbox to represent on/off states.">
+ <Toggle initial={false}>
+ {({ on, toggle }) => <Checkbox value={on} onToggle={toggle} text="Toggle me" />}
+ </Toggle>
+ </UseCase>
+ <UseCase text="value = true" usage="This is permanently on.">
+ <Checkbox value={true} text="Always on" />
+ </UseCase>
+ <UseCase text="value = false" usage="This is permanantly off.">
+ <Checkbox value={false} text="Always off" />
+ </UseCase>
+ </Story>
+ ))
+ .add("Styling", () => (
+ <Story>
+ <UseCase text="multiline = true" usage="For really really long text">
+ <Toggle initial={false}>
+ {({ on, toggle }) => (
+ <View>
+ <Checkbox
+ text="We’re an App Design & Development Team. Experts in mobile & web technologies. We create beautiful, functional mobile apps and websites."
+ value={on}
+ multiline
+ onToggle={toggle}
+ />
+ </View>
+ )}
+ </Toggle>
+ </UseCase>
+ <UseCase text=".style" usage="Override the container style">
+ <Toggle initial={false}>
+ {({ on, toggle }) => (
+ <View>
+ <Checkbox
+ text="Hello there!"
+ value={on}
+ style={{
+ backgroundColor: "purple",
+ marginLeft: 40,
+ paddingVertical: 30,
+ paddingLeft: 60,
+ }}
+ onToggle={toggle}
+ />
+ </View>
+ )}
+ </Toggle>
+ </UseCase>
+ <UseCase text=".outlineStyle" usage="Override the outline style">
+ <Toggle initial={false}>
+ {({ on, toggle }) => (
+ <View>
+ <Checkbox
+ text="Outline is the box frame"
+ value={on}
+ outlineStyle={{
+ borderColor: "green",
+ borderRadius: 10,
+ borderWidth: 4,
+ width: 60,
+ height: 20,
+ }}
+ onToggle={toggle}
+ />
+ </View>
+ )}
+ </Toggle>
+ </UseCase>
+ <UseCase text=".fillStyle" usage="Override the fill style">
+ <Toggle initial={false}>
+ {({ on, toggle }) => (
+ <View>
+ <Checkbox
+ text="Fill er up"
+ value={on}
+ fillStyle={{ backgroundColor: "red", borderRadius: 8 }}
+ onToggle={toggle}
+ />
+ </View>
+ )}
+ </Toggle>
+ </UseCase>
+
+ <UseCase text="Array style" usage="Use array styles">
+ <Toggle initial={false}>
+ {({ on, toggle }) => (
+ <View>
+ <Checkbox
+ text="Check it twice"
+ value={on}
+ onToggle={toggle}
+ style={arrayStyle}
+ outlineStyle={arrayOutlineStyle}
+ fillStyle={arrayFillStyle}
+ />
+ </View>
+ )}
+ </Toggle>
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/checkbox/checkbox.tsx b/app/components/checkbox/checkbox.tsx
new file mode 100644
index 0000000..fc3992a
--- /dev/null
+++ b/app/components/checkbox/checkbox.tsx
@@ -0,0 +1,53 @@
+import * as React from "react"
+import { TextStyle, TouchableOpacity, View, ViewStyle } from "react-native"
+import { Text } from "../text/text"
+import { color, spacing } from "../../theme"
+import { CheckboxProps } from "./checkbox.props"
+
+const ROOT: ViewStyle = {
+ flexDirection: "row",
+ paddingVertical: spacing[1],
+ alignSelf: "flex-start",
+}
+
+const DIMENSIONS = { width: 16, height: 16 }
+
+const OUTLINE: ViewStyle = {
+ ...DIMENSIONS,
+ marginTop: 2, // finicky and will depend on font/line-height/baseline/weather
+ justifyContent: "center",
+ alignItems: "center",
+ borderWidth: 1,
+ borderColor: color.primaryDarker,
+ borderRadius: 1,
+}
+
+const FILL: ViewStyle = {
+ width: DIMENSIONS.width - 4,
+ height: DIMENSIONS.height - 4,
+ backgroundColor: color.primary,
+}
+
+const LABEL: TextStyle = { paddingLeft: spacing[2] }
+
+export function Checkbox(props: CheckboxProps) {
+ const numberOfLines = props.multiline ? 0 : 1
+
+ const rootStyle = [ROOT, props.style]
+ const outlineStyle = [OUTLINE, props.outlineStyle]
+ const fillStyle = [FILL, props.fillStyle]
+
+ const onPress = props.onToggle ? () => props.onToggle && props.onToggle(!props.value) : null
+
+ return (
+ <TouchableOpacity
+ activeOpacity={1}
+ disabled={!props.onToggle}
+ onPress={onPress}
+ style={rootStyle}
+ >
+ <View style={outlineStyle}>{props.value && <View style={fillStyle} />}</View>
+ <Text text={props.text} tx={props.tx} numberOfLines={numberOfLines} style={LABEL} />
+ </TouchableOpacity>
+ )
+}
diff --git a/app/components/form-row/form-row.presets.ts b/app/components/form-row/form-row.presets.ts
new file mode 100644
index 0000000..7c10294
--- /dev/null
+++ b/app/components/form-row/form-row.presets.ts
@@ -0,0 +1,71 @@
+import { ViewStyle } from "react-native"
+import { color, spacing } from "../../theme"
+
+/**
+ * The size of the border radius.
+ */
+const RADIUS = 8
+
+/**
+ * The default style of the container.
+ */
+const ROOT: ViewStyle = {
+ borderWidth: 1,
+ borderColor: color.line,
+ padding: spacing[2],
+}
+
+/**
+ * What each of the presets look like.
+ */
+export const PRESETS = {
+ /**
+ * Rounded borders on the the top only.
+ */
+ top: {
+ ...ROOT,
+ borderTopLeftRadius: RADIUS,
+ borderTopRightRadius: RADIUS,
+ borderBottomWidth: 0,
+ },
+ /**
+ * No rounded borders.
+ */
+ middle: {
+ ...ROOT,
+ borderBottomWidth: 0,
+ },
+ /**
+ * Rounded borders on the bottom.
+ */
+ bottom: {
+ ...ROOT,
+ borderBottomLeftRadius: RADIUS,
+ borderBottomRightRadius: RADIUS,
+ },
+ /**
+ * Rounded borders everywhere.
+ */
+ soloRound: {
+ ...ROOT,
+ borderRadius: RADIUS,
+ },
+ /**
+ * Straight borders everywhere.
+ */
+ soloStraight: {
+ ...ROOT,
+ },
+ /**
+ * Transparent borders useful to keep things lined up.
+ */
+ clear: {
+ ...ROOT,
+ borderColor: color.transparent,
+ },
+}
+
+/**
+ * The names of the presets supported by FormRow.
+ */
+export type FormRowPresets = keyof typeof PRESETS
diff --git a/app/components/form-row/form-row.props.tsx b/app/components/form-row/form-row.props.tsx
new file mode 100644
index 0000000..f010206
--- /dev/null
+++ b/app/components/form-row/form-row.props.tsx
@@ -0,0 +1,23 @@
+import * as React from "react"
+import { StyleProp, ViewStyle } from "react-native"
+import { FormRowPresets } from "./form-row.presets"
+
+/**
+ * The properties you can pass to FormRow.
+ */
+export interface FormRowProps {
+ /**
+ * Children components.
+ */
+ children?: React.ReactNode
+
+ /**
+ * Override the container style... useful for margins and padding.
+ */
+ style?: StyleProp<ViewStyle>
+
+ /**
+ * The type of border.
+ */
+ preset: FormRowPresets
+}
diff --git a/app/components/form-row/form-row.story.tsx b/app/components/form-row/form-row.story.tsx
new file mode 100644
index 0000000..509afbb
--- /dev/null
+++ b/app/components/form-row/form-row.story.tsx
@@ -0,0 +1,107 @@
+/* eslint-disable react-native/no-inline-styles */
+/* eslint-disable react-native/no-color-literals */
+
+import * as React from "react"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { Text, FormRow } from "../"
+import { color } from "../../theme/color"
+import { ViewStyle } from "react-native"
+
+declare let module
+
+const TEXT_STYLE_OVERRIDE = {
+ color: color.storybookTextColor,
+}
+const arrayStyle: ViewStyle[] = [{ borderWidth: 5 }, { borderColor: "#32cd32" }]
+
+storiesOf("FormRow", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Assembled", () => (
+ <Story>
+ <UseCase
+ text="Fully Assembled"
+ usage="FormRow has many parts designed to fit together. Here is what it looks like all assembled."
+ >
+ <FormRow preset="top">
+ <Text preset="fieldLabel" style={TEXT_STYLE_OVERRIDE}>
+ Hello! I am at the top
+ </Text>
+ </FormRow>
+ <FormRow preset="middle">
+ <Text style={TEXT_STYLE_OVERRIDE}>
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Commodi officia quo rerum
+ impedit asperiores hic ex quae, quam dolores vel odit doloribus, tempore atque deserunt
+ possimus incidunt, obcaecati numquam officiis.
+ </Text>
+ </FormRow>
+ <FormRow preset="middle">
+ <Text preset="secondary" style={TEXT_STYLE_OVERRIDE}>
+ ...one more thing
+ </Text>
+ </FormRow>
+ <FormRow preset="bottom">
+ <Text style={TEXT_STYLE_OVERRIDE}>🎉 Footers!</Text>
+ </FormRow>
+ </UseCase>
+ <UseCase text="Alternatives" usage="Less commonly used presets.">
+ <FormRow preset="clear">
+ <Text style={TEXT_STYLE_OVERRIDE}>
+ My borders are still there, but they are clear. This causes the text to still align
+ properly due to the box model of flexbox.
+ </Text>
+ </FormRow>
+ <FormRow preset="soloRound">
+ <Text style={TEXT_STYLE_OVERRIDE}>I'm round</Text>
+ </FormRow>
+ <FormRow preset="soloStraight" style={{ marginTop: 10, backgroundColor: "#ffe" }}>
+ <Text style={TEXT_STYLE_OVERRIDE}>I'm square and have a custom style.</Text>
+ </FormRow>
+ </UseCase>
+ </Story>
+ ))
+ .add("Presets", () => (
+ <Story>
+ <UseCase text="top" usage="The top of a form.">
+ <FormRow preset="top">
+ <Text style={TEXT_STYLE_OVERRIDE}>Curved borders at the top.</Text>
+ <Text style={TEXT_STYLE_OVERRIDE}>Nothing below</Text>
+ </FormRow>
+ </UseCase>
+ <UseCase text="middle" usage="A row in the middle of a form.">
+ <FormRow preset="middle">
+ <Text style={TEXT_STYLE_OVERRIDE}>No curves and empty at the bottom.</Text>
+ </FormRow>
+ </UseCase>
+ <UseCase text="bottom" usage="The bottom of a form.">
+ <FormRow preset="bottom">
+ <Text style={TEXT_STYLE_OVERRIDE}>Curved at the bottom</Text>
+ <Text style={TEXT_STYLE_OVERRIDE}>Line at the top.</Text>
+ </FormRow>
+ </UseCase>
+ <UseCase text="soloRound" usage="A standalone curved form row.">
+ <FormRow preset="soloRound">
+ <Text style={TEXT_STYLE_OVERRIDE}>Curves all around.</Text>
+ </FormRow>
+ </UseCase>
+ <UseCase text="soloStraight" usage="A standalone straight form row.">
+ <FormRow preset="soloStraight">
+ <Text style={TEXT_STYLE_OVERRIDE}>Curves nowhere.</Text>
+ </FormRow>
+ </UseCase>
+ <UseCase text="clear" usage="Identical dimensions but transparent edges.">
+ <FormRow preset="clear">
+ <Text style={TEXT_STYLE_OVERRIDE}>Curves nowhere.</Text>
+ </FormRow>
+ </UseCase>
+ </Story>
+ ))
+ .add("Styling", () => (
+ <Story>
+ <UseCase text="Style array" usage="Form row with an array of styles">
+ <FormRow preset="soloStraight" style={arrayStyle}>
+ <Text style={TEXT_STYLE_OVERRIDE}>Array style.</Text>
+ </FormRow>
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/form-row/form-row.tsx b/app/components/form-row/form-row.tsx
new file mode 100644
index 0000000..2045336
--- /dev/null
+++ b/app/components/form-row/form-row.tsx
@@ -0,0 +1,13 @@
+import * as React from "react"
+import { View } from "react-native"
+import { PRESETS } from "./form-row.presets"
+import { FormRowProps } from "./form-row.props"
+
+/**
+ * A horizontal container component used to hold a row of a form.
+ */
+export function FormRow(props: FormRowProps) {
+ const viewStyle = [PRESETS[props.preset], props.style]
+
+ return <View style={viewStyle}>{props.children}</View>
+}
diff --git a/app/components/header/header.props.ts b/app/components/header/header.props.ts
new file mode 100644
index 0000000..07f2c4d
--- /dev/null
+++ b/app/components/header/header.props.ts
@@ -0,0 +1,45 @@
+import { StyleProp, TextStyle, ViewStyle } from "react-native"
+import { IconTypes } from "../icon/icons"
+import { TxKeyPath } from "../../i18n"
+
+export interface HeaderProps {
+ /**
+ * Main header, e.g. POWERED BY IGNITE
+ */
+ headerTx?: TxKeyPath
+
+ /**
+ * header non-i18n
+ */
+ headerText?: string
+
+ /**
+ * Icon that should appear on the left
+ */
+ leftIcon?: IconTypes
+
+ /**
+ * What happens when you press the left icon
+ */
+ onLeftPress?(): void
+
+ /**
+ * Icon that should appear on the right
+ */
+ rightIcon?: IconTypes
+
+ /**
+ * What happens when you press the right icon
+ */
+ onRightPress?(): void
+
+ /**
+ * Container style overrides.
+ */
+ style?: StyleProp<ViewStyle>
+
+ /**
+ * Title style overrides.
+ */
+ titleStyle?: StyleProp<TextStyle>
+}
diff --git a/app/components/header/header.story.tsx b/app/components/header/header.story.tsx
new file mode 100644
index 0000000..05093b3
--- /dev/null
+++ b/app/components/header/header.story.tsx
@@ -0,0 +1,43 @@
+import * as React from "react"
+import { View, Alert } from "react-native"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { Header } from "./header"
+import { color } from "../../theme"
+
+declare let module
+
+const VIEWSTYLE = {
+ flex: 1,
+ backgroundColor: color.storybookDarkBg,
+}
+
+storiesOf("Header", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Behavior", () => (
+ <Story>
+ <UseCase noPad text="default" usage="The default usage">
+ <View style={VIEWSTYLE}>
+ <Header headerTx="demoScreen.howTo" />
+ </View>
+ </UseCase>
+ <UseCase noPad text="leftIcon" usage="A left nav icon">
+ <View style={VIEWSTYLE}>
+ <Header
+ headerTx="demoScreen.howTo"
+ leftIcon="back"
+ onLeftPress={() => Alert.alert("left nav")}
+ />
+ </View>
+ </UseCase>
+ <UseCase noPad text="rightIcon" usage="A right nav icon">
+ <View style={VIEWSTYLE}>
+ <Header
+ headerTx="demoScreen.howTo"
+ rightIcon="bullet"
+ onRightPress={() => Alert.alert("right nav")}
+ />
+ </View>
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/header/header.tsx b/app/components/header/header.tsx
new file mode 100644
index 0000000..b4a6634
--- /dev/null
+++ b/app/components/header/header.tsx
@@ -0,0 +1,61 @@
+import React from "react"
+import { View, ViewStyle, TextStyle } from "react-native"
+import { HeaderProps } from "./header.props"
+import { Button } from "../button/button"
+import { Text } from "../text/text"
+import { Icon } from "../icon/icon"
+import { spacing } from "../../theme"
+import { translate } from "../../i18n/"
+
+// static styles
+const ROOT: ViewStyle = {
+ flexDirection: "row",
+ paddingHorizontal: spacing[4],
+ alignItems: "center",
+ paddingTop: spacing[5],
+ paddingBottom: spacing[5],
+ justifyContent: "flex-start",
+}
+const TITLE: TextStyle = { textAlign: "center" }
+const TITLE_MIDDLE: ViewStyle = { flex: 1, justifyContent: "center" }
+const LEFT: ViewStyle = { width: 32 }
+const RIGHT: ViewStyle = { width: 32 }
+
+/**
+ * Header that appears on many screens. Will hold navigation buttons and screen title.
+ */
+export function Header(props: HeaderProps) {
+ const {
+ onLeftPress,
+ onRightPress,
+ rightIcon,
+ leftIcon,
+ headerText,
+ headerTx,
+ style,
+ titleStyle,
+ } = props
+ const header = headerText || (headerTx && translate(headerTx)) || ""
+
+ return (
+ <View style={[ROOT, style]}>
+ {leftIcon ? (
+ <Button preset="link" onPress={onLeftPress}>
+ <Icon icon={leftIcon} />
+ </Button>
+ ) : (
+ <View style={LEFT} />
+ )}
+ <View style={TITLE_MIDDLE}>
+ <Text style={[TITLE, titleStyle]} text={header} />
+ </View>
+ {rightIcon ? (
+ <Button preset="link" onPress={onRightPress}>
+ <Icon icon={rightIcon} />
+ </Button>
+ ) : (
+ <View style={RIGHT} />
+ )}
+ </View>
+ )
+}
diff --git a/app/components/icon/icon.props.ts b/app/components/icon/icon.props.ts
new file mode 100644
index 0000000..71ce0b7
--- /dev/null
+++ b/app/components/icon/icon.props.ts
@@ -0,0 +1,21 @@
+import { ImageStyle, StyleProp, ViewStyle } from "react-native"
+import { IconTypes } from "./icons"
+
+export interface IconProps {
+ /**
+ * Style overrides for the icon image
+ */
+ style?: StyleProp<ImageStyle>
+
+ /**
+ * Style overrides for the icon container
+ */
+
+ containerStyle?: StyleProp<ViewStyle>
+
+ /**
+ * The name of the icon
+ */
+
+ icon?: IconTypes
+}
diff --git a/app/components/icon/icon.story.tsx b/app/components/icon/icon.story.tsx
new file mode 100644
index 0000000..d119ed4
--- /dev/null
+++ b/app/components/icon/icon.story.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { Icon } from "./icon"
+
+declare let module
+
+storiesOf("Icon", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Names", () => (
+ <Story>
+ <UseCase text="back" usage="The icon for going back">
+ <Icon icon="back" />
+ </UseCase>
+ <UseCase text="bullet" usage="The icon for a bullet point">
+ <Icon icon="bullet" />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/icon/icon.tsx b/app/components/icon/icon.tsx
new file mode 100644
index 0000000..4735e13
--- /dev/null
+++ b/app/components/icon/icon.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+import { View, ImageStyle } from "react-native"
+import { AutoImage as Image } from "../auto-image/auto-image"
+import { IconProps } from "./icon.props"
+import { icons } from "./icons"
+
+const ROOT: ImageStyle = {
+ resizeMode: "contain",
+}
+
+export function Icon(props: IconProps) {
+ const { style: styleOverride, icon, containerStyle } = props
+
+ return (
+ <View style={containerStyle}>
+ <Image style={[ROOT, styleOverride]} source={icons[icon]} />
+ </View>
+ )
+}
diff --git a/app/components/icon/icons/arrow-left.png b/app/components/icon/icons/arrow-left.png
new file mode 100644
index 0000000..9d607d7
--- /dev/null
+++ b/app/components/icon/icons/arrow-left.png
Binary files differ
diff --git a/app/components/icon/icons/[email protected] b/app/components/icon/icons/[email protected]
new file mode 100644
index 0000000..9d607d7
--- /dev/null
+++ b/app/components/icon/icons/[email protected]
Binary files differ
diff --git a/app/components/icon/icons/bullet.png b/app/components/icon/icons/bullet.png
new file mode 100644
index 0000000..8fc256f
--- /dev/null
+++ b/app/components/icon/icons/bullet.png
Binary files differ
diff --git a/app/components/icon/icons/[email protected] b/app/components/icon/icons/[email protected]
new file mode 100644
index 0000000..8fc256f
--- /dev/null
+++ b/app/components/icon/icons/[email protected]
Binary files differ
diff --git a/app/components/icon/icons/index.ts b/app/components/icon/icons/index.ts
new file mode 100644
index 0000000..00e8a59
--- /dev/null
+++ b/app/components/icon/icons/index.ts
@@ -0,0 +1,6 @@
+export const icons = {
+ back: require("./arrow-left.png"),
+ bullet: require("./bullet.png"),
+}
+
+export type IconTypes = keyof typeof icons
diff --git a/app/components/index.ts b/app/components/index.ts
new file mode 100644
index 0000000..5ed4a28
--- /dev/null
+++ b/app/components/index.ts
@@ -0,0 +1,12 @@
+export * from "./bullet-item/bullet-item"
+export * from "./button/button"
+export * from "./checkbox/checkbox"
+export * from "./form-row/form-row"
+export * from "./header/header"
+export * from "./icon/icon"
+export * from "./screen/screen"
+export * from "./switch/switch"
+export * from "./text/text"
+export * from "./text-field/text-field"
+export * from "./wallpaper/wallpaper"
+export * from "./auto-image/auto-image"
diff --git a/app/components/screen/screen.presets.ts b/app/components/screen/screen.presets.ts
new file mode 100644
index 0000000..a016b77
--- /dev/null
+++ b/app/components/screen/screen.presets.ts
@@ -0,0 +1,66 @@
+import { ViewStyle } from "react-native"
+import { color } from "../../theme"
+
+/**
+ * All screen keyboard offsets.
+ */
+export const offsets = {
+ none: 0,
+}
+
+/**
+ * The variations of keyboard offsets.
+ */
+export type KeyboardOffsets = keyof typeof offsets
+
+/**
+ * All the variations of screens.
+ */
+export const presets = {
+ /**
+ * No scrolling. Suitable for full-screen carousels and components
+ * which have built-in scrolling like FlatList.
+ */
+ fixed: {
+ outer: {
+ backgroundColor: color.background,
+ flex: 1,
+ height: "100%",
+ } as ViewStyle,
+ inner: {
+ justifyContent: "flex-start",
+ alignItems: "stretch",
+ height: "100%",
+ width: "100%",
+ } as ViewStyle,
+ },
+
+ /**
+ * Scrolls. Suitable for forms or other things requiring a keyboard.
+ *
+ * Pick this one if you don't know which one you want yet.
+ */
+ scroll: {
+ outer: {
+ backgroundColor: color.background,
+ flex: 1,
+ height: "100%",
+ } as ViewStyle,
+ inner: { justifyContent: "flex-start", alignItems: "stretch" } as ViewStyle,
+ },
+}
+
+/**
+ * The variations of screens.
+ */
+export type ScreenPresets = keyof typeof presets
+
+/**
+ * Is this preset a non-scrolling one?
+ *
+ * @param preset The preset to check
+ */
+export function isNonScrolling(preset?: ScreenPresets) {
+ // any of these things will make you scroll
+ return !preset || !presets[preset] || preset === "fixed"
+}
diff --git a/app/components/screen/screen.props.ts b/app/components/screen/screen.props.ts
new file mode 100644
index 0000000..0326fd7
--- /dev/null
+++ b/app/components/screen/screen.props.ts
@@ -0,0 +1,46 @@
+import React from "react"
+import { StyleProp, ViewStyle } from "react-native"
+import { KeyboardOffsets, ScreenPresets } from "./screen.presets"
+
+export interface ScreenProps {
+ /**
+ * Children components.
+ */
+ children?: React.ReactNode
+
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<ViewStyle>
+
+ /**
+ * One of the different types of presets.
+ */
+ preset?: ScreenPresets
+
+ /**
+ * An optional background color
+ */
+ backgroundColor?: string
+
+ /**
+ * An optional status bar setting. Defaults to light-content.
+ */
+ statusBar?: "light-content" | "dark-content"
+
+ /**
+ * Should we not wrap in SafeAreaView? Defaults to false.
+ */
+ unsafe?: boolean
+
+ /**
+ * By how much should we offset the keyboard? Defaults to none.
+ */
+ keyboardOffset?: KeyboardOffsets
+
+ /**
+ * Should keyboard persist on screen tap. Defaults to handled.
+ * Only applies to scroll preset.
+ */
+ keyboardShouldPersistTaps?: "handled" | "always" | "never"
+}
diff --git a/app/components/screen/screen.tsx b/app/components/screen/screen.tsx
new file mode 100644
index 0000000..ba84547
--- /dev/null
+++ b/app/components/screen/screen.tsx
@@ -0,0 +1,66 @@
+import * as React from "react"
+import { KeyboardAvoidingView, Platform, ScrollView, StatusBar, View } from "react-native"
+import { useSafeAreaInsets } from "react-native-safe-area-context"
+import { ScreenProps } from "./screen.props"
+import { isNonScrolling, offsets, presets } from "./screen.presets"
+
+const isIos = Platform.OS === "ios"
+
+function ScreenWithoutScrolling(props: ScreenProps) {
+ const insets = useSafeAreaInsets()
+ const preset = presets.fixed
+ const style = props.style || {}
+ const backgroundStyle = props.backgroundColor ? { backgroundColor: props.backgroundColor } : {}
+ const insetStyle = { paddingTop: props.unsafe ? 0 : insets.top }
+
+ return (
+ <KeyboardAvoidingView
+ style={[preset.outer, backgroundStyle]}
+ behavior={isIos ? "padding" : undefined}
+ keyboardVerticalOffset={offsets[props.keyboardOffset || "none"]}
+ >
+ <StatusBar barStyle={props.statusBar || "light-content"} />
+ <View style={[preset.inner, style, insetStyle]}>{props.children}</View>
+ </KeyboardAvoidingView>
+ )
+}
+
+function ScreenWithScrolling(props: ScreenProps) {
+ const insets = useSafeAreaInsets()
+ const preset = presets.scroll
+ const style = props.style || {}
+ const backgroundStyle = props.backgroundColor ? { backgroundColor: props.backgroundColor } : {}
+ const insetStyle = { paddingTop: props.unsafe ? 0 : insets.top }
+
+ return (
+ <KeyboardAvoidingView
+ style={[preset.outer, backgroundStyle]}
+ behavior={isIos ? "padding" : undefined}
+ keyboardVerticalOffset={offsets[props.keyboardOffset || "none"]}
+ >
+ <StatusBar barStyle={props.statusBar || "light-content"} />
+ <View style={[preset.outer, backgroundStyle, insetStyle]}>
+ <ScrollView
+ style={[preset.outer, backgroundStyle]}
+ contentContainerStyle={[preset.inner, style]}
+ keyboardShouldPersistTaps={props.keyboardShouldPersistTaps || "handled"}
+ >
+ {props.children}
+ </ScrollView>
+ </View>
+ </KeyboardAvoidingView>
+ )
+}
+
+/**
+ * The starting component on every screen in the app.
+ *
+ * @param props The screen props
+ */
+export function Screen(props: ScreenProps) {
+ if (isNonScrolling(props.preset)) {
+ return <ScreenWithoutScrolling {...props} />
+ } else {
+ return <ScreenWithScrolling {...props} />
+ }
+}
diff --git a/app/components/switch/switch.props.ts b/app/components/switch/switch.props.ts
new file mode 100644
index 0000000..8235457
--- /dev/null
+++ b/app/components/switch/switch.props.ts
@@ -0,0 +1,39 @@
+import { StyleProp, ViewStyle } from "react-native"
+
+export interface SwitchProps {
+ /**
+ * On or off.
+ */
+ value?: boolean
+ /**
+ * Fires when the on/off switch triggers.
+ *
+ * @param newValue The new value we're switching to.
+ */
+ onToggle?: (newValue: boolean) => void
+
+ /**
+ * A style override to apply to the container. Useful for margins and paddings.
+ */
+ style?: StyleProp<ViewStyle>
+
+ /**
+ * Additional track styling when on.
+ */
+ trackOnStyle?: StyleProp<ViewStyle>
+
+ /**
+ * Additional track styling when off.
+ */
+ trackOffStyle?: StyleProp<ViewStyle>
+
+ /**
+ * Additional thumb styling when on.
+ */
+ thumbOnStyle?: StyleProp<ViewStyle>
+
+ /**
+ * Additional thumb styling when off.
+ */
+ thumbOffStyle?: StyleProp<ViewStyle>
+}
diff --git a/app/components/switch/switch.story.tsx b/app/components/switch/switch.story.tsx
new file mode 100644
index 0000000..998d1df
--- /dev/null
+++ b/app/components/switch/switch.story.tsx
@@ -0,0 +1,110 @@
+/* eslint-disable react-native/no-inline-styles */
+/* eslint-disable react-native/no-color-literals */
+
+import * as React from "react"
+import { View, ViewStyle } from "react-native"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { Toggle } from "react-powerplug"
+import { Switch } from "./switch"
+
+declare let module
+
+const styleArray: ViewStyle[] = [{ borderColor: "#686868" }]
+
+const trackOffStyle: ViewStyle[] = [
+ { backgroundColor: "#686868" },
+ {
+ height: 80,
+ borderRadius: 0,
+ },
+]
+const trackOnStyle: ViewStyle[] = [
+ {
+ backgroundColor: "#b1008e",
+ borderColor: "#686868",
+ },
+ {
+ height: 80,
+ borderRadius: 0,
+ },
+]
+const thumbOffStyle: ViewStyle[] = [
+ {
+ backgroundColor: "#b1008e",
+ borderColor: "#686868",
+ },
+ {
+ height: 80,
+ borderRadius: 0,
+ },
+]
+const thumbOnStyle: ViewStyle[] = [
+ { backgroundColor: "#f0c" },
+ {
+ height: 80,
+ borderRadius: 0,
+ borderColor: "#686868",
+ },
+]
+
+storiesOf("Switch", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Behaviour", () => (
+ <Story>
+ <UseCase text="The Toggle Switch" usage="Use the switch to represent on/off states.">
+ <Toggle initial={false}>
+ {({ on, toggle }) => <Switch value={on} onToggle={toggle} />}
+ </Toggle>
+ </UseCase>
+ <UseCase text="value = true" usage="This is permanently on.">
+ <Switch value={true} />
+ </UseCase>
+ <UseCase text="value = false" usage="This is permanantly off.">
+ <Switch value={false} />
+ </UseCase>
+ </Story>
+ ))
+ .add("Styling", () => (
+ <Story>
+ <UseCase text="Custom Styling" usage="Promise me this won't happen.">
+ <Toggle initial={false}>
+ {({ on, toggle }) => (
+ <View>
+ <Switch
+ trackOnStyle={{ backgroundColor: "green", borderColor: "black" }}
+ trackOffStyle={{ backgroundColor: "red", borderColor: "maroon" }}
+ thumbOnStyle={{ backgroundColor: "cyan" }}
+ thumbOffStyle={{ backgroundColor: "pink" }}
+ value={on}
+ onToggle={toggle}
+ />
+ </View>
+ )}
+ </Toggle>
+ </UseCase>
+
+ <UseCase text="Style array" usage="This either.">
+ <Toggle initial={false}>
+ {({ on, toggle }) => (
+ <View>
+ <Switch
+ style={styleArray}
+ trackOffStyle={trackOffStyle}
+ trackOnStyle={trackOnStyle}
+ thumbOffStyle={thumbOffStyle}
+ thumbOnStyle={thumbOnStyle}
+ // trackOnStyle={{ backgroundColor: "green", borderColor: "black" }}
+ // trackOffStyle={{ backgroundColor: "red", borderColor: "maroon" }}
+ // thumbOnStyle={{ backgroundColor: "cyan" }}
+ // thumbOffStyle={{ backgroundColor: "pink" }}
+
+ value={on}
+ onToggle={toggle}
+ />
+ </View>
+ )}
+ </Toggle>
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/switch/switch.tsx b/app/components/switch/switch.tsx
new file mode 100644
index 0000000..0813747
--- /dev/null
+++ b/app/components/switch/switch.tsx
@@ -0,0 +1,114 @@
+import React from "react"
+import { ViewStyle, Animated, Easing, TouchableWithoutFeedback } from "react-native"
+import { color } from "../../theme"
+import { SwitchProps } from "./switch.props"
+
+// dimensions
+const THUMB_SIZE = 30
+const WIDTH = 56
+const MARGIN = 2
+const OFF_POSITION = -0.5
+const ON_POSITION = WIDTH - THUMB_SIZE - MARGIN
+const BORDER_RADIUS = (THUMB_SIZE * 3) / 4
+
+// colors
+const ON_COLOR = color.primary
+const OFF_COLOR = color.palette.offWhite
+const BORDER_ON_COLOR = ON_COLOR
+const BORDER_OFF_COLOR = "rgba(0, 0, 0, 0.1)"
+
+// animation
+const DURATION = 250
+
+// the track always has these props
+const TRACK = {
+ height: THUMB_SIZE + MARGIN,
+ width: WIDTH,
+ borderRadius: BORDER_RADIUS,
+ borderWidth: MARGIN / 2,
+ backgroundColor: color.background,
+}
+
+// the thumb always has these props
+const THUMB: ViewStyle = {
+ position: "absolute",
+ width: THUMB_SIZE,
+ height: THUMB_SIZE,
+ borderColor: BORDER_OFF_COLOR,
+ borderRadius: THUMB_SIZE / 2,
+ borderWidth: MARGIN / 2,
+ backgroundColor: color.background,
+ shadowColor: BORDER_OFF_COLOR,
+ shadowOffset: { width: 1, height: 2 },
+ shadowOpacity: 1,
+ shadowRadius: 2,
+ elevation: 2,
+}
+
+const makeAnimatedValue = (switchOn) => new Animated.Value(switchOn ? 1 : 0)
+
+export function Switch(props: SwitchProps) {
+ const [timer] = React.useState<Animated.Value>(makeAnimatedValue(props.value))
+ const startAnimation = React.useMemo(
+ () => (newValue: boolean) => {
+ const toValue = newValue ? 1 : 0
+ const easing = Easing.out(Easing.circle)
+ Animated.timing(timer, {
+ toValue,
+ duration: DURATION,
+ easing,
+ useNativeDriver: true,
+ }).start()
+ },
+ [timer],
+ )
+
+ const [previousValue, setPreviousValue] = React.useState<boolean>(props.value)
+ React.useEffect(() => {
+ if (props.value !== previousValue) {
+ startAnimation(props.value)
+ setPreviousValue(props.value)
+ }
+ }, [props.value])
+
+ const handlePress = React.useMemo(() => () => props.onToggle && props.onToggle(!props.value), [
+ props.onToggle,
+ props.value,
+ ])
+
+ if (!timer) {
+ return null
+ }
+
+ const translateX = timer.interpolate({
+ inputRange: [0, 1],
+ outputRange: [OFF_POSITION, ON_POSITION],
+ })
+
+ const style = props.style
+
+ const trackStyle = [
+ TRACK,
+ {
+ backgroundColor: props.value ? ON_COLOR : OFF_COLOR,
+ borderColor: props.value ? BORDER_ON_COLOR : BORDER_OFF_COLOR,
+ },
+ props.value ? props.trackOnStyle : props.trackOffStyle,
+ ]
+
+ const thumbStyle = [
+ THUMB,
+ {
+ transform: [{ translateX }],
+ },
+ props.value ? props.thumbOnStyle : props.thumbOffStyle,
+ ]
+
+ return (
+ <TouchableWithoutFeedback onPress={handlePress} style={style}>
+ <Animated.View style={trackStyle}>
+ <Animated.View style={thumbStyle} />
+ </Animated.View>
+ </TouchableWithoutFeedback>
+ )
+}
diff --git a/app/components/text-field/text-field.story.tsx b/app/components/text-field/text-field.story.tsx
new file mode 100644
index 0000000..74a4da0
--- /dev/null
+++ b/app/components/text-field/text-field.story.tsx
@@ -0,0 +1,159 @@
+/* eslint-disable react-native/no-inline-styles */
+/* eslint-disable react-native/no-color-literals */
+
+import * as React from "react"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { Text, TextField } from "../"
+import { State } from "react-powerplug"
+import { ViewStyle, TextStyle, Alert } from "react-native"
+
+declare let module
+
+const styleArray: ViewStyle[] = [{ paddingHorizontal: 30 }, { borderWidth: 30 }]
+
+const inputStyleArray: TextStyle[] = [
+ {
+ backgroundColor: "rebeccapurple",
+ color: "white",
+ padding: 40,
+ },
+ {
+ borderWidth: 10,
+ borderRadius: 4,
+ borderColor: "#7fff00",
+ },
+]
+let alertWhenFocused = true
+
+storiesOf("TextField", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Labelling", () => (
+ <Story>
+ <UseCase text="Normal text" usage="Use placeholder and label to set the text.">
+ <State initial={{ value: "" }}>
+ {({ state, setState }) => (
+ <TextField
+ onChangeText={(value) => setState({ value })}
+ value={state.value}
+ label="Name"
+ placeholder="omg your name"
+ />
+ )}
+ </State>
+ </UseCase>
+
+ <UseCase text="i18n text" usage="Use placeholderTx and labelTx for i18n lookups">
+ <State initial={{ value: "" }}>
+ {({ state, setState }) => (
+ <TextField
+ onChangeText={(value) => setState({ value })}
+ value={state.value}
+ placeholderTx="storybook.placeholder"
+ labelTx="storybook.field"
+ />
+ )}
+ </State>
+ </UseCase>
+ </Story>
+ ))
+ .add("Style Overrides", () => (
+ <Story>
+ <UseCase
+ noPad
+ text="Container Styles"
+ usage="Useful for applying margins when laying out a form to remove padding if the form brings its own."
+ >
+ <State initial={{ value: "Inigo" }}>
+ {({ state, setState }) => (
+ <TextField
+ onChangeText={(value) => setState({ value })}
+ value={state.value}
+ label="First Name"
+ style={{ paddingTop: 0, paddingHorizontal: 40 }}
+ />
+ )}
+ </State>
+ <State initial={{ value: "Montoya" }}>
+ {({ state, setState }) => (
+ <TextField
+ onChangeText={(value) => setState({ value })}
+ value={state.value}
+ label="Last Name"
+ style={{ paddingBottom: 0 }}
+ />
+ )}
+ </State>
+ </UseCase>
+ <UseCase
+ text="Input Styles"
+ usage="Useful for 1-off exceptions. Try to steer towards presets for this kind of thing."
+ >
+ <State initial={{ value: "fancy colour" }}>
+ {({ state, setState }) => (
+ <TextField
+ onChangeText={(value) => setState({ value })}
+ value={state.value}
+ label="Name"
+ inputStyle={{
+ backgroundColor: "rebeccapurple",
+ color: "white",
+ padding: 40,
+ borderWidth: 10,
+ borderRadius: 4,
+ borderColor: "hotpink",
+ }}
+ />
+ )}
+ </State>
+ <Text text="* attention designers: i am so sorry" preset="secondary" />
+ </UseCase>
+
+ <UseCase text="Style array" usage="Useful for 1-off exceptions, but using style arrays.">
+ <State initial={{ value: "fancy colour" }}>
+ {({ state, setState }) => (
+ <TextField
+ onChangeText={(value) => setState({ value })}
+ value={state.value}
+ label="Name"
+ style={styleArray}
+ inputStyle={inputStyleArray}
+ />
+ )}
+ </State>
+ <Text text="* attention designers: i am so sorry" preset="secondary" />
+ </UseCase>
+ </Story>
+ ))
+ .add("Ref Forwarding", () => (
+ <Story>
+ <UseCase text="Ref Forwarding" usage="">
+ <State initial={{ value: "fancy colour" }}>
+ {({ state, setState }) => (
+ <TextField
+ onChangeText={(value) => setState({ value })}
+ value={state.value}
+ label="Name"
+ inputStyle={{
+ backgroundColor: "rebeccapurple",
+ color: "white",
+ padding: 40,
+ borderWidth: 10,
+ borderRadius: 4,
+ borderColor: "hotpink",
+ }}
+ forwardedRef={(ref) => ref}
+ onFocus={() => {
+ if (alertWhenFocused) {
+ // Prevent text field focus from being repeatedly triggering alert
+ alertWhenFocused = false
+ Alert.alert("Text field focuesed with forwarded ref!")
+ }
+ }}
+ />
+ )}
+ </State>
+ <Text text="* attention designers: i am so sorry" preset="secondary" />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/text-field/text-field.tsx b/app/components/text-field/text-field.tsx
new file mode 100644
index 0000000..eea1a70
--- /dev/null
+++ b/app/components/text-field/text-field.tsx
@@ -0,0 +1,98 @@
+import React from "react"
+import { StyleProp, TextInput, TextInputProps, TextStyle, View, ViewStyle } from "react-native"
+import { color, spacing, typography } from "../../theme"
+import { translate, TxKeyPath } from "../../i18n"
+import { Text } from "../text/text"
+
+// the base styling for the container
+const CONTAINER: ViewStyle = {
+ paddingVertical: spacing[3],
+}
+
+// the base styling for the TextInput
+const INPUT: TextStyle = {
+ fontFamily: typography.primary,
+ color: color.text,
+ minHeight: 44,
+ fontSize: 18,
+ backgroundColor: color.palette.white,
+}
+
+// currently we have no presets, but that changes quickly when you build your app.
+const PRESETS: { [name: string]: ViewStyle } = {
+ default: {},
+}
+
+export interface TextFieldProps extends TextInputProps {
+ /**
+ * The placeholder i18n key.
+ */
+ placeholderTx?: TxKeyPath
+
+ /**
+ * The Placeholder text if no placeholderTx is provided.
+ */
+ placeholder?: string
+
+ /**
+ * The label i18n key.
+ */
+ labelTx?: TxKeyPath
+
+ /**
+ * The label text if no labelTx is provided.
+ */
+ label?: string
+
+ /**
+ * Optional container style overrides useful for margins & padding.
+ */
+ style?: StyleProp<ViewStyle>
+
+ /**
+ * Optional style overrides for the input.
+ */
+ inputStyle?: StyleProp<TextStyle>
+
+ /**
+ * Various look & feels.
+ */
+ preset?: keyof typeof PRESETS
+
+ forwardedRef?: any
+}
+
+/**
+ * A component which has a label and an input together.
+ */
+export function TextField(props: TextFieldProps) {
+ const {
+ placeholderTx,
+ placeholder,
+ labelTx,
+ label,
+ preset = "default",
+ style: styleOverride,
+ inputStyle: inputStyleOverride,
+ forwardedRef,
+ ...rest
+ } = props
+
+ const containerStyles = [CONTAINER, PRESETS[preset], styleOverride]
+ const inputStyles = [INPUT, inputStyleOverride]
+ const actualPlaceholder = placeholderTx ? translate(placeholderTx) : placeholder
+
+ return (
+ <View style={containerStyles}>
+ <Text preset="fieldLabel" tx={labelTx} text={label} />
+ <TextInput
+ placeholder={actualPlaceholder}
+ placeholderTextColor={color.palette.lighterGrey}
+ underlineColorAndroid={color.transparent}
+ {...rest}
+ style={inputStyles}
+ ref={forwardedRef}
+ />
+ </View>
+ )
+}
diff --git a/app/components/text/text.presets.ts b/app/components/text/text.presets.ts
new file mode 100644
index 0000000..9622268
--- /dev/null
+++ b/app/components/text/text.presets.ts
@@ -0,0 +1,48 @@
+import { TextStyle } from "react-native"
+import { color, typography } from "../../theme"
+
+/**
+ * All text will start off looking like this.
+ */
+const BASE: TextStyle = {
+ fontFamily: typography.primary,
+ color: color.text,
+ fontSize: 15,
+}
+
+/**
+ * All the variations of text styling within the app.
+ *
+ * You want to customize these to whatever you need in your app.
+ */
+export const presets = {
+ /**
+ * The default text styles.
+ */
+ default: BASE,
+
+ /**
+ * A bold version of the default text.
+ */
+ bold: { ...BASE, fontWeight: "bold" } as TextStyle,
+
+ /**
+ * Large headers.
+ */
+ header: { ...BASE, fontSize: 24, fontWeight: "bold" } as TextStyle,
+
+ /**
+ * Field labels that appear on forms above the inputs.
+ */
+ fieldLabel: { ...BASE, fontSize: 13, color: color.dim } as TextStyle,
+
+ /**
+ * A smaller piece of secondard information.
+ */
+ secondary: { ...BASE, fontSize: 9, color: color.dim } as TextStyle,
+}
+
+/**
+ * A list of preset names.
+ */
+export type TextPresets = keyof typeof presets
diff --git a/app/components/text/text.props.ts b/app/components/text/text.props.ts
new file mode 100644
index 0000000..d2c55dc
--- /dev/null
+++ b/app/components/text/text.props.ts
@@ -0,0 +1,37 @@
+import { StyleProp, TextProps as TextProperties, TextStyle } from "react-native"
+import i18n from "i18n-js"
+import { TextPresets } from "./text.presets"
+import { TxKeyPath } from "../../i18n"
+
+export interface TextProps extends TextProperties {
+ /**
+ * Children components.
+ */
+ children?: React.ReactNode
+
+ /**
+ * Text which is looked up via i18n.
+ */
+ tx?: TxKeyPath
+
+ /**
+ * Optional options to pass to i18n. Useful for interpolation
+ * as well as explicitly setting locale or translation fallbacks.
+ */
+ txOptions?: i18n.TranslateOptions
+
+ /**
+ * The text to display if not using `tx` or nested components.
+ */
+ text?: string
+
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<TextStyle>
+
+ /**
+ * One of the different types of text presets.
+ */
+ preset?: TextPresets
+}
diff --git a/app/components/text/text.story.tsx b/app/components/text/text.story.tsx
new file mode 100644
index 0000000..5582c1b
--- /dev/null
+++ b/app/components/text/text.story.tsx
@@ -0,0 +1,92 @@
+/* eslint-disable react-native/no-inline-styles */
+/* eslint-disable react-native/no-color-literals */
+
+import * as React from "react"
+import { View, ViewStyle } from "react-native"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { color } from "../../theme"
+import { Text } from "./text"
+
+declare let module
+
+const VIEWSTYLE = {
+ flex: 1,
+ backgroundColor: color.storybookDarkBg,
+}
+const viewStyleArray: ViewStyle[] = [VIEWSTYLE, { backgroundColor: "#7fff00" }]
+
+storiesOf("Text", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Style Presets", () => (
+ <Story>
+ <UseCase text="default" usage="Used for normal body text.">
+ <View style={VIEWSTYLE}>
+ <Text>Hello!</Text>
+ <Text style={{ paddingTop: 10 }}>
+ Check out{"\n"}
+ my{"\n"}
+ line height
+ </Text>
+ <Text style={{ paddingTop: 10 }}>The quick brown fox jumped over the slow lazy dog.</Text>
+ <Text>$123,456,789.00</Text>
+ </View>
+ </UseCase>
+ <UseCase text="bold" usage="Used for bolded body text.">
+ <View style={VIEWSTYLE}>
+ <Text preset="bold">Osnap! I'm puffy.</Text>
+ </View>
+ </UseCase>
+ <UseCase text="header" usage="Used for major section headers.">
+ <View style={VIEWSTYLE}>
+ <Text preset="header">Behold!</Text>
+ </View>
+ </UseCase>
+ </Story>
+ ))
+ .add("Passing Content", () => (
+ <Story>
+ <UseCase
+ text="text"
+ usage="Used when you want to pass a value but don't want to open a child."
+ >
+ <View style={VIEWSTYLE}>
+ <Text text="Heyo!" />
+ </View>
+ </UseCase>
+ <UseCase text="tx" usage="Used for looking up i18n keys.">
+ <View style={VIEWSTYLE}>
+ <Text tx="common.ok" />
+ <Text tx="common.cancel" />
+ </View>
+ </UseCase>
+ <UseCase
+ text="children"
+ usage="Used like you would normally use a React Native <Text> component."
+ >
+ <View style={VIEWSTYLE}>
+ <Text>Passing strings as children.</Text>
+ </View>
+ </UseCase>
+ <UseCase text="nested children" usage="You can embed them and change styles too.">
+ <View style={VIEWSTYLE}>
+ <Text>
+ {" "}
+ Hello <Text preset="bold">bolded</Text> World.
+ </Text>
+ </View>
+ </UseCase>
+ </Story>
+ ))
+ .add("Styling", () => (
+ <Story>
+ <UseCase text="Style array" usage="Text with style array">
+ <View style={viewStyleArray}>
+ <Text>
+ {" "}
+ Hello <Text preset="bold">bolded</Text> World.
+ </Text>
+ </View>
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/text/text.tsx b/app/components/text/text.tsx
new file mode 100644
index 0000000..3ea613b
--- /dev/null
+++ b/app/components/text/text.tsx
@@ -0,0 +1,28 @@
+import * as React from "react"
+import { Text as ReactNativeText } from "react-native"
+import { presets } from "./text.presets"
+import { TextProps } from "./text.props"
+import { translate } from "../../i18n"
+
+/**
+ * For your text displaying needs.
+ *
+ * This component is a HOC over the built-in React Native one.
+ */
+export function Text(props: TextProps) {
+ // grab the props
+ const { preset = "default", tx, txOptions, text, children, style: styleOverride, ...rest } = props
+
+ // figure out which content to use
+ const i18nText = tx && translate(tx, txOptions)
+ const content = i18nText || text || children
+
+ const style = presets[preset] || presets.default
+ const styles = [style, styleOverride]
+
+ return (
+ <ReactNativeText {...rest} style={styles}>
+ {content}
+ </ReactNativeText>
+ )
+}
diff --git a/app/components/wallpaper/bg.png b/app/components/wallpaper/bg.png
new file mode 100644
index 0000000..641838e
--- /dev/null
+++ b/app/components/wallpaper/bg.png
Binary files differ
diff --git a/app/components/wallpaper/[email protected] b/app/components/wallpaper/[email protected]
new file mode 100644
index 0000000..3ae8396
--- /dev/null
+++ b/app/components/wallpaper/[email protected]
Binary files differ
diff --git a/app/components/wallpaper/wallpaper.presets.ts b/app/components/wallpaper/wallpaper.presets.ts
new file mode 100644
index 0000000..3885b8f
--- /dev/null
+++ b/app/components/wallpaper/wallpaper.presets.ts
@@ -0,0 +1,34 @@
+import { ImageStyle } from "react-native"
+
+/**
+ * All wallpaper will start off looking like this.
+ */
+const BASE: ImageStyle = {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+}
+
+/**
+ * All the variations of wallpaper styling within the app.
+ *
+ * You want to customize these to whatever you need in your app.
+ */
+export const presets = {
+ /**
+ * The default wallpaper styles.
+ */
+ stretch: {
+ ...BASE,
+ resizeMode: "stretch",
+ width: null, // Have to set these to null because android ¯\_(ツ)_/¯
+ height: null,
+ } as ImageStyle,
+}
+
+/**
+ * A list of preset names.
+ */
+export type WallpaperPresets = keyof typeof presets
diff --git a/app/components/wallpaper/wallpaper.props.ts b/app/components/wallpaper/wallpaper.props.ts
new file mode 100644
index 0000000..592bac9
--- /dev/null
+++ b/app/components/wallpaper/wallpaper.props.ts
@@ -0,0 +1,19 @@
+import { ImageStyle, StyleProp } from "react-native"
+import { WallpaperPresets } from "./wallpaper.presets"
+
+export interface WallpaperProps {
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<ImageStyle>
+
+ /**
+ * An optional background image to override the default image.
+ */
+ backgroundImage?: string
+
+ /**
+ * One of the different types of wallpaper presets.
+ */
+ preset?: WallpaperPresets
+}
diff --git a/app/components/wallpaper/wallpaper.story.tsx b/app/components/wallpaper/wallpaper.story.tsx
new file mode 100644
index 0000000..8f5488a
--- /dev/null
+++ b/app/components/wallpaper/wallpaper.story.tsx
@@ -0,0 +1,16 @@
+import * as React from "react"
+import { storiesOf } from "@storybook/react-native"
+import { StoryScreen, Story, UseCase } from "../../../storybook/views"
+import { Wallpaper } from "./wallpaper"
+
+declare let module
+
+storiesOf("Wallpaper", module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add("Style Presets", () => (
+ <Story>
+ <UseCase text="default/stretch" usage="Full screen wallpaper image.">
+ <Wallpaper />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app/components/wallpaper/wallpaper.tsx b/app/components/wallpaper/wallpaper.tsx
new file mode 100644
index 0000000..ebba75a
--- /dev/null
+++ b/app/components/wallpaper/wallpaper.tsx
@@ -0,0 +1,25 @@
+import React from "react"
+import { AutoImage as Image } from "../auto-image/auto-image"
+import { presets } from "./wallpaper.presets"
+import { WallpaperProps } from "./wallpaper.props"
+
+const defaultImage = require("./bg.png")
+
+/**
+ * For your text displaying needs.
+ *
+ * This component is a HOC over the built-in React Native one.
+ */
+export function Wallpaper(props: WallpaperProps) {
+ // grab the props
+ const { preset = "stretch", style: styleOverride, backgroundImage } = props
+
+ // assemble the style
+ const presetToUse = presets[preset] || presets.stretch
+ const styles = [presetToUse, styleOverride]
+
+ // figure out which image to use
+ const source = backgroundImage || defaultImage
+
+ return <Image source={source} style={styles} />
+}