thêm chức năng Sos và thêm glustack-ui

This commit is contained in:
Tran Anh Tuan
2025-11-04 16:24:54 +07:00
parent e535aaa1e8
commit 2137925ba9
33 changed files with 5533 additions and 171 deletions

76
components/AlarmList.tsx Normal file
View File

@@ -0,0 +1,76 @@
import dayjs from "dayjs";
import { FlatList, Text, TouchableOpacity, View } from "react-native";
type AlarmItem = {
name: string;
t: number;
level: number;
id: string;
};
type AlarmProp = {
alarmsData: AlarmItem[];
onPress?: (alarm: AlarmItem) => void;
};
const AlarmList = ({ alarmsData, onPress }: AlarmProp) => {
const sortedAlarmsData = [...alarmsData].sort((a, b) => b.level - a.level);
return (
<FlatList
data={sortedAlarmsData}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => onPress?.(item)}
className="flex flex-row gap-5 p-3 justify-start items-baseline w-full"
>
<View
className={`flex-none h-3 w-3 rounded-full ${getBackgroundColorByLevel(
item.level
)}`}
></View>
<View className="flex">
<Text className={`grow text-lg ${getTextColorByLevel(item.level)}`}>
{item.name}
</Text>
<Text className="grow text-md text-gray-400">
{formatTimestamp(item.t)}
</Text>
</View>
</TouchableOpacity>
)}
keyExtractor={(item) => item.id}
/>
);
};
const getBackgroundColorByLevel = (level: number) => {
switch (level) {
case 1:
return "bg-yellow-500";
case 2:
return "bg-orange-500";
case 3:
return "bg-red-500";
default:
return "bg-gray-500";
}
};
const getTextColorByLevel = (level: number) => {
switch (level) {
case 1:
return "text-yellow-600";
case 2:
return "text-orange-600";
case 3:
return "text-red-600";
default:
return "text-gray-600";
}
};
const formatTimestamp = (timestamp: number) => {
return dayjs.unix(timestamp).format("DD/MM/YYYY HH:mm:ss");
};
export default AlarmList;

View File

@@ -0,0 +1,17 @@
import { Text, View } from "react-native";
interface DescriptionProps {
title?: string;
description?: string;
}
export const Description = ({
title = "",
description = "",
}: DescriptionProps) => {
return (
<View className="flex-row gap-2 ">
<Text className="opacity-50 text-lg">{title}:</Text>
<Text className="text-lg">{description}</Text>
</View>
);
};

View File

@@ -0,0 +1,113 @@
import { convertToDMS, kmhToKnot } from "@/utils/geom";
import { MaterialIcons } from "@expo/vector-icons";
import { useEffect, useRef, useState } from "react";
import { Animated, TouchableOpacity, View } from "react-native";
import { Description } from "./Description";
type GPSInfoPanelProps = {
gpsData: Model.GPSResonse | undefined;
};
const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
const [isExpanded, setIsExpanded] = useState(true);
const [panelHeight, setPanelHeight] = useState(0);
const translateY = useRef(new Animated.Value(0)).current;
const blockBottom = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(translateY, {
toValue: isExpanded ? 0 : 200, // Dịch chuyển xuống 200px khi thu gọn
duration: 500,
useNativeDriver: true,
}).start();
}, [isExpanded]);
useEffect(() => {
const targetBottom = isExpanded ? panelHeight + 12 : 10;
Animated.timing(blockBottom, {
toValue: targetBottom,
duration: 500,
useNativeDriver: false,
}).start();
}, [isExpanded, panelHeight, blockBottom]);
const togglePanel = () => {
setIsExpanded(!isExpanded);
};
return (
<>
{/* Khối hình vuông */}
<Animated.View
style={{
position: "absolute",
bottom: blockBottom,
left: 5,
width: 48,
height: 48,
backgroundColor: "blue",
borderRadius: 4,
zIndex: 30,
}}
/>
<Animated.View
style={{
transform: [{ translateY }],
}}
className="absolute bottom-0 gap-3 right-0 p-3 left-0 h-auto w-full rounded-t-xl bg-white shadow-md"
onLayout={(event) => setPanelHeight(event.nativeEvent.layout.height)}
>
{/* Nút toggle ở top-right */}
<TouchableOpacity
onPress={togglePanel}
className="absolute top-2 right-2 z-10 bg-white rounded-full p-1 shadow-sm"
>
<MaterialIcons
name={isExpanded ? "expand-more" : "expand-less"}
size={20}
color="#666"
/>
</TouchableOpacity>
<View className="flex-row justify-between">
<View className="flex-1">
<Description
title="Kinh độ"
description={convertToDMS(gpsData?.lat ?? 0, true)}
/>
</View>
<View className="flex-1">
<Description
title="Vĩ độ"
description={convertToDMS(gpsData?.lon ?? 0, false)}
/>
</View>
</View>
<View className="flex-row justify-between">
<View className="flex-1">
<Description
title="Tốc độ"
description={`${kmhToKnot(gpsData?.s ?? 0).toString()} knot`}
/>
</View>
<View className="flex-1">
<Description title="Hướng" description={`${gpsData?.h ?? 0}°`} />
</View>
</View>
</Animated.View>
{/* Nút floating để mở lại panel khi thu gọn */}
{!isExpanded && (
<TouchableOpacity
onPress={togglePanel}
className="absolute bottom-5 right-2 z-20 bg-white rounded-full p-2 shadow-lg"
>
<MaterialIcons name="info-outline" size={24} />
</TouchableOpacity>
)}
</>
);
};
export default GPSInfoPanel;

