diff --git a/every-jap-dict/App.tsx b/every-jap-dict/App.tsx index f5c4493..34d1a38 100755 --- a/every-jap-dict/App.tsx +++ b/every-jap-dict/App.tsx @@ -1,6 +1,6 @@ import { StatusBar } from 'expo-status-bar'; -import { FlatList, Modal, Pressable, StyleSheet, View } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { FlatList, Keyboard, Modal, Pressable, StyleSheet, View } from 'react-native'; +import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'; import '@/global.css'; @@ -13,6 +13,7 @@ import { HStack } from './components/ui/hstack'; import { Button, ButtonText } from './components/ui/button'; import DictWebView from './components/DictWebView'; import AdBannerPlaceholder from './components/AdBannerPlaceholder'; +import { Toast, ToastDescription, ToastTitle, useToast } from './components/ui/toast'; function MainScreen() { const [q, setQ] = useState(''); @@ -20,11 +21,15 @@ function MainScreen() { const [activeId, setActiveId] = useState(''); const [wordbookOpen, setWordbookOpen] = useState(false); + const insets = useSafeAreaInsets(); + const pack = jaPack; const providers = pack.providers; const canSearch = q.trim().length > 0; const hasQuery = query.trim().length > 0; + const toast = useToast(); + const { add, items, remove, clear, loading } = useWordbook(); const urls = useMemo( @@ -38,13 +43,31 @@ function MainScreen() { const onSearch = () => { if (!canSearch) return; + Keyboard.dismiss(); setQuery(q.trim()); setActiveId(providers[0]?.id ?? ''); }; const onSave = () => { if (!canSearch) return; + Keyboard.dismiss(); add(q.trim(), pack.code); + + toast.show({ + placement: 'top', + duration: 2000, + render: () => ( + + + + 단어장에 저장되었습니다 + + + ), + }); }; const onPickWord = (word: string) => { @@ -61,10 +84,21 @@ function MainScreen() { {/* 액션 버튼 */} - - @@ -77,6 +111,7 @@ function MainScreen() { value={q} onChangeText={setQ} returnKeyType="search" + submitBehavior="blurAndSubmit" onSubmitEditing={onSearch} /> @@ -108,7 +143,7 @@ function MainScreen() { {/* 패널: WebView 모두 마운트 후 토글 */} - {!canSearch ? ( + {!hasQuery ? ( 검색어를 입력하고 [검색]을 눌러주세요 @@ -175,12 +210,14 @@ function MainScreen() { export default function App() { return ( - - - - - - + + + + + + + + ); } diff --git a/every-jap-dict/components/ui/box/index.tsx b/every-jap-dict/components/ui/box/index.tsx new file mode 100644 index 0000000..f22bb2f --- /dev/null +++ b/every-jap-dict/components/ui/box/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { View, ViewProps } from 'react-native'; + +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; +import { boxStyle } from './styles'; + +type IBoxProps = ViewProps & + VariantProps & { className?: string }; + +const Box = React.forwardRef, IBoxProps>( + function Box({ className, ...props }, ref) { + return ( + + ); + } +); + +Box.displayName = 'Box'; +export { Box }; diff --git a/every-jap-dict/components/ui/box/index.web.tsx b/every-jap-dict/components/ui/box/index.web.tsx new file mode 100644 index 0000000..8839eeb --- /dev/null +++ b/every-jap-dict/components/ui/box/index.web.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { boxStyle } from './styles'; + +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; + +type IBoxProps = React.ComponentPropsWithoutRef<'div'> & + VariantProps & { className?: string }; + +const Box = React.forwardRef(function Box( + { className, ...props }, + ref +) { + return ( +
+ ); +}); + +Box.displayName = 'Box'; +export { Box }; diff --git a/every-jap-dict/components/ui/box/styles.tsx b/every-jap-dict/components/ui/box/styles.tsx new file mode 100644 index 0000000..d2ade82 --- /dev/null +++ b/every-jap-dict/components/ui/box/styles.tsx @@ -0,0 +1,10 @@ +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { isWeb } 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 boxStyle = tva({ + base: baseStyle, +}); diff --git a/every-jap-dict/components/ui/pressable/index.tsx b/every-jap-dict/components/ui/pressable/index.tsx new file mode 100644 index 0000000..6b8f6ca --- /dev/null +++ b/every-jap-dict/components/ui/pressable/index.tsx @@ -0,0 +1,39 @@ +'use client'; +import React from 'react'; +import { createPressable } from '@gluestack-ui/core/pressable/creator'; +import { Pressable as RNPressable } from 'react-native'; + +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; + +const UIPressable = createPressable({ + Root: withStyleContext(RNPressable), +}); + +const pressableStyle = tva({ + base: 'data-[focus-visible=true]:outline-none data-[focus-visible=true]:ring-indicator-info data-[focus-visible=true]:ring-2 data-[disabled=true]:opacity-40', +}); + +type IPressableProps = Omit< + React.ComponentProps, + 'context' +> & + VariantProps; +const Pressable = React.forwardRef< + React.ComponentRef, + IPressableProps +>(function Pressable({ className, ...props }, ref) { + return ( + + ); +}); + +Pressable.displayName = 'Pressable'; +export { Pressable }; diff --git a/every-jap-dict/components/ui/toast/index.tsx b/every-jap-dict/components/ui/toast/index.tsx new file mode 100644 index 0000000..2dba79e --- /dev/null +++ b/every-jap-dict/components/ui/toast/index.tsx @@ -0,0 +1,240 @@ +'use client'; +import React from 'react'; +import { createToastHook } from '@gluestack-ui/core/toast/creator'; +import { AccessibilityInfo, Text, View, ViewStyle } from 'react-native'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { cssInterop } from 'nativewind'; +import { + Motion, + AnimatePresence, + MotionComponentProps, +} from '@legendapp/motion'; +import { + withStyleContext, + useStyleContext, +} from '@gluestack-ui/utils/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; + +type IMotionViewProps = React.ComponentProps & + MotionComponentProps; + +const MotionView = Motion.View as React.ComponentType; + +const useToast = createToastHook(MotionView, AnimatePresence); +const SCOPE = 'TOAST'; + +cssInterop(MotionView, { className: 'style' }); + +const toastStyle = tva({ + base: 'p-4 m-1 rounded-md gap-1 web:pointer-events-auto shadow-hard-5 border-outline-100', + variants: { + action: { + error: 'bg-error-800', + warning: 'bg-warning-700', + success: 'bg-success-700', + info: 'bg-info-700', + muted: 'bg-background-800', + }, + + variant: { + solid: '', + outline: 'border bg-background-0', + }, + }, +}); + +const toastTitleStyle = tva({ + base: 'text-typography-0 font-medium font-body tracking-md text-left', + variants: { + isTruncated: { + true: '', + }, + 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', + }, + }, + parentVariants: { + variant: { + solid: '', + outline: '', + }, + action: { + error: '', + warning: '', + success: '', + info: '', + muted: '', + }, + }, + parentCompoundVariants: [ + { + variant: 'outline', + action: 'error', + class: 'text-error-800', + }, + { + variant: 'outline', + action: 'warning', + class: 'text-warning-800', + }, + { + variant: 'outline', + action: 'success', + class: 'text-success-800', + }, + { + variant: 'outline', + action: 'info', + class: 'text-info-800', + }, + { + variant: 'outline', + action: 'muted', + class: 'text-background-800', + }, + ], +}); + +const toastDescriptionStyle = tva({ + base: 'font-normal font-body tracking-md text-left', + variants: { + isTruncated: { + true: '', + }, + 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', + }, + }, + parentVariants: { + variant: { + solid: 'text-typography-50', + outline: 'text-typography-900', + }, + }, +}); + +const Root = withStyleContext(View, SCOPE); +type IToastProps = React.ComponentProps & { + className?: string; +} & VariantProps; + +const Toast = React.forwardRef, IToastProps>( + function Toast( + { className, variant = 'solid', action = 'muted', ...props }, + ref + ) { + return ( + + ); + } +); + +type IToastTitleProps = React.ComponentProps & { + className?: string; +} & VariantProps; + +const ToastTitle = React.forwardRef< + React.ComponentRef, + IToastTitleProps +>(function ToastTitle({ className, size = 'md', children, ...props }, ref) { + const { variant: parentVariant, action: parentAction } = + useStyleContext(SCOPE); + React.useEffect(() => { + // Issue from react-native side + // Hack for now, will fix this later + AccessibilityInfo.announceForAccessibility(children as string); + }, [children]); + + return ( + + {children} + + ); +}); + +type IToastDescriptionProps = React.ComponentProps & { + className?: string; +} & VariantProps; + +const ToastDescription = React.forwardRef< + React.ComponentRef, + IToastDescriptionProps +>(function ToastDescription({ className, size = 'md', ...props }, ref) { + const { variant: parentVariant } = useStyleContext(SCOPE); + return ( + + ); +}); + +Toast.displayName = 'Toast'; +ToastTitle.displayName = 'ToastTitle'; +ToastDescription.displayName = 'ToastDescription'; + +export { useToast, Toast, ToastTitle, ToastDescription }; diff --git a/every-jap-dict/package-lock.json b/every-jap-dict/package-lock.json index a722081..2f3341e 100755 --- a/every-jap-dict/package-lock.json +++ b/every-jap-dict/package-lock.json @@ -11,7 +11,7 @@ "@expo/html-elements": "^0.10.1", "@gluestack-ui/core": "^3.0.0", "@gluestack-ui/utils": "^3.0.0", - "@legendapp/motion": "^2.3.0", + "@legendapp/motion": "^2.4.0", "@react-native-async-storage/async-storage": "^2.2.0", "babel-plugin-module-resolver": "^5.0.2", "expo": "~53.0.22", @@ -24,7 +24,7 @@ "react-dom": "^19.1.1", "react-native": "0.79.6", "react-native-reanimated": "^4.1.0", - "react-native-safe-area-context": "^4.11.0", + "react-native-safe-area-context": "^5.6.1", "react-native-svg": "^15.2.0", "react-native-webview": "^13.16.0", "react-native-worklets": "^0.5.0", @@ -2525,15 +2525,15 @@ } }, "node_modules/@legendapp/motion": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@legendapp/motion/-/motion-2.3.0.tgz", - "integrity": "sha512-LtTD06eyz/Ge23FAR6BY+i9Gsgr/ZgxE12FneML8LrZGcZOSPN2Ojz3N2eJaTiA50kqoeqrGCaYJja8KgKpL6Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@legendapp/motion/-/motion-2.4.0.tgz", + "integrity": "sha512-AAYpRLGvxGD5hIGl9sVHyoUufr66zoH82PuxYcKiPSMdCBI3jwZFWh6CuHjV1leRKVIRk2py1rSvIVabG8eqcw==", "license": "MIT", "dependencies": { "@legendapp/tools": "2.0.1" }, "peerDependencies": { - "nativewind": "^2.0.0", + "nativewind": "*", "react": ">=16", "react-native": "*" } @@ -11866,9 +11866,9 @@ } }, "node_modules/react-native-safe-area-context": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.11.0.tgz", - "integrity": "sha512-Bg7bozxEB+ZS+H3tVYs5yY1cvxNXgR6nRQwpSMkYR9IN5CbxohLnSprrOPG/ostTCd4F6iCk0c51pExEhifSKQ==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz", + "integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==", "license": "MIT", "peerDependencies": { "react": "*", diff --git a/every-jap-dict/package.json b/every-jap-dict/package.json index 1856cb6..de92510 100755 --- a/every-jap-dict/package.json +++ b/every-jap-dict/package.json @@ -12,7 +12,7 @@ "@expo/html-elements": "^0.10.1", "@gluestack-ui/core": "^3.0.0", "@gluestack-ui/utils": "^3.0.0", - "@legendapp/motion": "^2.3.0", + "@legendapp/motion": "^2.4.0", "@react-native-async-storage/async-storage": "^2.2.0", "babel-plugin-module-resolver": "^5.0.2", "expo": "~53.0.22", @@ -25,7 +25,7 @@ "react-dom": "^19.1.1", "react-native": "0.79.6", "react-native-reanimated": "^4.1.0", - "react-native-safe-area-context": "^4.11.0", + "react-native-safe-area-context": "^5.6.1", "react-native-svg": "^15.2.0", "react-native-webview": "^13.16.0", "react-native-worklets": "^0.5.0",