parent
5c60fb968e
commit
187b2d52f7
@ -1,27 +0,0 @@ |
||||
import { StatusBar } from 'expo-status-bar'; |
||||
import { StyleSheet, Text, View } from 'react-native'; |
||||
|
||||
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'; |
||||
import '@/global.css'; |
||||
|
||||
export default function App() { |
||||
return ( |
||||
|
||||
<GluestackUIProvider mode="light"> |
||||
<View style={styles.container}> |
||||
<Text>Open up App.tsx to start working on your app!</Text> |
||||
<StatusBar style="auto" /> |
||||
</View> |
||||
</GluestackUIProvider> |
||||
|
||||
); |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
backgroundColor: '#fff', |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
}, |
||||
}); |
@ -0,0 +1,25 @@ |
||||
import { StatusBar } from "expo-status-bar"; |
||||
import { StyleSheet, Text, View } from "react-native"; |
||||
|
||||
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider"; |
||||
import "@/global.css"; |
||||
|
||||
export default function App() { |
||||
return ( |
||||
<GluestackUIProvider mode="light"> |
||||
<View style={styles.container}> |
||||
<Text>Open up App.tsx to start working on your app!</Text> |
||||
<StatusBar style="auto" /> |
||||
</View> |
||||
</GluestackUIProvider> |
||||
); |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
backgroundColor: "#fff", |
||||
alignItems: "center", |
||||
justifyContent: "center", |
||||
}, |
||||
}); |
@ -0,0 +1,41 @@ |
||||
import { Tabs } from "expo-router"; |
||||
import { MaterialIcons } from "@expo/vector-icons"; |
||||
|
||||
export default function TabLayout() { |
||||
return ( |
||||
<Tabs |
||||
screenOptions={{ |
||||
headerShown: false, |
||||
tabBarStyle: { height: 56 }, |
||||
tabBarLabelStyle: { fontSize: 12 }, |
||||
}} |
||||
> |
||||
<Tabs.Screen |
||||
name="index" |
||||
options={{ |
||||
title: "할 일", |
||||
tabBarIcon: ({ color, focused, size }) => ( |
||||
<MaterialIcons |
||||
name={focused ? "check-box" : "check-box-outline-blank"} |
||||
size={size ?? 24} |
||||
color={color} |
||||
/> |
||||
), |
||||
}} |
||||
/> |
||||
<Tabs.Screen |
||||
name="done" |
||||
options={{ |
||||
title: "완료됨", |
||||
tabBarIcon: ({ color, focused, size }) => ( |
||||
<MaterialIcons |
||||
name={focused ? "done-all" : "done"} |
||||
size={size ?? 24} |
||||
color={color} |
||||
/> |
||||
), |
||||
}} |
||||
/> |
||||
</Tabs> |
||||
); |
||||
} |
@ -0,0 +1,5 @@ |
||||
import { SafeAreaView } from "react-native-safe-area-context"; |
||||
|
||||
export default function DoneTab() { |
||||
return <SafeAreaView className="flex-1"></SafeAreaView>; |
||||
} |
@ -0,0 +1,5 @@ |
||||
import { SafeAreaView } from "react-native-safe-area-context"; |
||||
|
||||
export default function ActiveTab() { |
||||
return <SafeAreaView className="flex-1"></SafeAreaView>; |
||||
} |
@ -0,0 +1,27 @@ |
||||
import { StatusBar } from "expo-status-bar"; |
||||
import { StyleSheet, Text, View } from "react-native"; |
||||
import { SafeAreaView } from "react-native-safe-area-context"; |
||||
import { Stack } from "expo-router"; |
||||
|
||||
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider"; |
||||
import "@/global.css"; |
||||
|
||||
export default function App() { |
||||
return ( |
||||
<GluestackUIProvider mode="light"> |
||||
<SafeAreaView style={{ flex: 1 }}> |
||||
<StatusBar style="auto" /> |
||||
<Stack screenOptions={{ headerShown: false }} /> |
||||
</SafeAreaView> |
||||
</GluestackUIProvider> |
||||
); |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
backgroundColor: "#fff", |
||||
// alignItems: "center",
|
||||
// justifyContent: "center",
|
||||
}, |
||||
}); |
@ -0,0 +1,59 @@ |
||||
import { Todo } from "@/store/useTodos"; |
||||
import { FlatList } from "react-native"; |
||||
import { VStack } from "./ui/vstack"; |
||||
import { Text } from "./ui/text"; |
||||
import { HStack } from "./ui/hstack"; |
||||
import { |
||||
Checkbox, |
||||
CheckboxIcon, |
||||
CheckboxIndicator, |
||||
CheckboxLabel, |
||||
} from "./ui/checkbox"; |
||||
import { Button, ButtonText } from "./ui/button"; |
||||
import { GS } from "./ui/gluestack-ui-provider/gluestack-ui-token"; |
||||
|
||||
type Props = { |
||||
data: Todo[]; |
||||
onToggle: (id: string) => void; |
||||
onRemove?: (id: string) => void; |
||||
}; |
||||
|
||||
export default function TodoList({ data, onToggle, onRemove }: Props) { |
||||
return ( |
||||
<FlatList |
||||
data={data} |
||||
keyExtractor={(it) => it.id} |
||||
contentContainerStyle={{ paddingBottom: 80 }} |
||||
renderItem={({ item }) => ( |
||||
<HStack className="items-center py-2"> |
||||
<Checkbox |
||||
isChecked={item.done} |
||||
onChange={(_isChecked) => onToggle(item.id)} |
||||
value={item.title} |
||||
> |
||||
<CheckboxIndicator> |
||||
<CheckboxIcon /> |
||||
</CheckboxIndicator> |
||||
<CheckboxLabel className="data-[checked=true]:line-through"> |
||||
{item.title} |
||||
</CheckboxLabel> |
||||
</Checkbox> |
||||
{onRemove ? ( |
||||
<Button |
||||
variant={GS.variant.link} |
||||
action={GS.action.negative} |
||||
onPress={() => onRemove(item.id)} |
||||
> |
||||
<ButtonText>삭제</ButtonText> |
||||
</Button> |
||||
) : null} |
||||
</HStack> |
||||
)} |
||||
ListEmptyComponent={ |
||||
<Text className="text-neutral-400 mt-6 text-center"> |
||||
할 일이 없습니다. |
||||
</Text> |
||||
} |
||||
/> |
||||
); |
||||
} |
@ -0,0 +1,439 @@ |
||||
'use client'; |
||||
import React from 'react'; |
||||
import { createButton } from '@gluestack-ui/core/button/creator'; |
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { |
||||
withStyleContext, |
||||
useStyleContext, |
||||
} from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { cssInterop } from 'nativewind'; |
||||
import { ActivityIndicator, Pressable, Text, View } from 'react-native'; |
||||
import type { VariantProps } from 'tailwind-variants'; |
||||
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator'; |
||||
|
||||
const SCOPE = 'BUTTON'; |
||||
|
||||
const Root = withStyleContext(Pressable, SCOPE); |
||||
|
||||
const UIButton = createButton({ |
||||
Root: Root, |
||||
Text, |
||||
Group: View, |
||||
Spinner: ActivityIndicator, |
||||
Icon: UIIcon, |
||||
}); |
||||
|
||||
cssInterop(PrimitiveIcon, { |
||||
className: { |
||||
target: 'style', |
||||
nativeStyleToProp: { |
||||
height: true, |
||||
width: true, |
||||
fill: true, |
||||
color: 'classNameColor', |
||||
stroke: true, |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const buttonStyle = tva({ |
||||
base: 'group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2', |
||||
variants: { |
||||
action: { |
||||
primary: |
||||
'bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info', |
||||
secondary: |
||||
'bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info', |
||||
positive: |
||||
'bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info', |
||||
negative: |
||||
'bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info', |
||||
default: |
||||
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', |
||||
}, |
||||
variant: { |
||||
link: 'px-0', |
||||
outline: |
||||
'bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', |
||||
solid: '', |
||||
}, |
||||
|
||||
size: { |
||||
xs: 'px-3.5 h-8', |
||||
sm: 'px-4 h-9', |
||||
md: 'px-5 h-10', |
||||
lg: 'px-6 h-11', |
||||
xl: 'px-7 h-12', |
||||
}, |
||||
}, |
||||
compoundVariants: [ |
||||
{ |
||||
action: 'primary', |
||||
variant: 'link', |
||||
class: |
||||
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent', |
||||
}, |
||||
{ |
||||
action: 'secondary', |
||||
variant: 'link', |
||||
class: |
||||
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent', |
||||
}, |
||||
{ |
||||
action: 'positive', |
||||
variant: 'link', |
||||
class: |
||||
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent', |
||||
}, |
||||
{ |
||||
action: 'negative', |
||||
variant: 'link', |
||||
class: |
||||
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent', |
||||
}, |
||||
{ |
||||
action: 'primary', |
||||
variant: 'outline', |
||||
class: |
||||
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', |
||||
}, |
||||
{ |
||||
action: 'secondary', |
||||
variant: 'outline', |
||||
class: |
||||
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', |
||||
}, |
||||
{ |
||||
action: 'positive', |
||||
variant: 'outline', |
||||
class: |
||||
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', |
||||
}, |
||||
{ |
||||
action: 'negative', |
||||
variant: 'outline', |
||||
class: |
||||
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', |
||||
}, |
||||
], |
||||
}); |
||||
|
||||
const buttonTextStyle = tva({ |
||||
base: 'text-typography-0 font-semibold web:select-none', |
||||
parentVariants: { |
||||
action: { |
||||
primary: |
||||
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700', |
||||
secondary: |
||||
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700', |
||||
positive: |
||||
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700', |
||||
negative: |
||||
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700', |
||||
}, |
||||
variant: { |
||||
link: 'data-[hover=true]:underline data-[active=true]:underline', |
||||
outline: '', |
||||
solid: |
||||
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', |
||||
}, |
||||
size: { |
||||
xs: 'text-xs', |
||||
sm: 'text-sm', |
||||
md: 'text-base', |
||||
lg: 'text-lg', |
||||
xl: 'text-xl', |
||||
}, |
||||
}, |
||||
parentCompoundVariants: [ |
||||
{ |
||||
variant: 'solid', |
||||
action: 'primary', |
||||
class: |
||||
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', |
||||
}, |
||||
{ |
||||
variant: 'solid', |
||||
action: 'secondary', |
||||
class: |
||||
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800', |
||||
}, |
||||
{ |
||||
variant: 'solid', |
||||
action: 'positive', |
||||
class: |
||||
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', |
||||
}, |
||||
{ |
||||
variant: 'solid', |
||||
action: 'negative', |
||||
class: |
||||
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', |
||||
}, |
||||
{ |
||||
variant: 'outline', |
||||
action: 'primary', |
||||
class: |
||||
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500', |
||||
}, |
||||
{ |
||||
variant: 'outline', |
||||
action: 'secondary', |
||||
class: |
||||
'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700', |
||||
}, |
||||
{ |
||||
variant: 'outline', |
||||
action: 'positive', |
||||
class: |
||||
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500', |
||||
}, |
||||
{ |
||||
variant: 'outline', |
||||
action: 'negative', |
||||
class: |
||||
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500', |
||||
}, |
||||
], |
||||
}); |
||||
|
||||
const buttonIconStyle = tva({ |
||||
base: 'fill-none', |
||||
parentVariants: { |
||||
variant: { |
||||
link: 'data-[hover=true]:underline data-[active=true]:underline', |
||||
outline: '', |
||||
solid: |
||||
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', |
||||
}, |
||||
size: { |
||||
xs: 'h-3.5 w-3.5', |
||||
sm: 'h-4 w-4', |
||||
md: 'h-[18px] w-[18px]', |
||||
lg: 'h-[18px] w-[18px]', |
||||
xl: 'h-5 w-5', |
||||
}, |
||||
action: { |
||||
primary: |
||||
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700', |
||||
secondary: |
||||
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700', |
||||
positive: |
||||
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700', |
||||
|
||||
negative: |
||||
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700', |
||||
}, |
||||
}, |
||||
parentCompoundVariants: [ |
||||
{ |
||||
variant: 'solid', |
||||
action: 'primary', |
||||
class: |
||||
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', |
||||
}, |
||||
{ |
||||
variant: 'solid', |
||||
action: 'secondary', |
||||
class: |
||||
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800', |
||||
}, |
||||
{ |
||||
variant: 'solid', |
||||
action: 'positive', |
||||
class: |
||||
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', |
||||
}, |
||||
{ |
||||
variant: 'solid', |
||||
action: 'negative', |
||||
class: |
||||
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', |
||||
}, |
||||
], |
||||
}); |
||||
|
||||
const buttonGroupStyle = tva({ |
||||
base: '', |
||||
variants: { |
||||
space: { |
||||
'xs': 'gap-1', |
||||
'sm': 'gap-2', |
||||
'md': 'gap-3', |
||||
'lg': 'gap-4', |
||||
'xl': 'gap-5', |
||||
'2xl': 'gap-6', |
||||
'3xl': 'gap-7', |
||||
'4xl': 'gap-8', |
||||
}, |
||||
isAttached: { |
||||
true: 'gap-0', |
||||
}, |
||||
flexDirection: { |
||||
'row': 'flex-row', |
||||
'column': 'flex-col', |
||||
'row-reverse': 'flex-row-reverse', |
||||
'column-reverse': 'flex-col-reverse', |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
type IButtonProps = Omit< |
||||
React.ComponentPropsWithoutRef<typeof UIButton>, |
||||
'context' |
||||
> & |
||||
VariantProps<typeof buttonStyle> & { className?: string }; |
||||
|
||||
const Button = React.forwardRef< |
||||
React.ElementRef<typeof UIButton>, |
||||
IButtonProps |
||||
>( |
||||
( |
||||
{ className, variant = 'solid', size = 'md', action = 'primary', ...props }, |
||||
ref |
||||
) => { |
||||
return ( |
||||
<UIButton |
||||
ref={ref} |
||||
{...props} |
||||
className={buttonStyle({ variant, size, action, class: className })} |
||||
context={{ variant, size, action }} |
||||
/> |
||||
); |
||||
} |
||||
); |
||||
|
||||
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text> & |
||||
VariantProps<typeof buttonTextStyle> & { className?: string }; |
||||
|
||||
const ButtonText = React.forwardRef< |
||||
React.ElementRef<typeof UIButton.Text>, |
||||
IButtonTextProps |
||||
>(({ className, variant, size, action, ...props }, ref) => { |
||||
const { |
||||
variant: parentVariant, |
||||
size: parentSize, |
||||
action: parentAction, |
||||
} = useStyleContext(SCOPE); |
||||
|
||||
return ( |
||||
<UIButton.Text |
||||
ref={ref} |
||||
{...props} |
||||
className={buttonTextStyle({ |
||||
parentVariants: { |
||||
variant: parentVariant, |
||||
size: parentSize, |
||||
action: parentAction, |
||||
}, |
||||
variant: variant as 'link' | 'outline' | 'solid' | undefined, |
||||
size, |
||||
action: action as |
||||
| 'primary' |
||||
| 'secondary' |
||||
| 'positive' |
||||
| 'negative' |
||||
| undefined, |
||||
class: className, |
||||
})} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
const ButtonSpinner = UIButton.Spinner; |
||||
|
||||
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon> & |
||||
VariantProps<typeof buttonIconStyle> & { |
||||
className?: string | undefined; |
||||
as?: React.ElementType; |
||||
height?: number; |
||||
width?: number; |
||||
}; |
||||
|
||||
const ButtonIcon = React.forwardRef< |
||||
React.ElementRef<typeof UIButton.Icon>, |
||||
IButtonIcon |
||||
>(({ className, size, ...props }, ref) => { |
||||
const { |
||||
variant: parentVariant, |
||||
size: parentSize, |
||||
action: parentAction, |
||||
} = useStyleContext(SCOPE); |
||||
|
||||
if (typeof size === 'number') { |
||||
return ( |
||||
<UIButton.Icon |
||||
ref={ref} |
||||
{...props} |
||||
className={buttonIconStyle({ class: className })} |
||||
size={size} |
||||
/> |
||||
); |
||||
} else if ( |
||||
(props.height !== undefined || props.width !== undefined) && |
||||
size === undefined |
||||
) { |
||||
return ( |
||||
<UIButton.Icon |
||||
ref={ref} |
||||
{...props} |
||||
className={buttonIconStyle({ class: className })} |
||||
/> |
||||
); |
||||
} |
||||
return ( |
||||
<UIButton.Icon |
||||
{...props} |
||||
className={buttonIconStyle({ |
||||
parentVariants: { |
||||
size: parentSize, |
||||
variant: parentVariant, |
||||
action: parentAction, |
||||
}, |
||||
size, |
||||
class: className, |
||||
})} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group> & |
||||
VariantProps<typeof buttonGroupStyle>; |
||||
|
||||
const ButtonGroup = React.forwardRef< |
||||
React.ElementRef<typeof UIButton.Group>, |
||||
IButtonGroupProps |
||||
>( |
||||
( |
||||
{ |
||||
className, |
||||
space = 'md', |
||||
isAttached = false, |
||||
flexDirection = 'column', |
||||
...props |
||||
}, |
||||
ref |
||||
) => { |
||||
return ( |
||||
<UIButton.Group |
||||
className={buttonGroupStyle({ |
||||
class: className, |
||||
space, |
||||
isAttached: isAttached as boolean, |
||||
flexDirection: flexDirection as any, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
} |
||||
); |
||||
|
||||
Button.displayName = 'Button'; |
||||
ButtonText.displayName = 'ButtonText'; |
||||
ButtonSpinner.displayName = 'ButtonSpinner'; |
||||
ButtonIcon.displayName = 'ButtonIcon'; |
||||
ButtonGroup.displayName = 'ButtonGroup'; |
||||
|
||||
export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup }; |
@ -0,0 +1,242 @@ |
||||
'use client'; |
||||
import React from 'react'; |
||||
import { createCheckbox } from '@gluestack-ui/core/checkbox/creator'; |
||||
import { View, Pressable, Text, Platform } from 'react-native'; |
||||
import type { TextProps, ViewProps } from 'react-native'; |
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { |
||||
PrimitiveIcon, |
||||
IPrimitiveIcon, |
||||
UIIcon, |
||||
} from '@gluestack-ui/core/icon/creator'; |
||||
import { |
||||
withStyleContext, |
||||
useStyleContext, |
||||
} from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { cssInterop } from 'nativewind'; |
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; |
||||
|
||||
const IndicatorWrapper = React.forwardRef< |
||||
React.ComponentRef<typeof View>, |
||||
ViewProps |
||||
>(function IndicatorWrapper({ ...props }, ref) { |
||||
return <View {...props} ref={ref} />; |
||||
}); |
||||
|
||||
const LabelWrapper = React.forwardRef< |
||||
React.ComponentRef<typeof Text>, |
||||
TextProps |
||||
>(function LabelWrapper({ ...props }, ref) { |
||||
return <Text {...props} ref={ref} />; |
||||
}); |
||||
|
||||
const IconWrapper = React.forwardRef< |
||||
React.ComponentRef<typeof PrimitiveIcon>, |
||||
IPrimitiveIcon |
||||
>(function IconWrapper({ ...props }, ref) { |
||||
return <UIIcon {...props} ref={ref} />; |
||||
}); |
||||
|
||||
const SCOPE = 'CHECKBOX'; |
||||
const UICheckbox = createCheckbox({ |
||||
// @ts-expect-error : internal implementation for r-19/react-native-web
|
||||
Root: |
||||
Platform.OS === 'web' |
||||
? withStyleContext(View, SCOPE) |
||||
: withStyleContext(Pressable, SCOPE), |
||||
Group: View, |
||||
Icon: IconWrapper, |
||||
Label: LabelWrapper, |
||||
Indicator: IndicatorWrapper, |
||||
}); |
||||
|
||||
cssInterop(PrimitiveIcon, { |
||||
className: { |
||||
target: 'style', |
||||
nativeStyleToProp: { |
||||
height: true, |
||||
width: true, |
||||
fill: true, |
||||
color: 'classNameColor', |
||||
stroke: true, |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const checkboxStyle = tva({ |
||||
base: 'group/checkbox flex-row items-center justify-start web:cursor-pointer data-[disabled=true]:cursor-not-allowed', |
||||
variants: { |
||||
size: { |
||||
lg: 'gap-2', |
||||
md: 'gap-2', |
||||
sm: 'gap-1.5', |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const checkboxIndicatorStyle = tva({ |
||||
base: 'justify-center items-center border-outline-400 bg-transparent rounded web:data-[focus-visible=true]:outline-none web:data-[focus-visible=true]:ring-2 web:data-[focus-visible=true]:ring-indicator-primary data-[checked=true]:bg-primary-600 data-[checked=true]:border-primary-600 data-[hover=true]:data-[checked=false]:border-outline-500 data-[hover=true]:bg-transparent data-[hover=true]:data-[invalid=true]:border-error-700 data-[hover=true]:data-[checked=true]:bg-primary-700 data-[hover=true]:data-[checked=true]:border-primary-700 data-[hover=true]:data-[checked=true]:data-[disabled=true]:border-primary-600 data-[hover=true]:data-[checked=true]:data-[disabled=true]:bg-primary-600 data-[hover=true]:data-[checked=true]:data-[disabled=true]:opacity-40 data-[hover=true]:data-[checked=true]:data-[disabled=true]:data-[invalid=true]:border-error-700 data-[hover=true]:data-[disabled=true]:border-outline-400 data-[hover=true]:data-[disabled=true]:data-[invalid=true]:border-error-700 data-[active=true]:data-[checked=true]:bg-primary-800 data-[active=true]:data-[checked=true]:border-primary-800 data-[invalid=true]:border-error-700 data-[disabled=true]:opacity-40', |
||||
parentVariants: { |
||||
size: { |
||||
lg: 'w-6 h-6 border-[3px]', |
||||
md: 'w-5 h-5 border-2', |
||||
sm: 'w-4 h-4 border-2', |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const checkboxLabelStyle = tva({ |
||||
base: 'text-typography-600 data-[checked=true]:text-typography-900 data-[hover=true]:text-typography-900 data-[hover=true]:data-[checked=true]:text-typography-900 data-[hover=true]:data-[checked=true]:data-[disabled=true]:text-typography-900 data-[hover=true]:data-[disabled=true]:text-typography-400 data-[active=true]:text-typography-900 data-[active=true]:data-[checked=true]:text-typography-900 data-[disabled=true]:opacity-40 web:select-none', |
||||
parentVariants: { |
||||
size: { |
||||
lg: 'text-lg', |
||||
md: 'text-base', |
||||
sm: 'text-sm', |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const checkboxIconStyle = tva({ |
||||
base: 'text-typography-50 fill-none', |
||||
|
||||
parentVariants: { |
||||
size: { |
||||
sm: 'h-3 w-3', |
||||
md: 'h-4 w-4', |
||||
lg: 'h-5 w-5', |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const CheckboxGroup = UICheckbox.Group; |
||||
|
||||
type ICheckboxProps = React.ComponentPropsWithoutRef<typeof UICheckbox> & |
||||
VariantProps<typeof checkboxStyle>; |
||||
|
||||
const Checkbox = React.forwardRef< |
||||
React.ComponentRef<typeof UICheckbox>, |
||||
ICheckboxProps |
||||
>(function Checkbox({ className, size = 'md', ...props }, ref) { |
||||
return ( |
||||
<UICheckbox |
||||
className={checkboxStyle({ |
||||
class: className, |
||||
size, |
||||
})} |
||||
{...props} |
||||
context={{ |
||||
size, |
||||
}} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
type ICheckboxIndicatorProps = React.ComponentPropsWithoutRef< |
||||
typeof UICheckbox.Indicator |
||||
> & |
||||
VariantProps<typeof checkboxIndicatorStyle>; |
||||
|
||||
const CheckboxIndicator = React.forwardRef< |
||||
React.ComponentRef<typeof UICheckbox.Indicator>, |
||||
ICheckboxIndicatorProps |
||||
>(function CheckboxIndicator({ className, ...props }, ref) { |
||||
const { size: parentSize } = useStyleContext(SCOPE); |
||||
|
||||
return ( |
||||
<UICheckbox.Indicator |
||||
className={checkboxIndicatorStyle({ |
||||
parentVariants: { |
||||
size: parentSize, |
||||
}, |
||||
class: className, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
type ICheckboxLabelProps = React.ComponentPropsWithoutRef< |
||||
typeof UICheckbox.Label |
||||
> & |
||||
VariantProps<typeof checkboxLabelStyle>; |
||||
const CheckboxLabel = React.forwardRef< |
||||
React.ComponentRef<typeof UICheckbox.Label>, |
||||
ICheckboxLabelProps |
||||
>(function CheckboxLabel({ className, ...props }, ref) { |
||||
const { size: parentSize } = useStyleContext(SCOPE); |
||||
return ( |
||||
<UICheckbox.Label |
||||
className={checkboxLabelStyle({ |
||||
parentVariants: { |
||||
size: parentSize, |
||||
}, |
||||
class: className, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
type ICheckboxIconProps = React.ComponentPropsWithoutRef< |
||||
typeof UICheckbox.Icon |
||||
> & |
||||
VariantProps<typeof checkboxIconStyle>; |
||||
|
||||
const CheckboxIcon = React.forwardRef< |
||||
React.ComponentRef<typeof UICheckbox.Icon>, |
||||
ICheckboxIconProps |
||||
>(function CheckboxIcon({ className, size, ...props }, ref) { |
||||
const { size: parentSize } = useStyleContext(SCOPE); |
||||
|
||||
if (typeof size === 'number') { |
||||
return ( |
||||
<UICheckbox.Icon |
||||
ref={ref} |
||||
{...props} |
||||
className={checkboxIconStyle({ class: className })} |
||||
size={size} |
||||
/> |
||||
); |
||||
} else if ( |
||||
(props.height !== undefined || props.width !== undefined) && |
||||
size === undefined |
||||
) { |
||||
return ( |
||||
<UICheckbox.Icon |
||||
ref={ref} |
||||
{...props} |
||||
className={checkboxIconStyle({ class: className })} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<UICheckbox.Icon |
||||
className={checkboxIconStyle({ |
||||
parentVariants: { |
||||
size: parentSize, |
||||
}, |
||||
size, |
||||
class: className, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
Checkbox.displayName = 'Checkbox'; |
||||
CheckboxIndicator.displayName = 'CheckboxIndicator'; |
||||
CheckboxLabel.displayName = 'CheckboxLabel'; |
||||
CheckboxIcon.displayName = 'CheckboxIcon'; |
||||
|
||||
export { |
||||
Checkbox, |
||||
CheckboxIndicator, |
||||
CheckboxLabel, |
||||
CheckboxIcon, |
||||
CheckboxGroup, |
||||
}; |
@ -0,0 +1,19 @@ |
||||
export const GS = { |
||||
size: { |
||||
xs: "xs", |
||||
sm: "sm", |
||||
md: "md", |
||||
lg: "lg", |
||||
}, |
||||
variant: { |
||||
solid: "solid", |
||||
outline: "outline", |
||||
link: "link", |
||||
}, |
||||
action: { |
||||
primary: "primary", |
||||
secondary: "secondary", |
||||
positive: "positive", |
||||
negative: "negative", |
||||
}, |
||||
} as const; |
@ -0,0 +1,27 @@ |
||||
import React from 'react'; |
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { View } from 'react-native'; |
||||
import type { ViewProps } from 'react-native'; |
||||
import { hstackStyle } from './styles'; |
||||
|
||||
type IHStackProps = ViewProps & VariantProps<typeof hstackStyle>; |
||||
|
||||
const HStack = React.forwardRef<React.ComponentRef<typeof View>, IHStackProps>( |
||||
function HStack({ className, space, reversed, ...props }, ref) { |
||||
return ( |
||||
<View |
||||
className={hstackStyle({ |
||||
space, |
||||
reversed: reversed as boolean, |
||||
class: className, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
} |
||||
); |
||||
|
||||
HStack.displayName = 'HStack'; |
||||
|
||||
export { HStack }; |
@ -0,0 +1,26 @@ |
||||
import React from 'react'; |
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { hstackStyle } from './styles'; |
||||
|
||||
type IHStackProps = React.ComponentPropsWithoutRef<'div'> & |
||||
VariantProps<typeof hstackStyle>; |
||||
|
||||
const HStack = React.forwardRef<React.ComponentRef<'div'>, IHStackProps>( |
||||
function HStack({ className, space, reversed, ...props }, ref) { |
||||
return ( |
||||
<div |
||||
className={hstackStyle({ |
||||
space, |
||||
reversed: reversed as boolean, |
||||
class: className, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
} |
||||
); |
||||
|
||||
HStack.displayName = 'HStack'; |
||||
|
||||
export { HStack }; |
@ -0,0 +1,25 @@ |
||||
import { isWeb } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils'; |
||||
|
||||
const baseStyle = isWeb |
||||
? 'flex relative z-0 box-border border-0 list-none min-w-0 min-h-0 bg-transparent items-stretch m-0 p-0 text-decoration-none' |
||||
: ''; |
||||
|
||||
export const hstackStyle = tva({ |
||||
base: `flex-row ${baseStyle}`, |
||||
variants: { |
||||
space: { |
||||
'xs': 'gap-1', |
||||
'sm': 'gap-2', |
||||
'md': 'gap-3', |
||||
'lg': 'gap-4', |
||||
'xl': 'gap-5', |
||||
'2xl': 'gap-6', |
||||
'3xl': 'gap-7', |
||||
'4xl': 'gap-8', |
||||
}, |
||||
reversed: { |
||||
true: 'flex-row-reverse', |
||||
}, |
||||
}, |
||||
}); |
@ -0,0 +1,217 @@ |
||||
'use client'; |
||||
import React from 'react'; |
||||
import { createInput } from '@gluestack-ui/core/input/creator'; |
||||
import { View, Pressable, TextInput } from 'react-native'; |
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { |
||||
withStyleContext, |
||||
useStyleContext, |
||||
} from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { cssInterop } from 'nativewind'; |
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator'; |
||||
|
||||
const SCOPE = 'INPUT'; |
||||
|
||||
const UIInput = createInput({ |
||||
Root: withStyleContext(View, SCOPE), |
||||
Icon: UIIcon, |
||||
Slot: Pressable, |
||||
Input: TextInput, |
||||
}); |
||||
|
||||
cssInterop(PrimitiveIcon, { |
||||
className: { |
||||
target: 'style', |
||||
nativeStyleToProp: { |
||||
height: true, |
||||
width: true, |
||||
fill: true, |
||||
color: 'classNameColor', |
||||
stroke: true, |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const inputStyle = tva({ |
||||
base: 'border-background-300 flex-row overflow-hidden content-center data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[focus=true]:hover:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:hover:border-background-300 items-center', |
||||
|
||||
variants: { |
||||
size: { |
||||
xl: 'h-12', |
||||
lg: 'h-11', |
||||
md: 'h-10', |
||||
sm: 'h-9', |
||||
}, |
||||
|
||||
variant: { |
||||
underlined: |
||||
'rounded-none border-b data-[invalid=true]:border-b-2 data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700', |
||||
|
||||
outline: |
||||
'rounded border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error', |
||||
|
||||
rounded: |
||||
'rounded-full border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error', |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const inputIconStyle = tva({ |
||||
base: 'justify-center items-center text-typography-400 fill-none', |
||||
parentVariants: { |
||||
size: { |
||||
'2xs': 'h-3 w-3', |
||||
'xs': 'h-3.5 w-3.5', |
||||
'sm': 'h-4 w-4', |
||||
'md': 'h-[18px] w-[18px]', |
||||
'lg': 'h-5 w-5', |
||||
'xl': 'h-6 w-6', |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const inputSlotStyle = tva({ |
||||
base: 'justify-center items-center web:disabled:cursor-not-allowed', |
||||
}); |
||||
|
||||
const inputFieldStyle = tva({ |
||||
base: 'flex-1 text-typography-900 py-0 px-3 placeholder:text-typography-500 h-full ios:leading-[0px] web:cursor-text web:data-[disabled=true]:cursor-not-allowed', |
||||
|
||||
parentVariants: { |
||||
variant: { |
||||
underlined: 'web:outline-0 web:outline-none px-0', |
||||
outline: 'web:outline-0 web:outline-none', |
||||
rounded: 'web:outline-0 web:outline-none px-4', |
||||
}, |
||||
|
||||
size: { |
||||
'2xs': 'text-2xs', |
||||
'xs': 'text-xs', |
||||
'sm': 'text-sm', |
||||
'md': 'text-base', |
||||
'lg': 'text-lg', |
||||
'xl': 'text-xl', |
||||
'2xl': 'text-2xl', |
||||
'3xl': 'text-3xl', |
||||
'4xl': 'text-4xl', |
||||
'5xl': 'text-5xl', |
||||
'6xl': 'text-6xl', |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
type IInputProps = React.ComponentProps<typeof UIInput> & |
||||
VariantProps<typeof inputStyle> & { className?: string }; |
||||
const Input = React.forwardRef<React.ComponentRef<typeof UIInput>, IInputProps>( |
||||
function Input( |
||||
{ className, variant = 'outline', size = 'md', ...props }, |
||||
ref |
||||
) { |
||||
return ( |
||||
<UIInput |
||||
ref={ref} |
||||
{...props} |
||||
className={inputStyle({ variant, size, class: className })} |
||||
context={{ variant, size }} |
||||
/> |
||||
); |
||||
} |
||||
); |
||||
|
||||
type IInputIconProps = React.ComponentProps<typeof UIInput.Icon> & |
||||
VariantProps<typeof inputIconStyle> & { |
||||
className?: string; |
||||
height?: number; |
||||
width?: number; |
||||
}; |
||||
|
||||
const InputIcon = React.forwardRef< |
||||
React.ComponentRef<typeof UIInput.Icon>, |
||||
IInputIconProps |
||||
>(function InputIcon({ className, size, ...props }, ref) { |
||||
const { size: parentSize } = useStyleContext(SCOPE); |
||||
|
||||
if (typeof size === 'number') { |
||||
return ( |
||||
<UIInput.Icon |
||||
ref={ref} |
||||
{...props} |
||||
className={inputIconStyle({ class: className })} |
||||
size={size} |
||||
/> |
||||
); |
||||
} else if ( |
||||
(props.height !== undefined || props.width !== undefined) && |
||||
size === undefined |
||||
) { |
||||
return ( |
||||
<UIInput.Icon |
||||
ref={ref} |
||||
{...props} |
||||
className={inputIconStyle({ class: className })} |
||||
/> |
||||
); |
||||
} |
||||
return ( |
||||
<UIInput.Icon |
||||
ref={ref} |
||||
{...props} |
||||
className={inputIconStyle({ |
||||
parentVariants: { |
||||
size: parentSize, |
||||
}, |
||||
class: className, |
||||
})} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
type IInputSlotProps = React.ComponentProps<typeof UIInput.Slot> & |
||||
VariantProps<typeof inputSlotStyle> & { className?: string }; |
||||
|
||||
const InputSlot = React.forwardRef< |
||||
React.ComponentRef<typeof UIInput.Slot>, |
||||
IInputSlotProps |
||||
>(function InputSlot({ className, ...props }, ref) { |
||||
return ( |
||||
<UIInput.Slot |
||||
ref={ref} |
||||
{...props} |
||||
className={inputSlotStyle({ |
||||
class: className, |
||||
})} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
type IInputFieldProps = React.ComponentProps<typeof UIInput.Input> & |
||||
VariantProps<typeof inputFieldStyle> & { className?: string }; |
||||
|
||||
const InputField = React.forwardRef< |
||||
React.ComponentRef<typeof UIInput.Input>, |
||||
IInputFieldProps |
||||
>(function InputField({ className, ...props }, ref) { |
||||
const { variant: parentVariant, size: parentSize } = useStyleContext(SCOPE); |
||||
|
||||
return ( |
||||
<UIInput.Input |
||||
ref={ref} |
||||
{...props} |
||||
className={inputFieldStyle({ |
||||
parentVariants: { |
||||
variant: parentVariant, |
||||
size: parentSize, |
||||
}, |
||||
class: className, |
||||
})} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
Input.displayName = 'Input'; |
||||
InputIcon.displayName = 'InputIcon'; |
||||
InputSlot.displayName = 'InputSlot'; |
||||
InputField.displayName = 'InputField'; |
||||
|
||||
export { Input, InputField, InputIcon, InputSlot }; |
@ -0,0 +1,48 @@ |
||||
import React from 'react'; |
||||
|
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { Text as RNText } from 'react-native'; |
||||
import { textStyle } from './styles'; |
||||
|
||||
type ITextProps = React.ComponentProps<typeof RNText> & |
||||
VariantProps<typeof textStyle>; |
||||
|
||||
const Text = React.forwardRef<React.ComponentRef<typeof RNText>, ITextProps>( |
||||
function Text( |
||||
{ |
||||
className, |
||||
isTruncated, |
||||
bold, |
||||
underline, |
||||
strikeThrough, |
||||
size = 'md', |
||||
sub, |
||||
italic, |
||||
highlight, |
||||
...props |
||||
}, |
||||
ref |
||||
) { |
||||
return ( |
||||
<RNText |
||||
className={textStyle({ |
||||
isTruncated: isTruncated as boolean, |
||||
bold: bold as boolean, |
||||
underline: underline as boolean, |
||||
strikeThrough: strikeThrough as boolean, |
||||
size, |
||||
sub: sub as boolean, |
||||
italic: italic as boolean, |
||||
highlight: highlight as boolean, |
||||
class: className, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
} |
||||
); |
||||
|
||||
Text.displayName = 'Text'; |
||||
|
||||
export { Text }; |
@ -0,0 +1,45 @@ |
||||
import React from 'react'; |
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { textStyle } from './styles'; |
||||
|
||||
type ITextProps = React.ComponentProps<'span'> & VariantProps<typeof textStyle>; |
||||
|
||||
const Text = React.forwardRef<React.ComponentRef<'span'>, ITextProps>( |
||||
function Text( |
||||
{ |
||||
className, |
||||
isTruncated, |
||||
bold, |
||||
underline, |
||||
strikeThrough, |
||||
size = 'md', |
||||
sub, |
||||
italic, |
||||
highlight, |
||||
...props |
||||
}: { className?: string } & ITextProps, |
||||
ref |
||||
) { |
||||
return ( |
||||
<span |
||||
className={textStyle({ |
||||
isTruncated: isTruncated as boolean, |
||||
bold: bold as boolean, |
||||
underline: underline as boolean, |
||||
strikeThrough: strikeThrough as boolean, |
||||
size, |
||||
sub: sub as boolean, |
||||
italic: italic as boolean, |
||||
highlight: highlight as boolean, |
||||
class: className, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
} |
||||
); |
||||
|
||||
Text.displayName = 'Text'; |
||||
|
||||
export { Text }; |
@ -0,0 +1,47 @@ |
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { isWeb } from '@gluestack-ui/utils/nativewind-utils'; |
||||
|
||||
const baseStyle = isWeb |
||||
? 'font-sans tracking-sm my-0 bg-transparent border-0 box-border display-inline list-none margin-0 padding-0 position-relative text-start no-underline whitespace-pre-wrap word-wrap-break-word' |
||||
: ''; |
||||
|
||||
export const textStyle = tva({ |
||||
base: `text-typography-700 font-body ${baseStyle}`, |
||||
|
||||
variants: { |
||||
isTruncated: { |
||||
true: 'web:truncate', |
||||
}, |
||||
bold: { |
||||
true: 'font-bold', |
||||
}, |
||||
underline: { |
||||
true: 'underline', |
||||
}, |
||||
strikeThrough: { |
||||
true: 'line-through', |
||||
}, |
||||
size: { |
||||
'2xs': 'text-2xs', |
||||
'xs': 'text-xs', |
||||
'sm': 'text-sm', |
||||
'md': 'text-base', |
||||
'lg': 'text-lg', |
||||
'xl': 'text-xl', |
||||
'2xl': 'text-2xl', |
||||
'3xl': 'text-3xl', |
||||
'4xl': 'text-4xl', |
||||
'5xl': 'text-5xl', |
||||
'6xl': 'text-6xl', |
||||
}, |
||||
sub: { |
||||
true: 'text-xs', |
||||
}, |
||||
italic: { |
||||
true: 'italic', |
||||
}, |
||||
highlight: { |
||||
true: 'bg-yellow-500', |
||||
}, |
||||
}, |
||||
}); |
@ -0,0 +1,28 @@ |
||||
import React from 'react'; |
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { View } from 'react-native'; |
||||
|
||||
import { vstackStyle } from './styles'; |
||||
|
||||
type IVStackProps = React.ComponentProps<typeof View> & |
||||
VariantProps<typeof vstackStyle>; |
||||
|
||||
const VStack = React.forwardRef<React.ComponentRef<typeof View>, IVStackProps>( |
||||
function VStack({ className, space, reversed, ...props }, ref) { |
||||
return ( |
||||
<View |
||||
className={vstackStyle({ |
||||
space, |
||||
reversed: reversed as boolean, |
||||
class: className, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
} |
||||
); |
||||
|
||||
VStack.displayName = 'VStack'; |
||||
|
||||
export { VStack }; |
@ -0,0 +1,27 @@ |
||||
import React from 'react'; |
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; |
||||
|
||||
import { vstackStyle } from './styles'; |
||||
|
||||
type IVStackProps = React.ComponentProps<'div'> & |
||||
VariantProps<typeof vstackStyle>; |
||||
|
||||
const VStack = React.forwardRef<React.ComponentRef<'div'>, IVStackProps>( |
||||
function VStack({ className, space, reversed, ...props }, ref) { |
||||
return ( |
||||
<div |
||||
className={vstackStyle({ |
||||
space, |
||||
reversed: reversed as boolean, |
||||
class: className, |
||||
})} |
||||
{...props} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
} |
||||
); |
||||
|
||||
VStack.displayName = 'VStack'; |
||||
|
||||
export { VStack }; |
@ -0,0 +1,25 @@ |
||||
import { isWeb } from '@gluestack-ui/utils/nativewind-utils'; |
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils'; |
||||
|
||||
const baseStyle = isWeb |
||||
? 'flex flex-col relative z-0 box-border border-0 list-none min-w-0 min-h-0 bg-transparent items-stretch m-0 p-0 text-decoration-none' |
||||
: ''; |
||||
|
||||
export const vstackStyle = tva({ |
||||
base: `flex-col ${baseStyle}`, |
||||
variants: { |
||||
space: { |
||||
'xs': 'gap-1', |
||||
'sm': 'gap-2', |
||||
'md': 'gap-3', |
||||
'lg': 'gap-4', |
||||
'xl': 'gap-5', |
||||
'2xl': 'gap-6', |
||||
'3xl': 'gap-7', |
||||
'4xl': 'gap-8', |
||||
}, |
||||
reversed: { |
||||
true: 'flex-col-reverse', |
||||
}, |
||||
}, |
||||
}); |
@ -0,0 +1,90 @@ |
||||
import { useEffect, useMemo, useRef, useState } from "react"; |
||||
import AsyncStorage from "@react-native-async-storage/async-storage"; |
||||
|
||||
export type Todo = { |
||||
id: string; |
||||
title: string; |
||||
done: boolean; |
||||
createdAt: number; |
||||
}; |
||||
|
||||
const STORAGE_KEY = "todos.v1"; |
||||
const DEBOUNCE_MS = 300; |
||||
|
||||
export function useTodos() { |
||||
const [todos, setTodos] = useState<Todo[]>([]); |
||||
const timer = useRef<ReturnType<typeof setTimeout> | null>(null); |
||||
|
||||
// load once
|
||||
useEffect(() => { |
||||
(async () => { |
||||
const raw = await AsyncStorage.getItem(STORAGE_KEY); |
||||
if (raw) setTodos(JSON.parse(raw)); |
||||
})(); |
||||
}, []); |
||||
|
||||
// debounce save
|
||||
useEffect(() => { |
||||
if (timer.current) clearTimeout(timer.current); |
||||
timer.current = setTimeout(() => { |
||||
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(todos)).catch((e) => { |
||||
console.warn("Debounce warn: ", e); |
||||
}); |
||||
}, DEBOUNCE_MS); |
||||
|
||||
return () => { |
||||
if (timer.current) clearTimeout(timer.current); |
||||
}; |
||||
}, [todos]); |
||||
|
||||
const activeTodos = useMemo(() => todos.filter((t) => !t.done), [todos]); |
||||
const doneTodos = useMemo(() => todos.filter((t) => t.done), [todos]); |
||||
const leftCount = useMemo(() => activeTodos.length, [activeTodos]); |
||||
|
||||
const add = (title: string) => { |
||||
const t = title.trim(); |
||||
if (!t) return; |
||||
const id = `${Date.now}-${Math.random().toString(36).slice(2, 8)}`; |
||||
setTodos([ |
||||
{ |
||||
id, |
||||
title: t, |
||||
done: false, |
||||
createdAt: Date.now(), |
||||
}, |
||||
...todos, |
||||
]); |
||||
}; |
||||
|
||||
const toggle = (id: string) => |
||||
setTodos( |
||||
todos.map((it) => |
||||
it.id === id |
||||
? { |
||||
...it, |
||||
done: !it.done, |
||||
} |
||||
: it |
||||
) |
||||
); |
||||
|
||||
const remove = (id: string) => setTodos(todos.filter((it) => it.id !== id)); |
||||
|
||||
const clearDone = () => setTodos(todos.filter((it) => !it.done)); |
||||
|
||||
const completeAll = () => |
||||
setTodos(todos.map((it) => ({ ...it, done: true }))); |
||||
|
||||
return { |
||||
todos, |
||||
setTodos, |
||||
activeTodos, |
||||
doneTodos, |
||||
leftCount, |
||||
add, |
||||
toggle, |
||||
remove, |
||||
clearDone, |
||||
completeAll, |
||||
}; |
||||
} |
Loading…
Reference in new issue