View File

@@ -1,7 +1,9 @@
import { ANDROID_PLATFORM } from "@/constants";
import { usePlatform } from "@/hooks/use-platform";
import { getPolygonCenter } from "@/utils/polyline";
import React from "react";
import React, { useRef } from "react";
import { StyleSheet, Text, View } from "react-native";
import { Marker, Polygon } from "react-native-maps";
import { MapMarker, Marker, Polygon } from "react-native-maps";
export interface PolygonWithLabelProps {
coordinates: {
@@ -33,6 +35,8 @@ export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
if (!coordinates || coordinates.length < 3) {
return null;
}
const platform = usePlatform();
const markerRef = useRef<MapMarker>(null);
const centerPoint = getPolygonCenter(coordinates);
@@ -51,8 +55,7 @@ export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
const paddingScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.2), 0.5);
const minWidthScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.25), 0.9);
// console.log("Min Width Scale: ", minWidthScale);
markerRef.current?.showCallout();
return (
<>
<Polygon
@@ -64,10 +67,13 @@ export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
/>
{label && (
<Marker
ref={markerRef}
coordinate={centerPoint}
zIndex={50}
tracksViewChanges={false}
anchor={{ x: 0.5, y: 0.5 }}
title={platform === ANDROID_PLATFORM ? label : undefined}
description={platform === ANDROID_PLATFORM ? content : undefined}
>
<View style={styles.markerContainer}>
<View

View File

@@ -1,10 +1,12 @@
import { ANDROID_PLATFORM } from "@/constants";
import { usePlatform } from "@/hooks/use-platform";
import {
calculateTotalDistance,
getMiddlePointOfPolyline,
} from "@/utils/polyline";
import React from "react";
import React, { useRef } from "react";
import { StyleSheet, Text, View } from "react-native";
import { Marker, Polyline } from "react-native-maps";
import { MapMarker, Marker, Polyline } from "react-native-maps";
export interface PolylineWithLabelProps {
coordinates: {
@@ -12,6 +14,7 @@ export interface PolylineWithLabelProps {
longitude: number;
}[];
label?: string;
content?: string;
strokeColor?: string;
strokeWidth?: number;
showDistance?: boolean;
@@ -24,6 +27,7 @@ export interface PolylineWithLabelProps {
export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
coordinates,
label,
content,
strokeColor = "#FF5733",
strokeWidth = 4,
showDistance = false,
@@ -35,14 +39,15 @@ export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
const middlePoint = getMiddlePointOfPolyline(coordinates);
const distance = calculateTotalDistance(coordinates);
const platform = usePlatform();
const markerRef = useRef<MapMarker>(null);
let displayText = label || "";
if (showDistance) {
displayText += displayText
? ` (${distance.toFixed(2)}km)`
: `${distance.toFixed(2)}km`;
}
markerRef.current?.showCallout();
return (
<>
<Polyline
@@ -53,10 +58,13 @@ export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
/>
{displayText && (
<Marker
ref={markerRef}
coordinate={middlePoint}
zIndex={zIndex + 10}
tracksViewChanges={false}
anchor={{ x: 0.5, y: 0.5 }}
title={platform === ANDROID_PLATFORM ? label : undefined}
description={platform === ANDROID_PLATFORM ? content : undefined}
>
<View style={styles.markerContainer}>
<View style={styles.labelContainer}>

View File

@@ -0,0 +1,385 @@
import { showToastError } from "@/config";
import {
queryDeleteSos,
queryGetSos,
querySendSosMessage,
} from "@/controller/DeviceController";
import { sosMessage } from "@/utils/sosUtils";
import { MaterialIcons } from "@expo/vector-icons";
import { useEffect, useState } from "react";
import {
FlatList,
ScrollView,
StyleSheet,
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 [sosData, setSosData] = useState<Model.SosResponse | null>();
const [showConfirmSosDialog, setShowConfirmSosDialog] = useState(false);
const [selectedSosMessage, setSelectedSosMessage] = useState<number | null>(
null
);
const [customMessage, setCustomMessage] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const sosOptions = [
...sosMessage.map((msg) => ({ ma: msg.ma, moTa: msg.moTa })),
{ ma: 999, moTa: "Khác" },
];
const getSosData = async () => {
try {
const response = await queryGetSos();
setSosData(response.data);
} catch (error) {
console.error("Failed to fetch SOS data:", error);
}
};
useEffect(() => {
getSosData();
}, []);
const validateForm = () => {
const newErrors: { [key: string]: string } = {};
// Không cần validate sosMessage vì luôn có default value (11)
if (selectedSosMessage === 999 && customMessage.trim() === "") {
newErrors.customMessage = "Vui lòng nhập trạng thái";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleConfirmSos = async () => {
if (validateForm()) {
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
setShowConfirmSosDialog(false);
// Reset form
setSelectedSosMessage(null);
setCustomMessage("");
setErrors({});
await sendSosMessage(messageToSend);
}
};
const handleClickButton = async (isActive: boolean) => {
if (isActive) {
console.log("Active");
const resp = await queryDeleteSos();
if (resp.status === 200) {
await getSosData();
}
} else {
console.log("No Active");
setSelectedSosMessage(11); // Mặc định chọn lý do ma: 11
setShowConfirmSosDialog(true);
}
};
const sendSosMessage = async (message: string) => {
try {
const resp = await querySendSosMessage(message);
if (resp.status === 200) {
await getSosData();
}
} catch (error) {
console.error("Error when send sos: ", error);
showToastError("Không thể gửi tín hiệu SOS", "Lỗi");
}
};
return (
<>
<Button
className="shadow-md rounded-full"
size="lg"
action="negative"
onPress={() => handleClickButton(sosData?.active || false)}
>
<MaterialIcons name="warning" size={15} color="white" />
<ButtonText className="text-center">
{sosData?.active ? "Đang trong trạng thái khẩn cấp" : "Khẩn cấp"}
</ButtonText>
{/* <ButtonSpinner /> */}
{/* <ButtonIcon /> */}
</Button>
<Modal
isOpen={showConfirmSosDialog}
onClose={() => {
setShowConfirmSosDialog(false);
setSelectedSosMessage(null);
setCustomMessage("");
setErrors({});
}}
>
<ModalBackdrop />
<ModalContent>
<ModalHeader className="flex-col gap-0.5 items-center">
<Text
style={{ fontSize: 18, fontWeight: "bold", textAlign: "center" }}
>
Thông báo khẩn cấp
</Text>
</ModalHeader>
<ModalBody className="mb-4">
<ScrollView style={{ maxHeight: 400 }}>
{/* Dropdown Nội dung SOS */}
<View style={styles.formGroup}>
<Text style={styles.label}>Nội dung:</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 || "Chọn lý do"
: "Chọn lý do"}
</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" */}
{selectedSosMessage === 999 && (
<View style={styles.formGroup}>
<Text style={styles.label}>Nhập trạng thái</Text>
<TextInput
style={[
styles.input,
errors.customMessage ? styles.errorInput : {},
]}
placeholder="Mô tả trạng thái khẩn cấp"
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>Xác nhận</ButtonText>
</Button>
<Button
onPress={() => {
setShowConfirmSosDialog(false);
setSelectedSosMessage(null);
setCustomMessage("");
setErrors({});
}}
// className="w-1/3"
action="secondary"
>
<ButtonText>Hủy</ButtonText>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* Dropdown Modal - Nổi lên */}
{showDropdown && showConfirmSosDialog && (
<Modal isOpen={showDropdown} onClose={() => setShowDropdown(false)}>
<TouchableOpacity
style={styles.dropdownOverlay}
activeOpacity={1}
onPress={() => setShowDropdown(false)}
>
<View style={styles.dropdownModalContainer}>
<FlatList
data={sosOptions}
keyExtractor={(item) => item.ma.toString()}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.dropdownModalItem}
onPress={() => {
setSelectedSosMessage(item.ma);
setShowDropdown(false);
// Clear custom message nếu chọn khác lý do
if (item.ma !== 999) {
setCustomMessage("");
}
}}
>
<Text
style={[
styles.dropdownModalItemText,
selectedSosMessage === item.ma &&
styles.selectedItemText,
]}
>
{item.moTa}
</Text>
</TouchableOpacity>
)}
/>
</View>
</TouchableOpacity>
</Modal>
)}
</>
);
};
const styles = StyleSheet.create({
formGroup: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: "600",
marginBottom: 8,
color: "#333",
},
dropdownButton: {
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 12,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: "#fff",
},
errorBorder: {
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: {
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 12,
fontSize: 14,
color: "#333",
textAlignVertical: "top",
},
errorInput: {
borderColor: "#ff4444",
},
errorText: {
color: "#ff4444",
fontSize: 12,
marginTop: 4,
},
});
export default SosButton;

View File

@@ -0,0 +1,296 @@
'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,
};

View File

@@ -0,0 +1,434 @@
'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 };

View File

@@ -0,0 +1,309 @@
'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',
}),
};

View File

@@ -0,0 +1,87 @@
// 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>
);
}

View File

@@ -0,0 +1,38 @@
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>
);
}

View File

@@ -0,0 +1,96 @@
'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>
</>
);
}

View File

@@ -0,0 +1,19 @@
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);
}
};

View File

@@ -0,0 +1,276 @@
'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,
};

View File

@@ -0,0 +1,345 @@
'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,
};

View File

@@ -0,0 +1,131 @@
'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 };