Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2078995daf | |||
| f0e32ba1db | |||
| 221add5aae |
@@ -1,6 +1,6 @@
|
||||
import React, {ComponentProps} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {StyleSheet, TouchableWithoutFeedback} from 'react-native'
|
||||
import {TouchableWithoutFeedback} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {gradients} from 'lib/styles'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
@@ -8,6 +8,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {clamp} from 'lib/numbers'
|
||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {useStyles} from 'view/nova'
|
||||
|
||||
export interface FABProps
|
||||
extends ComponentProps<typeof TouchableWithoutFeedback> {
|
||||
@@ -21,59 +22,51 @@ export const FABInner = observer(function FABInnerImpl({
|
||||
...props
|
||||
}: FABProps) {
|
||||
const insets = useSafeAreaInsets()
|
||||
const {isMobile, isTablet} = useWebMediaQueries()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {fabMinimalShellTransform} = useMinimalShellMode()
|
||||
|
||||
const size = React.useMemo(() => {
|
||||
return isTablet ? styles.sizeLarge : styles.sizeRegular
|
||||
}, [isTablet])
|
||||
const tabletSpacing = React.useMemo(() => {
|
||||
return isTablet
|
||||
? {right: 50, bottom: 50}
|
||||
: {
|
||||
right: 24,
|
||||
bottom: clamp(insets.bottom, 15, 60) + 15,
|
||||
}
|
||||
}, [insets.bottom, isTablet])
|
||||
const styles = useStyles({
|
||||
outer: {
|
||||
position: 'absolute',
|
||||
z: 1,
|
||||
right: 24,
|
||||
bottom: clamp(insets.bottom, 15, 60) + 15,
|
||||
gtMobile: {
|
||||
right: 50,
|
||||
bottom: 50,
|
||||
},
|
||||
},
|
||||
sizing: {
|
||||
w: 60,
|
||||
h: 60,
|
||||
radius: 30,
|
||||
gtMobile: {
|
||||
w: 70,
|
||||
h: 70,
|
||||
radius: 35,
|
||||
},
|
||||
},
|
||||
inner: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback testID={testID} {...props}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.outer,
|
||||
size,
|
||||
tabletSpacing,
|
||||
styles.sizing,
|
||||
isMobile && fabMinimalShellTransform,
|
||||
]}>
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.inner, size]}>
|
||||
style={[styles.inner, styles.sizing]}>
|
||||
{icon}
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
</TouchableWithoutFeedback>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
sizeRegular: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
},
|
||||
sizeLarge: {
|
||||
width: 70,
|
||||
height: 70,
|
||||
borderRadius: 35,
|
||||
},
|
||||
outer: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
},
|
||||
inner: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { View, Text as RNText, Pressable as RNPressable } from 'react-native'
|
||||
|
||||
import { createSystem } from './lib/system'
|
||||
import { light, dark } from './themes'
|
||||
import { web } from './lib/utils'
|
||||
|
||||
export * from './lib/utils'
|
||||
|
||||
const {
|
||||
ThemeProvider,
|
||||
useTheme,
|
||||
useStyle,
|
||||
useStyles,
|
||||
styled,
|
||||
} = createSystem({
|
||||
light,
|
||||
dark,
|
||||
})
|
||||
|
||||
export {
|
||||
ThemeProvider,
|
||||
useTheme,
|
||||
useStyle,
|
||||
useStyles,
|
||||
styled,
|
||||
}
|
||||
|
||||
export const Box = styled(View)
|
||||
export const Pressable = styled(RNPressable)
|
||||
export const Text = styled(RNText, {
|
||||
color: 'l8',
|
||||
fontSize: 's',
|
||||
})
|
||||
|
||||
/**
|
||||
* @see https://necolas.github.io/react-native-web/docs/accessibility/#semantic-html
|
||||
* @see https://docs.expo.dev/develop/user-interface/fonts/
|
||||
*/
|
||||
export const H1 = styled(RNText, {
|
||||
role: web('heading'),
|
||||
color: 'l8',
|
||||
fontSize: 'l',
|
||||
gtMobile: {
|
||||
fontSize: 'xl',
|
||||
},
|
||||
...web({
|
||||
'aria-level': 1,
|
||||
}),
|
||||
})
|
||||
export const P = styled(RNText, {
|
||||
role: web('paragraph'),
|
||||
color: 'l8',
|
||||
fontSize: 's',
|
||||
})
|
||||
@@ -0,0 +1,149 @@
|
||||
import React from "react";
|
||||
import { Dimensions } from "react-native";
|
||||
|
||||
import {
|
||||
Theme,
|
||||
ResponsiveStyles,
|
||||
Tokens,
|
||||
Properties,
|
||||
Macros,
|
||||
Breakpoints,
|
||||
} from "./theme";
|
||||
|
||||
type ThemeConfig<
|
||||
T extends Tokens,
|
||||
P extends Properties,
|
||||
M extends Macros<T>,
|
||||
B extends Breakpoints,
|
||||
> = {
|
||||
[key: string]: Theme<T, P, M, B>;
|
||||
};
|
||||
|
||||
export function createSystem<
|
||||
T extends Tokens,
|
||||
P extends Properties,
|
||||
M extends Macros<T>,
|
||||
B extends Breakpoints,
|
||||
>(themes: ThemeConfig<T, P, M, B>) {
|
||||
const theme = Object.values(themes)[0];
|
||||
|
||||
type SystemTheme = Theme<T, P, M, B>;
|
||||
type ResponsiveStyleProps = ResponsiveStyles<T, P, M, B>;
|
||||
type DebugProps = {
|
||||
/**
|
||||
* Debug mode will log the styles and props to the console
|
||||
*/
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
type ThemeName = keyof typeof themes;
|
||||
const Context = React.createContext<{
|
||||
themeName: ThemeName;
|
||||
theme: SystemTheme;
|
||||
themes: {
|
||||
[key in ThemeName]: SystemTheme;
|
||||
};
|
||||
}>({
|
||||
themeName: Object.keys(themes)[0],
|
||||
theme,
|
||||
themes,
|
||||
});
|
||||
|
||||
const ThemeProvider = ({
|
||||
children,
|
||||
theme,
|
||||
}: React.PropsWithChildren<{ theme: ThemeName }>) => (
|
||||
<Context.Provider
|
||||
value={{
|
||||
themeName: theme,
|
||||
theme: themes[theme],
|
||||
themes,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
);
|
||||
|
||||
function useTheme() {
|
||||
return React.useContext(Context);
|
||||
}
|
||||
|
||||
function useBreakpoints() {
|
||||
const { theme } = useTheme();
|
||||
const [breakpoints, setBreakpoints] = React.useState(
|
||||
theme.getActiveBreakpoints({ width: Dimensions.get("window").width }),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = Dimensions.addEventListener("change", ({ window }) => {
|
||||
const bp = theme.getActiveBreakpoints({ width: window.width });
|
||||
if (bp.current !== breakpoints.current) setBreakpoints(bp);
|
||||
});
|
||||
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
}, [breakpoints, theme]);
|
||||
|
||||
return breakpoints;
|
||||
}
|
||||
|
||||
function useStyle(props: ResponsiveStyleProps) {
|
||||
const { theme } = useTheme();
|
||||
const breakpoints = useBreakpoints();
|
||||
return theme.style(props, breakpoints.active).styles;
|
||||
}
|
||||
|
||||
function useStyles<O extends Record<string, ResponsiveStyleProps>>(
|
||||
styles: Record<string, ResponsiveStyleProps>,
|
||||
): {
|
||||
[Name in keyof O]: ReturnType<SystemTheme["style"]>["styles"];
|
||||
} {
|
||||
const { theme } = useTheme();
|
||||
const breakpoints = useBreakpoints();
|
||||
return React.useMemo(() => {
|
||||
return Object.entries(styles).reduce(
|
||||
(acc, [key, style]) => {
|
||||
acc[key as keyof O] = theme.style(style, breakpoints.active).styles;
|
||||
return acc;
|
||||
},
|
||||
{} as { [Name in keyof O]: ReturnType<SystemTheme["style"]>["styles"] },
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [styles, breakpoints.current, theme]);
|
||||
}
|
||||
|
||||
function styled<T extends Record<string, any>>(
|
||||
Component: React.ComponentType<T>,
|
||||
defaultProps: ResponsiveStyleProps = {},
|
||||
) {
|
||||
return React.forwardRef<
|
||||
T,
|
||||
T &
|
||||
ResponsiveStyleProps &
|
||||
DebugProps & { children?: React.ReactNode | React.ReactNodeArray }
|
||||
>((props, ref) => {
|
||||
const { theme } = useTheme();
|
||||
const breakpoints = useBreakpoints();
|
||||
const { styles, props: rest } = theme.style<T>(
|
||||
{
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
breakpoints.active,
|
||||
);
|
||||
if (props.debug) console.log({ styles, props: rest });
|
||||
return <Component {...rest} style={[styles, rest.style]} ref={ref} />;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
Context,
|
||||
ThemeProvider,
|
||||
useTheme,
|
||||
useBreakpoints,
|
||||
useStyle,
|
||||
useStyles,
|
||||
styled,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import type {
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
ImageStyle,
|
||||
DimensionValue,
|
||||
ColorValue,
|
||||
} from "react-native";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
type StyleObject = ViewStyle & TextStyle & ImageStyle;
|
||||
type StyleObjectProperties = keyof StyleObject;
|
||||
type ColorProperties =
|
||||
| "color"
|
||||
| "backgroundColor"
|
||||
| "borderColor"
|
||||
| "borderTopColor"
|
||||
| "borderRightColor"
|
||||
| "borderBottomColor"
|
||||
| "borderLeftColor";
|
||||
type DimensionProperties =
|
||||
| "padding"
|
||||
| "paddingTop"
|
||||
| "paddingBottom"
|
||||
| "paddingLeft"
|
||||
| "paddingRight"
|
||||
| "paddingVertical"
|
||||
| "paddingHorizontal"
|
||||
| "margin"
|
||||
| "marginTop"
|
||||
| "marginBottom"
|
||||
| "marginLeft"
|
||||
| "marginRight"
|
||||
| "top"
|
||||
| "bottom"
|
||||
| "left"
|
||||
| "right"
|
||||
| "gap"
|
||||
| "rowGap"
|
||||
| "columnGap";
|
||||
|
||||
export type Tokens = {
|
||||
space?: DimensionValue[] | Record<string, DimensionValue>;
|
||||
} & {
|
||||
[Property in StyleObjectProperties]?: Record<
|
||||
string,
|
||||
Required<StyleObject>[Property]
|
||||
>;
|
||||
};
|
||||
export type Properties = Record<string, Readonly<StyleObjectProperties[]>>;
|
||||
export type Macros<T extends Tokens> = Record<
|
||||
string,
|
||||
(value: any, tokens: T) => StyleObject
|
||||
>;
|
||||
export type Breakpoints = {
|
||||
[Breakpoint: string]: number;
|
||||
};
|
||||
|
||||
export type Styles<T extends Tokens, P extends Properties, M extends Macros<T>> = {
|
||||
[Property in ColorProperties]?: keyof T["color"] | Omit<ColorValue, keyof T["color"]>;
|
||||
} & {
|
||||
[Property in DimensionProperties]?: keyof T["space"] | DimensionValue;
|
||||
} & {
|
||||
[Property in Exclude<
|
||||
StyleObjectProperties,
|
||||
ColorProperties | DimensionProperties
|
||||
>]?: keyof T[Property] | StyleObject[Property];
|
||||
} & {
|
||||
// this empty P is to avoid circular references back to Properties type
|
||||
[Property in keyof P]?: Styles<T, {}, M>[P[Property][0]];
|
||||
} & {
|
||||
[Property in keyof M]?: M[Property] extends (
|
||||
value: infer Value,
|
||||
tokens: T,
|
||||
) => StyleObject
|
||||
? Value
|
||||
: never;
|
||||
};
|
||||
|
||||
export type ResponsiveStyles<
|
||||
T extends Tokens, P extends Properties, M extends Macros<T>,
|
||||
B extends Breakpoints,
|
||||
> = Styles<T, P, M> | {
|
||||
[Breakpoint in keyof B]?: Styles<T, P, M>;
|
||||
};
|
||||
|
||||
export type Theme<
|
||||
T extends Tokens,
|
||||
P extends Properties,
|
||||
M extends Macros<T>,
|
||||
B extends Breakpoints,
|
||||
> = {
|
||||
config: {
|
||||
tokens: T;
|
||||
properties: P;
|
||||
macros: M;
|
||||
breakpoints: B;
|
||||
};
|
||||
style: <Props>(
|
||||
props: ResponsiveStyles<T, P, M, B> | Omit<Props, keyof P>,
|
||||
breakpoints?: (keyof B)[],
|
||||
) => {
|
||||
styles: StyleObject;
|
||||
props: Props;
|
||||
};
|
||||
getActiveBreakpoints: ({ width }: { width: number }) => {
|
||||
active: (keyof B)[];
|
||||
current: keyof B;
|
||||
};
|
||||
};
|
||||
|
||||
const specialTokenMapping = {
|
||||
color: "color",
|
||||
backgroundColor: "color",
|
||||
borderColor: "color",
|
||||
borderTopColor: "color",
|
||||
borderRightColor: "color",
|
||||
borderBottomColor: "color",
|
||||
borderLeftColor: "color",
|
||||
padding: "space",
|
||||
paddingTop: "space",
|
||||
paddingBottom: "space",
|
||||
paddingLeft: "space",
|
||||
paddingRight: "space",
|
||||
paddingVertical: "space",
|
||||
paddingHorizontal: "space",
|
||||
margin: "space",
|
||||
marginTop: "space",
|
||||
marginBottom: "space",
|
||||
marginLeft: "space",
|
||||
marginRight: "space",
|
||||
top: "space",
|
||||
bottom: "space",
|
||||
left: "space",
|
||||
right: "space",
|
||||
gap: "space",
|
||||
rowGap: "space",
|
||||
columnGap: "space",
|
||||
};
|
||||
|
||||
const propertyMapping: Properties = {
|
||||
// FlexStyle
|
||||
alignContent: ["alignContent"],
|
||||
alignItems: ["alignItems"],
|
||||
alignSelf: ["alignSelf"],
|
||||
aspectRatio: ["aspectRatio"],
|
||||
borderBottomWidth: ["borderBottomWidth"],
|
||||
borderEndWidth: ["borderEndWidth"],
|
||||
borderLeftWidth: ["borderLeftWidth"],
|
||||
borderRightWidth: ["borderRightWidth"],
|
||||
borderStartWidth: ["borderStartWidth"],
|
||||
borderTopWidth: ["borderTopWidth"],
|
||||
borderWidth: ["borderWidth"],
|
||||
bottom: ["bottom"],
|
||||
display: ["display"],
|
||||
end: ["end"],
|
||||
flex: ["flex"],
|
||||
flexBasis: ["flexBasis"],
|
||||
flexDirection: ["flexDirection"],
|
||||
rowGap: ["rowGap"],
|
||||
gap: ["gap"],
|
||||
columnGap: ["columnGap"],
|
||||
flexGrow: ["flexGrow"],
|
||||
flexShrink: ["flexShrink"],
|
||||
flexWrap: ["flexWrap"],
|
||||
height: ["height"],
|
||||
justifyContent: ["justifyContent"],
|
||||
left: ["left"],
|
||||
margin: ["marginTop", "marginBottom", "marginLeft", "marginRight"],
|
||||
marginBottom: ["marginBottom"],
|
||||
marginEnd: ["marginEnd"],
|
||||
marginHorizontal: ["marginLeft", "marginRight"],
|
||||
marginLeft: ["marginLeft"],
|
||||
marginRight: ["marginRight"],
|
||||
marginStart: ["marginStart"],
|
||||
marginTop: ["marginTop"],
|
||||
marginVertical: ["marginTop", "marginBottom"],
|
||||
maxHeight: ["maxHeight"],
|
||||
maxWidth: ["maxWidth"],
|
||||
overflow: ["overflow"],
|
||||
padding: ["paddingTop", "paddingBottom", "paddingLeft", "paddingRight"],
|
||||
paddingBottom: ["paddingBottom"],
|
||||
paddingEnd: ["paddingEnd"],
|
||||
paddingHorizontal: ["paddingLeft", "paddingRight"],
|
||||
paddingLeft: ["paddingLeft"],
|
||||
paddingRight: ["paddingRight"],
|
||||
paddingStart: ["paddingStart"],
|
||||
paddingTop: ["paddingTop"],
|
||||
paddingVertical: ["paddingTop", "paddingBottom"],
|
||||
position: ["position"],
|
||||
right: ["right"],
|
||||
start: ["start"],
|
||||
top: ["top"],
|
||||
width: ["width"],
|
||||
zIndex: ["zIndex"],
|
||||
direction: ["direction"],
|
||||
|
||||
// ShadowStyle
|
||||
shadowColor: ["shadowColor"],
|
||||
shadowOffset: ["shadowOffset"],
|
||||
shadowOpacity: ["shadowOpacity"],
|
||||
shadowRadius: ["shadowRadius"],
|
||||
|
||||
// TransformStyle
|
||||
transform: ["transform"],
|
||||
transformMatrix: ["transformMatrix"],
|
||||
rotation: ["rotation"],
|
||||
scaleX: ["scaleX"],
|
||||
scaleY: ["scaleY"],
|
||||
translateX: ["translateX"],
|
||||
translateY: ["translateY"],
|
||||
|
||||
// ViewStyle
|
||||
backfaceVisibility: ["backfaceVisibility"],
|
||||
backgroundColor: ["backgroundColor"],
|
||||
borderBlockColor: ["borderBlockColor"],
|
||||
borderBlockEndColor: ["borderBlockEndColor"],
|
||||
borderBlockStartColor: ["borderBlockStartColor"],
|
||||
borerBottomColor: ["borderBottomColor"],
|
||||
borderBottomEndRadius: ["borderBottomEndRadius"],
|
||||
borderBottomLeftRadius: ["borderBottomLeftRadius"],
|
||||
borderBottomRightRadius: ["borderBottomRightRadius"],
|
||||
borderBottomStartRadius: ["borderBottomStartRadius"],
|
||||
borderColor: ["borderColor"],
|
||||
borderCurve: ["borderCurve"],
|
||||
borderEndColor: ["borderEndColor"],
|
||||
borderEndEndRadius: ["borderEndEndRadius"],
|
||||
borderEndStartRadius: ["borderEndStartRadius"],
|
||||
borderLeftColor: ["borderLeftColor"],
|
||||
borderRadius: ["borderRadius"],
|
||||
borderRightColor: ["borderRightColor"],
|
||||
borderStartColor: ["borderStartColor"],
|
||||
borderStartEndRadius: ["borderStartEndRadius"],
|
||||
borderStartStartRadius: ["borderStartStartRadius"],
|
||||
borderStyle: ["borderStyle"],
|
||||
borderTopColor: ["borderTopColor"],
|
||||
borderTopEndRadius: ["borderTopEndRadius"],
|
||||
borderTopLeftRadius: ["borderTopLeftRadius"],
|
||||
borderTopRightRadius: ["borderTopRightRadius"],
|
||||
borderTopStartRadius: ["borderTopStartRadius"],
|
||||
opacity: ["opacity"],
|
||||
elevation: ["elevation"],
|
||||
pointerEvents: ["pointerEvents"],
|
||||
|
||||
// TextStyleIOS
|
||||
fontVariant: ["fontVariant"],
|
||||
textDecorationColor: ["textDecorationColor"],
|
||||
textDecorationStyle: ["textDecorationStyle"],
|
||||
writingDirection: ["writingDirection"],
|
||||
|
||||
// TextStyleAndroid
|
||||
textAlignVertical: ["textAlignVertical"],
|
||||
verticalAlign: ["verticalAlign"],
|
||||
includeFontPadding: ["includeFontPadding"],
|
||||
|
||||
// TextStyle
|
||||
color: ["color"],
|
||||
fontFamily: ["fontFamily"],
|
||||
fontSize: ["fontSize"],
|
||||
fontStyle: ["fontStyle"],
|
||||
fontWeight: ["fontWeight"],
|
||||
letterSpacing: ["letterSpacing"],
|
||||
lineHeight: ["lineHeight"],
|
||||
textAlign: ["textAlign"],
|
||||
textDecorationLine: ["textDecorationLine"],
|
||||
textShadowColor: ["textShadowColor"],
|
||||
textShadowOffset: ["textShadowOffset"],
|
||||
textTransform: ["textTransform"],
|
||||
|
||||
// ImageStyle
|
||||
resizeMode: ["resizeMode"],
|
||||
overlayColor: ["overlayColor"],
|
||||
tintColor: ["tintColor"],
|
||||
objectFit: ["objectFit"],
|
||||
};
|
||||
|
||||
export const createTheme = <
|
||||
T extends Tokens,
|
||||
P extends Properties,
|
||||
M extends Macros<T>,
|
||||
B extends Breakpoints,
|
||||
>({
|
||||
tokens,
|
||||
properties: userProperties = {},
|
||||
macros: userMacros = {},
|
||||
breakpoints: userBreakpoints = {},
|
||||
}: {
|
||||
tokens: T;
|
||||
properties?: Partial<P>;
|
||||
macros?: Partial<M>;
|
||||
breakpoints?: Partial<B>;
|
||||
}): Theme<T, P, M, B> => {
|
||||
const properties = Object.assign({}, propertyMapping, userProperties) as P;
|
||||
const macros = userMacros as M;
|
||||
const breakpoints = Object.entries(userBreakpoints)
|
||||
.sort(([_a, a], [_b, b]) => {
|
||||
return a - b;
|
||||
})
|
||||
.reduce((breakpoints, [key, value]) => {
|
||||
breakpoints[key as keyof B] = value;
|
||||
return breakpoints;
|
||||
}, {} as B);
|
||||
|
||||
type InnerTheme = Theme<
|
||||
typeof tokens,
|
||||
typeof properties,
|
||||
typeof macros,
|
||||
typeof breakpoints
|
||||
>;
|
||||
|
||||
function _applyStyleProperty(styles: StyleObject, prop: string, value: any) {
|
||||
for (const _prop of properties[prop]) {
|
||||
// @ts-ignore no index sig, it's fine
|
||||
const t = specialTokenMapping[_prop]
|
||||
? // @ts-ignore no index sig, it's fine
|
||||
tokens[specialTokenMapping[_prop]]
|
||||
: tokens[_prop];
|
||||
styles[_prop] = t?.[value] || value;
|
||||
}
|
||||
}
|
||||
|
||||
const style: InnerTheme["style"] = (rawProps, activeBreakpoints = []) => {
|
||||
let styles: StyleObject = {};
|
||||
const props: any = {};
|
||||
|
||||
Object.keys(rawProps).forEach((prop) => {
|
||||
// @ts-ignore no index sig, it's fine
|
||||
const value = rawProps[prop];
|
||||
|
||||
if (value === undefined) return;
|
||||
|
||||
if (properties[prop]) {
|
||||
_applyStyleProperty(styles, prop, value);
|
||||
} else if (macros[prop]) {
|
||||
styles = {
|
||||
...styles,
|
||||
// @ts-ignore no index sig, it's fine
|
||||
...(rawProps[prop] === true ? macros[prop](value, tokens) : {}),
|
||||
};
|
||||
} else if (breakpoints[prop]) {
|
||||
for (const b of Object.keys(breakpoints)) {
|
||||
if (activeBreakpoints.includes(b)) {
|
||||
// @ts-ignore no index sig, it's fine
|
||||
const breakpointStyles = (rawProps[b] || {})
|
||||
const r = style(breakpointStyles, activeBreakpoints);
|
||||
styles = { ...styles, ...r.styles };
|
||||
} else {
|
||||
// @ts-ignore no index sig, it's fine
|
||||
delete rawProps[b];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore no index sig, it's fine
|
||||
props[prop] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
styles: StyleSheet.create({ sheet: styles }).sheet,
|
||||
props: props,
|
||||
};
|
||||
};
|
||||
|
||||
function getActiveBreakpoints({ width }: { width: number }) {
|
||||
const active: (keyof typeof breakpoints)[] = Object.keys(
|
||||
breakpoints,
|
||||
).filter((breakpoint) => width >= breakpoints[breakpoint]);
|
||||
|
||||
return {
|
||||
active,
|
||||
current: active[active.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
config: {
|
||||
tokens,
|
||||
properties,
|
||||
macros,
|
||||
breakpoints,
|
||||
},
|
||||
style,
|
||||
getActiveBreakpoints,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Platform } from "react-native";
|
||||
|
||||
export function web(value: any) {
|
||||
return Platform.select({
|
||||
web: value,
|
||||
});
|
||||
}
|
||||
|
||||
export function ios(value: any) {
|
||||
return Platform.select({
|
||||
ios: value,
|
||||
});
|
||||
}
|
||||
|
||||
export function android(value: any) {
|
||||
return Platform.select({
|
||||
android: value,
|
||||
});
|
||||
}
|
||||
|
||||
export function native(value: any) {
|
||||
return Platform.select({
|
||||
native: value,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { createTheme } from './lib/theme'
|
||||
|
||||
const palette = {
|
||||
white: '#FFFFFF',
|
||||
black: '#0A0B0D',
|
||||
gray1: '#F7F9FC',
|
||||
gray2: '#E4E8EE',
|
||||
gray3: '#CED5DE',
|
||||
gray4: '#828A99',
|
||||
gray5: '#4D5564',
|
||||
gray6: '#1D2126',
|
||||
blue: '#1185FE',
|
||||
green: '#54D469',
|
||||
red: '#FB4566',
|
||||
}
|
||||
|
||||
export const light = createTheme({
|
||||
tokens: {
|
||||
color: {
|
||||
primary: palette.blue,
|
||||
l1: palette.white,
|
||||
l2: palette.gray1,
|
||||
l3: palette.gray2,
|
||||
l4: palette.gray3,
|
||||
l5: palette.gray4,
|
||||
l6: palette.gray5,
|
||||
l7: palette.gray6,
|
||||
l8: palette.black,
|
||||
},
|
||||
fontSize: {
|
||||
xs: 12,
|
||||
s: 14,
|
||||
m: 16,
|
||||
l: 18,
|
||||
xl: 22,
|
||||
},
|
||||
lineHeight: {
|
||||
xs: 12,
|
||||
s: 14,
|
||||
m: 16,
|
||||
l: 18,
|
||||
xl: 22,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
d: ['display'],
|
||||
w: ['width'],
|
||||
h: ['height'],
|
||||
c: ['color'],
|
||||
bg: ['backgroundColor'],
|
||||
ma: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
|
||||
mt: ['marginTop'],
|
||||
mb: ['marginBottom'],
|
||||
ml: ['marginLeft'],
|
||||
mr: ['marginRight'],
|
||||
my: ['marginTop', 'marginBottom'],
|
||||
mx: ['marginLeft', 'marginRight'],
|
||||
/**
|
||||
* Alias for `padding`, maps to all padding properties e.g. `paddingTop`,
|
||||
* `paddingBottom`, etc.
|
||||
*/
|
||||
pa: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
|
||||
pt: ['paddingTop'],
|
||||
pb: ['paddingBottom'],
|
||||
pl: ['paddingLeft'],
|
||||
pr: ['paddingRight'],
|
||||
py: ['paddingTop', 'paddingBottom'],
|
||||
px: ['paddingLeft', 'paddingRight'],
|
||||
z: ['zIndex'],
|
||||
fs: ['fontSize'],
|
||||
ff: ['fontFamily'],
|
||||
fw: ['fontWeight'],
|
||||
lh: ['lineHeight'],
|
||||
ta: ['textAlign'],
|
||||
radius: ['borderRadius'],
|
||||
},
|
||||
macros: {
|
||||
row: (_: boolean) => ({ flexDirection: 'row' }),
|
||||
column: (_: boolean) => ({ flex: 1 }),
|
||||
aic: (_: boolean) => ({ alignItems: 'center' }),
|
||||
aie: (_: boolean) => ({ alignItems: 'flex-end' }),
|
||||
jcs: (_: boolean) => ({ justifyContent: 'flex-start' }),
|
||||
jcc: (_: boolean) => ({ justifyContent: 'center' }),
|
||||
jce: (_: boolean) => ({ justifyContent: 'flex-end' }),
|
||||
jcb: (_: boolean) => ({ justifyContent: 'space-between' }),
|
||||
fdr: (_: boolean) => ({ flexDirection: 'row' }),
|
||||
rel: (_: boolean) => ({ position: 'relative' }),
|
||||
abs: (_: boolean) => ({ position: 'absolute' }),
|
||||
cover: (_: boolean) => ({
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}),
|
||||
tac: (_: boolean) => ({ textAlign: 'center' }),
|
||||
tar: (_: boolean) => ({ textAlign: 'right' }),
|
||||
mxa: (_: boolean) => ({ marginLeft: 'auto', marginRight: 'auto' }),
|
||||
mya: (_: boolean) => ({ marginTop: 'auto', marginBottom: 'auto' }),
|
||||
caps: (_: boolean) => ({ textTransform: 'uppercase' }),
|
||||
fontSize(value: 'xs' | 's' | 'm' | 'l' | 'xl', tokens) {
|
||||
return {
|
||||
fontSize: tokens.fontSize[value],
|
||||
lineHeight: tokens.lineHeight[value],
|
||||
}
|
||||
},
|
||||
},
|
||||
breakpoints: {
|
||||
gtMobile: 800,
|
||||
gtTablet: 1300,
|
||||
}
|
||||
})
|
||||
|
||||
export const dark = createTheme({
|
||||
...light.config,
|
||||
tokens: {
|
||||
...light.config.tokens,
|
||||
color: {
|
||||
...light.config.tokens.color,
|
||||
l1: palette.black,
|
||||
l2: palette.gray6,
|
||||
l3: palette.gray5,
|
||||
l4: palette.gray4,
|
||||
l5: palette.gray3,
|
||||
l6: palette.gray2,
|
||||
l7: palette.gray1,
|
||||
l8: palette.white,
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {PressableWithHover} from 'view/com/util/PressableWithHover'
|
||||
import {
|
||||
useLinkProps,
|
||||
useNavigation,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {Link} from 'view/com/util/Link'
|
||||
import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
|
||||
@@ -38,6 +36,7 @@ import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers'
|
||||
import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {router} from '../../../routes'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {Box, Text, Pressable, useTheme} from 'view/nova'
|
||||
|
||||
const ProfileCard = observer(function ProfileCardImpl() {
|
||||
const store = useStores()
|
||||
@@ -110,6 +109,7 @@ const NavItem = observer(function NavItemImpl({
|
||||
iconFilled,
|
||||
label,
|
||||
}: NavItemProps) {
|
||||
const { theme } = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {isDesktop, isTablet} = useWebMediaQueries()
|
||||
@@ -143,40 +143,62 @@ const NavItem = observer(function NavItemImpl({
|
||||
)
|
||||
|
||||
return (
|
||||
<PressableWithHover
|
||||
style={styles.navItemWrapper}
|
||||
hoverStyle={pal.viewLight}
|
||||
<Pressable
|
||||
fdr
|
||||
aic
|
||||
pa={12}
|
||||
radius={8}
|
||||
gap={10}
|
||||
style={state => ({
|
||||
// TODO
|
||||
backgroundColor: state.hovered ? theme.config.tokens.color.l2 : 'transparent',
|
||||
})}
|
||||
// @ts-ignore the function signature differs on web -prf
|
||||
onPress={onPressWrapped}
|
||||
// @ts-ignore web only -prf
|
||||
href={href}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={{noUnderline: 1}}
|
||||
accessibilityRole="tab"
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={[
|
||||
styles.navItemIconWrapper,
|
||||
isTablet && styles.navItemIconWrapperTablet,
|
||||
]}>
|
||||
<Box
|
||||
aic
|
||||
jcc
|
||||
w={28}
|
||||
h={28}
|
||||
mt={2}
|
||||
z={1}
|
||||
gtMobile={{
|
||||
w: 40,
|
||||
h: 40,
|
||||
}}
|
||||
gtTablet={{
|
||||
w: 28,
|
||||
h: 28,
|
||||
}}>
|
||||
{isCurrent ? iconFilled : icon}
|
||||
{typeof count === 'string' && count ? (
|
||||
<Text
|
||||
type="button"
|
||||
style={[
|
||||
styles.navItemCount,
|
||||
isTablet && styles.navItemCountTablet,
|
||||
]}>
|
||||
abs
|
||||
top={0}
|
||||
left={15}
|
||||
bg={colors.blue3}
|
||||
c={colors.white}
|
||||
fs='s'
|
||||
fw={'bold'}
|
||||
px={4}
|
||||
radius={6}>
|
||||
{count}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
|
||||
<Text fs='xl' fw={isCurrent ? 'bold' : 'normal'}>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
</PressableWithHover>
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -230,7 +252,10 @@ function ComposeBtn() {
|
||||
style={styles.newPostBtnLabel}
|
||||
/>
|
||||
</View>
|
||||
<Text type="button" style={styles.newPostBtnLabel}>
|
||||
<Text
|
||||
c={colors.white}
|
||||
fs='m'
|
||||
fw={'bold'}>
|
||||
New Post
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -418,42 +443,6 @@ const styles = StyleSheet.create({
|
||||
height: 30,
|
||||
},
|
||||
|
||||
navItemWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 12,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
gap: 10,
|
||||
},
|
||||
navItemIconWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 28,
|
||||
height: 28,
|
||||
marginTop: 2,
|
||||
zIndex: 1,
|
||||
},
|
||||
navItemIconWrapperTablet: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
navItemCount: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 15,
|
||||
backgroundColor: colors.blue3,
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 6,
|
||||
},
|
||||
navItemCountTablet: {
|
||||
left: 18,
|
||||
fontSize: 14,
|
||||
},
|
||||
|
||||
newPostBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -23,6 +23,7 @@ import {RoutesContainer, TabsNavigator} from '../../Navigation'
|
||||
import {isStateAtTabRoot} from 'lib/routes/helpers'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {useOTAUpdate} from 'lib/hooks/useOTAUpdate'
|
||||
import { ThemeProvider } from 'view/nova'
|
||||
|
||||
const ShellInner = observer(function ShellInnerImpl() {
|
||||
const store = useStores()
|
||||
@@ -87,14 +88,16 @@ export const Shell: React.FC = observer(function ShellImpl() {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<SafeAreaProvider style={pal.view}>
|
||||
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
|
||||
<StatusBar style={theme.colorScheme === 'dark' ? 'light' : 'dark'} />
|
||||
<RoutesContainer>
|
||||
<ShellInner />
|
||||
</RoutesContainer>
|
||||
</View>
|
||||
</SafeAreaProvider>
|
||||
<ThemeProvider theme='light'>
|
||||
<SafeAreaProvider style={pal.view}>
|
||||
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
|
||||
<StatusBar style={theme.colorScheme === 'dark' ? 'light' : 'dark'} />
|
||||
<RoutesContainer>
|
||||
<ShellInner />
|
||||
</RoutesContainer>
|
||||
</View>
|
||||
</SafeAreaProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user