This commit is contained in:
Eric Bailey
2024-01-13 12:11:25 -06:00
parent 45c807b83b
commit ceef55041a
32 changed files with 729 additions and 525 deletions
+4 -1
View File
@@ -41,7 +41,10 @@ import {
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash'
import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal'
import {
Provider as PortalProvider,
Outlet as PortalOutlet,
} from '#/components/Portal'
SplashScreen.preventAutoHideAsync()
+4 -1
View File
@@ -30,7 +30,10 @@ import {
} from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal'
import {
Provider as PortalProvider,
Outlet as PortalOutlet,
} from '#/components/Portal'
function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
+79 -13
View File
@@ -8,11 +8,8 @@ export type ReadonlyPalette = typeof lightPalette
export type Palette = Mutable<ReadonlyPalette>
export const lightPalette = {
white: '#FFFFFF',
black: '#080B12',
primary: tokens.color.blue_500,
positive: tokens.color.green_500,
negative: tokens.color.red_500,
white: tokens.color.white,
black: tokens.color.black,
contrast_25: tokens.color.gray_25,
contrast_50: tokens.color.gray_50,
@@ -25,14 +22,47 @@ export const lightPalette = {
contrast_700: tokens.color.gray_700,
contrast_800: tokens.color.gray_800,
contrast_900: tokens.color.gray_900,
primary_25: tokens.color.blue_25,
primary_50: tokens.color.blue_50,
primary_100: tokens.color.blue_100,
primary_200: tokens.color.blue_200,
primary_300: tokens.color.blue_300,
primary_400: tokens.color.blue_400,
primary_500: tokens.color.blue_500,
primary_600: tokens.color.blue_600,
primary_700: tokens.color.blue_700,
primary_800: tokens.color.blue_800,
primary_900: tokens.color.blue_900,
positive_25: tokens.color.green_25,
positive_50: tokens.color.green_50,
positive_100: tokens.color.green_100,
positive_200: tokens.color.green_200,
positive_300: tokens.color.green_300,
positive_400: tokens.color.green_400,
positive_500: tokens.color.green_500,
positive_600: tokens.color.green_600,
positive_700: tokens.color.green_700,
positive_800: tokens.color.green_800,
positive_900: tokens.color.green_900,
negative_25: tokens.color.red_25,
negative_50: tokens.color.red_50,
negative_100: tokens.color.red_100,
negative_200: tokens.color.red_200,
negative_300: tokens.color.red_300,
negative_400: tokens.color.red_400,
negative_500: tokens.color.red_500,
negative_600: tokens.color.red_600,
negative_700: tokens.color.red_700,
negative_800: tokens.color.red_800,
negative_900: tokens.color.red_900,
} as const
export const darkPalette: Palette = {
white: '#FFFFFF',
black: '#080B12',
primary: tokens.color.blue_500,
positive: tokens.color.green_500,
negative: tokens.color.red_500,
white: tokens.color.white,
black: tokens.color.black,
contrast_25: tokens.color.gray_900,
contrast_50: tokens.color.gray_800,
@@ -45,6 +75,42 @@ export const darkPalette: Palette = {
contrast_700: tokens.color.gray_100,
contrast_800: tokens.color.gray_50,
contrast_900: tokens.color.gray_25,
primary_25: tokens.color.blue_25,
primary_50: tokens.color.blue_50,
primary_100: tokens.color.blue_100,
primary_200: tokens.color.blue_200,
primary_300: tokens.color.blue_300,
primary_400: tokens.color.blue_400,
primary_500: tokens.color.blue_500,
primary_600: tokens.color.blue_600,
primary_700: tokens.color.blue_700,
primary_800: tokens.color.blue_800,
primary_900: tokens.color.blue_900,
positive_25: tokens.color.green_25,
positive_50: tokens.color.green_50,
positive_100: tokens.color.green_100,
positive_200: tokens.color.green_200,
positive_300: tokens.color.green_300,
positive_400: tokens.color.green_400,
positive_500: tokens.color.green_500,
positive_600: tokens.color.green_600,
positive_700: tokens.color.green_700,
positive_800: tokens.color.green_800,
positive_900: tokens.color.green_900,
negative_25: tokens.color.red_25,
negative_50: tokens.color.red_50,
negative_100: tokens.color.red_100,
negative_200: tokens.color.red_200,
negative_300: tokens.color.red_300,
negative_400: tokens.color.red_400,
negative_500: tokens.color.red_500,
negative_600: tokens.color.red_600,
negative_700: tokens.color.red_700,
negative_800: tokens.color.red_800,
negative_900: tokens.color.red_900,
} as const
export const light = {
@@ -52,7 +118,7 @@ export const light = {
palette: lightPalette,
atoms: {
text: {
color: tokens.color.gray_900,
color: lightPalette.black,
},
text_contrast_600: {
color: lightPalette.contrast_600,
@@ -110,7 +176,7 @@ export const dim: Theme = {
color: darkPalette.contrast_400,
},
text_inverted: {
color: darkPalette.contrast_900,
color: darkPalette.black,
},
bg: {
backgroundColor: darkPalette.contrast_25,
@@ -156,7 +222,7 @@ export const dark: Theme = {
color: darkPalette.contrast_400,
},
text_inverted: {
color: tokens.color.gray_900,
color: darkPalette.black,
},
bg: {
backgroundColor: darkPalette.black,
+3
View File
@@ -1,4 +1,7 @@
export const color = {
white: '#FFFFFF',
black: '#080B12',
gray_25: `#FCFCFD`,
gray_50: `#F9FAFB`,
gray_100: `#F3F4F6`,
+434
View File
@@ -0,0 +1,434 @@
import React from 'react'
import {
Pressable,
Text,
PressableProps,
TextProps,
ViewStyle,
AccessibilityProps,
} from 'react-native'
import {useTheme, atoms, tokens, web, native} from '#/alf'
export type ButtonVariant = 'solid' | 'outline' | 'ghost'
export type ButtonColor = 'primary' | 'secondary' | 'negative'
export type ButtonSize = 'small' | 'large'
export type VariantProps = {
/**
* The style variation of the button
*/
variant?: ButtonVariant
/**
* The color of the button
*/
color?: ButtonColor
/**
* The size of the button
*/
size?: ButtonSize
}
export type ButtonProps = Omit<
PressableProps,
'children' | 'style' | 'accessibilityLabel' | 'accessibilityHint'
> &
VariantProps & {
children:
| ((props: {
state: {
pressed: boolean
hovered: boolean
focused: boolean
}
props: VariantProps & {
disabled?: boolean
}
}) => React.ReactNode)
| React.ReactNode
| string
accessibilityLabel: Required<AccessibilityProps>['accessibilityLabel']
accessibilityHint: Required<AccessibilityProps>['accessibilityHint']
}
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
export function Button({
children,
variant,
color,
size,
accessibilityLabel,
accessibilityHint,
disabled = false,
...rest
}: ButtonProps) {
const t = useTheme()
const [state, setState] = React.useState({
pressed: false,
hovered: false,
focused: false,
})
const onPressIn = React.useCallback(() => {
setState(s => ({
...s,
pressed: true,
}))
}, [setState])
const onPressOut = React.useCallback(() => {
setState(s => ({
...s,
pressed: false,
}))
}, [setState])
const onHoverIn = React.useCallback(() => {
setState(s => ({
...s,
hovered: true,
}))
}, [setState])
const onHoverOut = React.useCallback(() => {
setState(s => ({
...s,
hovered: false,
}))
}, [setState])
const onFocus = React.useCallback(() => {
setState(s => ({
...s,
focused: true,
}))
}, [setState])
const onBlur = React.useCallback(() => {
setState(s => ({
...s,
focused: false,
}))
}, [setState])
const {baseStyles, hoverStyles} = React.useMemo(() => {
const baseStyles: ViewStyle[] = []
const hoverStyles: ViewStyle[] = []
const light = t.name === 'light'
if (color === 'primary') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.primary_500,
})
hoverStyles.push({
backgroundColor: t.palette.primary_600,
})
} else {
baseStyles.push({
backgroundColor: t.palette.primary_700,
})
}
} else if (variant === 'outline') {
baseStyles.push(atoms.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(atoms.border, {
borderColor: tokens.color.blue_500,
})
hoverStyles.push(atoms.border, {
backgroundColor: light
? t.palette.primary_100
: t.palette.primary_900,
})
} else {
baseStyles.push(atoms.border, {
borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: light
? t.palette.primary_100
: t.palette.primary_900,
})
}
}
} else if (color === 'secondary') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({
backgroundColor: light
? tokens.color.gray_200
: tokens.color.gray_800,
})
hoverStyles.push({
backgroundColor: light
? tokens.color.gray_300
: tokens.color.gray_900,
})
} else {
baseStyles.push({
backgroundColor: light
? tokens.color.gray_300
: tokens.color.gray_900,
})
}
} else if (variant === 'outline') {
baseStyles.push(atoms.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(atoms.border, {
borderColor: light ? tokens.color.gray_500 : tokens.color.gray_500,
})
hoverStyles.push(atoms.border, t.atoms.bg_contrast_50)
} else {
baseStyles.push(atoms.border, {
borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: light
? tokens.color.gray_100
: tokens.color.gray_800,
})
}
}
} else if (color === 'negative') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.negative_500,
})
hoverStyles.push({
backgroundColor: t.palette.negative_600,
})
} else {
baseStyles.push({
backgroundColor: t.palette.negative_700,
})
}
} else if (variant === 'outline') {
baseStyles.push(atoms.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(atoms.border, {
borderColor: t.palette.negative_600,
})
hoverStyles.push(atoms.border, {
backgroundColor: light ? t.palette.negative_50 : '#2D0614', // darker red
})
} else {
baseStyles.push(atoms.border, {
borderColor: light
? t.palette.negative_200
: t.palette.negative_900,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: light ? t.palette.negative_50 : '#2D0614', // darker red
})
}
}
}
if (size === 'large') {
baseStyles.push(
{paddingVertical: 15},
atoms.px_2xl,
atoms.rounded_sm,
atoms.gap_sm,
)
} else if (size === 'small') {
baseStyles.push(
{paddingVertical: 9},
atoms.px_md,
atoms.rounded_sm,
atoms.gap_xs,
)
}
return {
baseStyles,
hoverStyles,
}
}, [t, variant, color, size, disabled])
const childProps = React.useMemo(
() => ({
state,
props: {
variant,
color,
size,
disabled: disabled || false,
},
}),
[state, variant, color, size, disabled],
)
return (
<Pressable
role="button"
{...rest}
aria-label={accessibilityLabel}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
disabled={disabled || false}
accessibilityState={{
disabled: disabled || false,
}}
style={[
atoms.flex_row,
atoms.align_center,
...baseStyles,
...(state.hovered ? hoverStyles : []),
]}
onPressIn={onPressIn}
onPressOut={onPressOut}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}
onFocus={onFocus}
onBlur={onBlur}>
{typeof children === 'string' ? (
<ButtonText
variant={variant}
color={color}
size={size}
disabled={disabled || false}>
{children}
</ButtonText>
) : typeof children === 'function' ? (
children(childProps)
) : (
children
)}
</Pressable>
)
}
export function ButtonText({
children,
style,
variant,
color,
size,
disabled,
...rest
}: ButtonTextProps) {
const t = useTheme()
const textStyles = React.useMemo(() => {
const baseStyles = []
const light = t.name === 'light'
if (color === 'primary') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
}
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: light ? t.palette.primary_600 : t.palette.primary_500,
})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.primary_600})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
}
} else if (color === 'secondary') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({
color: light ? tokens.color.gray_700 : tokens.color.gray_100,
})
} else {
baseStyles.push({
color: light ? tokens.color.gray_400 : tokens.color.gray_700,
})
}
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: light ? tokens.color.gray_600 : tokens.color.gray_300,
})
} else {
baseStyles.push({
color: light ? tokens.color.gray_400 : tokens.color.gray_700,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({
color: light ? tokens.color.gray_600 : tokens.color.gray_300,
})
} else {
baseStyles.push({
color: light ? tokens.color.gray_400 : tokens.color.gray_600,
})
}
}
} else if (color === 'negative') {
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
}
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_500})
} else {
baseStyles.push({color: t.palette.negative_500, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_500})
} else {
baseStyles.push({color: t.palette.negative_500, opacity: 0.5})
}
}
}
if (size === 'large') {
baseStyles.push(
atoms.text_md,
web({paddingBottom: 1}),
native({marginTop: 2}),
)
} else {
baseStyles.push(
atoms.text_md,
web({paddingBottom: 1}),
native({marginTop: 2}),
)
}
return baseStyles
}, [t, variant, color, size, disabled])
return (
<Text
{...rest}
style={[atoms.font_bold, atoms.text_center, ...textStyles, style]}>
{children}
</Text>
)
}
@@ -1,5 +1,5 @@
import React from 'react'
import {DialogContextProps, DialogControlProps} from '#/view/com/Dialog/types'
import {DialogContextProps, DialogControlProps} from '#/components/Dialog/types'
export const Context = React.createContext<DialogContextProps>({
close: () => {},
@@ -3,17 +3,17 @@ import {View, Dimensions} from 'react-native'
import BottomSheet, {BottomSheetBackdrop} from '@gorhom/bottom-sheet'
import {useTheme, atoms as a} from '#/alf'
import {Portal} from '#/view/com/Portal'
import {Portal} from '#/components/Portal'
import {
DialogOuterProps,
DialogControlProps,
DialogInnerProps,
} from '#/view/com/Dialog/types'
import {Context} from '#/view/com/Dialog/context'
} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context'
export {useDialogControl, useDialogContext} from '#/view/com/Dialog/context'
export * from '#/view/com/Dialog/types'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
export function Outer({
children,
@@ -62,7 +62,7 @@ export function Outer({
{...props}
/>
)}
handleIndicatorStyle={{backgroundColor: t.palette.primary}}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}}
onClose={onClose}>
<Context.Provider value={context}>
@@ -4,15 +4,15 @@ import {FocusScope} from '@tamagui/focus-scope'
import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
import {useTheme, atoms as a, useBreakpoints, web} from '#/alf'
import {Text} from '#/view/com/Typography'
import {Portal} from '#/view/com/Portal'
import {Button} from '#/view/com/Button'
import {Text} from '#/components/Typography'
import {Portal} from '#/components/Portal'
import {Button} from '#/components/Button'
import {DialogOuterProps, DialogInnerProps} from '#/view/com/Dialog/types'
import {Context, useDialogContext} from '#/view/com/Dialog/context'
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
import {Context, useDialogContext} from '#/components/Dialog/context'
export {useDialogControl, useDialogContext} from '#/view/com/Dialog/context'
export * from '#/view/com/Dialog/types'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
const stopPropagation = (e: any) => e.stopPropagation()
@@ -15,7 +15,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url'
import {isWeb} from '#/platform/detection'
import {useTheme, web} from '#/alf'
import {Button, ButtonProps} from '#/view/com/Button'
import {Button, ButtonProps} from '#/components/Button'
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {
convertBskyAppUrlIfNeeded,
@@ -157,10 +157,10 @@ export function Link({
<Text
style={[
style,
{color: t.palette.primary},
{color: t.palette.primary_500},
state.hovered && {
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary,
textDecorationColor: t.palette.primary_500,
},
]}>
{children as string}
@@ -3,12 +3,12 @@ import {View, PressableProps, LayoutChangeEvent} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useTheme, atoms as a} from '#/alf'
import {H2, P} from '#/view/com/Typography'
import {Button} from '#/view/com/Button'
import {H2, P} from '#/components/Typography'
import {Button} from '#/components/Button'
import * as Dialog from '#/view/com/Dialog'
import * as Dialog from '#/components/Dialog'
export {useDialogControl as usePromptControl} from '#/view/com/Dialog'
export {useDialogControl as usePromptControl} from '#/components/Dialog'
const Context = React.createContext<{
titleId: string
@@ -102,7 +102,8 @@ export function Cancel({
const {close} = Dialog.useDialogContext()
return (
<Button
type="secondary"
variant="solid"
color="secondary"
size="small"
accessibilityLabel="Cancel"
accessibilityHint="Cancel this action"
@@ -123,7 +124,8 @@ export function Action({
}, [close, onPress])
return (
<Button
type="primary"
variant="solid"
color="primary"
size="small"
accessibilityLabel="Confirm"
accessibilityHint="Confirm this action"
@@ -6,16 +6,16 @@ import DateTimePicker, {
import {Logo} from '#/view/icons/Logo'
import {useTheme, atoms, tokens} from '#/alf'
import {Text} from '#/view/com/Typography'
import {useInteractionState} from '#/view/com/util/hooks/useInteractionState'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InputDateProps} from '#/view/com/forms/InputDate/types'
import {InputDateProps} from '#/components/forms/InputDate/types'
import {
localizeDate,
toSimpleDateString,
} from '#/view/com/forms/InputDate/utils'
} from '#/components/forms/InputDate/utils'
export * as utils from '#/view/com/forms/InputDate/utils'
export * as utils from '#/components/forms/InputDate/utils'
export function InputDate({
value: initialValue,
@@ -5,11 +5,11 @@ import DateTimePicker, {
} from '@react-native-community/datetimepicker'
import {useTheme, atoms} from '#/alf'
import {Text} from '#/view/com/Typography'
import {toSimpleDateString} from '#/view/com/forms/InputDate/utils'
import {Text} from '#/components/Typography'
import {toSimpleDateString} from '#/components/forms/InputDate/utils'
import {InputDateProps} from '#/view/com/forms/InputDate/types'
export * as utils from '#/view/com/forms/InputDate/utils'
import {InputDateProps} from '#/components/forms/InputDate/types'
export * as utils from '#/components/forms/InputDate/utils'
/**
* Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date
@@ -5,13 +5,13 @@ import {unstable_createElement} from 'react-native-web'
import {Logo} from '#/view/icons/Logo'
import {useTheme, atoms, tokens} from '#/alf'
import {Text} from '#/view/com/Typography'
import {useInteractionState} from '#/view/com/util/hooks/useInteractionState'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InputDateProps} from '#/view/com/forms/InputDate/types'
import {toSimpleDateString} from '#/view/com/forms/InputDate/utils'
import {InputDateProps} from '#/components/forms/InputDate/types'
import {toSimpleDateString} from '#/components/forms/InputDate/utils'
export * as utils from '#/view/com/forms/InputDate/utils'
export * as utils from '#/components/forms/InputDate/utils'
export function InputDate({
label,
@@ -1,6 +1,6 @@
import {TextInputProps} from 'react-native'
import {BaseProps} from '#/view/com/forms/types'
import {BaseProps} from '#/components/forms/types'
export type InputDateProps = BaseProps & {
/**
@@ -8,10 +8,10 @@ import {
} from 'react-native'
import {useTheme, atoms, web, tokens} from '#/alf'
import {Text} from '#/view/com/Typography'
import {useInteractionState} from '#/view/com/util/hooks/useInteractionState'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {BaseProps} from '#/view/com/forms/types'
import {BaseProps} from '#/components/forms/types'
type Props = BaseProps &
Omit<TextInputProps, 'placeholder'> & {
-297
View File
@@ -1,297 +0,0 @@
import React from 'react'
import {
Pressable,
Text,
PressableProps,
TextProps,
ViewStyle,
AccessibilityProps,
} from 'react-native'
import {useTheme, atoms, tokens, web, native} from '#/alf'
export type ButtonType = 'primary' | 'secondary' | 'negative'
export type ButtonSize = 'small' | 'large'
export type VariantProps = {
/**
* The presentation styles of the button
*/
type?: ButtonType
/**
* The size of the button
*/
size?: ButtonSize
}
export type ButtonProps = Omit<
PressableProps,
'children' | 'style' | 'accessibilityLabel' | 'accessibilityHint'
> &
VariantProps & {
children:
| ((props: {
state: {
pressed: boolean
hovered: boolean
focused: boolean
}
props: VariantProps & {
disabled?: boolean
}
}) => React.ReactNode)
| React.ReactNode
| string
accessibilityLabel: Required<AccessibilityProps>['accessibilityLabel']
accessibilityHint: Required<AccessibilityProps>['accessibilityHint']
}
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
export function Button({
children,
type,
size,
accessibilityLabel,
accessibilityHint,
disabled = false,
...rest
}: ButtonProps) {
const [state, setState] = React.useState({
pressed: false,
hovered: false,
focused: false,
})
const onPressIn = React.useCallback(() => {
setState(s => ({
...s,
pressed: true,
}))
}, [setState])
const onPressOut = React.useCallback(() => {
setState(s => ({
...s,
pressed: false,
}))
}, [setState])
const onHoverIn = React.useCallback(() => {
setState(s => ({
...s,
hovered: true,
}))
}, [setState])
const onHoverOut = React.useCallback(() => {
setState(s => ({
...s,
hovered: false,
}))
}, [setState])
const onFocus = React.useCallback(() => {
setState(s => ({
...s,
focused: true,
}))
}, [setState])
const onBlur = React.useCallback(() => {
setState(s => ({
...s,
focused: false,
}))
}, [setState])
const {baseStyles, hoverStyles} = React.useMemo(() => {
const baseStyles: ViewStyle[] = []
const hoverStyles: ViewStyle[] = []
switch (type) {
case 'primary': {
if (disabled) {
baseStyles.push({
backgroundColor: tokens.color.blue_200,
})
} else {
baseStyles.push({
backgroundColor: tokens.color.blue_500,
})
hoverStyles.push({
backgroundColor: tokens.color.blue_600,
})
}
break
}
case 'secondary': {
if (disabled) {
baseStyles.push({
backgroundColor: tokens.color.gray_200,
})
} else {
baseStyles.push({
backgroundColor: tokens.color.gray_300,
})
hoverStyles.push({
backgroundColor: tokens.color.gray_400,
})
}
break
}
case 'negative': {
if (disabled) {
baseStyles.push({
backgroundColor: tokens.color.red_400,
})
} else {
baseStyles.push({
backgroundColor: tokens.color.red_500,
})
}
break
}
default:
}
switch (size) {
case 'large': {
baseStyles.push(
{paddingVertical: 15},
atoms.px_2xl,
atoms.rounded_sm,
atoms.gap_sm,
)
break
}
case 'small': {
baseStyles.push(
{paddingVertical: 9},
atoms.px_md,
atoms.rounded_sm,
atoms.gap_xs,
)
break
}
default:
}
return {
baseStyles,
hoverStyles,
}
}, [type, size, disabled])
const childProps = React.useMemo(
() => ({
state,
props: {
type,
size,
disabled: disabled || false,
},
}),
[state, type, size, disabled],
)
return (
<Pressable
role="button"
{...rest}
aria-label={accessibilityLabel}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
disabled={disabled || false}
accessibilityState={{
disabled: disabled || false,
}}
style={[
atoms.flex_row,
atoms.align_center,
...baseStyles,
...(state.hovered ? hoverStyles : []),
]}
onPressIn={onPressIn}
onPressOut={onPressOut}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}
onFocus={onFocus}
onBlur={onBlur}>
{typeof children === 'string' ? (
<ButtonText type={type} size={size} disabled={disabled || false}>
{children}
</ButtonText>
) : typeof children === 'function' ? (
children(childProps)
) : (
children
)}
</Pressable>
)
}
export function ButtonText({
children,
style,
type,
size,
disabled,
...rest
}: ButtonTextProps) {
const t = useTheme()
const textStyles = React.useMemo(() => {
const baseStyles = []
switch (type) {
case 'primary': {
baseStyles.push({color: tokens.color.white})
break
}
case 'secondary': {
if (disabled) {
baseStyles.push({
color: tokens.color.gray_400,
})
} else {
baseStyles.push({
color: tokens.color.gray_800,
})
}
break
}
case 'negative': {
baseStyles.push({
color: tokens.color.white,
})
break
}
default:
baseStyles.push(t.atoms.text)
}
switch (size) {
case 'small': {
baseStyles.push(
atoms.text_md,
web({paddingBottom: 1}),
native({marginTop: 2}),
)
break
}
case 'large': {
baseStyles.push(
atoms.text_md,
web({paddingBottom: 1}),
native({marginTop: 2}),
)
break
}
default:
}
return baseStyles
}, [t, type, size, disabled])
return (
<Text
{...rest}
style={[atoms.font_bold, atoms.text_center, ...textStyles, style]}>
{children}
</Text>
)
}
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {View} from 'react-native'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Text, H3} from '#/view/com/Typography'
import {Text, H3} from '#/components/Typography'
export function Breakpoints() {
const t = useTheme()
+31 -109
View File
@@ -1,120 +1,42 @@
import React from 'react'
import {View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/view/com/Button'
import {Link} from '#/view/com/Link'
import {Text, H3} from '#/view/com/Typography'
import {atoms as a} from '#/alf'
import {Button, ButtonVariant, ButtonColor} from '#/components/Button'
import {H1} from '#/components/Typography'
export function Buttons() {
const t = useTheme()
return (
<View style={[a.gap_md, a.align_start]}>
<Button
accessibilityLabel="Click here"
accessibilityHint="Opens something">
Unstyled button
</Button>
<View style={[a.gap_md]}>
<H1>Buttons</H1>
<Button
accessibilityLabel="Click here"
accessibilityHint="Opens something">
{({state}) => (
<View style={[a.p_md, a.rounded_full, t.atoms.bg_contrast_300]}>
<Text>Entirely custom button, state: {JSON.stringify(state)}</Text>
<View style={[a.flex_row, a.gap_md, a.align_start]}>
{['primary', 'secondary', 'negative'].map(color => (
<View key={color} style={[a.gap_md, a.align_start]}>
{['solid', 'outline', 'ghost'].map(variant => (
<React.Fragment key={variant}>
<Button
variant={variant as ButtonVariant}
color={color as ButtonColor}
size="large"
accessibilityLabel="Click here"
accessibilityHint="Opens something">
Button
</Button>
<Button
disabled
variant={variant as ButtonVariant}
color={color as ButtonColor}
size="large"
accessibilityLabel="Click here"
accessibilityHint="Opens something">
Button
</Button>
</React.Fragment>
))}
</View>
)}
</Button>
<Button
accessibilityLabel="Click here"
accessibilityHint="Opens something"
type="primary"
size="large">
Default button
</Button>
<Button
accessibilityLabel="Click here"
accessibilityHint="Opens something"
type="primary"
size="large"
disabled>
Default button (disabled)
</Button>
<Button
accessibilityLabel="Click here"
accessibilityHint="Opens something"
type="primary"
size="large">
{({props}) => (
<>
<FontAwesomeIcon icon={['fas', 'plus']} size={12} />
<ButtonText {...props}>Default with an icon</ButtonText>
</>
)}
</Button>
<Button
accessibilityLabel="Click here"
accessibilityHint="Opens something"
type="secondary"
size="small">
Small button
</Button>
<Button
accessibilityLabel="Click here"
accessibilityHint="Opens something"
type="secondary"
size="small"
disabled>
Small button (disabled)
</Button>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
to="https://blueskyweb.xyz"
warnOnMismatchingTextChild
style={[a.text_md]}>
External
</Link>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
to="https://blueskyweb.xyz"
style={[a.text_md]}>
<H3>External with custom children</H3>
</Link>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
to="https://blueskyweb.xyz"
warnOnMismatchingTextChild
style={[a.text_md]}>
https://blueskyweb.xyz
</Link>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
to="https://bsky.app/profile/bsky.app"
warnOnMismatchingTextChild
style={[a.text_md]}>
Internal
</Link>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
type="primary"
size="large"
to="https://bsky.app/profile/bsky.app">
{({props}) => <ButtonText {...props}>Link as a button</ButtonText>}
</Link>
))}
</View>
</View>
)
}
+10 -7
View File
@@ -2,10 +2,10 @@ import React from 'react'
import {View} from 'react-native'
import {atoms as a} from '#/alf'
import {Button} from '#/view/com/Button'
import {H3, P} from '#/view/com/Typography'
import * as Dialog from '#/view/com/Dialog'
import * as Prompt from '#/view/com/Prompt'
import {Button} from '#/components/Button'
import {H3, P} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
export function Dialogs() {
const control = Dialog.useDialogControl()
@@ -14,7 +14,8 @@ export function Dialogs() {
return (
<>
<Button
type="secondary"
variant="outline"
color="secondary"
size="small"
onPress={() => control.open()}
accessibilityLabel="Open basic dialog"
@@ -23,7 +24,8 @@ export function Dialogs() {
</Button>
<Button
type="negative"
variant="solid"
color="primary"
size="small"
onPress={() => prompt.open()}
accessibilityLabel="Open prompt"
@@ -53,7 +55,8 @@ export function Dialogs() {
<P nativeID="dialog-description">Description</P>
<View style={[a.flex_row, a.justify_end]}>
<Button
type="primary"
variant="outline"
color="primary"
size="small"
onPress={() => control.close()}
accessibilityLabel="Open basic dialog"
+3 -3
View File
@@ -2,9 +2,9 @@ import React from 'react'
import {View} from 'react-native'
import {atoms as a} from '#/alf'
import {Text} from '#/view/com/Typography'
import {InputText} from '#/view/com/forms/InputText'
import {InputDate, utils} from '#/view/com/forms/InputDate'
import {Text} from '#/components/Typography'
import {InputText} from '#/components/forms/InputText'
import {InputDate, utils} from '#/components/forms/InputDate'
import {Logo} from '#/view/icons/Logo'
export function Forms() {
+59
View File
@@ -0,0 +1,59 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a} from '#/alf'
import {ButtonText} from '#/components/Button'
import {Link} from '#/components/Link'
import {H1, H3} from '#/components/Typography'
export function Links() {
return (
<View style={[a.gap_md, a.align_start]}>
<H1>Links</H1>
<View style={[a.gap_md, a.align_start]}>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
to="https://blueskyweb.xyz"
warnOnMismatchingTextChild
style={[a.text_md]}>
External
</Link>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
to="https://blueskyweb.xyz"
style={[a.text_md]}>
<H3>External with custom children</H3>
</Link>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
to="https://blueskyweb.xyz"
warnOnMismatchingTextChild
style={[a.text_md]}>
https://blueskyweb.xyz
</Link>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
to="https://bsky.app/profile/bsky.app"
warnOnMismatchingTextChild
style={[a.text_md]}>
Internal
</Link>
<Link
accessibilityLabel="Click here"
accessibilityHint="Opens something"
variant="solid"
color="primary"
size="large"
to="https://bsky.app/profile/bsky.app">
{({props}) => <ButtonText {...props}>Link as a button</ButtonText>}
</Link>
</View>
</View>
)
}
+43 -43
View File
@@ -2,62 +2,62 @@ import React from 'react'
import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/view/com/Typography'
import {Text, H1} from '#/components/Typography'
export function Spacing() {
const t = useTheme()
return (
<View>
<View style={[a.gap_md]}>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>2xs (2px)</Text>
<View style={[a.flex_1, a.pt_2xs, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.gap_md]}>
<H1>Spacing</H1>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xs (4px)</Text>
<View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>2xs (2px)</Text>
<View style={[a.flex_1, a.pt_2xs, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>sm (8px)</Text>
<View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xs (4px)</Text>
<View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>md (12px)</Text>
<View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>sm (8px)</Text>
<View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>lg (16px)</Text>
<View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>md (12px)</Text>
<View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xl (20px)</Text>
<View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>lg (16px)</Text>
<View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>2xl (24px)</Text>
<View style={[a.flex_1, a.pt_2xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>xl (20px)</Text>
<View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>3xl (28px)</Text>
<View style={[a.flex_1, a.pt_3xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>2xl (24px)</Text>
<View style={[a.flex_1, a.pt_2xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>4xl (32px)</Text>
<View style={[a.flex_1, a.pt_4xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>3xl (28px)</Text>
<View style={[a.flex_1, a.pt_3xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>5xl (40px)</Text>
<View style={[a.flex_1, a.pt_5xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>4xl (32px)</Text>
<View style={[a.flex_1, a.pt_4xl, t.atoms.bg_contrast_300]} />
</View>
<View style={[a.flex_row, a.align_center]}>
<Text style={{width: 80}}>5xl (40px)</Text>
<View style={[a.flex_1, a.pt_5xl, t.atoms.bg_contrast_300]} />
</View>
</View>
)
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/view/com/Typography'
import {Text} from '#/components/Typography'
import {Palette} from './Palette'
export function Theming() {
+4 -3
View File
@@ -1,11 +1,12 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a} from '#/alf'
import {Text, H1, H2, H3, H4, H5, H6, P} from '#/view/com/Typography'
import {Text, H1, H2, H3, H4, H5, H6, P} from '#/components/Typography'
export function Typography() {
return (
<>
<View style={[a.gap_md]}>
<H1>H1 Heading</H1>
<H2>H2 Heading</H2>
<H3>H3 Heading</H3>
@@ -24,6 +25,6 @@ export function Typography() {
<Text style={[a.text_sm]}>atoms.text_sm</Text>
<Text style={[a.text_xs]}>atoms.text_xs</Text>
<Text style={[a.text_2xs]}>atoms.text_2xs</Text>
</>
</View>
)
}
+10 -5
View File
@@ -4,12 +4,13 @@ import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {atoms as a, useTheme, ThemeProvider} from '#/alf'
import {useSetColorMode} from '#/state/shell'
import {Button} from '#/view/com/Button'
import {Button} from '#/components/Button'
import {Theming} from './Theming'
import {Typography} from './Typography'
import {Spacing} from './Spacing'
import {Buttons} from './Buttons'
import {Links} from './Links'
import {Forms} from './Forms'
import {Dialogs} from './Dialogs'
import {Breakpoints} from './Breakpoints'
@@ -21,10 +22,11 @@ export function Storybook() {
return (
<ScrollView>
<CenteredView style={[t.atoms.bg]}>
<View style={[a.p_xl, a.gap_2xl, {paddingBottom: 200}]}>
<View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}>
<View style={[a.flex_row, a.align_start, a.gap_md]}>
<Button
type="secondary"
variant="outline"
color="primary"
size="small"
accessibilityLabel='Set theme to "system"'
accessibilityHint="Set theme to system default"
@@ -32,7 +34,8 @@ export function Storybook() {
System
</Button>
<Button
type="secondary"
variant="solid"
color="secondary"
size="small"
accessibilityLabel='Set theme to "system"'
accessibilityHint="Set theme to system default"
@@ -40,7 +43,8 @@ export function Storybook() {
Light
</Button>
<Button
type="secondary"
variant="solid"
color="secondary"
size="small"
accessibilityLabel='Set theme to "system"'
accessibilityHint="Set theme to system default"
@@ -62,6 +66,7 @@ export function Storybook() {
<Typography />
<Spacing />
<Buttons />
<Links />
<Forms />
<Dialogs />
<Breakpoints />