diff --git a/src/alf/index.tsx b/src/alf/index.tsx index 1daa0bfedc..69a8798539 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -5,6 +5,7 @@ import * as themes from '#/alf/themes' export * as tokens from '#/alf/tokens' export {atoms} from '#/alf/atoms' export * from '#/alf/util/platform' +export * from '#/alf/util/flatten' type BreakpointName = keyof typeof breakpoints diff --git a/src/alf/themes.ts b/src/alf/themes.ts index 14e1de7ff6..f26f10a6f2 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -11,12 +11,36 @@ export const lightPalette = { primary: tokens.color.blue_500, positive: tokens.color.green_500, negative: tokens.color.red_500, + + contrast_0: tokens.color.gray_0, + contrast_100: tokens.color.gray_100, + contrast_200: tokens.color.gray_200, + contrast_300: tokens.color.gray_300, + contrast_400: tokens.color.gray_400, + contrast_500: tokens.color.gray_500, + contrast_600: tokens.color.gray_600, + contrast_700: tokens.color.gray_700, + contrast_800: tokens.color.gray_800, + contrast_900: tokens.color.gray_900, + contrast_1000: tokens.color.gray_1000, } as const export const darkPalette: Palette = { primary: tokens.color.blue_500, positive: tokens.color.green_400, negative: tokens.color.red_400, + + contrast_0: tokens.color.gray_1000, + contrast_100: tokens.color.gray_900, + contrast_200: tokens.color.gray_800, + contrast_300: tokens.color.gray_700, + contrast_400: tokens.color.gray_600, + contrast_500: tokens.color.gray_500, + contrast_600: tokens.color.gray_400, + contrast_700: tokens.color.gray_300, + contrast_800: tokens.color.gray_200, + contrast_900: tokens.color.gray_100, + contrast_1000: tokens.color.gray_0, } as const export const light = { diff --git a/src/alf/util/flatten.ts b/src/alf/util/flatten.ts new file mode 100644 index 0000000000..448716a082 --- /dev/null +++ b/src/alf/util/flatten.ts @@ -0,0 +1,3 @@ +import {StyleSheet} from 'react-native' + +export const flatten = StyleSheet.flatten diff --git a/src/view/com/Button.tsx b/src/view/com/Button.tsx index 7b3beb7dca..51fe1e3541 100644 --- a/src/view/com/Button.tsx +++ b/src/view/com/Button.tsx @@ -284,13 +284,7 @@ export function ButtonText({ return ( + style={[atoms.font_semibold, atoms.text_center, ...textStyles, style]}> {children} ) diff --git a/src/view/com/Dialog/context.ts b/src/view/com/Dialog/context.ts index bd150f8cbc..2330969933 100644 --- a/src/view/com/Dialog/context.ts +++ b/src/view/com/Dialog/context.ts @@ -15,5 +15,9 @@ export function useDialogControl() { close: () => {}, }) - return control + return { + ref: control, + open: () => control.current.open(), + close: () => control.current.close(), + } } diff --git a/src/view/com/Dialog/index.tsx b/src/view/com/Dialog/index.tsx index 6d6946ec44..7f7a630a14 100644 --- a/src/view/com/Dialog/index.tsx +++ b/src/view/com/Dialog/index.tsx @@ -12,12 +12,14 @@ import { } from '#/view/com/Dialog/types' import {Context} from '#/view/com/Dialog/context' -export {useDialogControl} from '#/view/com/Dialog/context' +export {useDialogControl, useDialogContext} from '#/view/com/Dialog/context' +export * from '#/view/com/Dialog/types' export function Outer({ + children, control, onClose, - children, + nativeOptions, }: React.PropsWithChildren) { const t = useTheme() const sheet = React.useRef(null) @@ -32,7 +34,7 @@ export function Outer({ }, [onClose]) useImperativeHandle( - control, + control.ref, () => ({ open, close, @@ -45,13 +47,14 @@ export function Outer({ return ( ( {}} onClose={onClose}> - {children} + + + {children} + ) } +// TODO a11y props here, or is that handled by the sheet? export function Inner(props: DialogInnerProps) { - const t = useTheme() return ( {props.children} @@ -101,7 +113,7 @@ export function Handle() { t.atoms.bg_contrast_200, { top: 12, - width: 80, + width: 50, height: 6, alignSelf: 'center', }, diff --git a/src/view/com/Dialog/index.web.tsx b/src/view/com/Dialog/index.web.tsx index ee390daba1..37d4cebc85 100644 --- a/src/view/com/Dialog/index.web.tsx +++ b/src/view/com/Dialog/index.web.tsx @@ -11,7 +11,8 @@ import {Button} from '#/view/com/Button' import {DialogOuterProps, DialogInnerProps} from '#/view/com/Dialog/types' import {Context, useDialogContext} from '#/view/com/Dialog/context' -export {useDialogControl} from '#/view/com/Dialog/context' +export {useDialogControl, useDialogContext} from '#/view/com/Dialog/context' +export * from '#/view/com/Dialog/types' const stopPropagation = (e: any) => e.stopPropagation() @@ -38,7 +39,7 @@ export function Outer({ }, [onClose, setIsOpen]) useImperativeHandle( - control, + control.ref, () => ({ open, close, @@ -58,7 +59,12 @@ export function Outer({ return () => document.removeEventListener('keydown', handler) }, [isOpen, close]) - const context = React.useMemo(() => ({close}), [close]) + const context = React.useMemo( + () => ({ + close, + }), + [close], + ) return ( <> @@ -111,12 +117,19 @@ export function Outer({ ) } -export function Inner({children, style}: DialogInnerProps) { +export function Inner({ + children, + style, + accessibilityLabelledBy, + accessibilityDescribedBy, +}: DialogInnerProps) { const t = useTheme() const {gtMobile} = useBreakpoints() return ( {children} diff --git a/src/view/com/Dialog/types.ts b/src/view/com/Dialog/types.ts index 4c41849aa3..cdcad90da6 100644 --- a/src/view/com/Dialog/types.ts +++ b/src/view/com/Dialog/types.ts @@ -1,5 +1,8 @@ import React from 'react' -import type {ViewStyle} from 'react-native' +import type {ViewStyle, AccessibilityProps} from 'react-native' +import {BottomSheetProps} from '@gorhom/bottom-sheet' + +type A11yProps = Required export type DialogContextProps = { close: () => void @@ -11,8 +14,20 @@ export type DialogControlProps = { } export type DialogOuterProps = { - control: React.RefObject + control: { + ref: React.RefObject + open: (index?: number) => void + close: () => void + } onClose?: () => void + nativeOptions?: { + sheet?: Omit + } + webOptions?: {} } -export type DialogInnerProps = React.PropsWithChildren<{style?: ViewStyle}> +export type DialogInnerProps = React.PropsWithChildren<{ + style?: ViewStyle + accessibilityLabelledBy: A11yProps['aria-labelledby'] + accessibilityDescribedBy: string +}> diff --git a/src/view/com/Prompt.tsx b/src/view/com/Prompt.tsx new file mode 100644 index 0000000000..fc55bc4a51 --- /dev/null +++ b/src/view/com/Prompt.tsx @@ -0,0 +1,134 @@ +import React from 'react' +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 * as Dialog from '#/view/com/Dialog' + +export {useDialogControl as usePromptControl} from '#/view/com/Dialog' + +const Context = React.createContext<{ + titleId: string + descriptionId: string +}>({ + titleId: '', + descriptionId: '', +}) + +export function Outer({ + children, + control, +}: React.PropsWithChildren<{ + control: Dialog.DialogOuterProps['control'] +}>) { + const insets = useSafeAreaInsets() + const titleId = React.useId() + const descriptionId = React.useId() + const [defaultSnapPoints, setDefaultSnapPoints] = React.useState< + string | number + >('25%') + + const context = React.useMemo( + () => ({titleId, descriptionId}), + [titleId, descriptionId], + ) + + const measureDefaultSnapPoint = React.useCallback( + (e: LayoutChangeEvent) => { + setDefaultSnapPoints(e.nativeEvent.layout.height + insets.bottom + 50) + }, + [insets, setDefaultSnapPoints], + ) + + return ( + + + + + + {children} + + + + + ) +} + +export function Title({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + const {titleId} = React.useContext(Context) + return ( +

+ {children} +

+ ) +} + +export function Description({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + const {descriptionId} = React.useContext(Context) + return ( +

+ {children} +

+ ) +} + +export function Actions({children}: React.PropsWithChildren<{}>) { + return ( + + {children} + + ) +} + +export function Cancel({ + children, +}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) { + const {close} = Dialog.useDialogContext() + return ( + + ) +} + +export function Action({ + children, + onPress, +}: React.PropsWithChildren<{onPress?: () => void}>) { + const {close} = Dialog.useDialogContext() + const handleOnPress = React.useCallback(() => { + close() + onPress?.() + }, [close, onPress]) + return ( + + ) +} diff --git a/src/view/com/Typography.tsx b/src/view/com/Typography.tsx index 6579c2e511..029fc2e24f 100644 --- a/src/view/com/Typography.tsx +++ b/src/view/com/Typography.tsx @@ -1,6 +1,7 @@ import React from 'react' import {Text as RNText, TextProps} from 'react-native' -import {useTheme, atoms, web} from '#/alf' + +import {useTheme, atoms, web, flatten} from '#/alf' export function Text({style, ...rest}: TextProps) { const t = useTheme() @@ -18,7 +19,7 @@ export function H1({style, ...rest}: TextProps) { ) } @@ -34,7 +35,7 @@ export function H2({style, ...rest}: TextProps) { ) } @@ -50,7 +51,7 @@ export function H3({style, ...rest}: TextProps) { ) } @@ -66,7 +67,7 @@ export function H4({style, ...rest}: TextProps) { ) } @@ -82,7 +83,7 @@ export function H5({style, ...rest}: TextProps) { ) } @@ -98,7 +99,26 @@ export function H6({style, ...rest}: TextProps) { + ) +} + +export function P({style, ...rest}: TextProps) { + const t = useTheme() + const attr = + web({ + role: 'paragraph', + }) || {} + const _style = flatten(style) + const lineHeight = + (_style?.lineHeight || atoms.text_md.lineHeight) * + atoms.leading_normal.lineHeight + return ( + ) } diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index 6abcc438b7..27b993c95e 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -8,32 +8,39 @@ import * as tokens from '#/alf/tokens' import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf' import {Button, ButtonText} from '#/view/com/Button' import {Link} from '#/view/com/Link' -import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography' +import {Text, H1, H2, H3, H4, H5, H6, P} from '#/view/com/Typography' import {InputText} from '#/view/com/forms/InputText' import {InputDate, utils} from '#/view/com/forms/InputDate' import {Logo} from '#/view/icons/Logo' import * as Dialog from '#/view/com/Dialog' +import * as Prompt from '#/view/com/Prompt' function ThemeSelector() { const setColorMode = useSetColorMode() return ( - + @@ -319,39 +326,58 @@ function Forms() { function Dialogs() { const control = Dialog.useDialogControl() + const prompt = Prompt.usePromptControl() return ( <> + + + + Are you sure? + + This action cannot be undone. This action cannot be undone. This + action cannot be undone. + + + Cancel + Confirm + + + - + - - - +

Dialog

+

Description

+ + +