Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
11 kB · 482 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483import React, {createContext, useContext, useMemo, useRef} from 'react'import { type AccessibilityProps, StyleSheet, TextInput, type TextInputProps, type TextStyle, View, type ViewStyle,} from 'react-native'
import {HITSLOP_20} from '#/lib/constants'import {mergeRefs} from '#/lib/merge-refs'import { android, applyFonts, atoms as a, platform, type TextStyleProp, tokens, useAlf, useTheme, utils, web,} from '#/alf'import {useInteractionState} from '#/components/hooks/useInteractionState'import {type Props as SVGIconProps} from '#/components/icons/common'import {Text} from '#/components/Typography'import {IS_WEB} from '#/env'
const Context = createContext<{ inputRef: React.RefObject<TextInput | null> | null isInvalid: boolean hovered: boolean onHoverIn: () => void onHoverOut: () => void focused: boolean onFocus: () => void onBlur: () => void}>({ inputRef: null, isInvalid: false, hovered: false, onHoverIn: () => {}, onHoverOut: () => {}, focused: false, onFocus: () => {}, onBlur: () => {},})Context.displayName = 'TextFieldContext'
export type RootProps = React.PropsWithChildren< {isInvalid?: boolean} & TextStyleProp>
export function Root({children, isInvalid = false, style}: RootProps) { const inputRef = useRef<TextInput>(null) const { state: hovered, onIn: onHoverIn, onOut: onHoverOut, } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const context = useMemo( () => ({ inputRef, hovered, onHoverIn, onHoverOut, focused, onFocus, onBlur, isInvalid, }), [ inputRef, hovered, onHoverIn, onHoverOut, focused, onFocus, onBlur, isInvalid, ], )
// Check if any child has multiline prop const hasMultiline = useMemo(() => { let found = false React.Children.forEach(children, child => { if ( React.isValidElement(child) && (child.props as {multiline?: boolean})?.multiline ) { found = true } }) return found }, [children])
return ( <Context.Provider value={context}> <View style={[ a.flex_row, a.align_center, a.relative, a.w_full, !(hasMultiline && IS_WEB) && a.px_md, style, ]} {...web({ onClick: () => inputRef.current?.focus(), onMouseOver: onHoverIn, onMouseOut: onHoverOut, })}> {children} </View> </Context.Provider> )}
export function useSharedInputStyles() { const t = useTheme() return useMemo(() => { const hover: ViewStyle[] = [ { borderColor: t.palette.contrast_100, }, ] const focus: ViewStyle[] = [ { backgroundColor: t.palette.contrast_50, borderColor: t.palette.primary_500, }, ] const error: ViewStyle[] = [ { backgroundColor: t.palette.negative_25, borderColor: t.palette.negative_300, }, ] const errorHover: ViewStyle[] = [ { backgroundColor: t.palette.negative_25, borderColor: t.palette.negative_500, }, ]
return { chromeHover: StyleSheet.flatten(hover), chromeFocus: StyleSheet.flatten(focus), chromeError: StyleSheet.flatten(error), chromeErrorHover: StyleSheet.flatten(errorHover), } }, [t])}
export type InputProps = Omit< TextInputProps, 'value' | 'onChangeText' | 'placeholder'> & { label: string /** * @deprecated Controlled inputs are *strongly* discouraged. Use `defaultValue` instead where possible. * * See https://github.com/facebook/react-native-website/pull/4247 * * Note: This guidance no longer applies once we migrate to the New Architecture! */ value?: string onChangeText?: (value: string) => void isInvalid?: boolean inputRef?: React.RefObject<TextInput | null> | React.ForwardedRef<TextInput> /** * Note: this currently falls back to the label if not specified. However, * most new designs have no placeholder. We should eventually remove this fallback * behaviour, but for now just pass `null` if you want no placeholder -sfn */ placeholder?: string | null | undefined}
export function createInput(Component: typeof TextInput) { return function Input({ label, placeholder, value, onChangeText, onFocus, onBlur, isInvalid, inputRef, style, ...rest }: InputProps) { const t = useTheme() const {fonts} = useAlf() const ctx = useContext(Context) const withinRoot = Boolean(ctx.inputRef)
const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = useSharedInputStyles()
if (!withinRoot) { return ( <Root isInvalid={isInvalid}> <Input label={label} placeholder={placeholder} value={value} onChangeText={onChangeText} isInvalid={isInvalid} {...rest} /> </Root> ) }
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
const flattened = StyleSheet.flatten([ a.relative, a.z_20, a.flex_1, a.text_md, t.atoms.text, a.px_xs, { // paddingVertical doesn't work w/multiline - esb lineHeight: a.text_md.fontSize * 1.2, textAlignVertical: rest.multiline ? 'top' : undefined, minHeight: rest.multiline ? 80 : undefined, minWidth: 0, paddingTop: 13, paddingBottom: 13, }, android({ paddingTop: 8, paddingBottom: 9, }), /* * Margins are needed here to avoid autofill background overlapping the * top and bottom borders - esb */ web({ paddingTop: 11, paddingBottom: 11, marginTop: 2, marginBottom: 2, }), rest.multiline && web({ resize: 'vertical', fieldSizing: 'content', paddingLeft: 16, paddingRight: 16, }), style, ])
applyFonts(flattened, fonts.family)
// should always be defined on `typography` // @ts-ignore if (flattened.fontSize) { // @ts-ignore flattened.fontSize = Math.round( // @ts-ignore flattened.fontSize * fonts.scaleMultiplier, ) }
return ( <> <Component accessibilityHint={undefined} hitSlop={HITSLOP_20} selectionColor={utils.alpha(t.palette.primary_500, 0.4)} cursorColor={t.palette.primary_500} selectionHandleColor={t.palette.primary_500} {...rest} accessibilityLabel={label} ref={refs} value={value} onChangeText={onChangeText} onFocus={e => { ctx.onFocus() onFocus?.(e) }} onBlur={e => { ctx.onBlur() onBlur?.(e) }} placeholder={placeholder === null ? undefined : placeholder || label} placeholderTextColor={t.palette.contrast_500} keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} style={flattened} />
<View style={[ a.z_10, a.absolute, a.inset_0, {borderRadius: 10}, t.atoms.bg_contrast_50, {borderColor: 'transparent', borderWidth: 2}, ctx.hovered ? chromeHover : {}, ctx.focused ? chromeFocus : {}, ctx.isInvalid || isInvalid ? chromeError : {}, (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) ? chromeErrorHover : {}, ]} /> </> ) }}
export const Input = createInput(TextInput)
export function LabelText({ nativeID, children,}: React.PropsWithChildren<{nativeID?: string}>) { const t = useTheme() return ( <Text nativeID={nativeID} style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium, a.mb_sm]}> {children} </Text> )}
export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) { const t = useTheme() const ctx = useContext(Context) const {hover, focus, errorHover, errorFocus} = useMemo(() => { const hover: TextStyle[] = [ { color: t.palette.contrast_800, }, ] const focus: TextStyle[] = [ { color: t.palette.primary_500, }, ] const errorHover: TextStyle[] = [ { color: t.palette.negative_500, }, ] const errorFocus: TextStyle[] = [ { color: t.palette.negative_500, }, ]
return { hover, focus, errorHover, errorFocus, } }, [t])
return ( <View style={[a.z_20, a.pr_xs]}> <Comp size="md" style={[ {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0}, ctx.hovered ? hover : {}, ctx.focused ? focus : {}, ctx.isInvalid && ctx.hovered ? errorHover : {}, ctx.isInvalid && ctx.focused ? errorFocus : {}, ]} /> </View> )}
export function SuffixText({ children, label, accessibilityHint, style,}: React.PropsWithChildren< TextStyleProp & { label: string accessibilityHint?: AccessibilityProps['accessibilityHint'] }>) { const t = useTheme() const ctx = useContext(Context) return ( <Text accessibilityLabel={label} accessibilityHint={accessibilityHint} numberOfLines={1} style={[ a.z_20, a.pr_sm, a.text_md, t.atoms.text_contrast_medium, a.pointer_events_none, web([{marginTop: -2}, a.leading_snug]), (ctx.hovered || ctx.focused) && {color: t.palette.contrast_800}, style, ]}> {children} </Text> )}
export function GhostText({ children, value,}: { children: string value: string}) { const t = useTheme() // eslint-disable-next-line bsky-internal/avoid-unwrapped-text return ( <View style={[ a.pointer_events_none, a.absolute, a.z_10, { paddingLeft: platform({ native: // input padding tokens.space.md + // icon tokens.space.xl + // icon padding tokens.space.xs + // text input padding tokens.space.xs, web: // icon tokens.space.xl + // icon padding tokens.space.xs + // text input padding tokens.space.xs, }), }, web(a.pr_md), a.overflow_hidden, a.max_w_full, ]} aria-hidden={true} accessibilityElementsHidden importantForAccessibility="no-hide-descendants"> <Text style={[ {color: 'transparent'}, a.text_md, {lineHeight: a.text_md.fontSize * 1.1875}, a.w_full, ]} numberOfLines={1}> {children} <Text style={[ t.atoms.text_contrast_low, a.text_md, {lineHeight: a.text_md.fontSize * 1.1875}, ]}> {value} </Text> </Text> </View> )}