remove gluestackk-ui
This commit is contained in:
409
MODAL_USAGE.md
Normal file
409
MODAL_USAGE.md
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# Modal Component
|
||||||
|
|
||||||
|
Modal component tương tự như Modal của Ant Design, được tạo cho React Native/Expo.
|
||||||
|
|
||||||
|
## Cài đặt
|
||||||
|
|
||||||
|
Component này sử dụng `@expo/vector-icons` cho các icon. Đảm bảo bạn đã cài đặt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx expo install @expo/vector-icons
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Modal from "@/components/ui/modal";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Các tính năng chính
|
||||||
|
|
||||||
|
### 1. Basic Modal
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { View, Text, Button } from "react-native";
|
||||||
|
import Modal from "@/components/ui/modal";
|
||||||
|
|
||||||
|
export default function BasicExample() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Button title="Open Modal" onPress={() => setOpen(true)} />
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Basic Modal"
|
||||||
|
onOk={() => {
|
||||||
|
console.log("OK clicked");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>Some contents...</Text>
|
||||||
|
<Text>Some contents...</Text>
|
||||||
|
<Text>Some contents...</Text>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Async Close (với confirmLoading)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { View, Text, Button } from "react-native";
|
||||||
|
import Modal from "@/components/ui/modal";
|
||||||
|
|
||||||
|
export default function AsyncExample() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
// Giả lập API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
setLoading(false);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Button title="Open Modal" onPress={() => setOpen(true)} />
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Async Modal"
|
||||||
|
confirmLoading={loading}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>Modal will be closed after 2 seconds when you click OK.</Text>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Customized Footer
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { View, Text, Button, TouchableOpacity, StyleSheet } from "react-native";
|
||||||
|
import Modal from "@/components/ui/modal";
|
||||||
|
|
||||||
|
export default function CustomFooterExample() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Button title="Open Modal" onPress={() => setOpen(true)} />
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Custom Footer Modal"
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
footer={
|
||||||
|
<View style={styles.customFooter}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.customButton}
|
||||||
|
onPress={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>Return</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.customButton, styles.submitButton]}
|
||||||
|
onPress={() => {
|
||||||
|
console.log("Submit");
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[styles.buttonText, styles.submitText]}>Submit</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text>Custom footer buttons</Text>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
customFooter: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
customButton: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 6,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#d9d9d9",
|
||||||
|
},
|
||||||
|
submitButton: {
|
||||||
|
backgroundColor: "#1890ff",
|
||||||
|
borderColor: "#1890ff",
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#000",
|
||||||
|
},
|
||||||
|
submitText: {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. No Footer
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Modal Without Footer"
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>Modal content without footer buttons</Text>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Centered Modal
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Centered Modal"
|
||||||
|
centered
|
||||||
|
onOk={() => setOpen(false)}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>This modal is centered on the screen</Text>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Custom Width
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title="Custom Width Modal"
|
||||||
|
width={700}
|
||||||
|
onOk={() => setOpen(false)}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Text>This modal has custom width</Text>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Confirm Modal với useModal Hook
|
||||||
|
|
||||||
|
**Đây là cách khuyến nghị sử dụng trong React Native để có context đầy đủ:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React from "react";
|
||||||
|
import { View, Button } from "react-native";
|
||||||
|
import Modal from "@/components/ui/modal";
|
||||||
|
|
||||||
|
export default function HookExample() {
|
||||||
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
|
|
||||||
|
const showConfirm = () => {
|
||||||
|
modal.confirm({
|
||||||
|
title: "Do you want to delete these items?",
|
||||||
|
content:
|
||||||
|
"When clicked the OK button, this dialog will be closed after 1 second",
|
||||||
|
onOk: async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
console.log("OK");
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
console.log("Cancel");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showInfo = () => {
|
||||||
|
modal.info({
|
||||||
|
title: "This is a notification message",
|
||||||
|
content: "Some additional information...",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSuccess = () => {
|
||||||
|
modal.success({
|
||||||
|
title: "Success",
|
||||||
|
content: "Operation completed successfully!",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showError = () => {
|
||||||
|
modal.error({
|
||||||
|
title: "Error",
|
||||||
|
content: "Something went wrong!",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showWarning = () => {
|
||||||
|
modal.warning({
|
||||||
|
title: "Warning",
|
||||||
|
content: "This is a warning message!",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ padding: 20, gap: 10 }}>
|
||||||
|
{/* contextHolder phải được đặt trong component */}
|
||||||
|
{contextHolder}
|
||||||
|
|
||||||
|
<Button title="Confirm" onPress={showConfirm} />
|
||||||
|
<Button title="Info" onPress={showInfo} />
|
||||||
|
<Button title="Success" onPress={showSuccess} />
|
||||||
|
<Button title="Error" onPress={showError} />
|
||||||
|
<Button title="Warning" onPress={showWarning} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Update Modal Instance
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React from "react";
|
||||||
|
import { View, Button } from "react-native";
|
||||||
|
import Modal from "@/components/ui/modal";
|
||||||
|
|
||||||
|
export default function UpdateExample() {
|
||||||
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
|
|
||||||
|
const showModal = () => {
|
||||||
|
const instance = modal.success({
|
||||||
|
title: "Loading...",
|
||||||
|
content: "Please wait...",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
instance.update({
|
||||||
|
title: "Success!",
|
||||||
|
content: "Operation completed successfully!",
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// Close after 4 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
instance.destroy();
|
||||||
|
}, 4000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{contextHolder}
|
||||||
|
<Button title="Show Updating Modal" onPress={showModal} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Modal Props
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
| --------------- | ----------------------------- | --------- | ------------------------------------------------------------ |
|
||||||
|
| open | boolean | false | Whether the modal dialog is visible or not |
|
||||||
|
| title | ReactNode | - | The modal dialog's title |
|
||||||
|
| closable | boolean | true | Whether a close (x) button is visible on top right or not |
|
||||||
|
| closeIcon | ReactNode | - | Custom close icon |
|
||||||
|
| maskClosable | boolean | true | Whether to close the modal dialog when the mask is clicked |
|
||||||
|
| centered | boolean | false | Centered Modal |
|
||||||
|
| width | number \| string | 520 | Width of the modal dialog |
|
||||||
|
| confirmLoading | boolean | false | Whether to apply loading visual effect for OK button |
|
||||||
|
| okText | string | 'OK' | Text of the OK button |
|
||||||
|
| cancelText | string | 'Cancel' | Text of the Cancel button |
|
||||||
|
| okType | 'primary' \| 'default' | 'primary' | Button type of the OK button |
|
||||||
|
| footer | ReactNode \| null | - | Footer content, set as footer={null} to hide default buttons |
|
||||||
|
| mask | boolean | true | Whether show mask or not |
|
||||||
|
| zIndex | number | 1000 | The z-index of the Modal |
|
||||||
|
| onOk | (e?) => void \| Promise<void> | - | Callback when clicking OK button |
|
||||||
|
| onCancel | (e?) => void | - | Callback when clicking cancel button or close icon |
|
||||||
|
| afterOpenChange | (open: boolean) => void | - | Callback when animation ends |
|
||||||
|
| afterClose | () => void | - | Callback when modal is closed completely |
|
||||||
|
| destroyOnClose | boolean | false | Whether to unmount child components on close |
|
||||||
|
| keyboard | boolean | true | Whether support press back button to close (Android) |
|
||||||
|
|
||||||
|
### Modal.useModal()
|
||||||
|
|
||||||
|
Khi bạn cần sử dụng Context, bạn có thể dùng `Modal.useModal()` để tạo `contextHolder` và chèn vào children. Modal được tạo bởi hooks sẽ có tất cả context nơi `contextHolder` được đặt.
|
||||||
|
|
||||||
|
**Returns:** `[modalMethods, contextHolder]`
|
||||||
|
|
||||||
|
- `modalMethods`: Object chứa các methods
|
||||||
|
|
||||||
|
- `info(config)`: Show info modal
|
||||||
|
- `success(config)`: Show success modal
|
||||||
|
- `error(config)`: Show error modal
|
||||||
|
- `warning(config)`: Show warning modal
|
||||||
|
- `confirm(config)`: Show confirm modal
|
||||||
|
|
||||||
|
- `contextHolder`: React element cần được render trong component tree
|
||||||
|
|
||||||
|
### Modal Methods Config
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
| ---------- | -------------------------------------------------------- | --------- | ----------------------------- |
|
||||||
|
| type | 'info' \| 'success' \| 'error' \| 'warning' \| 'confirm' | 'confirm' | Type of the modal |
|
||||||
|
| title | ReactNode | - | Title |
|
||||||
|
| content | ReactNode | - | Content |
|
||||||
|
| icon | ReactNode | - | Custom icon |
|
||||||
|
| okText | string | 'OK' | Text of the OK button |
|
||||||
|
| cancelText | string | 'Cancel' | Text of the Cancel button |
|
||||||
|
| onOk | (e?) => void \| Promise<void> | - | Callback when clicking OK |
|
||||||
|
| onCancel | (e?) => void | - | Callback when clicking Cancel |
|
||||||
|
|
||||||
|
### Modal Instance
|
||||||
|
|
||||||
|
Modal instance được trả về bởi `Modal.useModal()`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface ModalInstance {
|
||||||
|
destroy: () => void;
|
||||||
|
update: (config: ConfirmModalProps) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lưu ý
|
||||||
|
|
||||||
|
1. **React Native Limitations**: Các static methods như `Modal.info()`, `Modal.confirm()` gọi trực tiếp (không qua hook) không được hỗ trợ đầy đủ trong React Native do không thể render imperatively. Hãy sử dụng `Modal.useModal()` hook thay thế.
|
||||||
|
|
||||||
|
2. **Context Support**: Khi cần sử dụng Context (như Redux, Theme Context), bắt buộc phải dùng `Modal.useModal()` hook và đặt `contextHolder` trong component tree.
|
||||||
|
|
||||||
|
3. **Animation**: Modal sử dụng React Native's built-in Modal với `animationType="fade"`.
|
||||||
|
|
||||||
|
4. **Icons**: Component sử dụng `@expo/vector-icons` (Ionicons). Đảm bảo đã cài đặt package này.
|
||||||
|
|
||||||
|
5. **Keyboard**: Prop `keyboard` trong React Native chỉ hoạt động với nút back của Android (không có ESC key như web).
|
||||||
|
|
||||||
|
## So sánh với Ant Design Modal
|
||||||
|
|
||||||
|
| Feature | Ant Design (Web) | This Component (RN) |
|
||||||
|
| --------------------------- | ---------------- | --------------------------- |
|
||||||
|
| Basic Modal | ✅ | ✅ |
|
||||||
|
| Centered | ✅ | ✅ |
|
||||||
|
| Custom Footer | ✅ | ✅ |
|
||||||
|
| Confirm Dialog | ✅ | ✅ (via useModal) |
|
||||||
|
| Info/Success/Error/Warning | ✅ | ✅ (via useModal) |
|
||||||
|
| Async close | ✅ | ✅ |
|
||||||
|
| Custom width | ✅ | ✅ |
|
||||||
|
| Mask closable | ✅ | ✅ |
|
||||||
|
| Keyboard close | ✅ (ESC) | ✅ (Back button on Android) |
|
||||||
|
| Static methods without hook | ✅ | ⚠️ (Limited support) |
|
||||||
|
| useModal hook | ✅ | ✅ (Recommended) |
|
||||||
|
| Draggable | ✅ | ❌ (Not applicable) |
|
||||||
|
| destroyAll() | ✅ | ❌ |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
32
README.md
32
README.md
@@ -48,3 +48,35 @@ Join our community of developers creating universal apps.
|
|||||||
|
|
||||||
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
||||||
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
||||||
|
|
||||||
|
## Build app
|
||||||
|
|
||||||
|
- Add eas.json file to root folder and add this:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"version": ">= 16.27.0",
|
||||||
|
"appVersionSource": "remote"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"development": {
|
||||||
|
"developmentClient": true,
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"android": {
|
||||||
|
"buildType": "apk"
|
||||||
|
},
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"autoIncrement": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|||||||
8
app.json
8
app.json
@@ -12,8 +12,6 @@
|
|||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"CFBundleLocalizations": [
|
"CFBundleLocalizations": [
|
||||||
"en",
|
|
||||||
"vi",
|
|
||||||
"en",
|
"en",
|
||||||
"vi"
|
"vi"
|
||||||
]
|
]
|
||||||
@@ -80,6 +78,12 @@
|
|||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true,
|
"typedRoutes": true,
|
||||||
"reactCompiler": true
|
"reactCompiler": true
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"router": {},
|
||||||
|
"eas": {
|
||||||
|
"projectId": "d4ef1318-5427-4c96-ad88-97ec117829cc"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DarkTheme,
|
DarkTheme,
|
||||||
DefaultTheme,
|
DefaultTheme,
|
||||||
ThemeProvider as NavigationThemeProvider,
|
ThemeProvider,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { Stack, useRouter } from "expo-router";
|
import { Stack, useRouter } from "expo-router";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
@@ -9,65 +9,64 @@ import { useEffect } from "react";
|
|||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
// import Toast from "react-native-toast-message";
|
// import Toast from "react-native-toast-message";
|
||||||
|
|
||||||
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider/gluestack-ui-provider";
|
// import { toastConfig } from "@/config";
|
||||||
import { toastConfig } from "@/config";
|
import { toastConfig } from "@/config";
|
||||||
import { setRouterInstance } from "@/config/auth";
|
import { setRouterInstance } from "@/config/auth";
|
||||||
import "@/global.css";
|
import "@/global.css";
|
||||||
import { ThemeProvider, useThemeContext } from "@/hooks/use-theme-context";
|
|
||||||
import { I18nProvider } from "@/hooks/use-i18n";
|
import { I18nProvider } from "@/hooks/use-i18n";
|
||||||
|
import { ThemeProvider as AppThemeProvider } from "@/hooks/use-theme-context";
|
||||||
|
import { useColorScheme } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import "../global.css";
|
import "../global.css";
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const { colorScheme } = useThemeContext();
|
// const { colorScheme } = useThemeContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
console.log("Color Scheme: ", colorScheme);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRouterInstance(router);
|
setRouterInstance(router);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GluestackUIProvider mode={colorScheme}>
|
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
||||||
<NavigationThemeProvider
|
<Stack
|
||||||
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
|
screenOptions={{ headerShown: false }}
|
||||||
|
initialRouteName="auth/login"
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack.Screen
|
||||||
screenOptions={{ headerShown: false }}
|
name="auth/login"
|
||||||
initialRouteName="auth/login"
|
options={{
|
||||||
>
|
title: "Login",
|
||||||
<Stack.Screen
|
headerShown: false,
|
||||||
name="auth/login"
|
}}
|
||||||
options={{
|
/>
|
||||||
title: "Login",
|
|
||||||
headerShown: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack.Screen
|
{/* <Stack.Screen
|
||||||
name="(tabs)"
|
name="(tabs)"
|
||||||
options={{
|
options={{
|
||||||
title: "Home",
|
title: "Home",
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="modal"
|
name="modal"
|
||||||
options={{ presentation: "formSheet", title: "Modal" }}
|
options={{ presentation: "formSheet", title: "Modal" }}
|
||||||
/>
|
/> */}
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
|
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
|
||||||
</NavigationThemeProvider>
|
</ThemeProvider>
|
||||||
</GluestackUIProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ThemeProvider>
|
<AppThemeProvider>
|
||||||
<AppContent />
|
<AppContent />
|
||||||
</ThemeProvider>
|
</AppThemeProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ import { ThemedText } from "@/components/themed-text";
|
|||||||
import { ThemedView } from "@/components/themed-view";
|
import { ThemedView } from "@/components/themed-view";
|
||||||
import SliceSwitch from "@/components/ui/slice-switch";
|
import SliceSwitch from "@/components/ui/slice-switch";
|
||||||
import { DOMAIN, TOKEN } from "@/constants";
|
import { DOMAIN, TOKEN } from "@/constants";
|
||||||
|
import { Colors } from "@/constants/theme";
|
||||||
import { login } from "@/controller/AuthController";
|
import { login } from "@/controller/AuthController";
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
import {
|
||||||
|
ColorScheme as ThemeColorScheme,
|
||||||
|
useTheme,
|
||||||
|
} from "@/hooks/use-theme-context";
|
||||||
import { showErrorToast, showWarningToast } from "@/services/toast_service";
|
import { showErrorToast, showWarningToast } from "@/services/toast_service";
|
||||||
import {
|
import {
|
||||||
getStorageItem,
|
getStorageItem,
|
||||||
@@ -17,7 +22,7 @@ import {
|
|||||||
import { parseJwtToken } from "@/utils/token";
|
import { parseJwtToken } from "@/utils/token";
|
||||||
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Image,
|
Image,
|
||||||
@@ -38,6 +43,13 @@ export default function LoginScreen() {
|
|||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
|
const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
|
||||||
const { t, setLocale, locale } = useI18n();
|
const { t, setLocale, locale } = useI18n();
|
||||||
|
const { colors, colorScheme } = useTheme();
|
||||||
|
const styles = useMemo(
|
||||||
|
() => createStyles(colors, colorScheme),
|
||||||
|
[colors, colorScheme]
|
||||||
|
);
|
||||||
|
const placeholderColor = colors.textSecondary;
|
||||||
|
const buttonTextColor = colorScheme === "dark" ? colors.text : colors.surface;
|
||||||
const [isVNLang, setIsVNLang] = useState(false);
|
const [isVNLang, setIsVNLang] = useState(false);
|
||||||
|
|
||||||
const checkLogin = useCallback(async () => {
|
const checkLogin = useCallback(async () => {
|
||||||
@@ -154,7 +166,10 @@ export default function LoginScreen() {
|
|||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
<ScrollView
|
||||||
|
style={{ flex: 1, backgroundColor: colors.background }}
|
||||||
|
contentContainerStyle={styles.scrollContainer}
|
||||||
|
>
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.headerContainer}>
|
<View style={styles.headerContainer}>
|
||||||
@@ -177,7 +192,7 @@ export default function LoginScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder={t("auth.username_placeholder")}
|
placeholder={t("auth.username_placeholder")}
|
||||||
placeholderTextColor="#999"
|
placeholderTextColor={placeholderColor}
|
||||||
value={username}
|
value={username}
|
||||||
onChangeText={setUsername}
|
onChangeText={setUsername}
|
||||||
editable={!loading}
|
editable={!loading}
|
||||||
@@ -192,7 +207,7 @@ export default function LoginScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder={t("auth.password_placeholder")}
|
placeholder={t("auth.password_placeholder")}
|
||||||
placeholderTextColor="#999"
|
placeholderTextColor={placeholderColor}
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
secureTextEntry={!showPassword}
|
secureTextEntry={!showPassword}
|
||||||
@@ -217,7 +232,7 @@ export default function LoginScreen() {
|
|||||||
<Ionicons
|
<Ionicons
|
||||||
name={showPassword ? "eye-off" : "eye"}
|
name={showPassword ? "eye-off" : "eye"}
|
||||||
size={22}
|
size={22}
|
||||||
color="#666"
|
color={colors.icon}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -241,9 +256,13 @@ export default function LoginScreen() {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator color="#fff" size="small" />
|
<ActivityIndicator color={buttonTextColor} size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.loginButtonText}>{t("auth.login")}</Text>
|
<Text
|
||||||
|
style={[styles.loginButtonText, { color: buttonTextColor }]}
|
||||||
|
>
|
||||||
|
{t("auth.login")}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -252,10 +271,10 @@ export default function LoginScreen() {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
borderColor: "#ddd",
|
borderColor: colors.border,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
backgroundColor: "transparent",
|
backgroundColor: colors.surface,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
@@ -265,7 +284,7 @@ export default function LoginScreen() {
|
|||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="qr-code-scanner"
|
name="qr-code-scanner"
|
||||||
size={28}
|
size={28}
|
||||||
color="#007AFF"
|
color={colors.primary}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -282,13 +301,17 @@ export default function LoginScreen() {
|
|||||||
<SliceSwitch
|
<SliceSwitch
|
||||||
size="sm"
|
size="sm"
|
||||||
leftIcon="moon"
|
leftIcon="moon"
|
||||||
leftIconColor="white"
|
leftIconColor={
|
||||||
|
colorScheme === "dark" ? colors.background : colors.surface
|
||||||
|
}
|
||||||
rightIcon="sunny"
|
rightIcon="sunny"
|
||||||
rightIconColor="orange"
|
rightIconColor={
|
||||||
activeBackgroundColor="black"
|
colorScheme === "dark" ? colors.warning : "orange"
|
||||||
inactiveBackgroundColor="white"
|
}
|
||||||
inactiveOverlayColor="black"
|
activeBackgroundColor={colors.text}
|
||||||
activeOverlayColor="white"
|
inactiveBackgroundColor={colors.surface}
|
||||||
|
inactiveOverlayColor={colors.textSecondary}
|
||||||
|
activeOverlayColor={colors.background}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -310,129 +333,130 @@ export default function LoginScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const createStyles = (colors: typeof Colors.light, scheme: ThemeColorScheme) =>
|
||||||
scrollContainer: {
|
StyleSheet.create({
|
||||||
flexGrow: 1,
|
scrollContainer: {
|
||||||
justifyContent: "center",
|
flexGrow: 1,
|
||||||
},
|
justifyContent: "center",
|
||||||
container: {
|
backgroundColor: colors.background,
|
||||||
flex: 1,
|
},
|
||||||
paddingHorizontal: 20,
|
container: {
|
||||||
justifyContent: "center",
|
flex: 1,
|
||||||
},
|
paddingHorizontal: 20,
|
||||||
headerContainer: {
|
justifyContent: "center",
|
||||||
marginBottom: 40,
|
},
|
||||||
alignItems: "center",
|
headerContainer: {
|
||||||
},
|
marginBottom: 40,
|
||||||
logo: {
|
alignItems: "center",
|
||||||
width: 120,
|
},
|
||||||
height: 120,
|
logo: {
|
||||||
marginBottom: 20,
|
width: 120,
|
||||||
},
|
height: 120,
|
||||||
title: {
|
marginBottom: 20,
|
||||||
fontSize: 28,
|
},
|
||||||
fontWeight: "bold",
|
title: {
|
||||||
marginBottom: 8,
|
fontSize: 28,
|
||||||
},
|
fontWeight: "bold",
|
||||||
subtitle: {
|
marginBottom: 8,
|
||||||
fontSize: 16,
|
},
|
||||||
opacity: 0.7,
|
subtitle: {
|
||||||
},
|
fontSize: 16,
|
||||||
formContainer: {
|
opacity: 0.7,
|
||||||
gap: 16,
|
},
|
||||||
},
|
formContainer: {
|
||||||
inputGroup: {
|
gap: 16,
|
||||||
gap: 8,
|
},
|
||||||
},
|
inputGroup: {
|
||||||
label: {
|
gap: 8,
|
||||||
fontSize: 14,
|
},
|
||||||
fontWeight: "600",
|
label: {
|
||||||
},
|
fontSize: 14,
|
||||||
input: {
|
fontWeight: "600",
|
||||||
borderWidth: 1,
|
},
|
||||||
borderColor: "#ddd",
|
input: {
|
||||||
borderRadius: 8,
|
borderWidth: 1,
|
||||||
paddingHorizontal: 12,
|
borderColor: colors.border,
|
||||||
paddingVertical: 12,
|
borderRadius: 8,
|
||||||
fontSize: 16,
|
paddingHorizontal: 12,
|
||||||
backgroundColor: "#f5f5f5",
|
paddingVertical: 12,
|
||||||
color: "#000",
|
fontSize: 16,
|
||||||
},
|
backgroundColor: colors.surface,
|
||||||
loginButton: {
|
color: colors.text,
|
||||||
backgroundColor: "#007AFF",
|
},
|
||||||
paddingVertical: 14,
|
loginButton: {
|
||||||
borderRadius: 8,
|
backgroundColor: colors.primary,
|
||||||
alignItems: "center",
|
paddingVertical: 14,
|
||||||
marginTop: 16,
|
borderRadius: 8,
|
||||||
},
|
alignItems: "center",
|
||||||
loginButtonDisabled: {
|
marginTop: 16,
|
||||||
opacity: 0.6,
|
},
|
||||||
},
|
loginButtonDisabled: {
|
||||||
loginButtonText: {
|
opacity: 0.6,
|
||||||
color: "#fff",
|
},
|
||||||
fontSize: 16,
|
loginButtonText: {
|
||||||
fontWeight: "600",
|
fontSize: 16,
|
||||||
},
|
fontWeight: "600",
|
||||||
footerContainer: {
|
},
|
||||||
marginTop: 16,
|
footerContainer: {
|
||||||
alignItems: "center",
|
marginTop: 16,
|
||||||
},
|
alignItems: "center",
|
||||||
footerText: {
|
},
|
||||||
fontSize: 14,
|
footerText: {
|
||||||
},
|
fontSize: 14,
|
||||||
linkText: {
|
},
|
||||||
color: "#007AFF",
|
linkText: {
|
||||||
fontWeight: "600",
|
color: colors.primary,
|
||||||
},
|
fontWeight: "600",
|
||||||
copyrightContainer: {
|
},
|
||||||
marginTop: 20,
|
copyrightContainer: {
|
||||||
alignItems: "center",
|
marginTop: 20,
|
||||||
},
|
alignItems: "center",
|
||||||
copyrightText: {
|
},
|
||||||
fontSize: 12,
|
copyrightText: {
|
||||||
opacity: 0.6,
|
fontSize: 12,
|
||||||
textAlign: "center",
|
opacity: 0.6,
|
||||||
},
|
textAlign: "center",
|
||||||
languageSwitcherContainer: {
|
color: colors.textSecondary,
|
||||||
display: "flex",
|
},
|
||||||
flexDirection: "row",
|
languageSwitcherContainer: {
|
||||||
justifyContent: "center",
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
marginTop: 24,
|
alignItems: "center",
|
||||||
gap: 20,
|
marginTop: 24,
|
||||||
},
|
gap: 20,
|
||||||
languageSwitcherLabel: {
|
},
|
||||||
fontSize: 12,
|
languageSwitcherLabel: {
|
||||||
fontWeight: "600",
|
fontSize: 12,
|
||||||
textAlign: "center",
|
fontWeight: "600",
|
||||||
opacity: 0.8,
|
textAlign: "center",
|
||||||
},
|
opacity: 0.8,
|
||||||
languageButtonsContainer: {
|
},
|
||||||
flexDirection: "row",
|
languageButtonsContainer: {
|
||||||
gap: 10,
|
flexDirection: "row",
|
||||||
},
|
gap: 10,
|
||||||
languageButton: {
|
},
|
||||||
flex: 1,
|
languageButton: {
|
||||||
paddingVertical: 10,
|
flex: 1,
|
||||||
paddingHorizontal: 12,
|
paddingVertical: 10,
|
||||||
borderWidth: 1,
|
paddingHorizontal: 12,
|
||||||
borderColor: "#ddd",
|
borderWidth: 1,
|
||||||
borderRadius: 8,
|
borderColor: colors.border,
|
||||||
alignItems: "center",
|
borderRadius: 8,
|
||||||
backgroundColor: "transparent",
|
alignItems: "center",
|
||||||
},
|
backgroundColor: colors.surface,
|
||||||
languageButtonActive: {
|
},
|
||||||
backgroundColor: "#007AFF",
|
languageButtonActive: {
|
||||||
borderColor: "#007AFF",
|
backgroundColor: colors.primary,
|
||||||
},
|
borderColor: colors.primary,
|
||||||
languageButtonText: {
|
},
|
||||||
fontSize: 14,
|
languageButtonText: {
|
||||||
fontWeight: "500",
|
fontSize: 14,
|
||||||
color: "#666",
|
fontWeight: "500",
|
||||||
},
|
color: colors.textSecondary,
|
||||||
languageButtonTextActive: {
|
},
|
||||||
color: "#fff",
|
languageButtonTextActive: {
|
||||||
fontWeight: "600",
|
color: scheme === "dark" ? colors.text : colors.surface,
|
||||||
},
|
fontWeight: "600",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,24 +8,10 @@ import { showErrorToast } from "@/services/toast_service";
|
|||||||
import { sosMessage } from "@/utils/sosUtils";
|
import { sosMessage } from "@/utils/sosUtils";
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import { StyleSheet, Text, TextInput, View } from "react-native";
|
||||||
FlatList,
|
import IconButton from "../IconButton";
|
||||||
ScrollView,
|
import Select from "../Select";
|
||||||
StyleSheet,
|
import Modal from "../ui/modal";
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { Button, ButtonText } from "../ui/gluestack-ui-provider/button";
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalBackdrop,
|
|
||||||
ModalBody,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
} from "../ui/gluestack-ui-provider/modal";
|
|
||||||
|
|
||||||
const SosButton = () => {
|
const SosButton = () => {
|
||||||
const [sosData, setSosData] = useState<Model.SosResponse | null>();
|
const [sosData, setSosData] = useState<Model.SosResponse | null>();
|
||||||
@@ -34,17 +20,23 @@ const SosButton = () => {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [customMessage, setCustomMessage] = useState("");
|
const [customMessage, setCustomMessage] = useState("");
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const sosOptions = [
|
const sosOptions = [
|
||||||
...sosMessage.map((msg) => ({ ma: msg.ma, moTa: msg.moTa })),
|
...sosMessage.map((msg) => ({
|
||||||
{ ma: 999, moTa: "Khác" },
|
ma: msg.ma,
|
||||||
|
moTa: msg.moTa,
|
||||||
|
label: msg.moTa,
|
||||||
|
value: msg.ma,
|
||||||
|
})),
|
||||||
|
{ ma: 999, moTa: "Khác", label: "Khác", value: 999 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getSosData = async () => {
|
const getSosData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await queryGetSos();
|
const response = await queryGetSos();
|
||||||
|
// console.log("SoS ResponseL: ", response);
|
||||||
|
|
||||||
setSosData(response.data);
|
setSosData(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch SOS data:", error);
|
console.error("Failed to fetch SOS data:", error);
|
||||||
@@ -58,8 +50,6 @@ const SosButton = () => {
|
|||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: { [key: string]: string } = {};
|
const newErrors: { [key: string]: string } = {};
|
||||||
|
|
||||||
// Không cần validate sosMessage vì luôn có default value (11)
|
|
||||||
|
|
||||||
if (selectedSosMessage === 999 && customMessage.trim() === "") {
|
if (selectedSosMessage === 999 && customMessage.trim() === "") {
|
||||||
newErrors.customMessage = t("home.sos.statusRequired");
|
newErrors.customMessage = t("home.sos.statusRequired");
|
||||||
}
|
}
|
||||||
@@ -69,27 +59,34 @@ const SosButton = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmSos = async () => {
|
const handleConfirmSos = async () => {
|
||||||
if (validateForm()) {
|
if (!validateForm()) {
|
||||||
let messageToSend = "";
|
console.log("Form chưa validate");
|
||||||
if (selectedSosMessage === 999) {
|
return; // Không đóng modal nếu validate fail
|
||||||
messageToSend = customMessage.trim();
|
|
||||||
} else {
|
|
||||||
const selectedOption = sosOptions.find(
|
|
||||||
(opt) => opt.ma === selectedSosMessage
|
|
||||||
);
|
|
||||||
messageToSend = selectedOption ? selectedOption.moTa : "";
|
|
||||||
}
|
|
||||||
// Gửi dữ liệu đi
|
|
||||||
setShowConfirmSosDialog(false);
|
|
||||||
// Reset form
|
|
||||||
setSelectedSosMessage(null);
|
|
||||||
setCustomMessage("");
|
|
||||||
setErrors({});
|
|
||||||
await sendSosMessage(messageToSend);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let messageToSend = "";
|
||||||
|
if (selectedSosMessage === 999) {
|
||||||
|
messageToSend = customMessage.trim();
|
||||||
|
} else {
|
||||||
|
const selectedOption = sosOptions.find(
|
||||||
|
(opt) => opt.ma === selectedSosMessage
|
||||||
|
);
|
||||||
|
messageToSend = selectedOption ? selectedOption.moTa : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gửi dữ liệu đi
|
||||||
|
await sendSosMessage(messageToSend);
|
||||||
|
|
||||||
|
// Đóng modal và reset form sau khi gửi thành công
|
||||||
|
setShowConfirmSosDialog(false);
|
||||||
|
setSelectedSosMessage(null);
|
||||||
|
setCustomMessage("");
|
||||||
|
setErrors({});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickButton = async (isActive: boolean) => {
|
const handleClickButton = async (isActive: boolean) => {
|
||||||
|
console.log("Is Active: ", isActive);
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
const resp = await queryDeleteSos();
|
const resp = await queryDeleteSos();
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
@@ -115,167 +112,91 @@ const SosButton = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<IconButton
|
||||||
className="shadow-md rounded-full"
|
icon={<MaterialIcons name="warning" size={20} color="white" />}
|
||||||
size="lg"
|
type="danger"
|
||||||
action="negative"
|
size="middle"
|
||||||
onPress={() => handleClickButton(sosData?.active || false)}
|
onPress={() => handleClickButton(sosData?.active || false)}
|
||||||
|
style={{ borderRadius: 20 }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="warning" size={15} color="white" />
|
{sosData?.active ? t("home.sos.active") : t("home.sos.inactive")}
|
||||||
<ButtonText className="text-center">
|
</IconButton>
|
||||||
{sosData?.active ? t("home.sos.active") : t("home.sos.inactive")}
|
|
||||||
</ButtonText>
|
|
||||||
{/* <ButtonSpinner /> */}
|
|
||||||
{/* <ButtonIcon /> */}
|
|
||||||
</Button>
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showConfirmSosDialog}
|
open={showConfirmSosDialog}
|
||||||
onClose={() => {
|
onCancel={() => {
|
||||||
setShowConfirmSosDialog(false);
|
setShowConfirmSosDialog(false);
|
||||||
setSelectedSosMessage(null);
|
setSelectedSosMessage(null);
|
||||||
setCustomMessage("");
|
setCustomMessage("");
|
||||||
setErrors({});
|
setErrors({});
|
||||||
}}
|
}}
|
||||||
|
okText={t("home.sos.confirm")}
|
||||||
|
cancelText={t("home.sos.cancel")}
|
||||||
|
title={t("home.sos.title")}
|
||||||
|
centered
|
||||||
|
onOk={handleConfirmSos}
|
||||||
>
|
>
|
||||||
<ModalBackdrop />
|
{/* Select Nội dung SOS */}
|
||||||
<ModalContent>
|
<View style={styles.formGroup}>
|
||||||
<ModalHeader className="flex-col gap-0.5 items-center">
|
<Text style={styles.label}>{t("home.sos.content")}</Text>
|
||||||
<Text
|
|
||||||
style={{ fontSize: 18, fontWeight: "bold", textAlign: "center" }}
|
|
||||||
>
|
|
||||||
{t("home.sos.title")}
|
|
||||||
</Text>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody className="mb-4">
|
|
||||||
<ScrollView style={{ maxHeight: 400 }}>
|
|
||||||
{/* Dropdown Nội dung SOS */}
|
|
||||||
<View style={styles.formGroup}>
|
|
||||||
<Text style={styles.label}>{t("home.sos.content")}</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.dropdownButton,
|
|
||||||
errors.sosMessage ? styles.errorBorder : {},
|
|
||||||
]}
|
|
||||||
onPress={() => setShowDropdown(!showDropdown)}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.dropdownButtonText,
|
|
||||||
!selectedSosMessage && styles.placeholderText,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{selectedSosMessage !== null
|
|
||||||
? sosOptions.find((opt) => opt.ma === selectedSosMessage)
|
|
||||||
?.moTa || t("home.sos.selectReason")
|
|
||||||
: t("home.sos.selectReason")}
|
|
||||||
</Text>
|
|
||||||
<MaterialIcons
|
|
||||||
name={showDropdown ? "expand-less" : "expand-more"}
|
|
||||||
size={20}
|
|
||||||
color="#666"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
{errors.sosMessage && (
|
|
||||||
<Text style={styles.errorText}>{errors.sosMessage}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Input Custom Message nếu chọn "Khác" */}
|
<Select
|
||||||
{selectedSosMessage === 999 && (
|
value={selectedSosMessage ?? undefined}
|
||||||
<View style={styles.formGroup}>
|
options={sosOptions}
|
||||||
<Text style={styles.label}>{t("home.sos.statusInput")}</Text>
|
placeholder={t("home.sos.selectReason")}
|
||||||
<TextInput
|
onChange={(value) => {
|
||||||
style={[
|
setSelectedSosMessage(value as number);
|
||||||
styles.input,
|
// Clear custom message nếu chọn khác lý do
|
||||||
errors.customMessage ? styles.errorInput : {},
|
if (value !== 999) {
|
||||||
]}
|
|
||||||
placeholder={t("home.sos.enterStatus")}
|
|
||||||
placeholderTextColor="#999"
|
|
||||||
value={customMessage}
|
|
||||||
onChangeText={(text) => {
|
|
||||||
setCustomMessage(text);
|
|
||||||
if (text.trim() !== "") {
|
|
||||||
setErrors((prev) => {
|
|
||||||
const newErrors = { ...prev };
|
|
||||||
delete newErrors.customMessage;
|
|
||||||
return newErrors;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
multiline
|
|
||||||
numberOfLines={4}
|
|
||||||
/>
|
|
||||||
{errors.customMessage && (
|
|
||||||
<Text style={styles.errorText}>{errors.customMessage}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter className="flex-row items-start gap-2">
|
|
||||||
<Button
|
|
||||||
onPress={handleConfirmSos}
|
|
||||||
// className="w-1/3"
|
|
||||||
action="negative"
|
|
||||||
>
|
|
||||||
<ButtonText>{t("home.sos.confirm")}</ButtonText>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onPress={() => {
|
|
||||||
setShowConfirmSosDialog(false);
|
|
||||||
setSelectedSosMessage(null);
|
|
||||||
setCustomMessage("");
|
setCustomMessage("");
|
||||||
setErrors({});
|
}
|
||||||
}}
|
// Clear error if exists
|
||||||
// className="w-1/3"
|
if (errors.sosMessage) {
|
||||||
action="secondary"
|
setErrors((prev) => {
|
||||||
>
|
const newErrors = { ...prev };
|
||||||
<ButtonText>{t("home.sos.cancel")}</ButtonText>
|
delete newErrors.sosMessage;
|
||||||
</Button>
|
return newErrors;
|
||||||
</ModalFooter>
|
});
|
||||||
</ModalContent>
|
}
|
||||||
</Modal>
|
}}
|
||||||
|
showSearch={false}
|
||||||
|
style={[errors.sosMessage ? styles.errorBorder : undefined]}
|
||||||
|
/>
|
||||||
|
{errors.sosMessage && (
|
||||||
|
<Text style={styles.errorText}>{errors.sosMessage}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Dropdown Modal - Nổi lên */}
|
{/* Input Custom Message nếu chọn "Khác" */}
|
||||||
{showDropdown && showConfirmSosDialog && (
|
{selectedSosMessage === 999 && (
|
||||||
<Modal isOpen={showDropdown} onClose={() => setShowDropdown(false)}>
|
<View style={styles.formGroup}>
|
||||||
<TouchableOpacity
|
<Text style={styles.label}>{t("home.sos.statusInput")}</Text>
|
||||||
style={styles.dropdownOverlay}
|
<TextInput
|
||||||
activeOpacity={1}
|
style={[
|
||||||
onPress={() => setShowDropdown(false)}
|
styles.input,
|
||||||
>
|
errors.customMessage ? styles.errorInput : {},
|
||||||
<View style={styles.dropdownModalContainer}>
|
]}
|
||||||
<FlatList
|
placeholder={t("home.sos.enterStatus")}
|
||||||
data={sosOptions}
|
placeholderTextColor="#999"
|
||||||
keyExtractor={(item) => item.ma.toString()}
|
value={customMessage}
|
||||||
renderItem={({ item }) => (
|
onChangeText={(text) => {
|
||||||
<TouchableOpacity
|
setCustomMessage(text);
|
||||||
style={styles.dropdownModalItem}
|
if (text.trim() !== "") {
|
||||||
onPress={() => {
|
setErrors((prev) => {
|
||||||
setSelectedSosMessage(item.ma);
|
const newErrors = { ...prev };
|
||||||
setShowDropdown(false);
|
delete newErrors.customMessage;
|
||||||
// Clear custom message nếu chọn khác lý do
|
return newErrors;
|
||||||
if (item.ma !== 999) {
|
});
|
||||||
setCustomMessage("");
|
}
|
||||||
}
|
}}
|
||||||
}}
|
multiline
|
||||||
>
|
numberOfLines={4}
|
||||||
<Text
|
/>
|
||||||
style={[
|
{errors.customMessage && (
|
||||||
styles.dropdownModalItemText,
|
<Text style={styles.errorText}>{errors.customMessage}</Text>
|
||||||
selectedSosMessage === item.ma &&
|
)}
|
||||||
styles.selectedItemText,
|
</View>
|
||||||
]}
|
)}
|
||||||
>
|
</Modal>
|
||||||
{item.moTa}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -290,76 +211,9 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
color: "#333",
|
color: "#333",
|
||||||
},
|
},
|
||||||
dropdownButton: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#ddd",
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 12,
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
},
|
|
||||||
errorBorder: {
|
errorBorder: {
|
||||||
borderColor: "#ff4444",
|
borderColor: "#ff4444",
|
||||||
},
|
},
|
||||||
dropdownButtonText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: "#333",
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
placeholderText: {
|
|
||||||
color: "#999",
|
|
||||||
},
|
|
||||||
dropdownList: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#ddd",
|
|
||||||
borderRadius: 8,
|
|
||||||
marginTop: 4,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
overflow: "hidden",
|
|
||||||
},
|
|
||||||
dropdownItem: {
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: "#eee",
|
|
||||||
},
|
|
||||||
dropdownItemText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: "#333",
|
|
||||||
},
|
|
||||||
dropdownOverlay: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
dropdownModalContainer: {
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
borderRadius: 12,
|
|
||||||
maxHeight: 400,
|
|
||||||
minWidth: 280,
|
|
||||||
shadowColor: "#000",
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 10,
|
|
||||||
},
|
|
||||||
dropdownModalItem: {
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 14,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: "#f0f0f0",
|
|
||||||
},
|
|
||||||
dropdownModalItemText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: "#333",
|
|
||||||
},
|
|
||||||
selectedItemText: {
|
|
||||||
fontWeight: "600",
|
|
||||||
color: "#1054C9",
|
|
||||||
},
|
|
||||||
input: {
|
input: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#ddd",
|
borderColor: "#ddd",
|
||||||
|
|||||||
@@ -1,296 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import React from 'react';
|
|
||||||
import { createAlertDialog } from '@gluestack-ui/core/alert-dialog/creator';
|
|
||||||
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 {
|
|
||||||
Motion,
|
|
||||||
AnimatePresence,
|
|
||||||
createMotionAnimatedComponent,
|
|
||||||
MotionComponentProps,
|
|
||||||
} from '@legendapp/motion';
|
|
||||||
import { View, Pressable, ScrollView, ViewStyle } from 'react-native';
|
|
||||||
|
|
||||||
const SCOPE = 'ALERT_DIALOG';
|
|
||||||
|
|
||||||
const RootComponent = withStyleContext(View, SCOPE);
|
|
||||||
|
|
||||||
type IMotionViewProps = React.ComponentProps<typeof View> &
|
|
||||||
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
|
||||||
|
|
||||||
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
|
||||||
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const AnimatedPressable = createMotionAnimatedComponent(
|
|
||||||
Pressable
|
|
||||||
) as React.ComponentType<IAnimatedPressableProps>;
|
|
||||||
|
|
||||||
const UIAccessibleAlertDialog = createAlertDialog({
|
|
||||||
Root: RootComponent,
|
|
||||||
Body: ScrollView,
|
|
||||||
Content: MotionView,
|
|
||||||
CloseButton: Pressable,
|
|
||||||
Header: View,
|
|
||||||
Footer: View,
|
|
||||||
Backdrop: AnimatedPressable,
|
|
||||||
AnimatePresence: AnimatePresence,
|
|
||||||
});
|
|
||||||
|
|
||||||
cssInterop(MotionView, { className: 'style' });
|
|
||||||
cssInterop(AnimatedPressable, { className: 'style' });
|
|
||||||
|
|
||||||
const alertDialogStyle = tva({
|
|
||||||
base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none',
|
|
||||||
parentVariants: {
|
|
||||||
size: {
|
|
||||||
xs: '',
|
|
||||||
sm: '',
|
|
||||||
md: '',
|
|
||||||
lg: '',
|
|
||||||
full: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const alertDialogContentStyle = tva({
|
|
||||||
base: 'bg-background-0 rounded-lg overflow-hidden border border-outline-100 p-6',
|
|
||||||
parentVariants: {
|
|
||||||
size: {
|
|
||||||
xs: 'w-[60%] max-w-[360px]',
|
|
||||||
sm: 'w-[70%] max-w-[420px]',
|
|
||||||
md: 'w-[80%] max-w-[510px]',
|
|
||||||
lg: 'w-[90%] max-w-[640px]',
|
|
||||||
full: 'w-full',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const alertDialogCloseButtonStyle = tva({
|
|
||||||
base: 'group/alert-dialog-close-button z-10 rounded-sm p-2 data-[focus-visible=true]:bg-background-100 web:cursor-pointer outline-0',
|
|
||||||
});
|
|
||||||
|
|
||||||
const alertDialogHeaderStyle = tva({
|
|
||||||
base: 'justify-between items-center flex-row',
|
|
||||||
});
|
|
||||||
|
|
||||||
const alertDialogFooterStyle = tva({
|
|
||||||
base: 'flex-row justify-end items-center gap-3',
|
|
||||||
});
|
|
||||||
|
|
||||||
const alertDialogBodyStyle = tva({ base: '' });
|
|
||||||
|
|
||||||
const alertDialogBackdropStyle = tva({
|
|
||||||
base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default',
|
|
||||||
});
|
|
||||||
|
|
||||||
type IAlertDialogProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogContentProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Content
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogContentStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IAlertDialogCloseButtonProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.CloseButton
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogCloseButtonStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogHeaderProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Header
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogHeaderStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogFooterProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Footer
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogFooterStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogBodyProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Body
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogBodyStyle>;
|
|
||||||
|
|
||||||
type IAlertDialogBackdropProps = React.ComponentPropsWithoutRef<
|
|
||||||
typeof UIAccessibleAlertDialog.Backdrop
|
|
||||||
> &
|
|
||||||
VariantProps<typeof alertDialogBackdropStyle> & { className?: string };
|
|
||||||
|
|
||||||
const AlertDialog = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog>,
|
|
||||||
IAlertDialogProps
|
|
||||||
>(function AlertDialog({ className, size = 'md', ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogStyle({ class: className })}
|
|
||||||
context={{ size }}
|
|
||||||
pointerEvents="box-none"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Content>,
|
|
||||||
IAlertDialogContentProps
|
|
||||||
>(function AlertDialogContent({ className, size, ...props }, ref) {
|
|
||||||
const { size: parentSize } = useStyleContext(SCOPE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Content
|
|
||||||
pointerEvents="auto"
|
|
||||||
ref={ref}
|
|
||||||
initial={{
|
|
||||||
scale: 0.9,
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
scale: 1,
|
|
||||||
opacity: 1,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
scale: 0.9,
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 250,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogContentStyle({
|
|
||||||
parentVariants: {
|
|
||||||
size: parentSize,
|
|
||||||
},
|
|
||||||
size,
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogCloseButton = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.CloseButton>,
|
|
||||||
IAlertDialogCloseButtonProps
|
|
||||||
>(function AlertDialogCloseButton({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.CloseButton
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogCloseButtonStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogHeader = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Header>,
|
|
||||||
IAlertDialogHeaderProps
|
|
||||||
>(function AlertDialogHeader({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Header
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogHeaderStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogFooter = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Footer>,
|
|
||||||
IAlertDialogFooterProps
|
|
||||||
>(function AlertDialogFooter({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Footer
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogFooterStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogBody = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Body>,
|
|
||||||
IAlertDialogBodyProps
|
|
||||||
>(function AlertDialogBody({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Body
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogBodyStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AlertDialogBackdrop = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIAccessibleAlertDialog.Backdrop>,
|
|
||||||
IAlertDialogBackdropProps
|
|
||||||
>(function AlertDialogBackdrop({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIAccessibleAlertDialog.Backdrop
|
|
||||||
ref={ref}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 0.5,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 250,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={alertDialogBackdropStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
AlertDialog.displayName = 'AlertDialog';
|
|
||||||
AlertDialogContent.displayName = 'AlertDialogContent';
|
|
||||||
AlertDialogCloseButton.displayName = 'AlertDialogCloseButton';
|
|
||||||
AlertDialogHeader.displayName = 'AlertDialogHeader';
|
|
||||||
AlertDialogFooter.displayName = 'AlertDialogFooter';
|
|
||||||
AlertDialogBody.displayName = 'AlertDialogBody';
|
|
||||||
AlertDialogBackdrop.displayName = 'AlertDialogBackdrop';
|
|
||||||
|
|
||||||
export {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogCloseButton,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogBody,
|
|
||||||
AlertDialogBackdrop,
|
|
||||||
};
|
|
||||||
@@ -1,434 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import React from 'react';
|
|
||||||
import { createButton } from '@gluestack-ui/core/button/creator';
|
|
||||||
import {
|
|
||||||
tva,
|
|
||||||
withStyleContext,
|
|
||||||
useStyleContext,
|
|
||||||
type VariantProps,
|
|
||||||
} from '@gluestack-ui/utils/nativewind-utils';
|
|
||||||
import { cssInterop } from 'nativewind';
|
|
||||||
import { ActivityIndicator, Pressable, Text, View } from 'react-native';
|
|
||||||
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,
|
|
||||||
size,
|
|
||||||
action,
|
|
||||||
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,
|
|
||||||
flexDirection,
|
|
||||||
})}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Button.displayName = 'Button';
|
|
||||||
ButtonText.displayName = 'ButtonText';
|
|
||||||
ButtonSpinner.displayName = 'ButtonSpinner';
|
|
||||||
ButtonIcon.displayName = 'ButtonIcon';
|
|
||||||
ButtonGroup.displayName = 'ButtonGroup';
|
|
||||||
|
|
||||||
export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup };
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { vars } from 'nativewind';
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
light: vars({
|
|
||||||
'--color-primary-0': '179 179 179',
|
|
||||||
'--color-primary-50': '153 153 153',
|
|
||||||
'--color-primary-100': '128 128 128',
|
|
||||||
'--color-primary-200': '115 115 115',
|
|
||||||
'--color-primary-300': '102 102 102',
|
|
||||||
'--color-primary-400': '82 82 82',
|
|
||||||
'--color-primary-500': '51 51 51',
|
|
||||||
'--color-primary-600': '41 41 41',
|
|
||||||
'--color-primary-700': '31 31 31',
|
|
||||||
'--color-primary-800': '13 13 13',
|
|
||||||
'--color-primary-900': '10 10 10',
|
|
||||||
'--color-primary-950': '8 8 8',
|
|
||||||
|
|
||||||
/* Secondary */
|
|
||||||
'--color-secondary-0': '253 253 253',
|
|
||||||
'--color-secondary-50': '251 251 251',
|
|
||||||
'--color-secondary-100': '246 246 246',
|
|
||||||
'--color-secondary-200': '242 242 242',
|
|
||||||
'--color-secondary-300': '237 237 237',
|
|
||||||
'--color-secondary-400': '230 230 231',
|
|
||||||
'--color-secondary-500': '217 217 219',
|
|
||||||
'--color-secondary-600': '198 199 199',
|
|
||||||
'--color-secondary-700': '189 189 189',
|
|
||||||
'--color-secondary-800': '177 177 177',
|
|
||||||
'--color-secondary-900': '165 164 164',
|
|
||||||
'--color-secondary-950': '157 157 157',
|
|
||||||
|
|
||||||
/* Tertiary */
|
|
||||||
'--color-tertiary-0': '255 250 245',
|
|
||||||
'--color-tertiary-50': '255 242 229',
|
|
||||||
'--color-tertiary-100': '255 233 213',
|
|
||||||
'--color-tertiary-200': '254 209 170',
|
|
||||||
'--color-tertiary-300': '253 180 116',
|
|
||||||
'--color-tertiary-400': '251 157 75',
|
|
||||||
'--color-tertiary-500': '231 129 40',
|
|
||||||
'--color-tertiary-600': '215 117 31',
|
|
||||||
'--color-tertiary-700': '180 98 26',
|
|
||||||
'--color-tertiary-800': '130 73 23',
|
|
||||||
'--color-tertiary-900': '108 61 19',
|
|
||||||
'--color-tertiary-950': '84 49 18',
|
|
||||||
|
|
||||||
/* Error */
|
|
||||||
'--color-error-0': '254 233 233',
|
|
||||||
'--color-error-50': '254 226 226',
|
|
||||||
'--color-error-100': '254 202 202',
|
|
||||||
'--color-error-200': '252 165 165',
|
|
||||||
'--color-error-300': '248 113 113',
|
|
||||||
'--color-error-400': '239 68 68',
|
|
||||||
'--color-error-500': '230 53 53',
|
|
||||||
'--color-error-600': '220 38 38',
|
|
||||||
'--color-error-700': '185 28 28',
|
|
||||||
'--color-error-800': '153 27 27',
|
|
||||||
'--color-error-900': '127 29 29',
|
|
||||||
'--color-error-950': '83 19 19',
|
|
||||||
|
|
||||||
/* Success */
|
|
||||||
'--color-success-0': '228 255 244',
|
|
||||||
'--color-success-50': '202 255 232',
|
|
||||||
'--color-success-100': '162 241 192',
|
|
||||||
'--color-success-200': '132 211 162',
|
|
||||||
'--color-success-300': '102 181 132',
|
|
||||||
'--color-success-400': '72 151 102',
|
|
||||||
'--color-success-500': '52 131 82',
|
|
||||||
'--color-success-600': '42 121 72',
|
|
||||||
'--color-success-700': '32 111 62',
|
|
||||||
'--color-success-800': '22 101 52',
|
|
||||||
'--color-success-900': '20 83 45',
|
|
||||||
'--color-success-950': '27 50 36',
|
|
||||||
|
|
||||||
/* Warning */
|
|
||||||
'--color-warning-0': '255 249 245',
|
|
||||||
'--color-warning-50': '255 244 236',
|
|
||||||
'--color-warning-100': '255 231 213',
|
|
||||||
'--color-warning-200': '254 205 170',
|
|
||||||
'--color-warning-300': '253 173 116',
|
|
||||||
'--color-warning-400': '251 149 75',
|
|
||||||
'--color-warning-500': '231 120 40',
|
|
||||||
'--color-warning-600': '215 108 31',
|
|
||||||
'--color-warning-700': '180 90 26',
|
|
||||||
'--color-warning-800': '130 68 23',
|
|
||||||
'--color-warning-900': '108 56 19',
|
|
||||||
'--color-warning-950': '84 45 18',
|
|
||||||
|
|
||||||
/* Info */
|
|
||||||
'--color-info-0': '236 248 254',
|
|
||||||
'--color-info-50': '199 235 252',
|
|
||||||
'--color-info-100': '162 221 250',
|
|
||||||
'--color-info-200': '124 207 248',
|
|
||||||
'--color-info-300': '87 194 246',
|
|
||||||
'--color-info-400': '50 180 244',
|
|
||||||
'--color-info-500': '13 166 242',
|
|
||||||
'--color-info-600': '11 141 205',
|
|
||||||
'--color-info-700': '9 115 168',
|
|
||||||
'--color-info-800': '7 90 131',
|
|
||||||
'--color-info-900': '5 64 93',
|
|
||||||
'--color-info-950': '3 38 56',
|
|
||||||
|
|
||||||
/* Typography */
|
|
||||||
'--color-typography-0': '254 254 255',
|
|
||||||
'--color-typography-50': '245 245 245',
|
|
||||||
'--color-typography-100': '229 229 229',
|
|
||||||
'--color-typography-200': '219 219 220',
|
|
||||||
'--color-typography-300': '212 212 212',
|
|
||||||
'--color-typography-400': '163 163 163',
|
|
||||||
'--color-typography-500': '140 140 140',
|
|
||||||
'--color-typography-600': '115 115 115',
|
|
||||||
'--color-typography-700': '82 82 82',
|
|
||||||
'--color-typography-800': '64 64 64',
|
|
||||||
'--color-typography-900': '38 38 39',
|
|
||||||
'--color-typography-950': '23 23 23',
|
|
||||||
|
|
||||||
/* Outline */
|
|
||||||
'--color-outline-0': '253 254 254',
|
|
||||||
'--color-outline-50': '243 243 243',
|
|
||||||
'--color-outline-100': '230 230 230',
|
|
||||||
'--color-outline-200': '221 220 219',
|
|
||||||
'--color-outline-300': '211 211 211',
|
|
||||||
'--color-outline-400': '165 163 163',
|
|
||||||
'--color-outline-500': '140 141 141',
|
|
||||||
'--color-outline-600': '115 116 116',
|
|
||||||
'--color-outline-700': '83 82 82',
|
|
||||||
'--color-outline-800': '65 65 65',
|
|
||||||
'--color-outline-900': '39 38 36',
|
|
||||||
'--color-outline-950': '26 23 23',
|
|
||||||
|
|
||||||
/* Background */
|
|
||||||
'--color-background-0': '255 255 255',
|
|
||||||
'--color-background-50': '246 246 246',
|
|
||||||
'--color-background-100': '242 241 241',
|
|
||||||
'--color-background-200': '220 219 219',
|
|
||||||
'--color-background-300': '213 212 212',
|
|
||||||
'--color-background-400': '162 163 163',
|
|
||||||
'--color-background-500': '142 142 142',
|
|
||||||
'--color-background-600': '116 116 116',
|
|
||||||
'--color-background-700': '83 82 82',
|
|
||||||
'--color-background-800': '65 64 64',
|
|
||||||
'--color-background-900': '39 38 37',
|
|
||||||
'--color-background-950': '18 18 18',
|
|
||||||
|
|
||||||
/* Background Special */
|
|
||||||
'--color-background-error': '254 241 241',
|
|
||||||
'--color-background-warning': '255 243 234',
|
|
||||||
'--color-background-success': '237 252 242',
|
|
||||||
'--color-background-muted': '247 248 247',
|
|
||||||
'--color-background-info': '235 248 254',
|
|
||||||
|
|
||||||
/* Focus Ring Indicator */
|
|
||||||
'--color-indicator-primary': '55 55 55',
|
|
||||||
'--color-indicator-info': '83 153 236',
|
|
||||||
'--color-indicator-error': '185 28 28',
|
|
||||||
}),
|
|
||||||
dark: vars({
|
|
||||||
'--color-primary-0': '166 166 166',
|
|
||||||
'--color-primary-50': '175 175 175',
|
|
||||||
'--color-primary-100': '186 186 186',
|
|
||||||
'--color-primary-200': '197 197 197',
|
|
||||||
'--color-primary-300': '212 212 212',
|
|
||||||
'--color-primary-400': '221 221 221',
|
|
||||||
'--color-primary-500': '230 230 230',
|
|
||||||
'--color-primary-600': '240 240 240',
|
|
||||||
'--color-primary-700': '250 250 250',
|
|
||||||
'--color-primary-800': '253 253 253',
|
|
||||||
'--color-primary-900': '254 249 249',
|
|
||||||
'--color-primary-950': '253 252 252',
|
|
||||||
|
|
||||||
/* Secondary */
|
|
||||||
'--color-secondary-0': '20 20 20',
|
|
||||||
'--color-secondary-50': '23 23 23',
|
|
||||||
'--color-secondary-100': '31 31 31',
|
|
||||||
'--color-secondary-200': '39 39 39',
|
|
||||||
'--color-secondary-300': '44 44 44',
|
|
||||||
'--color-secondary-400': '56 57 57',
|
|
||||||
'--color-secondary-500': '63 64 64',
|
|
||||||
'--color-secondary-600': '86 86 86',
|
|
||||||
'--color-secondary-700': '110 110 110',
|
|
||||||
'--color-secondary-800': '135 135 135',
|
|
||||||
'--color-secondary-900': '150 150 150',
|
|
||||||
'--color-secondary-950': '164 164 164',
|
|
||||||
|
|
||||||
/* Tertiary */
|
|
||||||
'--color-tertiary-0': '84 49 18',
|
|
||||||
'--color-tertiary-50': '108 61 19',
|
|
||||||
'--color-tertiary-100': '130 73 23',
|
|
||||||
'--color-tertiary-200': '180 98 26',
|
|
||||||
'--color-tertiary-300': '215 117 31',
|
|
||||||
'--color-tertiary-400': '231 129 40',
|
|
||||||
'--color-tertiary-500': '251 157 75',
|
|
||||||
'--color-tertiary-600': '253 180 116',
|
|
||||||
'--color-tertiary-700': '254 209 170',
|
|
||||||
'--color-tertiary-800': '255 233 213',
|
|
||||||
'--color-tertiary-900': '255 242 229',
|
|
||||||
'--color-tertiary-950': '255 250 245',
|
|
||||||
|
|
||||||
/* Error */
|
|
||||||
'--color-error-0': '83 19 19',
|
|
||||||
'--color-error-50': '127 29 29',
|
|
||||||
'--color-error-100': '153 27 27',
|
|
||||||
'--color-error-200': '185 28 28',
|
|
||||||
'--color-error-300': '220 38 38',
|
|
||||||
'--color-error-400': '230 53 53',
|
|
||||||
'--color-error-500': '239 68 68',
|
|
||||||
'--color-error-600': '249 97 96',
|
|
||||||
'--color-error-700': '229 91 90',
|
|
||||||
'--color-error-800': '254 202 202',
|
|
||||||
'--color-error-900': '254 226 226',
|
|
||||||
'--color-error-950': '254 233 233',
|
|
||||||
|
|
||||||
/* Success */
|
|
||||||
'--color-success-0': '27 50 36',
|
|
||||||
'--color-success-50': '20 83 45',
|
|
||||||
'--color-success-100': '22 101 52',
|
|
||||||
'--color-success-200': '32 111 62',
|
|
||||||
'--color-success-300': '42 121 72',
|
|
||||||
'--color-success-400': '52 131 82',
|
|
||||||
'--color-success-500': '72 151 102',
|
|
||||||
'--color-success-600': '102 181 132',
|
|
||||||
'--color-success-700': '132 211 162',
|
|
||||||
'--color-success-800': '162 241 192',
|
|
||||||
'--color-success-900': '202 255 232',
|
|
||||||
'--color-success-950': '228 255 244',
|
|
||||||
|
|
||||||
/* Warning */
|
|
||||||
'--color-warning-0': '84 45 18',
|
|
||||||
'--color-warning-50': '108 56 19',
|
|
||||||
'--color-warning-100': '130 68 23',
|
|
||||||
'--color-warning-200': '180 90 26',
|
|
||||||
'--color-warning-300': '215 108 31',
|
|
||||||
'--color-warning-400': '231 120 40',
|
|
||||||
'--color-warning-500': '251 149 75',
|
|
||||||
'--color-warning-600': '253 173 116',
|
|
||||||
'--color-warning-700': '254 205 170',
|
|
||||||
'--color-warning-800': '255 231 213',
|
|
||||||
'--color-warning-900': '255 244 237',
|
|
||||||
'--color-warning-950': '255 249 245',
|
|
||||||
|
|
||||||
/* Info */
|
|
||||||
'--color-info-0': '3 38 56',
|
|
||||||
'--color-info-50': '5 64 93',
|
|
||||||
'--color-info-100': '7 90 131',
|
|
||||||
'--color-info-200': '9 115 168',
|
|
||||||
'--color-info-300': '11 141 205',
|
|
||||||
'--color-info-400': '13 166 242',
|
|
||||||
'--color-info-500': '50 180 244',
|
|
||||||
'--color-info-600': '87 194 246',
|
|
||||||
'--color-info-700': '124 207 248',
|
|
||||||
'--color-info-800': '162 221 250',
|
|
||||||
'--color-info-900': '199 235 252',
|
|
||||||
'--color-info-950': '236 248 254',
|
|
||||||
|
|
||||||
/* Typography */
|
|
||||||
'--color-typography-0': '23 23 23',
|
|
||||||
'--color-typography-50': '38 38 39',
|
|
||||||
'--color-typography-100': '64 64 64',
|
|
||||||
'--color-typography-200': '82 82 82',
|
|
||||||
'--color-typography-300': '115 115 115',
|
|
||||||
'--color-typography-400': '140 140 140',
|
|
||||||
'--color-typography-500': '163 163 163',
|
|
||||||
'--color-typography-600': '212 212 212',
|
|
||||||
'--color-typography-700': '219 219 220',
|
|
||||||
'--color-typography-800': '229 229 229',
|
|
||||||
'--color-typography-900': '245 245 245',
|
|
||||||
'--color-typography-950': '254 254 255',
|
|
||||||
|
|
||||||
/* Outline */
|
|
||||||
'--color-outline-0': '26 23 23',
|
|
||||||
'--color-outline-50': '39 38 36',
|
|
||||||
'--color-outline-100': '65 65 65',
|
|
||||||
'--color-outline-200': '83 82 82',
|
|
||||||
'--color-outline-300': '115 116 116',
|
|
||||||
'--color-outline-400': '140 141 141',
|
|
||||||
'--color-outline-500': '165 163 163',
|
|
||||||
'--color-outline-600': '211 211 211',
|
|
||||||
'--color-outline-700': '221 220 219',
|
|
||||||
'--color-outline-800': '230 230 230',
|
|
||||||
'--color-outline-900': '243 243 243',
|
|
||||||
'--color-outline-950': '253 254 254',
|
|
||||||
|
|
||||||
/* Background */
|
|
||||||
'--color-background-0': '18 18 18',
|
|
||||||
'--color-background-50': '39 38 37',
|
|
||||||
'--color-background-100': '65 64 64',
|
|
||||||
'--color-background-200': '83 82 82',
|
|
||||||
'--color-background-300': '116 116 116',
|
|
||||||
'--color-background-400': '142 142 142',
|
|
||||||
'--color-background-500': '162 163 163',
|
|
||||||
'--color-background-600': '213 212 212',
|
|
||||||
'--color-background-700': '229 228 228',
|
|
||||||
'--color-background-800': '242 241 241',
|
|
||||||
'--color-background-900': '246 246 246',
|
|
||||||
'--color-background-950': '255 255 255',
|
|
||||||
|
|
||||||
/* Background Special */
|
|
||||||
'--color-background-error': '66 43 43',
|
|
||||||
'--color-background-warning': '65 47 35',
|
|
||||||
'--color-background-success': '28 43 33',
|
|
||||||
'--color-background-muted': '51 51 51',
|
|
||||||
'--color-background-info': '26 40 46',
|
|
||||||
|
|
||||||
/* Focus Ring Indicator */
|
|
||||||
'--color-indicator-primary': '247 247 247',
|
|
||||||
'--color-indicator-info': '161 199 245',
|
|
||||||
'--color-indicator-error': '232 70 69',
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
// This is a Next.js 15 compatible version of the GluestackUIProvider
|
|
||||||
'use client';
|
|
||||||
import React, { useEffect, useLayoutEffect } from 'react';
|
|
||||||
import { config } from './config';
|
|
||||||
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
|
||||||
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
|
||||||
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
|
|
||||||
import { script } from './script';
|
|
||||||
|
|
||||||
const variableStyleTagId = 'nativewind-style';
|
|
||||||
const createStyle = (styleTagId: string) => {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = styleTagId;
|
|
||||||
style.appendChild(document.createTextNode(''));
|
|
||||||
return style;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSafeLayoutEffect =
|
|
||||||
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
||||||
|
|
||||||
export function GluestackUIProvider({
|
|
||||||
mode = 'light',
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
mode?: 'light' | 'dark' | 'system';
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
let cssVariablesWithMode = ``;
|
|
||||||
Object.keys(config).forEach((configKey) => {
|
|
||||||
cssVariablesWithMode +=
|
|
||||||
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
|
|
||||||
const cssVariables = Object.keys(
|
|
||||||
config[configKey as keyof typeof config]
|
|
||||||
).reduce((acc: string, curr: string) => {
|
|
||||||
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
|
|
||||||
return acc;
|
|
||||||
}, '');
|
|
||||||
cssVariablesWithMode += `${cssVariables} \n}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFlushStyles(cssVariablesWithMode);
|
|
||||||
|
|
||||||
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
|
|
||||||
script(e.matches ? 'dark' : 'light');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useSafeLayoutEffect(() => {
|
|
||||||
if (mode !== 'system') {
|
|
||||||
const documentElement = document.documentElement;
|
|
||||||
if (documentElement) {
|
|
||||||
documentElement.classList.add(mode);
|
|
||||||
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
|
|
||||||
documentElement.style.colorScheme = mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [mode]);
|
|
||||||
|
|
||||||
useSafeLayoutEffect(() => {
|
|
||||||
if (mode !== 'system') return;
|
|
||||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
|
|
||||||
media.addListener(handleMediaQuery);
|
|
||||||
|
|
||||||
return () => media.removeListener(handleMediaQuery);
|
|
||||||
}, [handleMediaQuery]);
|
|
||||||
|
|
||||||
useSafeLayoutEffect(() => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const documentElement = document.documentElement;
|
|
||||||
if (documentElement) {
|
|
||||||
const head = documentElement.querySelector('head');
|
|
||||||
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
|
|
||||||
if (!style) {
|
|
||||||
style = createStyle(variableStyleTagId);
|
|
||||||
style.innerHTML = cssVariablesWithMode;
|
|
||||||
if (head) head.appendChild(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OverlayProvider>
|
|
||||||
<ToastProvider>{props.children}</ToastProvider>
|
|
||||||
</OverlayProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { config } from './config';
|
|
||||||
import { View, ViewProps } from 'react-native';
|
|
||||||
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
|
||||||
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
|
||||||
import { useColorScheme } from 'nativewind';
|
|
||||||
|
|
||||||
export type ModeType = 'light' | 'dark' | 'system';
|
|
||||||
|
|
||||||
export function GluestackUIProvider({
|
|
||||||
mode = 'light',
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
mode?: ModeType;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
style?: ViewProps['style'];
|
|
||||||
}) {
|
|
||||||
const { colorScheme, setColorScheme } = useColorScheme();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setColorScheme(mode);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [mode]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
config[colorScheme!],
|
|
||||||
{ flex: 1, height: '100%', width: '100%' },
|
|
||||||
props.style,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<OverlayProvider>
|
|
||||||
<ToastProvider>{props.children}</ToastProvider>
|
|
||||||
</OverlayProvider>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import React, { useEffect, useLayoutEffect } from 'react';
|
|
||||||
import { config } from './config';
|
|
||||||
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
|
||||||
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
|
||||||
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
|
|
||||||
import { script } from './script';
|
|
||||||
|
|
||||||
export type ModeType = 'light' | 'dark' | 'system';
|
|
||||||
|
|
||||||
const variableStyleTagId = 'nativewind-style';
|
|
||||||
const createStyle = (styleTagId: string) => {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = styleTagId;
|
|
||||||
style.appendChild(document.createTextNode(''));
|
|
||||||
return style;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSafeLayoutEffect =
|
|
||||||
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
||||||
|
|
||||||
export function GluestackUIProvider({
|
|
||||||
mode = 'light',
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
mode?: ModeType;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
let cssVariablesWithMode = ``;
|
|
||||||
Object.keys(config).forEach((configKey) => {
|
|
||||||
cssVariablesWithMode +=
|
|
||||||
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
|
|
||||||
const cssVariables = Object.keys(
|
|
||||||
config[configKey as keyof typeof config]
|
|
||||||
).reduce((acc: string, curr: string) => {
|
|
||||||
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
|
|
||||||
return acc;
|
|
||||||
}, '');
|
|
||||||
cssVariablesWithMode += `${cssVariables} \n}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFlushStyles(cssVariablesWithMode);
|
|
||||||
|
|
||||||
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
|
|
||||||
script(e.matches ? 'dark' : 'light');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useSafeLayoutEffect(() => {
|
|
||||||
if (mode !== 'system') {
|
|
||||||
const documentElement = document.documentElement;
|
|
||||||
if (documentElement) {
|
|
||||||
documentElement.classList.add(mode);
|
|
||||||
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
|
|
||||||
documentElement.style.colorScheme = mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [mode]);
|
|
||||||
|
|
||||||
useSafeLayoutEffect(() => {
|
|
||||||
if (mode !== 'system') return;
|
|
||||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
|
|
||||||
media.addListener(handleMediaQuery);
|
|
||||||
|
|
||||||
return () => media.removeListener(handleMediaQuery);
|
|
||||||
}, [handleMediaQuery]);
|
|
||||||
|
|
||||||
useSafeLayoutEffect(() => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const documentElement = document.documentElement;
|
|
||||||
if (documentElement) {
|
|
||||||
const head = documentElement.querySelector('head');
|
|
||||||
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
|
|
||||||
if (!style) {
|
|
||||||
style = createStyle(variableStyleTagId);
|
|
||||||
style.innerHTML = cssVariablesWithMode;
|
|
||||||
if (head) head.appendChild(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<script
|
|
||||||
suppressHydrationWarning
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `(${script.toString()})('${mode}')`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<OverlayProvider>
|
|
||||||
<ToastProvider>{props.children}</ToastProvider>
|
|
||||||
</OverlayProvider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export const script = (mode: string) => {
|
|
||||||
const documentElement = document.documentElement;
|
|
||||||
|
|
||||||
function getSystemColorMode() {
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
? 'dark'
|
|
||||||
: 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isSystem = mode === 'system';
|
|
||||||
const theme = isSystem ? getSystemColorMode() : mode;
|
|
||||||
documentElement.classList.remove(theme === 'light' ? 'dark' : 'light');
|
|
||||||
documentElement.classList.add(theme);
|
|
||||||
documentElement.style.colorScheme = theme;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import React from 'react';
|
|
||||||
import { createModal } from '@gluestack-ui/core/modal/creator';
|
|
||||||
import { Pressable, View, ScrollView, ViewStyle } from 'react-native';
|
|
||||||
import {
|
|
||||||
Motion,
|
|
||||||
AnimatePresence,
|
|
||||||
createMotionAnimatedComponent,
|
|
||||||
MotionComponentProps,
|
|
||||||
} from '@legendapp/motion';
|
|
||||||
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';
|
|
||||||
|
|
||||||
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
|
||||||
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const AnimatedPressable = createMotionAnimatedComponent(
|
|
||||||
Pressable
|
|
||||||
) as React.ComponentType<IAnimatedPressableProps>;
|
|
||||||
const SCOPE = 'MODAL';
|
|
||||||
|
|
||||||
type IMotionViewProps = React.ComponentProps<typeof View> &
|
|
||||||
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
|
||||||
|
|
||||||
const UIModal = createModal({
|
|
||||||
Root: withStyleContext(View, SCOPE),
|
|
||||||
Backdrop: AnimatedPressable,
|
|
||||||
Content: MotionView,
|
|
||||||
Body: ScrollView,
|
|
||||||
CloseButton: Pressable,
|
|
||||||
Footer: View,
|
|
||||||
Header: View,
|
|
||||||
AnimatePresence: AnimatePresence,
|
|
||||||
});
|
|
||||||
|
|
||||||
cssInterop(AnimatedPressable, { className: 'style' });
|
|
||||||
cssInterop(MotionView, { className: 'style' });
|
|
||||||
|
|
||||||
const modalStyle = tva({
|
|
||||||
base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none',
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
xs: '',
|
|
||||||
sm: '',
|
|
||||||
md: '',
|
|
||||||
lg: '',
|
|
||||||
full: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalBackdropStyle = tva({
|
|
||||||
base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default',
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalContentStyle = tva({
|
|
||||||
base: 'bg-background-0 rounded-md overflow-hidden border border-outline-100 shadow-hard-2 p-6',
|
|
||||||
parentVariants: {
|
|
||||||
size: {
|
|
||||||
xs: 'w-[60%] max-w-[360px]',
|
|
||||||
sm: 'w-[70%] max-w-[420px]',
|
|
||||||
md: 'w-[80%] max-w-[510px]',
|
|
||||||
lg: 'w-[90%] max-w-[640px]',
|
|
||||||
full: 'w-full',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalBodyStyle = tva({
|
|
||||||
base: 'mt-2 mb-6',
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalCloseButtonStyle = tva({
|
|
||||||
base: 'group/modal-close-button z-10 rounded data-[focus-visible=true]:web:bg-background-100 web:outline-0 cursor-pointer',
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalHeaderStyle = tva({
|
|
||||||
base: 'justify-between items-center flex-row',
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalFooterStyle = tva({
|
|
||||||
base: 'flex-row justify-end items-center gap-2',
|
|
||||||
});
|
|
||||||
|
|
||||||
type IModalProps = React.ComponentProps<typeof UIModal> &
|
|
||||||
VariantProps<typeof modalStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalBackdropProps = React.ComponentProps<typeof UIModal.Backdrop> &
|
|
||||||
VariantProps<typeof modalBackdropStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalContentProps = React.ComponentProps<typeof UIModal.Content> &
|
|
||||||
VariantProps<typeof modalContentStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalHeaderProps = React.ComponentProps<typeof UIModal.Header> &
|
|
||||||
VariantProps<typeof modalHeaderStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalBodyProps = React.ComponentProps<typeof UIModal.Body> &
|
|
||||||
VariantProps<typeof modalBodyStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalFooterProps = React.ComponentProps<typeof UIModal.Footer> &
|
|
||||||
VariantProps<typeof modalFooterStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IModalCloseButtonProps = React.ComponentProps<typeof UIModal.CloseButton> &
|
|
||||||
VariantProps<typeof modalCloseButtonStyle> & { className?: string };
|
|
||||||
|
|
||||||
const Modal = React.forwardRef<React.ComponentRef<typeof UIModal>, IModalProps>(
|
|
||||||
({ className, size = 'md', ...props }, ref) => (
|
|
||||||
<UIModal
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
pointerEvents="box-none"
|
|
||||||
className={modalStyle({ size, class: className })}
|
|
||||||
context={{ size }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const ModalBackdrop = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Backdrop>,
|
|
||||||
IModalBackdropProps
|
|
||||||
>(function ModalBackdrop({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.Backdrop
|
|
||||||
ref={ref}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 0.5,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 250,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={modalBackdropStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalContent = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Content>,
|
|
||||||
IModalContentProps
|
|
||||||
>(function ModalContent({ className, size, ...props }, ref) {
|
|
||||||
const { size: parentSize } = useStyleContext(SCOPE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UIModal.Content
|
|
||||||
ref={ref}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
scale: 0.9,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 250,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={modalContentStyle({
|
|
||||||
parentVariants: {
|
|
||||||
size: parentSize,
|
|
||||||
},
|
|
||||||
size,
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
pointerEvents="auto"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalHeader = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Header>,
|
|
||||||
IModalHeaderProps
|
|
||||||
>(function ModalHeader({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.Header
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={modalHeaderStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalBody = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Body>,
|
|
||||||
IModalBodyProps
|
|
||||||
>(function ModalBody({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.Body
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={modalBodyStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalFooter = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.Footer>,
|
|
||||||
IModalFooterProps
|
|
||||||
>(function ModalFooter({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.Footer
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={modalFooterStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ModalCloseButton = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIModal.CloseButton>,
|
|
||||||
IModalCloseButtonProps
|
|
||||||
>(function ModalCloseButton({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIModal.CloseButton
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={modalCloseButtonStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Modal.displayName = 'Modal';
|
|
||||||
ModalBackdrop.displayName = 'ModalBackdrop';
|
|
||||||
ModalContent.displayName = 'ModalContent';
|
|
||||||
ModalHeader.displayName = 'ModalHeader';
|
|
||||||
ModalBody.displayName = 'ModalBody';
|
|
||||||
ModalFooter.displayName = 'ModalFooter';
|
|
||||||
ModalCloseButton.displayName = 'ModalCloseButton';
|
|
||||||
|
|
||||||
export {
|
|
||||||
Modal,
|
|
||||||
ModalBackdrop,
|
|
||||||
ModalContent,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
};
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import React from 'react';
|
|
||||||
import { View, Pressable, ScrollView, ViewStyle } from 'react-native';
|
|
||||||
import {
|
|
||||||
Motion,
|
|
||||||
createMotionAnimatedComponent,
|
|
||||||
AnimatePresence,
|
|
||||||
MotionComponentProps,
|
|
||||||
} from '@legendapp/motion';
|
|
||||||
import { createPopover } from '@gluestack-ui/core/popover/creator';
|
|
||||||
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';
|
|
||||||
|
|
||||||
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
|
||||||
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const AnimatedPressable = createMotionAnimatedComponent(
|
|
||||||
Pressable
|
|
||||||
) as React.ComponentType<IAnimatedPressableProps>;
|
|
||||||
|
|
||||||
const SCOPE = 'POPOVER';
|
|
||||||
|
|
||||||
type IMotionViewProps = React.ComponentProps<typeof View> &
|
|
||||||
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
|
||||||
|
|
||||||
const UIPopover = createPopover({
|
|
||||||
Root: withStyleContext(View, SCOPE),
|
|
||||||
Arrow: MotionView,
|
|
||||||
Backdrop: AnimatedPressable,
|
|
||||||
Body: ScrollView,
|
|
||||||
CloseButton: Pressable,
|
|
||||||
Content: MotionView,
|
|
||||||
Footer: View,
|
|
||||||
Header: View,
|
|
||||||
AnimatePresence: AnimatePresence,
|
|
||||||
});
|
|
||||||
|
|
||||||
cssInterop(MotionView, { className: 'style' });
|
|
||||||
cssInterop(AnimatedPressable, { className: 'style' });
|
|
||||||
|
|
||||||
const popoverStyle = tva({
|
|
||||||
base: 'group/popover w-full h-full justify-center items-center web:pointer-events-none',
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
xs: '',
|
|
||||||
sm: '',
|
|
||||||
md: '',
|
|
||||||
lg: '',
|
|
||||||
full: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const popoverArrowStyle = tva({
|
|
||||||
base: 'bg-background-0 z-[1] border absolute overflow-hidden h-3.5 w-3.5 border-outline-100',
|
|
||||||
variants: {
|
|
||||||
placement: {
|
|
||||||
'top left':
|
|
||||||
'data-[flip=false]:border-t-0 data-[flip=false]:border-l-0 data-[flip=true]:border-b-0 data-[flip=true]:border-r-0',
|
|
||||||
'top':
|
|
||||||
'data-[flip=false]:border-t-0 data-[flip=false]:border-l-0 data-[flip=true]:border-b-0 data-[flip=true]:border-r-0',
|
|
||||||
'top right':
|
|
||||||
'data-[flip=false]:border-t-0 data-[flip=false]:border-l-0 data-[flip=true]:border-b-0 data-[flip=true]:border-r-0',
|
|
||||||
'bottom':
|
|
||||||
'data-[flip=false]:border-b-0 data-[flip=false]:border-r-0 data-[flip=true]:border-t-0 data-[flip=true]:border-l-0',
|
|
||||||
'bottom left':
|
|
||||||
'data-[flip=false]:border-b-0 data-[flip=false]:border-r-0 data-[flip=true]:border-t-0 data-[flip=true]:border-l-0',
|
|
||||||
'bottom right':
|
|
||||||
'data-[flip=false]:border-b-0 data-[flip=false]:border-r-0 data-[flip=true]:border-t-0 data-[flip=true]:border-l-0',
|
|
||||||
'left':
|
|
||||||
'data-[flip=false]:border-l-0 data-[flip=false]:border-b-0 data-[flip=true]:border-r-0 data-[flip=true]:border-t-0',
|
|
||||||
'left top':
|
|
||||||
'data-[flip=false]:border-l-0 data-[flip=false]:border-b-0 data-[flip=true]:border-r-0 data-[flip=true]:border-t-0',
|
|
||||||
'left bottom':
|
|
||||||
'data-[flip=false]:border-l-0 data-[flip=false]:border-b-0 data-[flip=true]:border-r-0 data-[flip=true]:border-t-0',
|
|
||||||
'right':
|
|
||||||
'data-[flip=false]:border-r-0 data-[flip=false]:border-t-0 data-[flip=true]:border-l-0 data-[flip=true]:border-b-0',
|
|
||||||
'right top':
|
|
||||||
'data-[flip=false]:border-r-0 data-[flip=false]:border-t-0 data-[flip=true]:border-l-0 data-[flip=true]:border-b-0',
|
|
||||||
'right bottom':
|
|
||||||
'data-[flip=false]:border-r-0 data-[flip=false]:border-t-0 data-[flip=true]:border-l-0 data-[flip=true]:border-b-0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const popoverBackdropStyle = tva({
|
|
||||||
base: 'absolute left-0 top-0 right-0 bottom-0 web:cursor-default',
|
|
||||||
});
|
|
||||||
|
|
||||||
const popoverCloseButtonStyle = tva({
|
|
||||||
base: 'group/popover-close-button z-[1] rounded-sm data-[focus-visible=true]:web:bg-background-100 web:outline-0 web:cursor-pointer',
|
|
||||||
});
|
|
||||||
|
|
||||||
const popoverContentStyle = tva({
|
|
||||||
base: 'bg-background-0 rounded-lg overflow-hidden border border-outline-100 w-full',
|
|
||||||
parentVariants: {
|
|
||||||
size: {
|
|
||||||
xs: 'max-w-[360px] p-3.5',
|
|
||||||
sm: 'max-w-[420px] p-4',
|
|
||||||
md: 'max-w-[510px] p-[18px]',
|
|
||||||
lg: 'max-w-[640px] p-5',
|
|
||||||
full: 'p-6',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const popoverHeaderStyle = tva({
|
|
||||||
base: 'flex-row justify-between items-center',
|
|
||||||
});
|
|
||||||
|
|
||||||
const popoverBodyStyle = tva({
|
|
||||||
base: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const popoverFooterStyle = tva({
|
|
||||||
base: 'flex-row justify-between items-center',
|
|
||||||
});
|
|
||||||
|
|
||||||
type IPopoverProps = React.ComponentProps<typeof UIPopover> &
|
|
||||||
VariantProps<typeof popoverStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverArrowProps = React.ComponentProps<typeof UIPopover.Arrow> &
|
|
||||||
VariantProps<typeof popoverArrowStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverContentProps = React.ComponentProps<typeof UIPopover.Content> &
|
|
||||||
VariantProps<typeof popoverContentStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverHeaderProps = React.ComponentProps<typeof UIPopover.Header> &
|
|
||||||
VariantProps<typeof popoverHeaderStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverFooterProps = React.ComponentProps<typeof UIPopover.Footer> &
|
|
||||||
VariantProps<typeof popoverFooterStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverBodyProps = React.ComponentProps<typeof UIPopover.Body> &
|
|
||||||
VariantProps<typeof popoverBodyStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverBackdropProps = React.ComponentProps<typeof UIPopover.Backdrop> &
|
|
||||||
VariantProps<typeof popoverBackdropStyle> & { className?: string };
|
|
||||||
|
|
||||||
type IPopoverCloseButtonProps = React.ComponentProps<
|
|
||||||
typeof UIPopover.CloseButton
|
|
||||||
> &
|
|
||||||
VariantProps<typeof popoverCloseButtonStyle> & { className?: string };
|
|
||||||
|
|
||||||
const Popover = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover>,
|
|
||||||
IPopoverProps
|
|
||||||
>(function Popover(
|
|
||||||
{ className, size = 'md', placement = 'bottom', ...props },
|
|
||||||
ref
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<UIPopover
|
|
||||||
ref={ref}
|
|
||||||
placement={placement}
|
|
||||||
{...props}
|
|
||||||
className={popoverStyle({ size, class: className })}
|
|
||||||
context={{ size, placement }}
|
|
||||||
pointerEvents="box-none"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Content>,
|
|
||||||
IPopoverContentProps
|
|
||||||
>(function PopoverContent({ className, size, ...props }, ref) {
|
|
||||||
const { size: parentSize } = useStyleContext(SCOPE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UIPopover.Content
|
|
||||||
ref={ref}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
mass: 0.9,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 50,
|
|
||||||
delay: 50,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={popoverContentStyle({
|
|
||||||
parentVariants: {
|
|
||||||
size: parentSize,
|
|
||||||
},
|
|
||||||
size,
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
pointerEvents="auto"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverArrow = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Arrow>,
|
|
||||||
IPopoverArrowProps
|
|
||||||
>(function PopoverArrow({ className, ...props }, ref) {
|
|
||||||
const { placement } = useStyleContext(SCOPE);
|
|
||||||
return (
|
|
||||||
<UIPopover.Arrow
|
|
||||||
ref={ref}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 250,
|
|
||||||
mass: 0.9,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 50,
|
|
||||||
delay: 50,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
className={popoverArrowStyle({
|
|
||||||
class: className,
|
|
||||||
placement,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverBackdrop = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Backdrop>,
|
|
||||||
IPopoverBackdropProps
|
|
||||||
>(function PopoverBackdrop({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.Backdrop
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: 0.1,
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
damping: 18,
|
|
||||||
stiffness: 450,
|
|
||||||
mass: 0.9,
|
|
||||||
opacity: {
|
|
||||||
type: 'timing',
|
|
||||||
duration: 50,
|
|
||||||
delay: 50,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
className={popoverBackdropStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverBody = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Body>,
|
|
||||||
IPopoverBodyProps
|
|
||||||
>(function PopoverBody({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.Body
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={popoverBodyStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverCloseButton = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.CloseButton>,
|
|
||||||
IPopoverCloseButtonProps
|
|
||||||
>(function PopoverCloseButton({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.CloseButton
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={popoverCloseButtonStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverFooter = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Footer>,
|
|
||||||
IPopoverFooterProps
|
|
||||||
>(function PopoverFooter({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.Footer
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={popoverFooterStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PopoverHeader = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UIPopover.Header>,
|
|
||||||
IPopoverHeaderProps
|
|
||||||
>(function PopoverHeader({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UIPopover.Header
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={popoverHeaderStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Popover.displayName = 'Popover';
|
|
||||||
PopoverArrow.displayName = 'PopoverArrow';
|
|
||||||
PopoverBackdrop.displayName = 'PopoverBackdrop';
|
|
||||||
PopoverContent.displayName = 'PopoverContent';
|
|
||||||
PopoverHeader.displayName = 'PopoverHeader';
|
|
||||||
PopoverFooter.displayName = 'PopoverFooter';
|
|
||||||
PopoverBody.displayName = 'PopoverBody';
|
|
||||||
PopoverCloseButton.displayName = 'PopoverCloseButton';
|
|
||||||
|
|
||||||
export {
|
|
||||||
Popover,
|
|
||||||
PopoverBackdrop,
|
|
||||||
PopoverArrow,
|
|
||||||
PopoverCloseButton,
|
|
||||||
PopoverFooter,
|
|
||||||
PopoverHeader,
|
|
||||||
PopoverBody,
|
|
||||||
PopoverContent,
|
|
||||||
};
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import React from 'react';
|
|
||||||
import { createTooltip } from '@gluestack-ui/core/tooltip/creator';
|
|
||||||
import { View, Text, ViewStyle } from 'react-native';
|
|
||||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
|
||||||
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
|
||||||
import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils';
|
|
||||||
import {
|
|
||||||
Motion,
|
|
||||||
AnimatePresence,
|
|
||||||
MotionComponentProps,
|
|
||||||
} from '@legendapp/motion';
|
|
||||||
import { cssInterop } from 'nativewind';
|
|
||||||
|
|
||||||
type IMotionViewProps = React.ComponentProps<typeof View> &
|
|
||||||
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
|
||||||
|
|
||||||
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
|
||||||
|
|
||||||
export const UITooltip = createTooltip({
|
|
||||||
Root: withStyleContext(View),
|
|
||||||
Content: MotionView,
|
|
||||||
Text: Text,
|
|
||||||
AnimatePresence: AnimatePresence,
|
|
||||||
});
|
|
||||||
|
|
||||||
cssInterop(MotionView, { className: 'style' });
|
|
||||||
|
|
||||||
const tooltipStyle = tva({
|
|
||||||
base: 'w-full h-full web:pointer-events-none',
|
|
||||||
});
|
|
||||||
|
|
||||||
const tooltipContentStyle = tva({
|
|
||||||
base: 'py-1 px-3 rounded-sm bg-background-900 web:pointer-events-auto',
|
|
||||||
});
|
|
||||||
|
|
||||||
const tooltipTextStyle = tva({
|
|
||||||
base: 'font-normal tracking-normal web:select-none text-xs text-typography-50',
|
|
||||||
|
|
||||||
variants: {
|
|
||||||
isTruncated: {
|
|
||||||
true: 'line-clamp-1 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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
type ITooltipProps = React.ComponentProps<typeof UITooltip> &
|
|
||||||
VariantProps<typeof tooltipStyle> & { className?: string };
|
|
||||||
type ITooltipContentProps = React.ComponentProps<typeof UITooltip.Content> &
|
|
||||||
VariantProps<typeof tooltipContentStyle> & { className?: string };
|
|
||||||
type ITooltipTextProps = React.ComponentProps<typeof UITooltip.Text> &
|
|
||||||
VariantProps<typeof tooltipTextStyle> & { className?: string };
|
|
||||||
|
|
||||||
const Tooltip = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UITooltip>,
|
|
||||||
ITooltipProps
|
|
||||||
>(function Tooltip({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UITooltip
|
|
||||||
ref={ref}
|
|
||||||
className={tooltipStyle({ class: className })}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UITooltip.Content>,
|
|
||||||
ITooltipContentProps & { className?: string }
|
|
||||||
>(function TooltipContent({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UITooltip.Content
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={tooltipContentStyle({
|
|
||||||
class: className,
|
|
||||||
})}
|
|
||||||
pointerEvents="auto"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const TooltipText = React.forwardRef<
|
|
||||||
React.ComponentRef<typeof UITooltip.Text>,
|
|
||||||
ITooltipTextProps & { className?: string }
|
|
||||||
>(function TooltipText({ size, className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<UITooltip.Text
|
|
||||||
ref={ref}
|
|
||||||
className={tooltipTextStyle({ size, class: className })}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Tooltip.displayName = 'Tooltip';
|
|
||||||
TooltipContent.displayName = 'TooltipContent';
|
|
||||||
TooltipText.displayName = 'TooltipText';
|
|
||||||
|
|
||||||
export { Tooltip, TooltipContent, TooltipText };
|
|
||||||
578
components/ui/modal.tsx
Normal file
578
components/ui/modal.tsx
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import React, { ReactNode, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Dimensions,
|
||||||
|
Pressable,
|
||||||
|
Modal as RNModal,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export interface ModalProps {
|
||||||
|
/** Whether the modal dialog is visible or not */
|
||||||
|
open?: boolean;
|
||||||
|
/** The modal dialog's title */
|
||||||
|
title?: ReactNode;
|
||||||
|
/** Whether a close (x) button is visible on top right or not */
|
||||||
|
closable?: boolean;
|
||||||
|
/** Custom close icon */
|
||||||
|
closeIcon?: ReactNode;
|
||||||
|
/** Whether to close the modal dialog when the mask (area outside the modal) is clicked */
|
||||||
|
maskClosable?: boolean;
|
||||||
|
/** Centered Modal */
|
||||||
|
centered?: boolean;
|
||||||
|
/** Width of the modal dialog */
|
||||||
|
width?: number | string;
|
||||||
|
/** Whether to apply loading visual effect for OK button or not */
|
||||||
|
confirmLoading?: boolean;
|
||||||
|
/** Text of the OK button */
|
||||||
|
okText?: string;
|
||||||
|
/** Text of the Cancel button */
|
||||||
|
cancelText?: string;
|
||||||
|
/** Button type of the OK button */
|
||||||
|
okType?: "primary" | "default" | "dashed" | "text" | "link";
|
||||||
|
/** Footer content, set as footer={null} when you don't need default buttons */
|
||||||
|
footer?: ReactNode | null;
|
||||||
|
/** Whether show mask or not */
|
||||||
|
mask?: boolean;
|
||||||
|
/** The z-index of the Modal */
|
||||||
|
zIndex?: number;
|
||||||
|
/** Specify a function that will be called when a user clicks the OK button */
|
||||||
|
onOk?: (e?: any) => void | Promise<void>;
|
||||||
|
/** Specify a function that will be called when a user clicks mask, close button on top right or Cancel button */
|
||||||
|
onCancel?: (e?: any) => void;
|
||||||
|
/** Callback when the animation ends when Modal is turned on and off */
|
||||||
|
afterOpenChange?: (open: boolean) => void;
|
||||||
|
/** Specify a function that will be called when modal is closed completely */
|
||||||
|
afterClose?: () => void;
|
||||||
|
/** Custom className */
|
||||||
|
className?: string;
|
||||||
|
/** Modal body content */
|
||||||
|
children?: ReactNode;
|
||||||
|
/** Whether to unmount child components on close */
|
||||||
|
destroyOnClose?: boolean;
|
||||||
|
/** The ok button props */
|
||||||
|
okButtonProps?: any;
|
||||||
|
/** The cancel button props */
|
||||||
|
cancelButtonProps?: any;
|
||||||
|
/** Whether support press esc to close */
|
||||||
|
keyboard?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmModalProps extends Omit<ModalProps, "open"> {
|
||||||
|
/** Type of the confirm modal */
|
||||||
|
type?: "info" | "success" | "error" | "warning" | "confirm";
|
||||||
|
/** Content */
|
||||||
|
content?: ReactNode;
|
||||||
|
/** Custom icon */
|
||||||
|
icon?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Component
|
||||||
|
const Modal: React.FC<ModalProps> & {
|
||||||
|
info: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
success: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
error: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
warning: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
confirm: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
useModal: () => [
|
||||||
|
{
|
||||||
|
info: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
success: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
error: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
warning: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
confirm: (props: ConfirmModalProps) => ModalInstance;
|
||||||
|
},
|
||||||
|
ReactNode
|
||||||
|
];
|
||||||
|
} = ({
|
||||||
|
open = false,
|
||||||
|
title,
|
||||||
|
closable = true,
|
||||||
|
closeIcon,
|
||||||
|
maskClosable = true,
|
||||||
|
centered = false,
|
||||||
|
width = 520,
|
||||||
|
confirmLoading = false,
|
||||||
|
okText = "OK",
|
||||||
|
cancelText = "Cancel",
|
||||||
|
okType = "primary",
|
||||||
|
footer,
|
||||||
|
mask = true,
|
||||||
|
zIndex = 1000,
|
||||||
|
onOk,
|
||||||
|
onCancel,
|
||||||
|
afterOpenChange,
|
||||||
|
afterClose,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
destroyOnClose = false,
|
||||||
|
okButtonProps,
|
||||||
|
cancelButtonProps,
|
||||||
|
keyboard = true,
|
||||||
|
}) => {
|
||||||
|
const [visible, setVisible] = useState(open);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVisible(open);
|
||||||
|
if (afterOpenChange) {
|
||||||
|
afterOpenChange(open);
|
||||||
|
}
|
||||||
|
}, [open, afterOpenChange]);
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
if (onOk) {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await onOk();
|
||||||
|
// Không tự động đóng modal - để parent component quyết định
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Modal onOk error:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
} else {
|
||||||
|
// Nếu không có onCancel, tự động đóng modal
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMaskPress = () => {
|
||||||
|
if (maskClosable) {
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRequestClose = () => {
|
||||||
|
if (keyboard) {
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible && afterClose) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
afterClose();
|
||||||
|
}, 300); // Wait for animation to complete
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [visible, afterClose]);
|
||||||
|
|
||||||
|
const renderFooter = () => {
|
||||||
|
if (footer === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (footer !== undefined) {
|
||||||
|
return <View style={styles.footer}>{footer}</View>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.cancelButton]}
|
||||||
|
onPress={handleCancel}
|
||||||
|
disabled={loading || confirmLoading}
|
||||||
|
{...cancelButtonProps}
|
||||||
|
>
|
||||||
|
<Text style={styles.cancelButtonText}>{cancelText}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
styles.okButton,
|
||||||
|
okType === "primary" && styles.primaryButton,
|
||||||
|
(loading || confirmLoading) && styles.disabledButton,
|
||||||
|
]}
|
||||||
|
onPress={handleOk}
|
||||||
|
disabled={loading || confirmLoading}
|
||||||
|
{...okButtonProps}
|
||||||
|
>
|
||||||
|
{loading || confirmLoading ? (
|
||||||
|
<ActivityIndicator color="#fff" size="small" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.okButtonText}>{okText}</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalWidth =
|
||||||
|
typeof width === "number" ? width : Dimensions.get("window").width * 0.9;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RNModal
|
||||||
|
visible={visible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={handleRequestClose}
|
||||||
|
statusBarTranslucent
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.overlay,
|
||||||
|
centered && styles.centered,
|
||||||
|
{ zIndex },
|
||||||
|
!mask && styles.noMask,
|
||||||
|
]}
|
||||||
|
onPress={handleMaskPress}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={[styles.modal, { width: modalWidth, maxWidth: "90%" }]}
|
||||||
|
onPress={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
{(title || closable) && (
|
||||||
|
<View style={styles.header}>
|
||||||
|
{title && (
|
||||||
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{closable && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.closeButton}
|
||||||
|
onPress={handleCancel}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
|
>
|
||||||
|
{closeIcon || (
|
||||||
|
<Ionicons name="close" size={24} color="#666" />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<View style={styles.body}>
|
||||||
|
{(!destroyOnClose || visible) && children}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
{renderFooter()}
|
||||||
|
</Pressable>
|
||||||
|
</Pressable>
|
||||||
|
</RNModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Confirm Modal Component
|
||||||
|
const ConfirmModal: React.FC<
|
||||||
|
ConfirmModalProps & { visible: boolean; onClose: () => void }
|
||||||
|
> = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
type = "confirm",
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
icon,
|
||||||
|
okText = "OK",
|
||||||
|
cancelText = "Cancel",
|
||||||
|
onOk,
|
||||||
|
onCancel,
|
||||||
|
...restProps
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const getIcon = () => {
|
||||||
|
if (icon !== undefined) return icon;
|
||||||
|
|
||||||
|
const iconProps = { size: 24, style: { marginRight: 12 } };
|
||||||
|
switch (type) {
|
||||||
|
case "info":
|
||||||
|
return (
|
||||||
|
<Ionicons name="information-circle" color="#1890ff" {...iconProps} />
|
||||||
|
);
|
||||||
|
case "success":
|
||||||
|
return (
|
||||||
|
<Ionicons name="checkmark-circle" color="#52c41a" {...iconProps} />
|
||||||
|
);
|
||||||
|
case "error":
|
||||||
|
return <Ionicons name="close-circle" color="#ff4d4f" {...iconProps} />;
|
||||||
|
case "warning":
|
||||||
|
return <Ionicons name="warning" color="#faad14" {...iconProps} />;
|
||||||
|
default:
|
||||||
|
return <Ionicons name="help-circle" color="#1890ff" {...iconProps} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
if (onOk) {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await onOk();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Confirm modal onOk error:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={visible}
|
||||||
|
title={title}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={type === "confirm" ? handleCancel : undefined}
|
||||||
|
okText={okText}
|
||||||
|
cancelText={cancelText}
|
||||||
|
confirmLoading={loading}
|
||||||
|
footer={
|
||||||
|
type === "confirm" ? undefined : (
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.okButton, styles.primaryButton]}
|
||||||
|
onPress={handleOk}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator color="#fff" size="small" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.okButtonText}>{okText}</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<View style={styles.confirmContent}>
|
||||||
|
{getIcon()}
|
||||||
|
<Text style={styles.confirmText}>{content}</Text>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modal Instance
|
||||||
|
export interface ModalInstance {
|
||||||
|
destroy: () => void;
|
||||||
|
update: (config: ConfirmModalProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container for imperatively created modals - Not used in React Native
|
||||||
|
// Static methods will return instance but won't render imperatively
|
||||||
|
// Use Modal.useModal() hook for proper context support
|
||||||
|
|
||||||
|
const createConfirmModal = (config: ConfirmModalProps): ModalInstance => {
|
||||||
|
console.warn(
|
||||||
|
"Modal static methods are not fully supported in React Native. Please use Modal.useModal() hook for better context support."
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy: () => {
|
||||||
|
console.warn(
|
||||||
|
"Modal.destroy() called but static modals are not supported in React Native"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
update: (newConfig: ConfirmModalProps) => {
|
||||||
|
console.warn(
|
||||||
|
"Modal.update() called but static modals are not supported in React Native"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static methods
|
||||||
|
Modal.info = (props: ConfirmModalProps) =>
|
||||||
|
createConfirmModal({ ...props, type: "info" });
|
||||||
|
Modal.success = (props: ConfirmModalProps) =>
|
||||||
|
createConfirmModal({ ...props, type: "success" });
|
||||||
|
Modal.error = (props: ConfirmModalProps) =>
|
||||||
|
createConfirmModal({ ...props, type: "error" });
|
||||||
|
Modal.warning = (props: ConfirmModalProps) =>
|
||||||
|
createConfirmModal({ ...props, type: "warning" });
|
||||||
|
Modal.confirm = (props: ConfirmModalProps) =>
|
||||||
|
createConfirmModal({ ...props, type: "confirm" });
|
||||||
|
|
||||||
|
// useModal hook
|
||||||
|
Modal.useModal = () => {
|
||||||
|
const [modals, setModals] = useState<ReactNode[]>([]);
|
||||||
|
|
||||||
|
const createModal = (
|
||||||
|
config: ConfirmModalProps,
|
||||||
|
type: ConfirmModalProps["type"]
|
||||||
|
) => {
|
||||||
|
const id = `modal-${Date.now()}-${Math.random()}`;
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
setModals((prev) => prev.filter((modal: any) => modal.key !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = (newConfig: ConfirmModalProps) => {
|
||||||
|
setModals((prev) =>
|
||||||
|
prev.map((modal: any) =>
|
||||||
|
modal.key === id ? (
|
||||||
|
<ConfirmModal
|
||||||
|
key={id}
|
||||||
|
visible={true}
|
||||||
|
onClose={destroy}
|
||||||
|
{...config}
|
||||||
|
{...newConfig}
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
modal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalElement = (
|
||||||
|
<ConfirmModal
|
||||||
|
key={id}
|
||||||
|
visible={true}
|
||||||
|
onClose={destroy}
|
||||||
|
{...config}
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
setModals((prev) => [...prev, modalElement]);
|
||||||
|
|
||||||
|
return { destroy, update };
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalMethods = {
|
||||||
|
info: (props: ConfirmModalProps) => createModal(props, "info"),
|
||||||
|
success: (props: ConfirmModalProps) => createModal(props, "success"),
|
||||||
|
error: (props: ConfirmModalProps) => createModal(props, "error"),
|
||||||
|
warning: (props: ConfirmModalProps) => createModal(props, "warning"),
|
||||||
|
confirm: (props: ConfirmModalProps) => createModal(props, "confirm"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextHolder = <>{modals}</>;
|
||||||
|
|
||||||
|
return [modalMethods, contextHolder];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
overlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.45)",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
paddingTop: 100,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
centered: {
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingTop: 0,
|
||||||
|
},
|
||||||
|
noMask: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderRadius: 8,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 8,
|
||||||
|
maxHeight: "90%",
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingTop: 20,
|
||||||
|
paddingBottom: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#f0f0f0",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#000",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
padding: 4,
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingVertical: 20,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingBottom: 20,
|
||||||
|
paddingTop: 12,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 6,
|
||||||
|
minWidth: 70,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: 36,
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#d9d9d9",
|
||||||
|
},
|
||||||
|
cancelButtonText: {
|
||||||
|
color: "#000",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
okButton: {
|
||||||
|
backgroundColor: "#1890ff",
|
||||||
|
},
|
||||||
|
primaryButton: {
|
||||||
|
backgroundColor: "#1890ff",
|
||||||
|
},
|
||||||
|
okButtonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
disabledButton: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
confirmContent: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
confirmText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#000",
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
@@ -1,26 +1,30 @@
|
|||||||
/**
|
/**
|
||||||
* Theme Context Hook for managing app-wide theme state
|
* Theme Context Hook for managing app-wide theme state.
|
||||||
* Supports Light, Dark, and System (automatic) modes
|
* Supports Light, Dark, and System (automatic) modes across Expo platforms.
|
||||||
*
|
*
|
||||||
* IMPORTANT: Requires expo-system-ui plugin in app.json for Android support
|
* IMPORTANT: Requires expo-system-ui plugin in app.json for Android status bar support.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Colors, ColorName } from "@/constants/theme";
|
import { ColorName, Colors } from "@/constants/theme";
|
||||||
import { getStorageItem, setStorageItem } from "@/utils/storage";
|
import { getStorageItem, setStorageItem } from "@/utils/storage";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
ReactNode,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
useColorScheme as useSystemColorScheme,
|
|
||||||
Appearance,
|
Appearance,
|
||||||
|
AppState,
|
||||||
|
AppStateStatus,
|
||||||
|
useColorScheme as useRNColorScheme,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
|
||||||
type ThemeMode = "light" | "dark" | "system";
|
export type ThemeMode = "light" | "dark" | "system";
|
||||||
type ColorScheme = "light" | "dark";
|
export type ColorScheme = "light" | "dark";
|
||||||
|
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
themeMode: ThemeMode;
|
themeMode: ThemeMode;
|
||||||
@@ -28,146 +32,162 @@ interface ThemeContextType {
|
|||||||
colors: typeof Colors.light;
|
colors: typeof Colors.light;
|
||||||
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
||||||
getColor: (colorName: ColorName) => string;
|
getColor: (colorName: ColorName) => string;
|
||||||
|
isHydrated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
const THEME_STORAGE_KEY = "theme_mode";
|
const THEME_STORAGE_KEY = "theme_mode";
|
||||||
|
|
||||||
|
const getSystemScheme = (): ColorScheme => {
|
||||||
|
const scheme = Appearance.getColorScheme();
|
||||||
|
// console.log("[Theme] Appearance.getColorScheme():", scheme);
|
||||||
|
return scheme === "dark" ? "dark" : "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isThemeMode = (value: unknown): value is ThemeMode => {
|
||||||
|
return value === "light" || value === "dark" || value === "system";
|
||||||
|
};
|
||||||
|
|
||||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
// State để force re-render khi system theme thay đổi
|
const [systemScheme, setSystemScheme] =
|
||||||
const [systemTheme, setSystemTheme] = useState<ColorScheme>(() => {
|
useState<ColorScheme>(getSystemScheme);
|
||||||
const current = Appearance.getColorScheme();
|
|
||||||
console.log("[Theme] Initial system theme:", current);
|
|
||||||
return current === "dark" ? "dark" : "light";
|
|
||||||
});
|
|
||||||
|
|
||||||
// State lưu user's choice (light/dark/system)
|
|
||||||
const [themeMode, setThemeModeState] = useState<ThemeMode>("system");
|
const [themeMode, setThemeModeState] = useState<ThemeMode>("system");
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isHydrated, setIsHydrated] = useState(false);
|
||||||
|
|
||||||
// Listen vào system theme changes - đăng ký ngay từ đầu
|
const syncSystemScheme = useCallback(() => {
|
||||||
|
const next = getSystemScheme();
|
||||||
|
// console.log("[Theme] syncSystemScheme computed:", next);
|
||||||
|
setSystemScheme((current) => (current === next ? current : next));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const rnScheme = useRNColorScheme();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[Theme] Registering appearance listener");
|
if (!rnScheme) return;
|
||||||
|
const next = rnScheme === "dark" ? "dark" : "light";
|
||||||
|
// console.log("[Theme] useColorScheme hook emitted:", rnScheme);
|
||||||
|
setSystemScheme((current) => (current === next ? current : next));
|
||||||
|
}, [rnScheme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const subscription = Appearance.addChangeListener(({ colorScheme }) => {
|
const subscription = Appearance.addChangeListener(({ colorScheme }) => {
|
||||||
const newScheme = colorScheme === "dark" ? "dark" : "light";
|
const next = colorScheme === "dark" ? "dark" : "light";
|
||||||
console.log(
|
// console.log("[Theme] Appearance listener fired with:", colorScheme);
|
||||||
"[Theme] System theme changed to:",
|
setSystemScheme((current) => (current === next ? current : next));
|
||||||
newScheme,
|
|
||||||
"at",
|
|
||||||
new Date().toLocaleTimeString()
|
|
||||||
);
|
|
||||||
setSystemTheme(newScheme);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Double check current theme khi mount
|
syncSystemScheme();
|
||||||
const currentScheme = Appearance.getColorScheme();
|
|
||||||
const current = currentScheme === "dark" ? "dark" : "light";
|
|
||||||
if (current !== systemTheme) {
|
|
||||||
console.log("[Theme] Syncing system theme on mount:", current);
|
|
||||||
setSystemTheme(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log("[Theme] Removing appearance listener");
|
|
||||||
subscription.remove();
|
subscription.remove();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [syncSystemScheme]);
|
||||||
|
|
||||||
// Xác định colorScheme cuối cùng
|
|
||||||
const colorScheme: ColorScheme =
|
|
||||||
themeMode === "system" ? systemTheme : themeMode;
|
|
||||||
|
|
||||||
const colors = Colors[colorScheme];
|
|
||||||
|
|
||||||
// Log để debug
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(
|
|
||||||
"[Theme] Current state - Mode:",
|
|
||||||
themeMode,
|
|
||||||
"| Scheme:",
|
|
||||||
colorScheme,
|
|
||||||
"| System:",
|
|
||||||
systemTheme
|
|
||||||
);
|
|
||||||
}, [themeMode, colorScheme, systemTheme]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadThemeMode = async () => {
|
// console.log("[Theme] System scheme detected:", systemScheme);
|
||||||
try {
|
}, [systemScheme]);
|
||||||
const savedThemeMode = await getStorageItem(THEME_STORAGE_KEY);
|
|
||||||
if (
|
useEffect(() => {
|
||||||
savedThemeMode &&
|
const handleAppStateChange = (nextState: AppStateStatus) => {
|
||||||
["light", "dark", "system"].includes(savedThemeMode)
|
if (nextState === "active") {
|
||||||
) {
|
// console.log("[Theme] AppState active → scheduling system scheme sync");
|
||||||
setThemeModeState(savedThemeMode as ThemeMode);
|
setTimeout(() => {
|
||||||
}
|
// console.log("[Theme] AppState sync callback running");
|
||||||
} catch (error) {
|
syncSystemScheme();
|
||||||
console.warn("Failed to load theme mode:", error);
|
}, 100);
|
||||||
} finally {
|
|
||||||
setIsLoaded(true);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadThemeMode();
|
|
||||||
|
const subscription = AppState.addEventListener(
|
||||||
|
"change",
|
||||||
|
handleAppStateChange
|
||||||
|
);
|
||||||
|
return () => subscription.remove();
|
||||||
|
}, [syncSystemScheme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const hydrateThemeMode = async () => {
|
||||||
|
try {
|
||||||
|
const savedThemeMode = await getStorageItem(THEME_STORAGE_KEY);
|
||||||
|
if (isMounted && isThemeMode(savedThemeMode)) {
|
||||||
|
setThemeModeState(savedThemeMode);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Theme] Failed to load theme mode:", error);
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setIsHydrated(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
hydrateThemeMode();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setThemeMode = async (mode: ThemeMode) => {
|
const colorScheme: ColorScheme =
|
||||||
|
themeMode === "system" ? systemScheme : themeMode;
|
||||||
|
|
||||||
|
const colors = useMemo(() => Colors[colorScheme], [colorScheme]);
|
||||||
|
|
||||||
|
const setThemeMode = useCallback(async (mode: ThemeMode) => {
|
||||||
|
setThemeModeState(mode);
|
||||||
try {
|
try {
|
||||||
setThemeModeState(mode);
|
|
||||||
await setStorageItem(THEME_STORAGE_KEY, mode);
|
await setStorageItem(THEME_STORAGE_KEY, mode);
|
||||||
console.log("[Theme] Changed to:", mode);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to save theme mode:", error);
|
console.warn("[Theme] Failed to save theme mode:", error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getColor = (colorName: ColorName): string => {
|
useEffect(() => {
|
||||||
return colors[colorName] || colors.text;
|
// console.log("[Theme] window defined:", typeof window !== "undefined");
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// Chờ theme load xong trước khi render
|
const getColor = useCallback(
|
||||||
if (!isLoaded) {
|
(colorName: ColorName) => colors[colorName] ?? colors.text,
|
||||||
// Render với default theme (system) khi đang load
|
[colors]
|
||||||
return (
|
);
|
||||||
<ThemeContext.Provider
|
|
||||||
value={{
|
|
||||||
themeMode: "system",
|
|
||||||
colorScheme: systemTheme,
|
|
||||||
colors: Colors[systemTheme],
|
|
||||||
setThemeMode: async () => {},
|
|
||||||
getColor: (colorName: ColorName) =>
|
|
||||||
Colors[systemTheme][colorName] || Colors[systemTheme].text,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ThemeContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const value: ThemeContextType = {
|
useEffect(() => {
|
||||||
themeMode,
|
// console.log("[Theme] Mode:", themeMode);
|
||||||
colorScheme,
|
}, [themeMode]);
|
||||||
colors,
|
|
||||||
setThemeMode,
|
useEffect(() => {
|
||||||
getColor,
|
// console.log("[Theme] Derived colorScheme:", colorScheme);
|
||||||
};
|
}, [colorScheme]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
themeMode,
|
||||||
|
colorScheme,
|
||||||
|
colors,
|
||||||
|
setThemeMode,
|
||||||
|
getColor,
|
||||||
|
isHydrated,
|
||||||
|
}),
|
||||||
|
[themeMode, colorScheme, colors, setThemeMode, getColor, isHydrated]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useThemeContext(): ThemeContextType {
|
export function useTheme(): ThemeContextType {
|
||||||
const context = useContext(ThemeContext);
|
const context = useContext(ThemeContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useThemeContext must be used within a ThemeProvider");
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy hook cho backward compatibility
|
export const useThemeContext = useTheme;
|
||||||
|
|
||||||
export function useColorScheme(): ColorScheme {
|
export function useColorScheme(): ColorScheme {
|
||||||
const { colorScheme } = useThemeContext();
|
return useTheme().colorScheme;
|
||||||
return colorScheme;
|
|
||||||
}
|
}
|
||||||
|
|||||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -10,8 +10,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/html-elements": "^0.10.1",
|
"@expo/html-elements": "^0.10.1",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@gluestack-ui/core": "^3.0.12",
|
|
||||||
"@gluestack-ui/utils": "^3.0.11",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@islacel/react-native-custom-switch": "^1.0.10",
|
"@islacel/react-native-custom-switch": "^1.0.10",
|
||||||
"@legendapp/motion": "^2.5.3",
|
"@legendapp/motion": "^2.5.3",
|
||||||
@@ -2357,43 +2355,6 @@
|
|||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@gluestack-ui/core": {
|
|
||||||
"version": "3.0.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@gluestack-ui/core/-/core-3.0.12.tgz",
|
|
||||||
"integrity": "sha512-TyNjDUJrZF/FTqcSEPBR87wZQ3yvbWuTjn0tG5AFYzYfMCw0IpfTigmzoajN9KHensN0xNwHoAkXKaHlhy11yQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@gluestack-ui/utils": ">=2.0.0",
|
|
||||||
"react": ">=16.8.0",
|
|
||||||
"react-native": ">=0.64.0",
|
|
||||||
"react-native-safe-area-context": ">=4.0.0",
|
|
||||||
"react-native-svg": ">=12.0.0",
|
|
||||||
"react-native-web": ">=0.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@gluestack-ui/utils": {
|
|
||||||
"version": "3.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@gluestack-ui/utils/-/utils-3.0.11.tgz",
|
|
||||||
"integrity": "sha512-4stxK98v07NFAGvSI4Dxje/xbnftaY45VcZglZUxlAr8FFVLNFcjXUTSnVWqog0DBp2oJ7Nk/AYUpT2KkpI+7A==",
|
|
||||||
"dependencies": {
|
|
||||||
"dom-helpers": "^6.0.1",
|
|
||||||
"react-aria": "^3.41.1",
|
|
||||||
"react-stately": "^3.39.0",
|
|
||||||
"tailwind-variants": "0.1.20"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16.8.0",
|
|
||||||
"react-native": ">=0.64.0",
|
|
||||||
"react-native-web": ">=0.19.0",
|
|
||||||
"tailwindcss": ">=3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@hookform/resolvers": {
|
"node_modules/@hookform/resolvers": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
|
||||||
@@ -7379,6 +7340,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-view-buffer": {
|
"node_modules/data-view-buffer": {
|
||||||
@@ -7625,16 +7587,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dom-helpers": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-IKySryuFwseGkrCA/pIqlwUPOD50w1Lj/B2Yief3vBOP18k5y4t+hTqKh55gULDVeJMRitcozve+g/wVFf4sFQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.27.1",
|
|
||||||
"csstype": "^3.1.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/html-elements": "^0.10.1",
|
"@expo/html-elements": "^0.10.1",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@gluestack-ui/core": "^3.0.12",
|
|
||||||
"@gluestack-ui/utils": "^3.0.11",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@islacel/react-native-custom-switch": "^1.0.10",
|
"@islacel/react-native-custom-switch": "^1.0.10",
|
||||||
"@legendapp/motion": "^2.5.3",
|
"@legendapp/motion": "^2.5.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user