summaryrefslogtreecommitdiff
path: root/app_expo/components
diff options
context:
space:
mode:
Diffstat (limited to 'app_expo/components')
-rw-r--r--app_expo/components/auto-image/auto-image.story.tsx33
-rw-r--r--app_expo/components/auto-image/auto-image.tsx46
-rw-r--r--app_expo/components/bullet-item/bullet-item.tsx41
-rw-r--r--app_expo/components/button/button.presets.ts58
-rw-r--r--app_expo/components/button/button.props.ts35
-rw-r--r--app_expo/components/button/button.story.tsx33
-rw-r--r--app_expo/components/button/button.tsx36
-rw-r--r--app_expo/components/checkbox/checkbox.props.ts44
-rw-r--r--app_expo/components/checkbox/checkbox.story.tsx121
-rw-r--r--app_expo/components/checkbox/checkbox.tsx53
-rw-r--r--app_expo/components/form-row/form-row.presets.ts71
-rw-r--r--app_expo/components/form-row/form-row.props.tsx23
-rw-r--r--app_expo/components/form-row/form-row.story.tsx107
-rw-r--r--app_expo/components/form-row/form-row.tsx13
-rw-r--r--app_expo/components/graph-ui/graph-ui.story.tsx15
-rw-r--r--app_expo/components/graph-ui/graph-ui.tsx48
-rw-r--r--app_expo/components/graph/graph.story.tsx15
-rw-r--r--app_expo/components/graph/graph.tsx529
-rw-r--r--app_expo/components/graph/graphbak.tsx448
-rw-r--r--app_expo/components/graph/graphgood.tsx440
-rw-r--r--app_expo/components/header/header.props.ts45
-rw-r--r--app_expo/components/header/header.story.tsx43
-rw-r--r--app_expo/components/header/header.tsx61
-rw-r--r--app_expo/components/icon/icon.props.ts21
-rw-r--r--app_expo/components/icon/icon.story.tsx19
-rw-r--r--app_expo/components/icon/icon.tsx19
-rw-r--r--app_expo/components/icon/icons/arrow-left.pngbin0 -> 329 bytes
-rw-r--r--app_expo/components/icon/icons/[email protected]bin0 -> 329 bytes
-rw-r--r--app_expo/components/icon/icons/bullet.pngbin0 -> 204 bytes
-rw-r--r--app_expo/components/icon/icons/[email protected]bin0 -> 204 bytes
-rw-r--r--app_expo/components/icon/icons/index.ts6
-rw-r--r--app_expo/components/index.ts16
-rw-r--r--app_expo/components/local-button/local-button.story.tsx15
-rw-r--r--app_expo/components/local-button/local-button.tsx50
-rw-r--r--app_expo/components/local/local.story.tsx15
-rw-r--r--app_expo/components/local/local.tsx36
-rw-r--r--app_expo/components/screen/screen.presets.ts66
-rw-r--r--app_expo/components/screen/screen.props.ts46
-rw-r--r--app_expo/components/screen/screen.tsx66
-rw-r--r--app_expo/components/switch/switch.props.ts39
-rw-r--r--app_expo/components/switch/switch.story.tsx116
-rw-r--r--app_expo/components/switch/switch.tsx114
-rw-r--r--app_expo/components/text-field/text-field.story.tsx159
-rw-r--r--app_expo/components/text-field/text-field.tsx98
-rw-r--r--app_expo/components/text/text.presets.ts48
-rw-r--r--app_expo/components/text/text.props.ts37
-rw-r--r--app_expo/components/text/text.story.tsx92
-rw-r--r--app_expo/components/text/text.tsx28
-rw-r--r--app_expo/components/tweaks/tweaks.story.tsx15
-rw-r--r--app_expo/components/tweaks/tweaks.tsx617
-rw-r--r--app_expo/components/wallpaper/wallpaper.presets.ts34
-rw-r--r--app_expo/components/wallpaper/wallpaper.props.ts19
-rw-r--r--app_expo/components/wallpaper/wallpaper.story.tsx16
-rw-r--r--app_expo/components/wallpaper/wallpaper.tsx25
54 files changed, 4190 insertions, 0 deletions
diff --git a/app_expo/components/auto-image/auto-image.story.tsx b/app_expo/components/auto-image/auto-image.story.tsx
new file mode 100644
index 0000000..af74efe
--- /dev/null
+++ b/app_expo/components/auto-image/auto-image.story.tsx
@@ -0,0 +1,33 @@
+/* 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_expo/components/auto-image/auto-image.tsx b/app_expo/components/auto-image/auto-image.tsx
new file mode 100644
index 0000000..a8bfe37
--- /dev/null
+++ b/app_expo/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_expo/components/bullet-item/bullet-item.tsx b/app_expo/components/bullet-item/bullet-item.tsx
new file mode 100644
index 0000000..f6b2f17
--- /dev/null
+++ b/app_expo/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_expo/components/button/button.presets.ts b/app_expo/components/button/button.presets.ts
new file mode 100644
index 0000000..bc0ad3f
--- /dev/null
+++ b/app_expo/components/button/button.presets.ts
@@ -0,0 +1,58 @@
+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_expo/components/button/button.props.ts b/app_expo/components/button/button.props.ts
new file mode 100644
index 0000000..810e0aa
--- /dev/null
+++ b/app_expo/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_expo/components/button/button.story.tsx b/app_expo/components/button/button.story.tsx
new file mode 100644
index 0000000..54dc2a9
--- /dev/null
+++ b/app_expo/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_expo/components/button/button.tsx b/app_expo/components/button/button.tsx
new file mode 100644
index 0000000..03b8f85
--- /dev/null
+++ b/app_expo/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_expo/components/checkbox/checkbox.props.ts b/app_expo/components/checkbox/checkbox.props.ts
new file mode 100644
index 0000000..4a212e3
--- /dev/null
+++ b/app_expo/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_expo/components/checkbox/checkbox.story.tsx b/app_expo/components/checkbox/checkbox.story.tsx
new file mode 100644
index 0000000..a6dce83
--- /dev/null
+++ b/app_expo/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_expo/components/checkbox/checkbox.tsx b/app_expo/components/checkbox/checkbox.tsx
new file mode 100644
index 0000000..fba748e
--- /dev/null
+++ b/app_expo/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_expo/components/form-row/form-row.presets.ts b/app_expo/components/form-row/form-row.presets.ts
new file mode 100644
index 0000000..0c796c2
--- /dev/null
+++ b/app_expo/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_expo/components/form-row/form-row.props.tsx b/app_expo/components/form-row/form-row.props.tsx
new file mode 100644
index 0000000..55b632e
--- /dev/null
+++ b/app_expo/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_expo/components/form-row/form-row.story.tsx b/app_expo/components/form-row/form-row.story.tsx
new file mode 100644
index 0000000..ea84e04
--- /dev/null
+++ b/app_expo/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_expo/components/form-row/form-row.tsx b/app_expo/components/form-row/form-row.tsx
new file mode 100644
index 0000000..c6453bc
--- /dev/null
+++ b/app_expo/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_expo/components/graph-ui/graph-ui.story.tsx b/app_expo/components/graph-ui/graph-ui.story.tsx
new file mode 100644
index 0000000..7564c93
--- /dev/null
+++ b/app_expo/components/graph-ui/graph-ui.story.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react'
+import { storiesOf } from '@storybook/react-native'
+import { StoryScreen, Story, UseCase } from '../../../storybook/views'
+import { color } from '../../theme'
+import { GraphUi } from './graph-ui'
+
+storiesOf('GraphUi', module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add('Style Presets', () => (
+ <Story>
+ <UseCase text="Primary" usage="The primary.">
+ <GraphUi style={{ backgroundColor: color.error }} />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app_expo/components/graph-ui/graph-ui.tsx b/app_expo/components/graph-ui/graph-ui.tsx
new file mode 100644
index 0000000..36a675a
--- /dev/null
+++ b/app_expo/components/graph-ui/graph-ui.tsx
@@ -0,0 +1,48 @@
+import * as React from 'react'
+import { StyleProp, TextStyle, View, ViewStyle } from 'react-native'
+import { observer } from 'mobx-react-lite'
+import { color, typography } from '../../theme'
+import { LocalButton, Text, Tweaks } from '../'
+import { flatten } from 'ramda'
+
+const CONTAINER: ViewStyle = {
+ justifyContent: 'center',
+}
+
+const TEXT: TextStyle = {
+ fontFamily: typography.primary,
+ fontSize: 14,
+ color: color.primary,
+}
+
+export interface GraphUiProps {
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<ViewStyle>
+ physics
+ setPhysics
+}
+
+/**
+ * Describe your component here
+ */
+export const GraphUi = observer(function GraphUi(props: GraphUiProps) {
+ const { style, physics, setPhysics } = props
+ const styles = flatten([CONTAINER, style])
+
+ return (
+ <View
+ style={{
+ height: '100%',
+ width: '100%',
+ borderStyle: 'solid',
+ borderWidth: 5,
+ position: 'relative',
+ }}
+ >
+ <Tweaks physics={physics} setPhysics={setPhysics} />
+ <LocalButton physics={physics} setPhysics={setPhysics} />
+ </View>
+ )
+})
diff --git a/app_expo/components/graph/graph.story.tsx b/app_expo/components/graph/graph.story.tsx
new file mode 100644
index 0000000..3b094d9
--- /dev/null
+++ b/app_expo/components/graph/graph.story.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react'
+import { storiesOf } from '@storybook/react-native'
+import { StoryScreen, Story, UseCase } from '../../../storybook/views'
+import { color } from '../../theme'
+import { Graph } from './graph'
+
+storiesOf('Graph', module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add('Style Presets', () => (
+ <Story>
+ <UseCase text="Primary" usage="The primary.">
+ <Graph style={{ backgroundColor: color.error }} />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app_expo/components/graph/graph.tsx b/app_expo/components/graph/graph.tsx
new file mode 100644
index 0000000..0c959ec
--- /dev/null
+++ b/app_expo/components/graph/graph.tsx
@@ -0,0 +1,529 @@
+import * as React from 'react'
+import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
+import { StyleProp, TextStyle, View, ViewStyle } from 'react-native'
+import { observer } from 'mobx-react-lite'
+import { color, typography } from '../../theme'
+import { Text } from '../'
+import { flatten } from 'ramda'
+
+//import data from "../../data/miserables.json"
+//import genRandomTree from "../../data/randomdata";
+//import gData from "../../data/rando.json"
+
+import { ForceGraph2D, ForceGraph3D, ForceGraphVR, ForceGraphAR } from 'react-force-graph'
+import * as d3 from 'd3-force-3d'
+//import * as three from "three"
+import SpriteText from 'three-spritetext'
+
+const CONTAINER: ViewStyle = {
+ justifyContent: 'center',
+}
+
+const TEXT: TextStyle = {
+ fontFamily: typography.primary,
+ fontSize: 14,
+ color: color.primary,
+}
+
+export interface GraphProps {
+ style?: StyleProp<ViewStyle>
+ physics
+ gData
+ setPhysics
+ nodeIds: string[]
+ threeDim
+ setThreeDim
+ local
+ setLocal
+}
+
+export const Graph = observer(function Graph(props: GraphProps): JSX.Element {
+ const { style, physics, setPhysics, gData, threeDim, setThreeDim, local, setLocal } = props
+ const styles = flatten([CONTAINER, style])
+
+ const fgRef = useRef()
+
+ const GROUPS: number = 12
+ const NODE_R: number = 8
+ //const gData = genRandomTree(200);
+
+ //const [charge, setCharge] = useState(-30);
+ //const [link, setLink] = useState(-30);
+
+ useEffect(() => {
+ const fg = fgRef.current
+ //fg.d3Force('center').strength(0.05);
+ if (physics.gravityOn) {
+ fg.d3Force('x', d3.forceX().strength(physics.gravity))
+ fg.d3Force('y', d3.forceY().strength(physics.gravity))
+ if (threeDim) {
+ if (physics.galaxy) {
+ fg.d3Force('x', d3.forceX().strength(physics.gravity / 5))
+ fg.d3Force('z', d3.forceZ().strength(physics.gravity / 5))
+ } else {
+ fg.d3Force('x', d3.forceX().strength(physics.gravity))
+ fg.d3Force('z', d3.forceZ().strength(physics.gravity))
+ }
+ } else {
+ fg.d3Force('z', null)
+ }
+ } else {
+ fg.d3Force('x', null)
+ fg.d3Force('y', null)
+ threeDim ? fg.d3Force('z', null) : null
+ }
+ fg.d3Force('link').strength(physics.linkStrength)
+ fg.d3Force('link').iterations(physics.linkIts)
+ physics.collision
+ ? fg.d3Force('collide', d3.forceCollide().radius(20))
+ : fg.d3Force('collide', null)
+ fg.d3Force('charge').strength(physics.charge)
+ })
+
+ // For the expandable version of the graph
+
+ /* const nodesById = useMemo(() => {
+ * const nodesById = Object.fromEntries(gData.nodes.map((node) => [node.index, node]))
+ * console.log(nodesById)
+ * // link parent/children
+ * gData.nodes.forEach((node) => {
+ * typeof physics.rootId === "number"
+ * ? (node.collapsed = node.index !== physics.rootId)
+ * : (node.collapsed = node.id !== physics.rootId)
+ * node.childLinks = []
+ * })
+ * gData.links.forEach((link) => nodesById[link.sourceIndex].childLinks.push(link))
+ * return nodesById
+ * }, [gData])
+ * const getPrunedTree = useCallback(() => {
+ * const visibleNodes = []
+ * const visibleLinks = []
+ * ;(function traverseTree(node = nodesById[physics.rootId]) {
+ * visibleNodes.push(node)
+ * if (node.collapsed) return
+ * visibleLinks.push(...node.childLinks)
+ * node.childLinks
+ * .map((link) =>
+ * typeof link.targetIndex === "object" ? link.targetIndex : nodesById[link.targetIndex],
+ * ) // get child node
+ * .forEach(traverseTree)
+ * })()
+
+ * return { nodes: visibleNodes, links: visibleLinks }
+ * }, [nodesById])
+ * const [prunedTree, setPrunedTree] = useState(getPrunedTree())
+ */
+ const handleNodeClick = useCallback((node) => {
+ node.collapsed = !node.collapsed // toggle collapse state
+ setPrunedTree(getPrunedTree())
+ }, [])
+
+ //highlighting
+ const [highlightNodes, setHighlightNodes] = useState(new Set())
+ const [highlightLinks, setHighlightLinks] = useState(new Set())
+ const [hoverNode, setHoverNode] = useState(null)
+
+ const updateHighlight = () => {
+ setHighlightNodes(highlightNodes)
+ setHighlightLinks(highlightLinks)
+ }
+
+ const handleBackgroundClick = (event) => {
+ highlightNodes.clear()
+ highlightLinks.clear()
+
+ setSelectedNode(null)
+ updateHighlight()
+ }
+
+ const handleNodeHover = (node) => {
+ console.log('hover')
+ if (!selectedNode) {
+ highlightNodes.clear()
+ highlightLinks.clear()
+ if (node) {
+ highlightNodes.add(node)
+ node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor))
+ node.links.forEach((link) => highlightLinks.add(link))
+ }
+
+ setHoverNode(node || null)
+ updateHighlight()
+ }
+ }
+
+ const handleLinkHover = (link) => {
+ highlightNodes.clear()
+ highlightLinks.clear()
+
+ if (link) {
+ highlightLinks.add(link)
+ highlightNodes.add(link.source)
+ highlightNodes.add(link.target)
+ }
+
+ updateHighlight()
+ }
+
+ // Normally the graph doesn't update when you just change the physics parameters
+ // This forces the graph to make a small update when you do
+ useEffect(() => {
+ fgRef.current.d3ReheatSimulation()
+ }, [physics])
+ /* const paintRing = useCallback((node, ctx) => {
+ * // add ring just for highlighted nodes
+ * ctx.beginPath();
+ * ctx.arc(node.x, node.y, NODE_R * 1.4, 0, 2 * Math.PI, false);
+ * ctx.fillStyle = node === hoverNode ? 'red' : 'orange';
+ * ctx.fill();
+ * }, [hoverNode]);
+ */
+
+ /* autoPauseRedraw={false}
+linkWidth={link => highlightLinks.has(link) ? 5 : 1}
+linkDirectionalParticles={4}
+linkDirectionalParticleWidth={link => highlightLinks.has(link) ? 4 : 0}
+nodeCanvasObjectMode={node => highlightNodes.has(node) ? 'before' : undefined}
+nodeCanvasObject={paintRing}
+onNodeHover={handleNodeHover}
+onLinkHover={handleLinkHover}
+ nodeRelSize={NODE_R} */
+
+ //nodeColor={(node) =>
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ //}
+
+ const [selectedNode, setSelectedNode] = useState({})
+
+ //shitty handler to check for doubleClicks
+ const [doubleClick, setDoubleClick] = useState(0)
+ const [localGraphData, setLocalGraphData] = useState({
+ nodes: [],
+ links: [],
+ })
+
+ useEffect(() => {
+ localGraphData.nodes.length && !local && setLocal(true)
+ }, [localGraphData])
+
+ const getLocalGraphData = (node) => {
+ console.log(localGraphData)
+ localGraphData.nodes.length ? setLocalGraphData({ nodes: [], links: [] }) : null
+ let g = localGraphData
+ console.log(g.nodes)
+ if (!node.local) {
+ g = { nodes: [], links: [] }
+ console.log('length is 0')
+ node.local = true //keep track of these boys
+ g.nodes.push(node) //only add the clicked node if its the first
+ }
+ node.links.length &&
+ node.links.forEach((neighborLink) => {
+ if (!neighborLink.local) {
+ console.log('0')
+ neighborLink.local = true
+ g.links.push(neighborLink)
+ console.log(neighborLink)
+ const targetNode = gData.nodes[neighborLink.targetIndex]
+ const sourceNode = gData.nodes[neighborLink.sourceIndex]
+ if (targetNode.id !== sourceNode.id) {
+ if (targetNode.id === node.id) {
+ console.log('1. I am the target, the source is ')
+ console.log(sourceNode)
+ if (!sourceNode.local) {
+ console.log('2. The source is not local')
+ sourceNode.local = true
+ g.nodes.push(sourceNode)
+ } else {
+ console.log('2.5 The source is already local')
+ }
+ } else {
+ console.log('3. I am the source')
+ if (!targetNode.local) {
+ console.log('4. The target is not local.')
+ targetNode.local = true
+ g.nodes.push(targetNode)
+ } else {
+ console.log('The target is already local')
+ }
+ }
+ }
+ }
+ })
+ setLocalGraphData(g)
+ }
+
+ const selectClick = (node, event) => {
+ window.open('org-protocol://roam-node?node=' + node.id, '_self')
+ highlightNodes.clear()
+ highlightLinks.clear()
+ console.log(localGraphData)
+ if (event.timeStamp - doubleClick < 400) {
+ getLocalGraphData(node)
+ }
+ if (node) {
+ highlightNodes.add(node)
+ node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor))
+ node.links.forEach((link) => highlightLinks.add(link))
+ }
+
+ setSelectedNode(node || null)
+ updateHighlight()
+ setDoubleClick(event.timeStamp)
+ }
+
+ useEffect(() => {
+ if (local && selectedNode) {
+ getLocalGraphData(selectedNode)
+ }
+ }, [local])
+ return (
+ <View style={style}>
+ {!threeDim ? (
+ <ForceGraph2D
+ ref={fgRef}
+ //autoPauseRedraw={false}
+ //graphData={gData}
+ graphData={local ? localGraphData : gData}
+ //nodeAutoColorBy={physics.colorful ? (node)=>node.index%GROUPS : undefined}
+ nodeColor={
+ !physics.colorful
+ ? (node) => {
+ if (highlightNodes.size === 0) {
+ return 'rgb(100, 100, 100, 1)'
+ } else {
+ return highlightNodes.has(node) ? '#a991f1' : 'rgb(50, 50, 50, 0.5)'
+ }
+ }
+ : (node) => {
+ if (node.neighbors.length === 1 || node.neighbors.length === 2) {
+ return [
+ '#ff665c',
+ '#e69055',
+ '#7bc275',
+ '#4db5bd',
+ '#FCCE7B',
+ '#51afef',
+ '#1f5582',
+ '#C57BDB',
+ '#a991f1',
+ '#5cEfFF',
+ '#6A8FBF',
+ ][node.neighbors[0].index % 11]
+ } else {
+ return [
+ '#ff665c',
+ '#e69055',
+ '#7bc275',
+ '#4db5bd',
+ '#FCCE7B',
+ '#51afef',
+ '#1f5582',
+ '#C57BDB',
+ '#a991f1',
+ '#5cEfFF',
+ '#6A8FBF',
+ ][node.index % 11]
+ }
+ }
+ }
+ //linkAutoColorBy={physics.colorful ? ((d) => gData.nodes[d.sourceIndex].id % GROUPS) : undefined}
+ linkColor={
+ !physics.colorful
+ ? (link) => {
+ if (highlightLinks.size === 0) {
+ return 'rgb(50, 50, 50, 0.8)'
+ } else {
+ return highlightLinks.has(link) ? '#a991f1' : 'rgb(50, 50, 50, 0.2)'
+ }
+ }
+ : (link) =>
+ [
+ '#ff665c',
+ '#e69055',
+ '#7bc275',
+ '#4db5bd',
+ '#FCCE7B',
+ '#51afef',
+ '#1f5582',
+ '#C57BDB',
+ '#a991f1',
+ '#5cEfFF',
+ '#6A8FBF',
+ ][gData.nodes[link.sourceIndex].index % 11]
+ }
+ linkDirectionalParticles={physics.particles}
+ onNodeClick={selectClick}
+ nodeLabel={(node) => node.title}
+ linkWidth={(link) =>
+ highlightLinks.has(link) ? 3 * physics.linkWidth : physics.linkWidth
+ }
+ linkOpacity={physics.linkOpacity}
+ nodeRelSize={physics.nodeRel}
+ nodeVal={(node) => {
+ return highlightNodes.has(node) ? node.neighbors.length + 5 : node.neighbors.length + 3
+ }}
+ linkDirectionalParticleWidth={physics.particleWidth}
+ nodeCanvasObject={(node, ctx, globalScale) => {
+ if (physics.labels) {
+ if (globalScale > physics.labelScale || highlightNodes.has(node)) {
+ const label = node.title.substring(0, Math.min(node.title.length, 30))
+ const fontSize = 12 / globalScale
+ ctx.font = `${fontSize}px Sans-Serif`
+ const textWidth = ctx.measureText(label).width
+ const bckgDimensions = [textWidth * 1.1, fontSize].map((n) => n + fontSize * 0.5) // some padding
+ const fadeFactor = Math.min(
+ (3 * (globalScale - physics.labelScale)) / physics.labelScale,
+ 1,
+ )
+
+ ctx.fillStyle =
+ 'rgba(20, 20, 20, ' +
+ (highlightNodes.size === 0
+ ? 0.5 * fadeFactor
+ : highlightNodes.has(node)
+ ? 0.5
+ : 0.15 * fadeFactor) +
+ ')'
+ ctx.fillRect(
+ node.x - bckgDimensions[0] / 2,
+ node.y - bckgDimensions[1] / 2,
+ ...bckgDimensions,
+ )
+
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ ctx.fillStyle =
+ 'rgb(255, 255, 255, ' +
+ (highlightNodes.size === 0
+ ? fadeFactor
+ : highlightNodes.has(node)
+ ? 1
+ : 0.3 * fadeFactor) +
+ ')'
+ ctx.fillText(label, node.x, node.y)
+
+ node.__bckgDimensions = bckgDimensions // to re-use in nodePointerAreaPaint
+ }
+ }
+ }}
+ nodeCanvasObjectMode={() => 'after'}
+ onNodeHover={physics.hover ? handleNodeHover : null}
+ //onLinkHover={physics.hover ? handleLinkHover : null}
+ d3AlphaDecay={physics.alphaDecay}
+ d3AlphaMin={physics.alphaTarget}
+ d3VelocityDecay={physics.velocityDecay}
+ onBackgroundClick={handleBackgroundClick}
+ backgroundColor={'#242730'}
+ />
+ ) : (
+ <ForceGraph3D
+ ref={fgRef}
+ graphData={!local ? gData : localGraphData}
+ //graphData={gData}
+ nodeColor={
+ !physics.colorful
+ ? (node) => {
+ if (highlightNodes.size === 0) {
+ return 'rgb(100, 100, 100, 1)'
+ } else {
+ return highlightNodes.has(node) ? 'purple' : 'rgb(50, 50, 50, 0.5)'
+ }
+ }
+ : (node) => {
+ if (node.neighbors.length === 1 || node.neighbors.length === 2) {
+ return [
+ '#ff665c',
+ '#e69055',
+ '#7bc275',
+ '#4db5bd',
+ '#FCCE7B',
+ '#51afef',
+ '#1f5582',
+ '#C57BDB',
+ '#a991f1',
+ '#5cEfFF',
+ '#6A8FBF',
+ ][node.neighbors[0].index % 11]
+ } else {
+ return [
+ '#ff665c',
+ '#e69055',
+ '#7bc275',
+ '#4db5bd',
+ '#FCCE7B',
+ '#51afef',
+ '#1f5582',
+ '#C57BDB',
+ '#a991f1',
+ '#5cEfFF',
+ '#6A8FBF',
+ ][node.index % 11]
+ }
+ }
+ }
+ //linkAutoColorBy={physics.colorful ? ((d) => gData.nodes[d.sourceIndex].id % GROUPS) : undefined}
+ linkColor={
+ !physics.colorful
+ ? (link) => {
+ if (highlightLinks.size === 0) {
+ return 'rgb(50, 50, 50, 0.8)'
+ } else {
+ return highlightLinks.has(link) ? 'purple' : 'rgb(50, 50, 50, 0.2)'
+ }
+ }
+ : (link) =>
+ [
+ '#ff665c',
+ '#e69055',
+ '#7bc275',
+ '#4db5bd',
+ '#FCCE7B',
+ '#51afef',
+ '#1f5582',
+ '#C57BDB',
+ '#a991f1',
+ '#5cEfFF',
+ '#6A8FBF',
+ ][gData.nodes[link.sourceIndex].index % 11]
+ }
+ linkDirectionalParticles={physics.particles}
+ nodeLabel={(node) => node.title}
+ linkWidth={(link) =>
+ highlightLinks.has(link) ? 3 * physics.linkWidth : physics.linkWidth
+ }
+ linkOpacity={physics.linkOpacity}
+ nodeRelSize={physics.nodeRel}
+ nodeVal={(node) =>
+ highlightNodes.has(node) ? node.neighbors.length * 3 : node.neighbors.length * 2
+ }
+ linkDirectionalParticleWidth={physics.particleWidth}
+ onNodeHover={physics.hover ? handleNodeHover : null}
+ d3AlphaDecay={physics.alphaDecay}
+ d3AlphaMin={physics.alphaTarget}
+ d3VelocityDecay={physics.velocityDecay}
+ nodeThreeObject={
+ !physics.labels
+ ? undefined
+ : (node) => {
+ if (highlightNodes.has(node)) {
+ console.log(node.title)
+ const sprite = new SpriteText(node.title.substring(0, 30))
+ console.log('didnt crash here 2')
+ sprite.color = '#ffffff'
+ sprite.textHeight = 8
+ return sprite
+ } else {
+ return undefined
+ }
+ }
+ }
+ nodeThreeObjectExtend={true}
+ onNodeClick={selectClick}
+ onBackgroundClick={handleBackgroundClick}
+ backgroundColor={'#242730'}
+ />
+ )}
+ </View>
+ )
+})
diff --git a/app_expo/components/graph/graphbak.tsx b/app_expo/components/graph/graphbak.tsx
new file mode 100644
index 0000000..5291678
--- /dev/null
+++ b/app_expo/components/graph/graphbak.tsx
@@ -0,0 +1,448 @@
+
+import * as React from "react"
+import { useState, useEffect, useRef, useMemo, useCallback } from "react"
+import { StyleProp, TextStyle, View, ViewStyle } from "react-native"
+import { observer } from "mobx-react-lite"
+import { color, typography } from "../../theme"
+import { Text } from "../"
+import { flatten } from "ramda"
+
+//import data from "../../data/miserables.json"
+//import genRandomTree from "../../data/randomdata";
+//import rando from "../../data/rando.json"
+
+import { ForceGraph2D, ForceGraph3D, ForceGraphVR, ForceGraphAR } from "react-force-graph"
+import * as d3 from "d3-force-3d"
+import * as three from "three"
+import SpriteText from "three-spritetext"
+
+const CONTAINER: ViewStyle = {
+ justifyContent: "center",
+}
+
+const TEXT: TextStyle = {
+ fontFamily: typography.primary,
+ fontSize: 14,
+ color: color.primary,
+}
+
+export interface GraphProps {
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<ViewStyle>
+ physics
+ gData
+ setPhysics
+ nodeIds: string[]
+}
+
+/**
+ * Describe your component here
+ */
+export const Graph = observer(function Graph(props: GraphProps): JSX.Element {
+ const { style, physics, setPhysics, gData, nodeIds } = props
+ const styles = flatten([CONTAINER, style])
+
+ const fgRef = useRef()
+
+ const GROUPS: number = 12
+ const NODE_R: number = 8
+ //const gData = genRandomTree(200);
+
+ //const [charge, setCharge] = useState(-30);
+ //const [link, setLink] = useState(-30);
+
+ useEffect(() => {
+ const fg = fgRef.current
+ //fg.d3Force('center').strength(0.05);
+ if (physics.gravityOn) {
+ fg.d3Force("x", d3.forceX().strength(physics.gravity))
+ fg.d3Force("y", d3.forceY().strength(physics.gravity))
+ if (physics.threedim) {
+ if (physics.galaxy) {
+ fg.d3Force("y", d3.forceY().strength(physics.gravity / 5))
+ fg.d3Force("z", d3.forceZ().strength(physics.gravity / 5))
+ } else {
+ fg.d3Force("y", d3.forceY().strength(physics.gravity))
+ fg.d3Force("z", d3.forceZ().strength(physics.gravity))
+ }
+ } else {
+ fg.d3Force("z", null)
+ }
+ } else {
+ fg.d3Force("x", null)
+ fg.d3Force("y", null)
+ physics.threedim ? fg.d3Force("z", null) : null
+ }
+ fg.d3Force("link").strength(physics.linkStrength)
+ fg.d3Force("link").iterations(physics.linkIts)
+ physics.collision
+ ? fg.d3Force("collide", d3.forceCollide().radius(20))
+ : fg.d3Force("collide", null)
+ fg.d3Force("charge").strength(physics.charge)
+ })
+
+ // For the expandable version of the graph
+
+ const nodesById = useMemo(() => {
+ const nodesById = Object.fromEntries(gData.nodes.map((node) => [node.index, node]))
+ console.log(nodesById)
+ // link parent/children
+ gData.nodes.forEach((node) => {
+ typeof physics.rootId === "number"
+ ? (node.collapsed = node.index !== physics.rootId)
+ : (node.collapsed = node.id !== physics.rootId)
+ node.childLinks = []
+ })
+ gData.links.forEach((link) => nodesById[link.sourceIndex].childLinks.push(link))
+ return nodesById
+ }, [gData])
+
+ const getPrunedTree = useCallback(() => {
+ const visibleNodes = []
+ const visibleLinks = []
+ ;(function traverseTree(node = nodesById[physics.rootId]) {
+ visibleNodes.push(node)
+ if (node.collapsed) return
+ visibleLinks.push(...node.childLinks)
+ node.childLinks
+ .map((link) =>
+ typeof link.targetIndex === "object" ? link.targetIndex : nodesById[link.targetIndex],
+ ) // get child node
+ .forEach(traverseTree)
+ })()
+
+ return { nodes: visibleNodes, links: visibleLinks }
+ }, [nodesById])
+
+ const [prunedTree, setPrunedTree] = useState(getPrunedTree())
+
+ const handleNodeClick = useCallback((node) => {
+ node.collapsed = !node.collapsed // toggle collapse state
+ setPrunedTree(getPrunedTree())
+ }, [])
+
+ //highlighting
+ const [highlightNodes, setHighlightNodes] = useState(new Set())
+ const [highlightLinks, setHighlightLinks] = useState(new Set())
+ const [hoverNode, setHoverNode] = useState(null)
+
+ const updateHighlight = () => {
+ setHighlightNodes(highlightNodes)
+ setHighlightLinks(highlightLinks)
+ }
+
+ const handleBackgroundClick = (event) => {
+ highlightNodes.clear()
+ highlightLinks.clear()
+
+ setSelectedNode(null)
+ updateHighlight()
+ }
+
+ const handleNodeHover = (node) => {
+ console.log("hover")
+ if (!selectedNode) {
+ highlightNodes.clear()
+ highlightLinks.clear()
+ if (node) {
+ highlightNodes.add(node)
+ node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor))
+ node.links.forEach((link) => highlightLinks.add(link))
+ }
+
+ setHoverNode(node || null)
+ updateHighlight()
+ }
+ }
+
+ const handleLinkHover = (link) => {
+ highlightNodes.clear()
+ highlightLinks.clear()
+
+ if (link) {
+ highlightLinks.add(link)
+ highlightNodes.add(link.source)
+ highlightNodes.add(link.target)
+ }
+
+ updateHighlight()
+ }
+
+ // Normally the graph doesn't update when you just change the physics parameters
+ // This forces the graph to make a small update when you do
+ useEffect(() => {
+ fgRef.current.d3ReheatSimulation()
+ }, [physics])
+ /* const paintRing = useCallback((node, ctx) => {
+ * // add ring just for highlighted nodes
+ * ctx.beginPath();
+ * ctx.arc(node.x, node.y, NODE_R * 1.4, 0, 2 * Math.PI, false);
+ * ctx.fillStyle = node === hoverNode ? 'red' : 'orange';
+ * ctx.fill();
+ * }, [hoverNode]);
+ */
+
+ /* autoPauseRedraw={false}
+linkWidth={link => highlightLinks.has(link) ? 5 : 1}
+linkDirectionalParticles={4}
+linkDirectionalParticleWidth={link => highlightLinks.has(link) ? 4 : 0}
+nodeCanvasObjectMode={node => highlightNodes.has(node) ? 'before' : undefined}
+nodeCanvasObject={paintRing}
+onNodeHover={handleNodeHover}
+onLinkHover={handleLinkHover}
+ nodeRelSize={NODE_R} */
+
+ //nodeColor={(node) =>
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ //}
+
+ const [selectedNode, setSelectedNode] = useState({})
+
+ //shitty handler to check for doubleClicks
+ const [doubleClick, setDoubleClick] = useState(0)
+ const [localGraphData, setLocalGraphData] = useState({ nodes: [], links: [] })
+
+ useEffect(() => {
+ !physics.local && setPhysics({ ...physics, local: true })
+ }, [localGraphData])
+
+ const getLocalGraphData = (node) {
+ console.log(localGraphData)
+ localGraphData.nodes.length ? setLocalGraphData({nodes: [], links: []}) : null;
+ let g = localGraphData
+ console.log(g.nodes)
+ if (g.nodes.length === 0) {
+ console.log("length is 0")
+ node.local = true //keep track of these boys
+ g.nodes.push(node) //only add the clicked node if its the first
+ }
+ node.links.forEach((neighborLink) => {
+ if (!neighborLink.local) {
+ console.log("0")
+ neighborLink.local = true
+ g.links.push(neighborLink)
+ const targetNode = gData.nodes[neighborLink.targetIndex]
+ const sourceNode = gData.nodes[neighborLink.sourceIndex]
+ if (targetNode.id === node.id) {
+ console.log("1. I am the target, the source is ")
+ console.log(sourceNode)
+ if (!sourceNode.local) {
+ console.log("2. The source is not local")
+ sourceNode.local = true
+ g.nodes.push(sourceNode)
+ } else {
+ console.log("2.5 The source is already local")
+ }
+ } else {
+ console.log("3. I am the source")
+ if (!targetNode.local) {
+ console.log("4. The target is not local.")
+ targetNode.local = true
+ g.nodes.push(targetNode)
+ } else {
+ console.log("The target is already local")
+ }
+ }
+ }
+ })
+ setLocalGraphData(g)
+ };
+
+ const selectClick = (node, event) => {
+ highlightNodes.clear()
+ highlightLinks.clear()
+ console.log(localGraphData)
+ if (event.timeStamp - doubleClick < 400) {
+ getLocalGraphData(node);
+ }
+ if (node) {
+ highlightNodes.add(node)
+ node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor))
+ node.links.forEach((link) => highlightLinks.add(link))
+ }
+
+ setSelectedNode(node || null)
+ updateHighlight()
+ setDoubleClick(event.timeStamp)
+ }
+
+ return (
+ <View>
+ {!physics.threedim ? (
+ <ForceGraph2D
+ ref={fgRef}
+ autoPauseRedraw={false}
+ //graphData={gData}
+ graphData={physics.local ? localGraphData : (physics.collapse ? prunedTree : gData)}
+ nodeAutoColorBy={physics.colorful ? "id" : undefined}
+ nodeColor={
+ !physics.colorful
+ ? (node) => {
+ if (highlightNodes.size === 0) {
+ return "rgb(100, 100, 100, 1)"
+ } else {
+ return highlightNodes.has(node) ? "purple" : "rgb(50, 50, 50, 0.5)"
+ }
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ : undefined
+ }
+ linkAutoColorBy={physics.colorful ? "target" : undefined}
+ //linkAutoColorBy={(d) => gData.nodes[d.source].id % GROUPS}
+ linkColor={
+ !physics.colorful
+ ? (link) => {
+ if (highlightLinks.size === 0) {
+ return "rgb(50, 50, 50, 0.8)"
+ } else {
+ return highlightLinks.has(link) ? "purple" : "rgb(50, 50, 50, 0.2)"
+ }
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ : undefined
+ //highlightLinks.has(link) ? "purple" : "grey"
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ linkDirectionalParticles={physics.particles}
+ onNodeClick={!physics.collapse ? selectClick : handleNodeClick}
+ nodeLabel={(node) => node.title}
+ //nodeVal ={(node)=> node.childLinks.length * 0.5 + 1}
+ //d3VelocityDecay={visco}
+ linkWidth={(link) =>
+ highlightLinks.has(link) ? 3 * physics.linkWidth : physics.linkWidth
+ }
+ linkOpacity={physics.linkOpacity}
+ nodeRelSize={physics.nodeRel}
+ nodeVal={(node) =>
+ highlightNodes.has(node) ? node.neighbors.length + 5 : node.neighbors.length + 3
+ }
+ linkDirectionalParticleWidth={physics.particleWidth}
+ nodeCanvasObject={(node, ctx, globalScale) => {
+ if (physics.labels) {
+ if (globalScale > physics.labelScale || highlightNodes.has(node)) {
+ const label = node.title.substring(0, Math.min(node.title.length, 30))
+ const fontSize = 12 / globalScale
+ ctx.font = `${fontSize}px Sans-Serif`
+ const textWidth = ctx.measureText(label).width
+ const bckgDimensions = [textWidth * 1.1, fontSize].map((n) => n + fontSize * 0.5) // some padding
+ const fadeFactor = Math.min(
+ (3 * (globalScale - physics.labelScale)) / physics.labelScale,
+ 1,
+ )
+
+ ctx.fillStyle =
+ "rgba(20, 20, 20, " +
+ (highlightNodes.size === 0
+ ? 0.5 * fadeFactor
+ : highlightNodes.has(node)
+ ? 0.5
+ : 0.15 * fadeFactor) +
+ ")"
+ ctx.fillRect(
+ node.x - bckgDimensions[0] / 2,
+ node.y - bckgDimensions[1] / 2,
+ ...bckgDimensions,
+ )
+
+ ctx.textAlign = "center"
+ ctx.textBaseline = "middle"
+ ctx.fillStyle =
+ "rgb(255, 255, 255, " +
+ (highlightNodes.size === 0
+ ? fadeFactor
+ : highlightNodes.has(node)
+ ? 1
+ : 0.3 * fadeFactor) +
+ ")"
+ ctx.fillText(label, node.x, node.y)
+
+ node.__bckgDimensions = bckgDimensions // to re-use in nodePointerAreaPaint
+ }
+ }
+ }}
+ nodeCanvasObjectMode={() => "after"}
+ onNodeHover={physics.hover ? handleNodeHover : null}
+ //onLinkHover={physics.hover ? handleLinkHover : null}
+ d3AlphaDecay={physics.alphaDecay}
+ d3AlphaMin={physics.alphaTarget}
+ d3VelocityDecay={physics.velocityDecay}
+ onBackgroundClick={handleBackgroundClick}
+ />
+ ) : (
+ <ForceGraph3D
+ ref={fgRef}
+ autoPauseRedraw={false}
+ graphData={gData}
+ //graphData={physics.collapse ? prunedTree : gData}
+ nodeAutoColorBy={physics.colorful ? "id" : undefined}
+ nodeColor={
+ !physics.colorful
+ ? (node) => {
+ if (highlightNodes.size === 0) {
+ return "rgb(100, 100, 100, 1)"
+ } else {
+ return highlightNodes.has(node) ? "purple" : "rgb(50, 50, 50, 0.5)"
+ }
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ : undefined
+ }
+ linkAutoColorBy={physics.colorful ? "target" : undefined}
+ //linkAutoColorBy={(d) => gData.nodes[d.source].id % GROUPS}
+ linkColor={
+ !physics.colorful
+ ? (link) => {
+ if (highlightLinks.size === 0) {
+ return "rgb(50, 50, 50, 0.8)"
+ } else {
+ return highlightLinks.has(link) ? "purple" : "rgb(50, 50, 50, 0.2)"
+ }
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ : undefined
+ //highlightLinks.has(link) ? "purple" : "grey"
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ linkDirectionalParticles={physics.particles}
+ //onNodeClick={!physics.collapse ? null : handleNodeClick}
+ nodeLabel={(node) => node.title}
+ //nodeVal ={(node)=> node.childLinks.length * 0.5 + 1}
+ //d3VelocityDecay={visco}
+ linkWidth={(link) =>
+ highlightLinks.has(link) ? 3 * physics.linkWidth : physics.linkWidth
+ }
+ linkOpacity={physics.linkOpacity}
+ nodeRelSize={physics.nodeRel}
+ nodeVal={(node) =>
+ highlightNodes.has(node) ? node.neighbors.length + 5 : node.neighbors.length + 3
+ }
+ linkDirectionalParticleWidth={physics.particleWidth}
+ onNodeHover={physics.hover ? handleNodeHover : null}
+ //onLinkHover={physics.hover ? handleLinkHover : null}
+ d3AlphaDecay={physics.alphaDecay}
+ d3AlphaMin={physics.alphaTarget}
+ d3VelocityDecay={physics.velocityDecay}
+ nodeThreeObject={
+ !physics.labels
+ ? undefined
+ : (node) => {
+ if (highlightNodes.has(node)) {
+ console.log(node.title)
+ const sprite = new SpriteText(node.title.substring(0, 30))
+ console.log("didnt crash here 2")
+ sprite.color = "#ffffff"
+ sprite.textHeight = 8
+ return sprite
+ } else {
+ return undefined
+ }
+ }
+ }
+ nodeThreeObjectExtend={true}
+ />
+ )}
+ </View>
+ )
+})
diff --git a/app_expo/components/graph/graphgood.tsx b/app_expo/components/graph/graphgood.tsx
new file mode 100644
index 0000000..4d70e25
--- /dev/null
+++ b/app_expo/components/graph/graphgood.tsx
@@ -0,0 +1,440 @@
+import * as React from 'react'
+import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
+import { StyleProp, TextStyle, View, ViewStyle } from 'react-native'
+import { observer } from 'mobx-react-lite'
+import { color, typography } from '../../theme'
+import { Text } from '../'
+import { flatten } from 'ramda'
+
+//import data from "../../data/miserables.json"
+//import genRandomTree from "../../data/randomdata";
+//import rando from "../../data/rando.json"
+
+import { ForceGraph2D, ForceGraph3D, ForceGraphVR, ForceGraphAR } from 'react-force-graph'
+import * as d3 from 'd3-force-3d'
+import * as three from 'three'
+import SpriteText from 'three-spritetext'
+
+const CONTAINER: ViewStyle = {
+ justifyContent: 'center',
+}
+
+const TEXT: TextStyle = {
+ fontFamily: typography.primary,
+ fontSize: 14,
+ color: color.primary,
+}
+
+export interface GraphProps {
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<ViewStyle>
+ physics
+ setPhysics
+ gData
+ nodeIds: string[]
+}
+
+/**
+ * Describe your component here
+ */
+export const Graph = observer(function Graph(props: GraphProps): JSX.Element {
+ const { style, physics, setPhysics, gData, nodeIds } = props
+ const styles = flatten([CONTAINER, style])
+
+ const fgRef = useRef()
+
+ const GROUPS: number = 12
+ const NODE_R: number = 8
+ //const gData = genRandomTree(200);
+
+ //const [charge, setCharge] = useState(-30);
+ //const [link, setLink] = useState(-30);
+
+ useEffect(() => {
+ const fg = fgRef.current
+ //fg.d3Force('center').strength(0.05);
+ if (physics.gravityOn) {
+ fg.d3Force('x', d3.forceX().strength(physics.gravity))
+ fg.d3Force('y', d3.forceY().strength(physics.gravity))
+ if (physics.threedim) {
+ if (physics.galaxy) {
+ fg.d3Force('y', d3.forceY().strength(physics.gravity / 5))
+ fg.d3Force('z', d3.forceZ().strength(physics.gravity / 5))
+ } else {
+ fg.d3Force('y', d3.forceY().strength(physics.gravity))
+ fg.d3Force('z', d3.forceZ().strength(physics.gravity))
+ }
+ } else {
+ fg.d3Force('z', null)
+ }
+ } else {
+ fg.d3Force('x', null)
+ fg.d3Force('y', null)
+ physics.threedim ? fg.d3Force('z', null) : null
+ }
+ fg.d3Force('link').strength(physics.linkStrength)
+ fg.d3Force('link').iterations(physics.linkIts)
+ physics.collision
+ ? fg.d3Force('collide', d3.forceCollide().radius(20))
+ : fg.d3Force('collide', null)
+ fg.d3Force('charge').strength(physics.charge)
+ })
+
+ // For the expandable version of the graph
+
+ const nodesById = useMemo(() => {
+ const nodesById = Object.fromEntries(gData.nodes.map((node) => [node.index, node]))
+ console.log(nodesById)
+ // link parent/children
+ gData.nodes.forEach((node) => {
+ typeof physics.rootId === 'number'
+ ? (node.collapsed = node.index !== physics.rootId)
+ : (node.collapsed = node.id !== physics.rootId)
+ node.childLinks = []
+ })
+ gData.links.forEach((link) => nodesById[link.sourceIndex].childLinks.push(link))
+ return nodesById
+ }, [gData])
+ const getPrunedTree = useCallback(() => {
+ const visibleNodes = []
+ const visibleLinks = []
+ ;(function traverseTree(node = nodesById[physics.rootId]) {
+ visibleNodes.push(node)
+ if (node.collapsed) return
+ visibleLinks.push(...node.childLinks)
+ node.childLinks
+ .map((link) =>
+ typeof link.targetIndex === 'object' ? link.targetIndex : nodesById[link.targetIndex],
+ ) // get child node
+ .forEach(traverseTree)
+ })()
+
+ return { nodes: visibleNodes, links: visibleLinks }
+ }, [nodesById])
+
+ const [prunedTree, setPrunedTree] = useState(getPrunedTree())
+
+ const handleNodeClick = useCallback((node) => {
+ node.collapsed = !node.collapsed // toggle collapse state
+ setPrunedTree(getPrunedTree())
+ }, [])
+
+ //highlighting
+ const [highlightNodes, setHighlightNodes] = useState(new Set())
+ const [highlightLinks, setHighlightLinks] = useState(new Set())
+ const [hoverNode, setHoverNode] = useState(null)
+
+ const updateHighlight = () => {
+ setHighlightNodes(highlightNodes)
+ setHighlightLinks(highlightLinks)
+ }
+
+ const handleBackgroundClick = (event) => {
+ highlightNodes.clear()
+ highlightLinks.clear()
+
+ setSelectedNode(null)
+ updateHighlight()
+ }
+
+ const handleNodeHover = (node) => {
+ console.log('hover')
+ if (!selectedNode) {
+ highlightNodes.clear()
+ highlightLinks.clear()
+ if (node) {
+ highlightNodes.add(node)
+ node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor))
+ node.links.forEach((link) => highlightLinks.add(link))
+ }
+
+ setHoverNode(node || null)
+ updateHighlight()
+ }
+ }
+
+ const handleLinkHover = (link) => {
+ highlightNodes.clear()
+ highlightLinks.clear()
+
+ if (link) {
+ highlightLinks.add(link)
+ highlightNodes.add(link.source)
+ highlightNodes.add(link.target)
+ }
+
+ updateHighlight()
+ }
+
+ // Normally the graph doesn't update when you just change the physics parameters
+ // This forces the graph to make a small update when you do
+ useEffect(() => {
+ fgRef.current.d3ReheatSimulation()
+ }, [physics])
+ /* const paintRing = useCallback((node, ctx) => {
+ * // add ring just for highlighted nodes
+ * ctx.beginPath();
+ * ctx.arc(node.x, node.y, NODE_R * 1.4, 0, 2 * Math.PI, false);
+ * ctx.fillStyle = node === hoverNode ? 'red' : 'orange';
+ * ctx.fill();
+ * }, [hoverNode]);
+ */
+
+ /* autoPauseRedraw={false}
+linkWidth={link => highlightLinks.has(link) ? 5 : 1}
+linkDirectionalParticles={4}
+linkDirectionalParticleWidth={link => highlightLinks.has(link) ? 4 : 0}
+nodeCanvasObjectMode={node => highlightNodes.has(node) ? 'before' : undefined}
+nodeCanvasObject={paintRing}
+onNodeHover={handleNodeHover}
+onLinkHover={handleLinkHover}
+ nodeRelSize={NODE_R} */
+
+ //nodeColor={(node) =>
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ //}
+
+ const [selectedNode, setSelectedNode] = useState({})
+
+ //shitty handler to check for doubleClicks
+ const [doubleClick, setDoubleClick] = useState(0)
+ const [localGraphData, setLocalGraphData] = useState({
+ nodes: [],
+ links: [],
+ })
+
+ const updateLocalGraph = (node) => {
+ console.log(localGraphData)
+ // localGraphData.nodes.length ? setLocalGraphData({ nodes: [], links: [] }) : null
+ let g = localGraphData
+ if (!node.local) {
+ node.local = true
+ g.nodes.push(node)
+ }
+ node.neighbors.forEach((neighbor) => {
+ if (neighbor !== node || !neighbor.local) {
+ const newNode: boolean = g.nodes.every((existingNode) => {
+ if (existingNode === neighbor) {
+ return false
+ } else {
+ return true
+ }
+ })
+ if (newNode) {
+ neighbor.local = true
+ g.nodes.push(neighbor)
+ }
+ }
+ })
+
+ node.links.forEach((neighborLink) => {
+ const newLink: boolean = g.links.every((existingLink) => {
+ if (existingLink === neighborLink) {
+ return false
+ } else {
+ return true
+ }
+ })
+ newLink && g.links.push(neighborLink)
+ })
+ setLocalGraphData(g)
+ setPhysics({ ...physics, local: true })
+ }
+
+ const selectClick = (node, event) => {
+ highlightNodes.clear()
+ highlightLinks.clear()
+ console.log(localGraphData)
+ if (event.timeStamp - doubleClick < 400) {
+ updateLocalGraph(node)
+ }
+ if (node) {
+ highlightNodes.add(node)
+ node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor))
+ node.links.forEach((link) => highlightLinks.add(link))
+ }
+ setSelectedNode(node || null)
+ updateHighlight()
+ setDoubleClick(event.timeStamp)
+ }
+
+ return (
+ <View>
+ {!physics.threedim ? (
+ <ForceGraph2D
+ ref={fgRef}
+ autoPauseRedraw={false}
+ //graphData={gData}
+ graphData={physics.local ? localGraphData : physics.collapse ? prunedTree : gData}
+ nodeAutoColorBy={physics.colorful ? 'id' : undefined}
+ nodeColor={
+ !physics.colorful
+ ? (node) => {
+ if (highlightNodes.size === 0) {
+ return 'rgb(100, 100, 100, 1)'
+ } else {
+ return highlightNodes.has(node) ? 'purple' : 'rgb(50, 50, 50, 0.5)'
+ }
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ : undefined
+ }
+ linkAutoColorBy={physics.colorful ? 'target' : undefined}
+ //linkAutoColorBy={(d) => gData.nodes[d.source].id % GROUPS}
+ linkColor={
+ !physics.colorful
+ ? (link) => {
+ if (highlightLinks.size === 0) {
+ return 'rgb(50, 50, 50, 0.8)'
+ } else {
+ return highlightLinks.has(link) ? 'purple' : 'rgb(50, 50, 50, 0.2)'
+ }
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ : undefined
+ //highlightLinks.has(link) ? "purple" : "grey"
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ linkDirectionalParticles={physics.particles}
+ onNodeClick={!physics.collapse ? selectClick : handleNodeClick}
+ nodeLabel={(node) => node.title}
+ //nodeVal ={(node)=> node.childLinks.length * 0.5 + 1}
+ //d3VelocityDecay={visco}
+ linkWidth={(link) =>
+ highlightLinks.has(link) ? 3 * physics.linkWidth : physics.linkWidth
+ }
+ linkOpacity={physics.linkOpacity}
+ nodeRelSize={physics.nodeRel}
+ nodeVal={(node) =>
+ highlightNodes.has(node) ? node.neighbors.length + 5 : node.neighbors.length + 3
+ }
+ linkDirectionalParticleWidth={physics.particleWidth}
+ nodeCanvasObject={(node, ctx, globalScale) => {
+ if (physics.labels) {
+ if (globalScale > physics.labelScale || highlightNodes.has(node)) {
+ const label = node.title.substring(0, Math.min(node.title.length, 30))
+ const fontSize = 12 / globalScale
+ ctx.font = `${fontSize}px Sans-Serif`
+ const textWidth = ctx.measureText(label).width
+ const bckgDimensions = [textWidth * 1.1, fontSize].map((n) => n + fontSize * 0.5) // some padding
+ const fadeFactor = Math.min(
+ (3 * (globalScale - physics.labelScale)) / physics.labelScale,
+ 1,
+ )
+
+ ctx.fillStyle =
+ 'rgba(20, 20, 20, ' +
+ (highlightNodes.size === 0
+ ? 0.5 * fadeFactor
+ : highlightNodes.has(node)
+ ? 0.5
+ : 0.15 * fadeFactor) +
+ ')'
+ ctx.fillRect(
+ node.x - bckgDimensions[0] / 2,
+ node.y - bckgDimensions[1] / 2,
+ ...bckgDimensions,
+ )
+
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ ctx.fillStyle =
+ 'rgb(255, 255, 255, ' +
+ (highlightNodes.size === 0
+ ? fadeFactor
+ : highlightNodes.has(node)
+ ? 1
+ : 0.3 * fadeFactor) +
+ ')'
+ ctx.fillText(label, node.x, node.y)
+
+ node.__bckgDimensions = bckgDimensions // to re-use in nodePointerAreaPaint
+ }
+ }
+ }}
+ nodeCanvasObjectMode={() => 'after'}
+ onNodeHover={physics.hover ? handleNodeHover : null}
+ //onLinkHover={physics.hover ? handleLinkHover : null}
+ d3AlphaDecay={physics.alphaDecay}
+ d3AlphaMin={physics.alphaTarget}
+ d3VelocityDecay={physics.velocityDecay}
+ onBackgroundClick={handleBackgroundClick}
+ />
+ ) : (
+ <ForceGraph3D
+ ref={fgRef}
+ autoPauseRedraw={false}
+ graphData={gData}
+ //graphData={physics.collapse ? prunedTree : gData}
+ nodeAutoColorBy={physics.colorful ? 'id' : undefined}
+ nodeColor={
+ !physics.colorful
+ ? (node) => {
+ if (highlightNodes.size === 0) {
+ return 'rgb(100, 100, 100, 1)'
+ } else {
+ return highlightNodes.has(node) ? 'purple' : 'rgb(50, 50, 50, 0.5)'
+ }
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ : undefined
+ }
+ linkAutoColorBy={physics.colorful ? 'target' : undefined}
+ //linkAutoColorBy={(d) => gData.nodes[d.source].id % GROUPS}
+ linkColor={
+ !physics.colorful
+ ? (link) => {
+ if (highlightLinks.size === 0) {
+ return 'rgb(50, 50, 50, 0.8)'
+ } else {
+ return highlightLinks.has(link) ? 'purple' : 'rgb(50, 50, 50, 0.2)'
+ }
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ : undefined
+ //highlightLinks.has(link) ? "purple" : "grey"
+ // !node.childLinks.length ? "green" : node.collapsed ? "red" : "yellow"
+ }
+ linkDirectionalParticles={physics.particles}
+ //onNodeClick={!physics.collapse ? null : handleNodeClick}
+ nodeLabel={(node) => node.title}
+ //nodeVal ={(node)=> node.childLinks.length * 0.5 + 1}
+ //d3VelocityDecay={visco}
+ linkWidth={(link) =>
+ highlightLinks.has(link) ? 3 * physics.linkWidth : physics.linkWidth
+ }
+ linkOpacity={physics.linkOpacity}
+ nodeRelSize={physics.nodeRel}
+ nodeVal={(node) =>
+ highlightNodes.has(node) ? node.neighbors.length + 5 : node.neighbors.length + 3
+ }
+ linkDirectionalParticleWidth={physics.particleWidth}
+ onNodeHover={physics.hover ? handleNodeHover : null}
+ //onLinkHover={physics.hover ? handleLinkHover : null}
+ d3AlphaDecay={physics.alphaDecay}
+ d3AlphaMin={physics.alphaTarget}
+ d3VelocityDecay={physics.velocityDecay}
+ nodeThreeObject={
+ !physics.labels
+ ? undefined
+ : (node) => {
+ if (highlightNodes.has(node)) {
+ console.log(node.title)
+ const sprite = new SpriteText(node.title.substring(0, 30))
+ console.log('didnt crash here 2')
+ sprite.color = '#ffffff'
+ sprite.textHeight = 8
+ return sprite
+ } else {
+ return undefined
+ }
+ }
+ }
+ nodeThreeObjectExtend={true}
+ />
+ )}
+ </View>
+ )
+})
diff --git a/app_expo/components/header/header.props.ts b/app_expo/components/header/header.props.ts
new file mode 100644
index 0000000..f142656
--- /dev/null
+++ b/app_expo/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_expo/components/header/header.story.tsx b/app_expo/components/header/header.story.tsx
new file mode 100644
index 0000000..db87b89
--- /dev/null
+++ b/app_expo/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_expo/components/header/header.tsx b/app_expo/components/header/header.tsx
new file mode 100644
index 0000000..25e0914
--- /dev/null
+++ b/app_expo/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_expo/components/icon/icon.props.ts b/app_expo/components/icon/icon.props.ts
new file mode 100644
index 0000000..1f3ed2e
--- /dev/null
+++ b/app_expo/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_expo/components/icon/icon.story.tsx b/app_expo/components/icon/icon.story.tsx
new file mode 100644
index 0000000..31c8499
--- /dev/null
+++ b/app_expo/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_expo/components/icon/icon.tsx b/app_expo/components/icon/icon.tsx
new file mode 100644
index 0000000..f596bb2
--- /dev/null
+++ b/app_expo/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_expo/components/icon/icons/arrow-left.png b/app_expo/components/icon/icons/arrow-left.png
new file mode 100644
index 0000000..9d607d7
--- /dev/null
+++ b/app_expo/components/icon/icons/arrow-left.png
Binary files differ
diff --git a/app_expo/components/icon/icons/[email protected] b/app_expo/components/icon/icons/[email protected]
new file mode 100644
index 0000000..9d607d7
--- /dev/null
+++ b/app_expo/components/icon/icons/[email protected]
Binary files differ
diff --git a/app_expo/components/icon/icons/bullet.png b/app_expo/components/icon/icons/bullet.png
new file mode 100644
index 0000000..8fc256f
--- /dev/null
+++ b/app_expo/components/icon/icons/bullet.png
Binary files differ
diff --git a/app_expo/components/icon/icons/[email protected] b/app_expo/components/icon/icons/[email protected]
new file mode 100644
index 0000000..8fc256f
--- /dev/null
+++ b/app_expo/components/icon/icons/[email protected]
Binary files differ
diff --git a/app_expo/components/icon/icons/index.ts b/app_expo/components/icon/icons/index.ts
new file mode 100644
index 0000000..792e408
--- /dev/null
+++ b/app_expo/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_expo/components/index.ts b/app_expo/components/index.ts
new file mode 100644
index 0000000..d93e972
--- /dev/null
+++ b/app_expo/components/index.ts
@@ -0,0 +1,16 @@
+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'
+export * from './graph/graph'
+export * from './tweaks/tweaks'
+export * from './local-button/local-button'
+export * from './graph-ui/graph-ui'
diff --git a/app_expo/components/local-button/local-button.story.tsx b/app_expo/components/local-button/local-button.story.tsx
new file mode 100644
index 0000000..0d35ab2
--- /dev/null
+++ b/app_expo/components/local-button/local-button.story.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react'
+import { storiesOf } from '@storybook/react-native'
+import { StoryScreen, Story, UseCase } from '../../../storybook/views'
+import { color } from '../../theme'
+import { LocalButton } from './local-button'
+
+storiesOf('LocalButton', module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add('Style Presets', () => (
+ <Story>
+ <UseCase text="Primary" usage="The primary.">
+ <LocalButton style={{ backgroundColor: color.error }} />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app_expo/components/local-button/local-button.tsx b/app_expo/components/local-button/local-button.tsx
new file mode 100644
index 0000000..c1021a3
--- /dev/null
+++ b/app_expo/components/local-button/local-button.tsx
@@ -0,0 +1,50 @@
+import * as React from 'react'
+import { StyleProp, TextStyle, View, ViewStyle } from 'react-native'
+import { observer } from 'mobx-react-lite'
+import { color, typography } from '../../theme'
+import { Text } from '../'
+import { flatten } from 'ramda'
+import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
+
+const CONTAINER: ViewStyle = {
+ justifyContent: 'center',
+}
+
+const TEXT: TextStyle = {
+ fontFamily: typography.primary,
+ fontSize: 14,
+ color: color.primary,
+}
+
+export interface LocalButtonProps {
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<ViewStyle>
+ local
+ setLocal
+}
+
+/**
+ * Describe your component here
+ */
+export const LocalButton = observer(function LocalButton(props: LocalButtonProps) {
+ const { style, local, setLocal } = props
+ const styles = flatten([CONTAINER, style])
+
+ return (
+ <View style={[style, { height: 50, width: 150 }]}>
+ <Icon.Button
+ name={!local ? 'graph-outline' : 'graph'}
+ backgroundColor="#a991f1"
+ onPress={() => {
+ setLocal(!local)
+ }}
+ size={30}
+ style={{ textAlign: 'center' }}
+ >
+ {!local ? 'Global Graph' : 'Local Graph'}
+ </Icon.Button>
+ </View>
+ )
+})
diff --git a/app_expo/components/local/local.story.tsx b/app_expo/components/local/local.story.tsx
new file mode 100644
index 0000000..2bfa065
--- /dev/null
+++ b/app_expo/components/local/local.story.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react'
+import { storiesOf } from '@storybook/react-native'
+import { StoryScreen, Story, UseCase } from '../../../storybook/views'
+import { color } from '../../theme'
+import { Local } from './local'
+
+storiesOf('Local', module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add('Style Presets', () => (
+ <Story>
+ <UseCase text="Primary" usage="The primary.">
+ <Local style={{ backgroundColor: color.error }} />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app_expo/components/local/local.tsx b/app_expo/components/local/local.tsx
new file mode 100644
index 0000000..1478057
--- /dev/null
+++ b/app_expo/components/local/local.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react'
+import { StyleProp, TextStyle, TouchableOpacity, View, ViewStyle } from 'react-native'
+import { observer } from 'mobx-react-lite'
+import { color, typography } from '../../theme'
+import { Text } from '../'
+import { flatten } from 'ramda'
+import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
+
+const CONTAINER: ViewStyle = {
+ justifyContent: 'center',
+}
+
+const TEXT: TextStyle = {
+ fontFamily: typography.primary,
+ fontSize: 14,
+ color: color.primary,
+}
+
+export interface LocalProps {
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<ViewStyle>
+}
+
+/**
+ * Describe your component here
+ */
+export const LocalButton = observer(function LocalButton(props: LocalProps): boolean {
+ const { style } = props
+ const styles = flatten([CONTAINER, style])
+
+ return (
+ <Icon color="#a99f1f" name="graph" style={{ position: 'absolute', zIndex: 100, width: 500 }} />
+ )
+})
diff --git a/app_expo/components/screen/screen.presets.ts b/app_expo/components/screen/screen.presets.ts
new file mode 100644
index 0000000..aa8d8cf
--- /dev/null
+++ b/app_expo/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_expo/components/screen/screen.props.ts b/app_expo/components/screen/screen.props.ts
new file mode 100644
index 0000000..1371c64
--- /dev/null
+++ b/app_expo/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_expo/components/screen/screen.tsx b/app_expo/components/screen/screen.tsx
new file mode 100644
index 0000000..dafe36e
--- /dev/null
+++ b/app_expo/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_expo/components/switch/switch.props.ts b/app_expo/components/switch/switch.props.ts
new file mode 100644
index 0000000..2549a95
--- /dev/null
+++ b/app_expo/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_expo/components/switch/switch.story.tsx b/app_expo/components/switch/switch.story.tsx
new file mode 100644
index 0000000..b10f8c6
--- /dev/null
+++ b/app_expo/components/switch/switch.story.tsx
@@ -0,0 +1,116 @@
+/* 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_expo/components/switch/switch.tsx b/app_expo/components/switch/switch.tsx
new file mode 100644
index 0000000..845d964
--- /dev/null
+++ b/app_expo/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_expo/components/text-field/text-field.story.tsx b/app_expo/components/text-field/text-field.story.tsx
new file mode 100644
index 0000000..5f4d408
--- /dev/null
+++ b/app_expo/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_expo/components/text-field/text-field.tsx b/app_expo/components/text-field/text-field.tsx
new file mode 100644
index 0000000..1d56b95
--- /dev/null
+++ b/app_expo/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_expo/components/text/text.presets.ts b/app_expo/components/text/text.presets.ts
new file mode 100644
index 0000000..4693417
--- /dev/null
+++ b/app_expo/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_expo/components/text/text.props.ts b/app_expo/components/text/text.props.ts
new file mode 100644
index 0000000..79ee12c
--- /dev/null
+++ b/app_expo/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_expo/components/text/text.story.tsx b/app_expo/components/text/text.story.tsx
new file mode 100644
index 0000000..edfe24d
--- /dev/null
+++ b/app_expo/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_expo/components/text/text.tsx b/app_expo/components/text/text.tsx
new file mode 100644
index 0000000..d9ffc8c
--- /dev/null
+++ b/app_expo/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_expo/components/tweaks/tweaks.story.tsx b/app_expo/components/tweaks/tweaks.story.tsx
new file mode 100644
index 0000000..770d50f
--- /dev/null
+++ b/app_expo/components/tweaks/tweaks.story.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react'
+import { storiesOf } from '@storybook/react-native'
+import { StoryScreen, Story, UseCase } from '../../../storybook/views'
+import { color } from '../../theme'
+import { Tweaks } from './tweaks'
+
+storiesOf('Tweaks', module)
+ .addDecorator((fn) => <StoryScreen>{fn()}</StoryScreen>)
+ .add('Style Presets', () => (
+ <Story>
+ <UseCase text="Primary" usage="The primary.">
+ <Tweaks style={{ backgroundColor: color.error }} />
+ </UseCase>
+ </Story>
+ ))
diff --git a/app_expo/components/tweaks/tweaks.tsx b/app_expo/components/tweaks/tweaks.tsx
new file mode 100644
index 0000000..ea35805
--- /dev/null
+++ b/app_expo/components/tweaks/tweaks.tsx
@@ -0,0 +1,617 @@
+import * as React from 'react'
+import {
+ ScrollView,
+ StyleProp,
+ TextStyle,
+ TouchableOpacity,
+ View,
+ ViewStyle,
+ StyleSheet,
+ Button,
+} from 'react-native'
+import { observer } from 'mobx-react-lite'
+import { color, typography } from '../../theme'
+import { Text } from '../'
+import { flatten } from 'ramda'
+import Slider from '@react-native-community/slider'
+import { useState } from 'react'
+import Accordion from 'react-native-collapsible/Accordion'
+import * as Animatable from 'react-native-animatable'
+import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
+import { Switch } from 'react-native-elements'
+
+const CONTAINER: ViewStyle = {
+ justifyContent: 'center',
+}
+
+const TEXT: TextStyle = {
+ fontFamily: typography.primary,
+ fontSize: 14,
+ color: color.primary,
+}
+
+export interface TweaksProps {
+ /**
+ * An optional style override useful for padding & margin.
+ */
+ style?: StyleProp<ViewStyle>
+ physics
+ setPhysics
+}
+
+/**
+ * Describe your component here
+ */
+export const Tweaks = observer(function Tweaks(props: TweaksProps): JSX.Element {
+ const { style, physics, setPhysics } = props
+ // const styles = flatten([CONTAINER, style])
+
+ const content = [
+ {
+ title: 'Physics',
+ content: (
+ <View>
+ <Text preset="fieldLabel" text="Gravity" />
+ <Switch
+ color="#a991f1"
+ trackColor={{
+ false: '#62686E',
+ true: '#a991f1',
+ }}
+ style={styles.switch}
+ value={physics.gravityOn}
+ onValueChange={() => {
+ setPhysics({ ...physics, gravityOn: !physics.gravityOn })
+ }}
+ />
+ <Text preset="fieldLabel" text={'Gravity: ' + physics.gravity} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0}
+ maximumValue={1}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, gravity: value })
+ }}
+ value={physics.gravity}
+ step={0.01}
+ />
+ <Text preset="fieldLabel" text={'Repulsive force: ' + physics.charge} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={-400}
+ maximumValue={100}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, charge: value })
+ }}
+ value={physics.charge}
+ step={1}
+ />
+ <Text preset="fieldLabel" text={'Link Force: ' + physics.linkStrength} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0}
+ maximumValue={2}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, linkStrength: value })
+ }}
+ value={physics.linkStrength}
+ step={0.01}
+ />
+ <Text preset="fieldLabel" text={"'Link Iterations': " + physics.linkIts} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={1}
+ maximumValue={10}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, linkIts: value })
+ }}
+ value={physics.linkIts}
+ step={1}
+ />
+ <Text preset="fieldLabel" text="Collision" />
+ <Switch
+ color="#a991f1"
+ trackColor={{
+ false: '#62686E',
+ true: '#a991f1',
+ }}
+ style={styles.switch}
+ value={physics.collision}
+ onValueChange={() => {
+ setPhysics({ ...physics, collision: !physics.collision })
+ }}
+ />
+ <Text preset="fieldLabel" text={'Alpha Decay: ' + physics.alphaDecay} />
+ <Slider
+ style={styles.slider}
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ minimumValue={0}
+ maximumValue={1}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, alphaDecay: value })
+ }}
+ value={physics.alphaDecay}
+ step={0.01}
+ />
+ <Text preset="fieldLabel" text={'Alhpa Target: ' + physics.alphaTarget} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0}
+ maximumValue={1}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, alphaTarget: value })
+ }}
+ value={physics.alphaTarget}
+ step={0.1}
+ />
+ <Text preset="fieldLabel" text={'Viscosity: ' + physics.velocityDecay} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0}
+ maximumValue={1}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, velocityDecay: value })
+ }}
+ value={physics.velocityDecay}
+ step={0.01}
+ />
+ <Text preset="fieldLabel" text={'Galaxy Mode (3D-only)'} />
+ <Switch
+ color="#a991f1"
+ trackColor={{
+ false: '#62686E',
+ true: '#a991f1',
+ }}
+ style={styles.switch}
+ value={physics.galaxy}
+ onValueChange={() => {
+ setPhysics({ ...physics, galaxy: !physics.galaxy })
+ }}
+ />
+ </View>
+ ),
+ },
+ {
+ title: 'Visual',
+ content: (
+ <View>
+ <Text preset="fieldLabel" text="Colorful" />
+ <Switch
+ color="#a991f1"
+ trackColor={{
+ false: '#62686E',
+ true: '#a991f1',
+ }}
+ style={styles.switch}
+ value={physics.colorful}
+ onValueChange={() => {
+ setPhysics({ ...physics, colorful: !physics.colorful })
+ }}
+ />
+ <Text preset="fieldLabel" text="Hover highlight" />
+ <Switch
+ color="#a991f1"
+ trackColor={{
+ false: '#62686E',
+ true: '#a991f1',
+ }}
+ style={styles.switch}
+ value={physics.hover}
+ onValueChange={() => {
+ setPhysics({ ...physics, hover: !physics.hover })
+ }}
+ />
+ <Text preset="fieldLabel" text={'Line Opacity: ' + physics.linkOpacity} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0}
+ maximumValue={1}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, linkOpacity: value })
+ }}
+ value={physics.linkOpacity}
+ step={0.01}
+ />
+ <Text preset="fieldLabel" text={'Line width: ' + physics.linkWidth} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0.1}
+ maximumValue={10}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, linkWidth: value })
+ }}
+ value={physics.linkWidth}
+ step={0.1}
+ />
+ <Text preset="fieldLabel" text={'Node size: ' + physics.nodeRel} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={1}
+ maximumValue={10}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, nodeRel: value })
+ }}
+ value={physics.nodeRel}
+ step={0.01}
+ />
+ <Text preset="fieldLabel" text={'Particles: ' + physics.particles} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0}
+ maximumValue={10}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, particles: value })
+ }}
+ value={physics.particles}
+ step={1}
+ />
+ <Text preset="fieldLabel" text={'Particle Size: ' + physics.particleWidth} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={1}
+ maximumValue={10}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, particleWidth: value })
+ }}
+ value={physics.particleWidth}
+ step={0.1}
+ />
+ <Text preset="fieldLabel" text="Labels" />
+ <Switch
+ color="#a991f1"
+ trackColor={{
+ false: '#62686E',
+ true: '#a991f1',
+ }}
+ style={styles.switch}
+ value={physics.labels}
+ onValueChange={() => {
+ setPhysics({ ...physics, labels: !physics.labels })
+ }}
+ />
+ <Text
+ preset="fieldLabel"
+ text={'Scale when labels become visible: ' + physics.labelScale}
+ />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0.1}
+ maximumValue={5}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, labelScale: value })
+ }}
+ value={physics.labelScale}
+ step={0.1}
+ />
+ </View>
+ ),
+ },
+ {
+ title: 'Modes',
+ content: <View></View>,
+ },
+ ]
+
+ const [activeSections, setActiveSections] = useState([])
+
+ const setSections = (sections) => {
+ setActiveSections(sections.includes(undefined) ? [] : sections)
+ }
+
+ const renderHeader = (section, _, isActive) => {
+ return (
+ <Animatable.View
+ duration={400}
+ style={[styles.header, isActive ? styles.active : styles.inactive]}
+ transition="backgroundColor"
+ >
+ <Text style={styles.headerText}>{section.title}</Text>
+ </Animatable.View>
+ )
+ }
+
+ const renderContent = (section, _, isActive) => {
+ return (
+ <Animatable.View
+ duration={400}
+ style={[styles.content, isActive ? styles.active : styles.inactive]}
+ transition="backgroundColor"
+ >
+ {section.content}
+ </Animatable.View>
+ )
+ }
+ const [tweaks, setTweaks] = useState(true)
+ if (true) {
+ if (tweaks) {
+ return (
+ <View style={styles.container}>
+ <View style={{ height: 30, width: '100%', backgroundColor: '#2a2e38' }}>
+ <TouchableOpacity
+ style={{
+ width: 30,
+ color: '#a991f1',
+ textAlign: 'center',
+ marginLeft: 'auto',
+ padding: 5,
+ }}
+ onPress={() => {
+ setTweaks(false)
+ }}
+ >
+ <Icon name="close-circle" color="#ffffff" size={20} />
+ </TouchableOpacity>
+ </View>
+ <ScrollView>
+ <Accordion
+ activeSections={activeSections}
+ sections={content}
+ touchAbleComponent={TouchableOpacity}
+ expandMultiple={true}
+ renderHeader={renderHeader}
+ renderContent={renderContent}
+ duration={200}
+ onChange={setSections}
+ renderAsFlatList={false}
+ />
+ </ScrollView>
+ </View>
+ )
+ } else {
+ return (
+ <TouchableOpacity
+ onPress={() => {
+ setTweaks(true)
+ }}
+ style={{
+ position: 'absolute',
+ top: 50,
+ left: 50,
+ width: 30,
+ color: '#ffffff',
+ zIndex: 100,
+ }}
+ >
+ <Icon name="cog" color="#ffffff" size={30} />
+ </TouchableOpacity>
+ )
+ }
+ } else {
+ return (
+ <View
+ style={{
+ position: 'absolute',
+ top: '5%',
+ left: '5%',
+ zIndex: 100,
+ width: 300,
+ backgroundColor: '#000000',
+ padding: 20,
+ }}
+ >
+ <Text preset="bold" text="Physics" />
+ <Text preset="fieldLabel" text={'Repulsive force: ' + physics.charge} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={-400}
+ maximumValue={100}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, charge: value })
+ }}
+ value={physics.charge}
+ step={1}
+ />
+ <Text preset="fieldLabel" text={'Link Force: ' + physics.linkStrength} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0}
+ maximumValue={2}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, linkStrength: value })
+ }}
+ value={physics.linkStrength}
+ step={0.1}
+ />
+ <Text preset="fieldLabel" text={"'Link Iterations': " + physics.linkIts} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={1}
+ maximumValue={10}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, linkIts: value })
+ }}
+ value={physics.linkIts}
+ step={1}
+ />
+ <Text preset="fieldLabel" text="Collision" />
+ <Switch
+ color="#a991f1"
+ trackColor={{
+ false: '#62686E',
+ true: '#a991f1',
+ }}
+ style={styles.switch}
+ value={physics.collision}
+ onValueChange={() => {
+ setPhysics({ ...physics, collision: !physics.collision })
+ }}
+ />
+ <Text preset="bold" text="Visual" />
+ <Text preset="fieldLabel" text={'Particles: ' + physics.particles} />
+ <Slider
+ minimumTrackTintColor="#a991f1"
+ maximumTrackTintColor="#242730"
+ thumbTintColor="#a991f1"
+ style={styles.slider}
+ minimumValue={0}
+ maximumValue={5}
+ onValueChange={(value) => {
+ setPhysics({ ...physics, particles: value })
+ }}
+ value={physics.particles}
+ step={1}
+ />
+ <Text preset="bold" text="Modes" />
+ <Text preset="fieldLabel" text="Expandable Graph" />
+ <Switch
+ color="#a991f1"
+ trackColor={{
+ false: '#62686E',
+ true: '#a991f1',
+ }}
+ style={styles.switch}
+ value={physics.collapse}
+ onValueChange={() => {
+ setPhysics({ ...physics, collapse: !physics.collapse })
+ }}
+ />
+ <Text preset="fieldLabel" text="3D" />
+ <Switch
+ color="#a991f1"
+ trackColor={{
+ false: '#62686E',
+ true: '#a991f1',
+ }}
+ style={styles.switch}
+ value={physics.threedim}
+ onValueChange={() => {
+ setPhysics({ ...physics, threedim: !physics.threedim })
+ }}
+ />
+ </View>
+ )
+ }
+})
+
+const styles = StyleSheet.create({
+ container: {
+ display: 'flex',
+ backgroundColor: '#2a2e38',
+ position: 'absolute',
+ zIndex: 5,
+ marginLeft: '2%',
+ marginTop: '2%',
+ maxWidth: 275,
+ borderRadius: 10,
+ borderStyle: 'solid',
+ borderWidth: 10,
+ borderColor: '#2a2e38',
+ maxHeight: '80%',
+ paddingBottom: 20,
+ },
+ title: {
+ textAlign: 'left',
+ fontSize: 22,
+ fontWeight: '300',
+ marginBottom: 20,
+ paddingLeft: 10,
+ },
+ header: {
+ backgroundColor: '#2a2e38',
+ padding: 10,
+ paddingBottom: 20,
+ textAlign: 'left',
+ },
+ headerText: {
+ textAlign: 'left',
+ paddingLeft: 30,
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ content: {
+ padding: 20,
+ paddingLeft: 60,
+ backgroundColor: '#000000',
+ },
+ active: {
+ backgroundColor: '#2a2e38',
+ },
+ inactive: {
+ backgroundColor: '#2a2e38',
+ },
+ selectors: {
+ marginBottom: 10,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+ selector: {
+ backgroundColor: '#2a2e38',
+ padding: 10,
+ },
+ activeSelector: {
+ fontWeight: 'bold',
+ },
+ selectTitle: {
+ fontSize: 14,
+ fontWeight: '500',
+ padding: 10,
+ },
+ multipleToggle: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginVertical: 30,
+ alignItems: 'center',
+ },
+ multipleToggle__title: {
+ fontSize: 16,
+ marginRight: 8,
+ },
+ slider: {
+ minimumTrackTintColor: '#a991f1',
+ thumbTintColor: '#a991f1',
+ height: 40,
+ width: '90%',
+ },
+ switch: {
+ width: '5',
+ height: 20,
+ marginVertical: 10,
+ },
+})
diff --git a/app_expo/components/wallpaper/wallpaper.presets.ts b/app_expo/components/wallpaper/wallpaper.presets.ts
new file mode 100644
index 0000000..148ad5c
--- /dev/null
+++ b/app_expo/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_expo/components/wallpaper/wallpaper.props.ts b/app_expo/components/wallpaper/wallpaper.props.ts
new file mode 100644
index 0000000..9d97f12
--- /dev/null
+++ b/app_expo/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_expo/components/wallpaper/wallpaper.story.tsx b/app_expo/components/wallpaper/wallpaper.story.tsx
new file mode 100644
index 0000000..14a5f62
--- /dev/null
+++ b/app_expo/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_expo/components/wallpaper/wallpaper.tsx b/app_expo/components/wallpaper/wallpaper.tsx
new file mode 100644
index 0000000..f5e24f1
--- /dev/null
+++ b/app_expo/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 null //<Image source={source} style={styles} />
+}