diff options
Diffstat (limited to 'app_expo/components')
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 Binary files differnew file mode 100644 index 0000000..9d607d7 --- /dev/null +++ b/app_expo/components/icon/icons/arrow-left.png diff --git a/app_expo/components/icon/icons/[email protected] b/app_expo/components/icon/icons/[email protected] Binary files differnew file mode 100644 index 0000000..9d607d7 --- /dev/null +++ b/app_expo/components/icon/icons/[email protected] diff --git a/app_expo/components/icon/icons/bullet.png b/app_expo/components/icon/icons/bullet.png Binary files differnew file mode 100644 index 0000000..8fc256f --- /dev/null +++ b/app_expo/components/icon/icons/bullet.png diff --git a/app_expo/components/icon/icons/[email protected] b/app_expo/components/icon/icons/[email protected] Binary files differnew file mode 100644 index 0000000..8fc256f --- /dev/null +++ b/app_expo/components/icon/icons/[email protected] 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} /> +} |