Outer layout, some primitive updates
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 756 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 263 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 263 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 246 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 237 B |
@@ -104,6 +104,9 @@ export const atoms = {
|
|||||||
flex: {
|
flex: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
},
|
},
|
||||||
|
flex_col: {
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
flex_row: {
|
flex_row: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
@@ -188,6 +191,9 @@ export const atoms = {
|
|||||||
lineHeight: tokens.fontSize._5xl,
|
lineHeight: tokens.fontSize._5xl,
|
||||||
},
|
},
|
||||||
leading_tight: {
|
leading_tight: {
|
||||||
|
lineHeight: 1.15,
|
||||||
|
},
|
||||||
|
leading_snug: {
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
leading_normal: {
|
leading_normal: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import {Dimensions} from 'react-native'
|
import {Dimensions} from 'react-native'
|
||||||
import * as themes from '#/alf/themes'
|
import * as themes from '#/alf/themes'
|
||||||
|
|
||||||
|
export * from '#/alf/types'
|
||||||
export * as tokens from '#/alf/tokens'
|
export * as tokens from '#/alf/tokens'
|
||||||
export {atoms} from '#/alf/atoms'
|
export {atoms} from '#/alf/atoms'
|
||||||
export * from '#/alf/util/platform'
|
export * from '#/alf/util/platform'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import {StyleProp, ViewStyle, TextStyle} from 'react-native'
|
||||||
|
|
||||||
type LiteralToCommon<T extends PropertyKey> = T extends number
|
type LiteralToCommon<T extends PropertyKey> = T extends number
|
||||||
? number
|
? number
|
||||||
: T extends string
|
: T extends string
|
||||||
@@ -14,3 +16,11 @@ export type Mutable<T> = {
|
|||||||
? LiteralToCommon<T[K]>
|
? LiteralToCommon<T[K]>
|
||||||
: Mutable<T[K]>
|
: Mutable<T[K]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TextStyleProp = {
|
||||||
|
style?: StyleProp<TextStyle>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ViewStyleProp = {
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}
|
||||||
|
|||||||
@@ -273,15 +273,15 @@ export function Button({
|
|||||||
|
|
||||||
if (shape === 'default') {
|
if (shape === 'default') {
|
||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_sm)
|
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||||
} else if (size === 'small') {
|
} else if (size === 'small') {
|
||||||
baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm)
|
baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm)
|
||||||
}
|
}
|
||||||
} else if (shape === 'round' || shape === 'square') {
|
} else if (shape === 'round' || shape === 'square') {
|
||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
baseStyles.push(a.p_lg)
|
baseStyles.push({padding: 15})
|
||||||
} else if (size === 'small') {
|
} else if (size === 'small') {
|
||||||
baseStyles.push(a.p_md)
|
baseStyles.push({padding: 10})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shape === 'round') {
|
if (shape === 'round') {
|
||||||
@@ -360,6 +360,7 @@ export function Button({
|
|||||||
flatten(style),
|
flatten(style),
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
...baseStyles,
|
...baseStyles,
|
||||||
...(state.hovered || state.pressed ? hoverStyles : []),
|
...(state.hovered || state.pressed ? hoverStyles : []),
|
||||||
@@ -506,9 +507,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
|||||||
const textStyles = useSharedButtonTextStyles()
|
const textStyles = useSharedButtonTextStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
|
||||||
{...rest}
|
|
||||||
style={[a.w_full, a.font_bold, a.text_center, textStyles, style]}>
|
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
@@ -519,11 +518,11 @@ export function ButtonIcon({
|
|||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<SVGIconProps>
|
icon: React.ComponentType<SVGIconProps>
|
||||||
}) {
|
}) {
|
||||||
const {size} = useButtonContext()
|
const {size, disabled} = useButtonContext()
|
||||||
const textStyles = useSharedButtonTextStyles()
|
const textStyles = useSharedButtonTextStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.z_20]}>
|
<View style={[a.z_20, {marginTop: -1, opacity: disabled ? 0.7 : 1}]}>
|
||||||
<Comp
|
<Comp
|
||||||
size={size === 'large' ? 'md' : 'sm'}
|
size={size === 'large' ? 'md' : 'sm'}
|
||||||
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
useSharedValue,
|
||||||
|
useAnimatedStyle,
|
||||||
|
withRepeat,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {Props, useCommonSVGProps} from '#/components/icons/common'
|
||||||
|
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
|
||||||
|
|
||||||
|
export function Loader(props: Props) {
|
||||||
|
const common = useCommonSVGProps(props)
|
||||||
|
const rotation = useSharedValue(0)
|
||||||
|
|
||||||
|
const animatedStyles = useAnimatedStyle(() => ({
|
||||||
|
transform: [{rotate: rotation.value + 'deg'}],
|
||||||
|
}))
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
rotation.value = withRepeat(
|
||||||
|
withTiming(360, {duration: 500, easing: Easing.linear}),
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
}, [rotation])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
{width: common.size, height: common.size},
|
||||||
|
animatedStyles,
|
||||||
|
]}>
|
||||||
|
<Icon {...props} style={[a.absolute, a.inset_0, props.style]} />
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,13 +12,14 @@ type ComponentMap = {
|
|||||||
[id: string]: Component
|
[id: string]: Component
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Context = React.createContext<ContextType>({
|
export function createPortalGroup() {
|
||||||
|
const Context = React.createContext<ContextType>({
|
||||||
outlet: null,
|
outlet: null,
|
||||||
append: () => {},
|
append: () => {},
|
||||||
remove: () => {},
|
remove: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Provider(props: React.PropsWithChildren<{}>) {
|
function Provider(props: React.PropsWithChildren<{}>) {
|
||||||
const map = React.useRef<ComponentMap>({})
|
const map = React.useRef<ComponentMap>({})
|
||||||
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
|
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
|
||||||
|
|
||||||
@@ -38,14 +39,14 @@ export function Provider(props: React.PropsWithChildren<{}>) {
|
|||||||
{props.children}
|
{props.children}
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Outlet() {
|
function Outlet() {
|
||||||
const ctx = React.useContext(Context)
|
const ctx = React.useContext(Context)
|
||||||
return ctx.outlet
|
return ctx.outlet
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Portal({children}: React.PropsWithChildren<{}>) {
|
function Portal({children}: React.PropsWithChildren<{}>) {
|
||||||
const {append, remove} = React.useContext(Context)
|
const {append, remove} = React.useContext(Context)
|
||||||
const id = React.useId()
|
const id = React.useId()
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -53,4 +54,12 @@ export function Portal({children}: React.PropsWithChildren<{}>) {
|
|||||||
return () => remove(id)
|
return () => remove(id)
|
||||||
}, [id, children, append, remove])
|
}, [id, children, append, remove])
|
||||||
return null
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {Provider, Outlet, Portal}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultPortal = createPortalGroup()
|
||||||
|
export const Provider = DefaultPortal.Provider
|
||||||
|
export const Outlet = DefaultPortal.Outlet
|
||||||
|
export const Portal = DefaultPortal.Portal
|
||||||
|
|||||||
@@ -3,6 +3,19 @@ import {Text as RNText, TextProps} from 'react-native'
|
|||||||
|
|
||||||
import {useTheme, atoms, web, flatten} from '#/alf'
|
import {useTheme, atoms, web, flatten} from '#/alf'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util to calculate lineHeight from a text size atom and a leading atom
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
|
||||||
|
*/
|
||||||
|
export function leading<
|
||||||
|
Size extends {fontSize: number},
|
||||||
|
Leading extends {lineHeight: number},
|
||||||
|
>(textSize: Size, leading: Leading) {
|
||||||
|
return (textSize.fontSize || atoms.text_md.fontSize) * leading.lineHeight
|
||||||
|
}
|
||||||
|
|
||||||
export function Text({style, ...rest}: TextProps) {
|
export function Text({style, ...rest}: TextProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return <RNText style={[atoms.text_sm, t.atoms.text, style]} {...rest} />
|
return <RNText style={[atoms.text_sm, t.atoms.text, style]} {...rest} />
|
||||||
@@ -111,9 +124,11 @@ export function P({style, ...rest}: TextProps) {
|
|||||||
role: 'paragraph',
|
role: 'paragraph',
|
||||||
}) || {}
|
}) || {}
|
||||||
const _style = flatten(style)
|
const _style = flatten(style)
|
||||||
const lineHeight =
|
const lineHeight = leading(
|
||||||
(_style?.lineHeight || atoms.text_md.lineHeight) *
|
// @ts-ignore
|
||||||
atoms.leading_normal.lineHeight
|
_style?.fontSize ? _style : atoms.text_md,
|
||||||
|
atoms.leading_normal,
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<RNText
|
<RNText
|
||||||
{...attr}
|
{...attr}
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
|
|||||||
export const ChevronLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
export const ChevronLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
path: 'M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z',
|
path: 'M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const ChevronRight_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z',
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Loader_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z',
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const PlusLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z',
|
||||||
|
})
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
||||||
export const PWI_ENABLED = true
|
export const PWI_ENABLED = true
|
||||||
|
// TODO
|
||||||
|
export const NEW_ONBOARDING_ENABLED = true
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useTheme,
|
||||||
|
atoms as a,
|
||||||
|
useBreakpoints,
|
||||||
|
web,
|
||||||
|
native,
|
||||||
|
flatten,
|
||||||
|
TextStyleProp,
|
||||||
|
} from '#/alf'
|
||||||
|
import {H2, P, leading} from '#/components/Typography'
|
||||||
|
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/context'
|
||||||
|
|
||||||
|
const COL_WIDTH = 500
|
||||||
|
|
||||||
|
export const OnboardingControls = createPortalGroup()
|
||||||
|
|
||||||
|
export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const t = useTheme()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const onboardDispatch = useOnboardingDispatch()
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}>
|
||||||
|
{IS_DEV && (
|
||||||
|
<View style={[a.absolute, a.p_xl, a.z_10, {right: 0, top: insets.top}]}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
color="negative"
|
||||||
|
size="small"
|
||||||
|
onPress={() => onboardDispatch({type: 'skip'})}
|
||||||
|
label="Clear onboarding state">
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.hasPrev && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
web(a.fixed),
|
||||||
|
native(a.absolute),
|
||||||
|
a.flex_row,
|
||||||
|
a.w_full,
|
||||||
|
a.justify_center,
|
||||||
|
a.z_20,
|
||||||
|
a.px_xl,
|
||||||
|
{
|
||||||
|
top: a.py_5xl.paddingTop + insets.top,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={[a.w_full, a.align_start, {maxWidth: COL_WIDTH}]}>
|
||||||
|
<Button
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
label="Go back"
|
||||||
|
style={[a.absolute, t.atoms.shadow_sm]}
|
||||||
|
onPress={() => dispatch({type: 'prev'})}>
|
||||||
|
<ButtonIcon icon={ChevronLeft} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
|
||||||
|
contentContainerStyle={{borderWidth: 0}}>
|
||||||
|
<View
|
||||||
|
style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}>
|
||||||
|
<View style={[a.flex_1, {maxWidth: COL_WIDTH}]}>
|
||||||
|
<View style={[a.w_full, a.align_center, a.pt_5xl]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_sm,
|
||||||
|
a.w_full,
|
||||||
|
{paddingTop: 17, maxWidth: '60%'},
|
||||||
|
]}>
|
||||||
|
{Array(state.totalSteps)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.pt_xs,
|
||||||
|
a.rounded_full,
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
i + 1 <= state.activeStepIndex
|
||||||
|
? t.palette.primary_500
|
||||||
|
: t.palette.contrast_100,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.w_full, a.mb_5xl, {paddingTop: 120}]}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{isWeb && gtMobile && (
|
||||||
|
<View style={[a.w_full, a.flex_row, a.justify_end, a.pt_5xl]}>
|
||||||
|
<OnboardingControls.Outlet />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{(!isWeb || !gtMobile) && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.align_center,
|
||||||
|
gtMobile ? a.px_5xl : a.px_xl,
|
||||||
|
{
|
||||||
|
paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={[a.w_full, {maxWidth: COL_WIDTH}]}>
|
||||||
|
<OnboardingControls.Outlet />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Title({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: React.PropsWithChildren<TextStyleProp>) {
|
||||||
|
return (
|
||||||
|
<H2
|
||||||
|
style={[
|
||||||
|
a.pb_sm,
|
||||||
|
{
|
||||||
|
lineHeight: leading(a.text_4xl, a.leading_tight),
|
||||||
|
},
|
||||||
|
flatten(style),
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</H2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Description({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: React.PropsWithChildren<TextStyleProp>) {
|
||||||
|
const t = useTheme()
|
||||||
|
return <P style={[t.atoms.text_contrast_700, flatten(style)]}>{children}</P>
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/context'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
|
||||||
|
export function StepAlgoFeeds() {
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<Title>Choose your algorithmic feeds</Title>
|
||||||
|
<Description>
|
||||||
|
Feeds are created by users and can give you entirely new experiences.
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<Button
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label="Continue setting up your account"
|
||||||
|
onPress={() => dispatch({type: 'next'})}>
|
||||||
|
<ButtonText>Continue</ButtonText>
|
||||||
|
<ButtonIcon icon={ChevronRight} />
|
||||||
|
</Button>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/context'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
|
||||||
|
export function StepFollowingFeed() {
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<Title>Your default feed is "Following"</Title>
|
||||||
|
<Description>
|
||||||
|
It only show posts from the people your follow, in the order they were
|
||||||
|
posted.
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<Button
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label="Continue setting up your account"
|
||||||
|
onPress={() => dispatch({type: 'next'})}>
|
||||||
|
<ButtonText>Continue</ButtonText>
|
||||||
|
<ButtonIcon icon={ChevronRight} />
|
||||||
|
</Button>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {useTheme, atoms as a} from '#/alf'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/context'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
|
||||||
|
export function StepInterests() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const [saving, setSaving] = React.useState(false)
|
||||||
|
|
||||||
|
const saveInterests = React.useCallback(async () => {
|
||||||
|
setSaving(true)
|
||||||
|
await new Promise(y => setTimeout(y, 1000))
|
||||||
|
setSaving(false)
|
||||||
|
dispatch({type: 'next'})
|
||||||
|
}, [setSaving, dispatch])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.p_lg,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_975,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<At size="xl" fill={t.palette.primary_500} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Title>What are your interests?</Title>
|
||||||
|
<Description>
|
||||||
|
We'll use this to help customize your experience.
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<Button
|
||||||
|
disabled={saving}
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label="Continue setting up your account"
|
||||||
|
onPress={saveInterests}>
|
||||||
|
<ButtonText>Continue</ButtonText>
|
||||||
|
<ButtonIcon icon={saving ? Loader : ChevronRight} />
|
||||||
|
</Button>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a, useBreakpoints} from '#/alf'
|
||||||
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/context'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
|
||||||
|
export function StepSuggestedFollows() {
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<Title>Here are some accounts for your to follow:</Title>
|
||||||
|
<Description>Based on your interest in Pets and Books.</Description>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||||
|
<Button
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label="Continue setting up your account"
|
||||||
|
onPress={() => dispatch({type: 'next'})}>
|
||||||
|
<ButtonText>Follow All</ButtonText>
|
||||||
|
<ButtonIcon icon={Plus} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
key={state.activeStep + '2'} // remove focus state on nav
|
||||||
|
variant="outline"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
label="Continue setting up your account"
|
||||||
|
onPress={() => dispatch({type: 'next'})}>
|
||||||
|
Skip
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
|
||||||
|
import {Context} from '#/screens/Onboarding/context'
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
OnboardingControls,
|
||||||
|
} from '#/screens/Onboarding/Layout'
|
||||||
|
|
||||||
|
export function StepTopicalFeeds() {
|
||||||
|
const {state, dispatch} = React.useContext(Context)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.align_start]}>
|
||||||
|
<Title>Feeds can be topic-based as well!</Title>
|
||||||
|
<Description>
|
||||||
|
Here are some topic-based feeds based on your interests in Pets and
|
||||||
|
Books.
|
||||||
|
</Description>
|
||||||
|
|
||||||
|
<OnboardingControls.Portal>
|
||||||
|
<Button
|
||||||
|
key={state.activeStep} // remove focus state on nav
|
||||||
|
variant="gradient"
|
||||||
|
color="gradient_sky"
|
||||||
|
size="large"
|
||||||
|
label="Continue setting up your account"
|
||||||
|
onPress={() => dispatch({type: 'next'})}>
|
||||||
|
<ButtonText>Continue</ButtonText>
|
||||||
|
<ButtonIcon icon={ChevronRight} />
|
||||||
|
</Button>
|
||||||
|
</OnboardingControls.Portal>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export type OnboardingState = {
|
||||||
|
hasPrev: boolean
|
||||||
|
totalSteps: number
|
||||||
|
activeStep:
|
||||||
|
| 'interests'
|
||||||
|
| 'suggestedFollows'
|
||||||
|
| 'followingFeed'
|
||||||
|
| 'algoFeeds'
|
||||||
|
| 'topicalFeeds'
|
||||||
|
activeStepIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnboardingAction =
|
||||||
|
| {
|
||||||
|
type: 'next'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'prev'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState: OnboardingState = {
|
||||||
|
hasPrev: false,
|
||||||
|
totalSteps: 5,
|
||||||
|
activeStep: 'interests',
|
||||||
|
activeStepIndex: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Context = React.createContext<{
|
||||||
|
state: OnboardingState
|
||||||
|
dispatch: React.Dispatch<OnboardingAction>
|
||||||
|
}>({
|
||||||
|
state: initialState,
|
||||||
|
dispatch: () => {},
|
||||||
|
})
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Portal} from '#/components/Portal'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Context,
|
||||||
|
OnboardingState,
|
||||||
|
OnboardingAction,
|
||||||
|
initialState,
|
||||||
|
} from '#/screens/Onboarding/context'
|
||||||
|
import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout'
|
||||||
|
import {StepInterests} from '#/screens/Onboarding/StepInterests'
|
||||||
|
import {StepSuggestedFollows} from '#/screens/Onboarding/StepSuggestedFollows'
|
||||||
|
import {StepFollowingFeed} from '#/screens/Onboarding/StepFollowingFeed'
|
||||||
|
import {StepAlgoFeeds} from '#/screens/Onboarding/StepAlgoFeeds'
|
||||||
|
import {StepTopicalFeeds} from '#/screens/Onboarding/StepTopicalFeeds'
|
||||||
|
|
||||||
|
function reducer(s: OnboardingState, a: OnboardingAction): OnboardingState {
|
||||||
|
const next = s
|
||||||
|
|
||||||
|
switch (a.type) {
|
||||||
|
case 'next': {
|
||||||
|
if (s.activeStep === 'interests') {
|
||||||
|
next.activeStep = 'suggestedFollows'
|
||||||
|
next.activeStepIndex = 2
|
||||||
|
} else if (s.activeStep === 'suggestedFollows') {
|
||||||
|
next.activeStep = 'followingFeed'
|
||||||
|
next.activeStepIndex = 3
|
||||||
|
} else if (s.activeStep === 'followingFeed') {
|
||||||
|
next.activeStep = 'algoFeeds'
|
||||||
|
next.activeStepIndex = 4
|
||||||
|
} else if (s.activeStep === 'algoFeeds') {
|
||||||
|
next.activeStep = 'topicalFeeds'
|
||||||
|
next.activeStepIndex = 5
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'prev': {
|
||||||
|
if (s.activeStep === 'suggestedFollows') {
|
||||||
|
next.activeStep = 'interests'
|
||||||
|
next.activeStepIndex = 1
|
||||||
|
} else if (s.activeStep === 'followingFeed') {
|
||||||
|
next.activeStep = 'suggestedFollows'
|
||||||
|
next.activeStepIndex = 2
|
||||||
|
} else if (s.activeStep === 'algoFeeds') {
|
||||||
|
next.activeStep = 'followingFeed'
|
||||||
|
next.activeStepIndex = 3
|
||||||
|
} else if (s.activeStep === 'topicalFeeds') {
|
||||||
|
next.activeStep = 'algoFeeds'
|
||||||
|
next.activeStepIndex = 4
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
...next,
|
||||||
|
hasPrev: next.activeStep !== 'interests',
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`onboarding`, state)
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Onboarding() {
|
||||||
|
const [state, dispatch] = React.useReducer(reducer, initialState)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<OnboardingControls.Provider>
|
||||||
|
<Context.Provider
|
||||||
|
value={React.useMemo(() => ({state, dispatch}), [state, dispatch])}>
|
||||||
|
<Layout>
|
||||||
|
{state.activeStep === 'interests' && <StepInterests />}
|
||||||
|
{state.activeStep === 'suggestedFollows' && (
|
||||||
|
<StepSuggestedFollows />
|
||||||
|
)}
|
||||||
|
{state.activeStep === 'followingFeed' && <StepFollowingFeed />}
|
||||||
|
{state.activeStep === 'algoFeeds' && <StepAlgoFeeds />}
|
||||||
|
{state.activeStep === 'topicalFeeds' && <StepTopicalFeeds />}
|
||||||
|
</Layout>
|
||||||
|
</Context.Provider>
|
||||||
|
</OnboardingControls.Provider>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {PWI_ENABLED} from '#/lib/build-flags'
|
import {PWI_ENABLED, NEW_ONBOARDING_ENABLED} from '#/lib/build-flags'
|
||||||
|
|
||||||
// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts
|
// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts
|
||||||
// MIT License
|
// MIT License
|
||||||
@@ -37,6 +37,7 @@ import {useSession} from '#/state/session'
|
|||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {LoggedOut} from '../com/auth/LoggedOut'
|
import {LoggedOut} from '../com/auth/LoggedOut'
|
||||||
import {Onboarding} from '../com/auth/Onboarding'
|
import {Onboarding} from '../com/auth/Onboarding'
|
||||||
|
import {Onboarding as NewOnboarding} from '#/screens/Onboarding'
|
||||||
|
|
||||||
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
|
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
|
||||||
requireAuth?: boolean
|
requireAuth?: boolean
|
||||||
@@ -107,8 +108,12 @@ function NativeStackNavigator({
|
|||||||
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
||||||
}
|
}
|
||||||
if (onboardingState.isActive) {
|
if (onboardingState.isActive) {
|
||||||
|
if (NEW_ONBOARDING_ENABLED) {
|
||||||
|
return <NewOnboarding />
|
||||||
|
} else {
|
||||||
return <Onboarding />
|
return <Onboarding />
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const newDescriptors: typeof descriptors = {}
|
const newDescriptors: typeof descriptors = {}
|
||||||
for (let key in descriptors) {
|
for (let key in descriptors) {
|
||||||
const descriptor = descriptors[key]
|
const descriptor = descriptors[key]
|
||||||
|
|||||||
Reference in New Issue
Block a user