Peace 1 week ago
parent f78b3f2c16
commit 256b222d69
  1. 59
      every-jap-dict/App.tsx
  2. 19
      every-jap-dict/components/ui/box/index.tsx
  3. 19
      every-jap-dict/components/ui/box/index.web.tsx
  4. 10
      every-jap-dict/components/ui/box/styles.tsx
  5. 39
      every-jap-dict/components/ui/pressable/index.tsx
  6. 240
      every-jap-dict/components/ui/toast/index.tsx
  7. 18
      every-jap-dict/package-lock.json
  8. 4
      every-jap-dict/package.json

@ -1,6 +1,6 @@
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { FlatList, Modal, Pressable, StyleSheet, View } from 'react-native'; import { FlatList, Keyboard, Modal, Pressable, StyleSheet, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'; import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
import '@/global.css'; import '@/global.css';
@ -13,6 +13,7 @@ import { HStack } from './components/ui/hstack';
import { Button, ButtonText } from './components/ui/button'; import { Button, ButtonText } from './components/ui/button';
import DictWebView from './components/DictWebView'; import DictWebView from './components/DictWebView';
import AdBannerPlaceholder from './components/AdBannerPlaceholder'; import AdBannerPlaceholder from './components/AdBannerPlaceholder';
import { Toast, ToastDescription, ToastTitle, useToast } from './components/ui/toast';
function MainScreen() { function MainScreen() {
const [q, setQ] = useState(''); const [q, setQ] = useState('');
@ -20,11 +21,15 @@ function MainScreen() {
const [activeId, setActiveId] = useState<string>(''); const [activeId, setActiveId] = useState<string>('');
const [wordbookOpen, setWordbookOpen] = useState(false); const [wordbookOpen, setWordbookOpen] = useState(false);
const insets = useSafeAreaInsets();
const pack = jaPack; const pack = jaPack;
const providers = pack.providers; const providers = pack.providers;
const canSearch = q.trim().length > 0; const canSearch = q.trim().length > 0;
const hasQuery = query.trim().length > 0; const hasQuery = query.trim().length > 0;
const toast = useToast();
const { add, items, remove, clear, loading } = useWordbook(); const { add, items, remove, clear, loading } = useWordbook();
const urls = useMemo( const urls = useMemo(
@ -38,13 +43,31 @@ function MainScreen() {
const onSearch = () => { const onSearch = () => {
if (!canSearch) return; if (!canSearch) return;
Keyboard.dismiss();
setQuery(q.trim()); setQuery(q.trim());
setActiveId(providers[0]?.id ?? ''); setActiveId(providers[0]?.id ?? '');
}; };
const onSave = () => { const onSave = () => {
if (!canSearch) return; if (!canSearch) return;
Keyboard.dismiss();
add(q.trim(), pack.code); add(q.trim(), pack.code);
toast.show({
placement: 'top',
duration: 2000,
render: () => (
<View style={{ marginTop: insets.top + 12, alignItems: 'center', width: '100%' }}>
<Toast
className="rounded-full opacity-80 mx-5 py-2 flex-row items-center gap-2 shadow-lg max-w-[90%]"
variant="solid"
>
<ToastTitle></ToastTitle>
<ToastDescription> </ToastDescription>
</Toast>
</View>
),
});
}; };
const onPickWord = (word: string) => { const onPickWord = (word: string) => {
@ -61,10 +84,21 @@ function MainScreen() {
<View className="p-4 gap-3"> <View className="p-4 gap-3">
{/* 액션 버튼 */} {/* 액션 버튼 */}
<HStack space="md" className="flex-row"> <HStack space="md" className="flex-row">
<Button className="flex-1" onPress={() => setWordbookOpen(true)} action="secondary"> <Button
className="flex-1"
onPress={() => setWordbookOpen(true)}
variant="outline"
action="secondary"
>
<ButtonText> </ButtonText> <ButtonText> </ButtonText>
</Button> </Button>
<Button className="flex-1" onPress={onSave} action="secondary" isDisabled={!canSearch}> <Button
className="flex-1"
onPress={onSave}
variant="outline"
action="secondary"
isDisabled={!canSearch}
>
<ButtonText> </ButtonText> <ButtonText> </ButtonText>
</Button> </Button>
</HStack> </HStack>
@ -77,6 +111,7 @@ function MainScreen() {
value={q} value={q}
onChangeText={setQ} onChangeText={setQ}
returnKeyType="search" returnKeyType="search"
submitBehavior="blurAndSubmit"
onSubmitEditing={onSearch} onSubmitEditing={onSearch}
/> />
</Input> </Input>
@ -108,7 +143,7 @@ function MainScreen() {
{/* 패널: WebView 모두 마운트 후 토글 */} {/* 패널: WebView 모두 마운트 후 토글 */}
<View className="flex-1"> <View className="flex-1">
{!canSearch ? ( {!hasQuery ? (
<View className="flex-1 items-center justify-center"> <View className="flex-1 items-center justify-center">
<Text> [] </Text> <Text> [] </Text>
</View> </View>
@ -175,12 +210,14 @@ function MainScreen() {
export default function App() { export default function App() {
return ( return (
<GluestackUIProvider mode="light"> <GluestackUIProvider mode="light">
<SafeAreaView style={{ flex: 1 }}> <SafeAreaProvider>
<StatusBar style="auto" /> <SafeAreaView style={{ flex: 1 }}>
<WordbookProvider> <StatusBar style="auto" />
<MainScreen /> <WordbookProvider>
</WordbookProvider> <MainScreen />
</SafeAreaView> </WordbookProvider>
</SafeAreaView>
</SafeAreaProvider>
</GluestackUIProvider> </GluestackUIProvider>
); );
} }

@ -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<typeof boxStyle> & { className?: string };
const Box = React.forwardRef<React.ComponentRef<typeof View>, IBoxProps>(
function Box({ className, ...props }, ref) {
return (
<View ref={ref} {...props} className={boxStyle({ class: className })} />
);
}
);
Box.displayName = 'Box';
export { Box };

@ -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<typeof boxStyle> & { className?: string };
const Box = React.forwardRef<HTMLDivElement, IBoxProps>(function Box(
{ className, ...props },
ref
) {
return (
<div ref={ref} className={boxStyle({ class: className })} {...props} />
);
});
Box.displayName = 'Box';
export { Box };

@ -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,
});

@ -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<typeof UIPressable>,
'context'
> &
VariantProps<typeof pressableStyle>;
const Pressable = React.forwardRef<
React.ComponentRef<typeof UIPressable>,
IPressableProps
>(function Pressable({ className, ...props }, ref) {
return (
<UIPressable
{...props}
ref={ref}
className={pressableStyle({
class: className,
})}
/>
);
});
Pressable.displayName = 'Pressable';
export { Pressable };

@ -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<typeof View> &
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
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<typeof Root> & {
className?: string;
} & VariantProps<typeof toastStyle>;
const Toast = React.forwardRef<React.ComponentRef<typeof Root>, IToastProps>(
function Toast(
{ className, variant = 'solid', action = 'muted', ...props },
ref
) {
return (
<Root
ref={ref}
className={toastStyle({ variant, action, class: className })}
context={{ variant, action }}
{...props}
/>
);
}
);
type IToastTitleProps = React.ComponentProps<typeof Text> & {
className?: string;
} & VariantProps<typeof toastTitleStyle>;
const ToastTitle = React.forwardRef<
React.ComponentRef<typeof Text>,
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 (
<Text
{...props}
ref={ref}
aria-live="assertive"
aria-atomic="true"
role="alert"
className={toastTitleStyle({
size,
class: className,
parentVariants: {
variant: parentVariant,
action: parentAction,
},
})}
>
{children}
</Text>
);
});
type IToastDescriptionProps = React.ComponentProps<typeof Text> & {
className?: string;
} & VariantProps<typeof toastDescriptionStyle>;
const ToastDescription = React.forwardRef<
React.ComponentRef<typeof Text>,
IToastDescriptionProps
>(function ToastDescription({ className, size = 'md', ...props }, ref) {
const { variant: parentVariant } = useStyleContext(SCOPE);
return (
<Text
ref={ref}
{...props}
className={toastDescriptionStyle({
size,
class: className,
parentVariants: {
variant: parentVariant,
},
})}
/>
);
});
Toast.displayName = 'Toast';
ToastTitle.displayName = 'ToastTitle';
ToastDescription.displayName = 'ToastDescription';
export { useToast, Toast, ToastTitle, ToastDescription };

@ -11,7 +11,7 @@
"@expo/html-elements": "^0.10.1", "@expo/html-elements": "^0.10.1",
"@gluestack-ui/core": "^3.0.0", "@gluestack-ui/core": "^3.0.0",
"@gluestack-ui/utils": "^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", "@react-native-async-storage/async-storage": "^2.2.0",
"babel-plugin-module-resolver": "^5.0.2", "babel-plugin-module-resolver": "^5.0.2",
"expo": "~53.0.22", "expo": "~53.0.22",
@ -24,7 +24,7 @@
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-native": "0.79.6", "react-native": "0.79.6",
"react-native-reanimated": "^4.1.0", "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-svg": "^15.2.0",
"react-native-webview": "^13.16.0", "react-native-webview": "^13.16.0",
"react-native-worklets": "^0.5.0", "react-native-worklets": "^0.5.0",
@ -2525,15 +2525,15 @@
} }
}, },
"node_modules/@legendapp/motion": { "node_modules/@legendapp/motion": {
"version": "2.3.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@legendapp/motion/-/motion-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@legendapp/motion/-/motion-2.4.0.tgz",
"integrity": "sha512-LtTD06eyz/Ge23FAR6BY+i9Gsgr/ZgxE12FneML8LrZGcZOSPN2Ojz3N2eJaTiA50kqoeqrGCaYJja8KgKpL6Q==", "integrity": "sha512-AAYpRLGvxGD5hIGl9sVHyoUufr66zoH82PuxYcKiPSMdCBI3jwZFWh6CuHjV1leRKVIRk2py1rSvIVabG8eqcw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@legendapp/tools": "2.0.1" "@legendapp/tools": "2.0.1"
}, },
"peerDependencies": { "peerDependencies": {
"nativewind": "^2.0.0", "nativewind": "*",
"react": ">=16", "react": ">=16",
"react-native": "*" "react-native": "*"
} }
@ -11866,9 +11866,9 @@
} }
}, },
"node_modules/react-native-safe-area-context": { "node_modules/react-native-safe-area-context": {
"version": "4.11.0", "version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.11.0.tgz", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz",
"integrity": "sha512-Bg7bozxEB+ZS+H3tVYs5yY1cvxNXgR6nRQwpSMkYR9IN5CbxohLnSprrOPG/ostTCd4F6iCk0c51pExEhifSKQ==", "integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",

@ -12,7 +12,7 @@
"@expo/html-elements": "^0.10.1", "@expo/html-elements": "^0.10.1",
"@gluestack-ui/core": "^3.0.0", "@gluestack-ui/core": "^3.0.0",
"@gluestack-ui/utils": "^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", "@react-native-async-storage/async-storage": "^2.2.0",
"babel-plugin-module-resolver": "^5.0.2", "babel-plugin-module-resolver": "^5.0.2",
"expo": "~53.0.22", "expo": "~53.0.22",
@ -25,7 +25,7 @@
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-native": "0.79.6", "react-native": "0.79.6",
"react-native-reanimated": "^4.1.0", "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-svg": "^15.2.0",
"react-native-webview": "^13.16.0", "react-native-webview": "^13.16.0",
"react-native-worklets": "^0.5.0", "react-native-worklets": "^0.5.0",

Loading…
Cancel
Save