mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 16:49:39 +07:00
feat: update ui
This commit is contained in:
parent
ca3fe7150b
commit
887cb64878
42110
frontend/.tamagui/tamagui.config.json
Normal file
42110
frontend/.tamagui/tamagui.config.json
Normal file
File diff suppressed because it is too large
Load Diff
36
frontend/app.config.ts
Normal file
36
frontend/app.config.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ExpoConfig, ConfigContext } from "expo/config";
|
||||||
|
|
||||||
|
export default ({ config }: ConfigContext): ExpoConfig => ({
|
||||||
|
...config,
|
||||||
|
name: "frontend",
|
||||||
|
slug: "frontend",
|
||||||
|
version: "1.0.0",
|
||||||
|
orientation: "portrait",
|
||||||
|
icon: "./assets/images/icon.png",
|
||||||
|
scheme: "myapp",
|
||||||
|
userInterfaceStyle: "automatic",
|
||||||
|
newArchEnabled: true,
|
||||||
|
splash: {
|
||||||
|
image: "./assets/images/splash.png",
|
||||||
|
resizeMode: "contain",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
},
|
||||||
|
ios: {
|
||||||
|
supportsTablet: true,
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
adaptiveIcon: {
|
||||||
|
foregroundImage: "./assets/images/adaptive-icon.png",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
web: {
|
||||||
|
bundler: "metro",
|
||||||
|
output: "static",
|
||||||
|
favicon: "./assets/images/favicon.png",
|
||||||
|
},
|
||||||
|
plugins: ["expo-router"],
|
||||||
|
experiments: {
|
||||||
|
typedRoutes: true,
|
||||||
|
},
|
||||||
|
});
|
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"expo": {
|
|
||||||
"name": "frontend",
|
|
||||||
"slug": "frontend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"icon": "./assets/images/icon.png",
|
|
||||||
"scheme": "myapp",
|
|
||||||
"userInterfaceStyle": "automatic",
|
|
||||||
"newArchEnabled": true,
|
|
||||||
"splash": {
|
|
||||||
"image": "./assets/images/splash.png",
|
|
||||||
"resizeMode": "contain",
|
|
||||||
"backgroundColor": "#ffffff"
|
|
||||||
},
|
|
||||||
"ios": {
|
|
||||||
"supportsTablet": true
|
|
||||||
},
|
|
||||||
"android": {
|
|
||||||
"adaptiveIcon": {
|
|
||||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
|
||||||
"backgroundColor": "#ffffff"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web": {
|
|
||||||
"bundler": "metro",
|
|
||||||
"output": "static",
|
|
||||||
"favicon": "./assets/images/favicon.png"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"expo-router"
|
|
||||||
],
|
|
||||||
"experiments": {
|
|
||||||
"typedRoutes": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +1,15 @@
|
|||||||
import {
|
|
||||||
DarkTheme,
|
|
||||||
DefaultTheme,
|
|
||||||
ThemeProvider,
|
|
||||||
} from "@react-navigation/native";
|
|
||||||
import { useFonts } from "expo-font";
|
import { useFonts } from "expo-font";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import * as SplashScreen from "expo-splash-screen";
|
import * as SplashScreen from "expo-splash-screen";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
|
import Providers from "./_providers";
|
||||||
|
|
||||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const colorScheme: string = "light";
|
|
||||||
const [loaded] = useFonts({
|
const [loaded] = useFonts({
|
||||||
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
||||||
});
|
});
|
||||||
@ -30,11 +25,11 @@ export default function RootLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
<Providers>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="+not-found" />
|
<Stack.Screen name="+not-found" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
</ThemeProvider>
|
</Providers>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
48
frontend/app/_providers.tsx
Normal file
48
frontend/app/_providers.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, { PropsWithChildren, useMemo, useState } from "react";
|
||||||
|
import tamaguiConfig from "@/tamagui.config";
|
||||||
|
import {
|
||||||
|
DarkTheme,
|
||||||
|
DefaultTheme,
|
||||||
|
ThemeProvider,
|
||||||
|
} from "@react-navigation/native";
|
||||||
|
import { TamaguiProvider, Theme } from "@tamagui/core";
|
||||||
|
import useThemeStore from "@/stores/theme";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
type Props = PropsWithChildren;
|
||||||
|
|
||||||
|
const Providers = ({ children }: Props) => {
|
||||||
|
const colorScheme = useThemeStore((i) => i.theme);
|
||||||
|
const [queryClient] = useState(() => new QueryClient());
|
||||||
|
|
||||||
|
const theme = useMemo(() => {
|
||||||
|
return colorScheme === "dark"
|
||||||
|
? tamaguiConfig.themes.dark_blue
|
||||||
|
: tamaguiConfig.themes.light_blue;
|
||||||
|
}, [colorScheme]);
|
||||||
|
|
||||||
|
const navTheme = useMemo(() => {
|
||||||
|
const base = colorScheme === "dark" ? DarkTheme : DefaultTheme;
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
colors: {
|
||||||
|
...base.colors,
|
||||||
|
background: theme.background.val,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [theme, colorScheme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider value={navTheme}>
|
||||||
|
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}>
|
||||||
|
<Theme name="blue">
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{children}
|
||||||
|
</QueryClientProvider>
|
||||||
|
</Theme>
|
||||||
|
</TamaguiProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Providers;
|
69
frontend/app/hosts/_comp/host-form.tsx
Normal file
69
frontend/app/hosts/_comp/host-form.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import Icons from "@/components/ui/icons";
|
||||||
|
import Select, { SelectItem } from "@/components/ui/select";
|
||||||
|
import api from "@/lib/api";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import React from "react";
|
||||||
|
import { Button, Input, Label, ScrollView, Text, View, XStack } from "tamagui";
|
||||||
|
|
||||||
|
type Props = {};
|
||||||
|
|
||||||
|
const typeOptions: SelectItem[] = [
|
||||||
|
{ label: "SSH", value: "ssh" },
|
||||||
|
{ label: "Proxmox VE", value: "pve" },
|
||||||
|
{ label: "Incus", value: "incus" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const HostForm = (props: Props) => {
|
||||||
|
const keys = useQuery({
|
||||||
|
queryKey: ["keychains"],
|
||||||
|
queryFn: () => api("/keychains"),
|
||||||
|
select: (i) => i.rows,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ScrollView contentContainerStyle={{ padding: "$4" }}>
|
||||||
|
<Label>Hostname</Label>
|
||||||
|
<Input placeholder="IP or hostname..." />
|
||||||
|
|
||||||
|
<Label>Type</Label>
|
||||||
|
<Select items={typeOptions} />
|
||||||
|
|
||||||
|
<Label>Port</Label>
|
||||||
|
<Input keyboardType="number-pad" placeholder="SSH Port" />
|
||||||
|
|
||||||
|
<Label>Label</Label>
|
||||||
|
<Input placeholder="Label..." />
|
||||||
|
|
||||||
|
<XStack gap="$3" mt="$3" mb="$1">
|
||||||
|
<Label flex={1}>Credentials</Label>
|
||||||
|
<Button size="$3" icon={<Icons size={16} name="plus" />}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</XStack>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
placeholder="Username & Password"
|
||||||
|
items={keys.data?.map((key: any) => ({
|
||||||
|
label: key.label,
|
||||||
|
value: key.id,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
mt="$3"
|
||||||
|
placeholder="Private Key"
|
||||||
|
items={keys.data?.map((key: any) => ({
|
||||||
|
label: key.label,
|
||||||
|
value: key.id,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View p="$4">
|
||||||
|
<Button icon={<Icons name="content-save" size={18} />}>Save</Button>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HostForm;
|
12
frontend/app/hosts/create.tsx
Normal file
12
frontend/app/hosts/create.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
import HostForm from "./_comp/host-form";
|
||||||
|
|
||||||
|
export default function CreateHostPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: "Add Host" }} />
|
||||||
|
<HostForm />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
12
frontend/app/hosts/edit.tsx
Normal file
12
frontend/app/hosts/edit.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
import HostForm from "./_comp/host-form";
|
||||||
|
|
||||||
|
export default function EditHostPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: "Edit Host" }} />
|
||||||
|
<HostForm />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
80
frontend/app/hosts/index.tsx
Normal file
80
frontend/app/hosts/index.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { View, Text, Button, ScrollView, Spinner, Card, XStack } from "tamagui";
|
||||||
|
import React from "react";
|
||||||
|
import useThemeStore from "@/stores/theme";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import api from "@/lib/api";
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
import Pressable from "@/components/ui/pressable";
|
||||||
|
import Icons from "@/components/ui/icons";
|
||||||
|
|
||||||
|
export default function Hosts() {
|
||||||
|
const { toggle } = useThemeStore();
|
||||||
|
|
||||||
|
const hosts = useQuery({
|
||||||
|
queryKey: ["hosts"],
|
||||||
|
queryFn: () => api("/hosts"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "Hosts",
|
||||||
|
headerRight: () => (
|
||||||
|
<Button onPress={() => toggle()} mr="$2">
|
||||||
|
Toggle Theme
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hosts.isLoading ? (
|
||||||
|
<View alignItems="center" justifyContent="center" flex={1}>
|
||||||
|
<Spinner size="large" />
|
||||||
|
<Text mt="$4">Loading...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
padding: "$2",
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
// gap: "$4",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hosts.data.rows?.map((host: any) => (
|
||||||
|
<Pressable
|
||||||
|
key={host.id}
|
||||||
|
flexBasis="100%"
|
||||||
|
$gtXs={{ flexBasis: "50%" }}
|
||||||
|
$gtSm={{ flexBasis: "33.3%" }}
|
||||||
|
$gtMd={{ flexBasis: "25%" }}
|
||||||
|
$gtLg={{ flexBasis: "20%" }}
|
||||||
|
p="$2"
|
||||||
|
group
|
||||||
|
>
|
||||||
|
<Card elevate bordered p="$4">
|
||||||
|
<XStack>
|
||||||
|
<View flex={1}>
|
||||||
|
<Text>{host.label}</Text>
|
||||||
|
<Text fontSize="$3" mt="$2">
|
||||||
|
{host.host}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
circular
|
||||||
|
display="none"
|
||||||
|
$group-hover={{ display: "block" }}
|
||||||
|
>
|
||||||
|
<Icons name="pencil" size={16} />
|
||||||
|
</Button>
|
||||||
|
</XStack>
|
||||||
|
</Card>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
6
frontend/app/index.tsx
Normal file
6
frontend/app/index.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Redirect } from "expo-router";
|
||||||
|
|
||||||
|
export default function index() {
|
||||||
|
return <Redirect href="/hosts" />;
|
||||||
|
}
|
20
frontend/babel.config.js
Normal file
20
frontend/babel.config.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: ["babel-preset-expo"],
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
"@tamagui/babel-plugin",
|
||||||
|
{
|
||||||
|
components: ["tamagui"],
|
||||||
|
config: "./tamagui.config.ts",
|
||||||
|
logTimings: true,
|
||||||
|
disableExtraction: process.env.NODE_ENV === "development",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// NOTE: this is only necessary if you are using reanimated for animations
|
||||||
|
"react-native-reanimated/plugin",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
8
frontend/components/ui/icons.tsx
Normal file
8
frontend/components/ui/icons.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
||||||
|
import { styled } from "tamagui";
|
||||||
|
|
||||||
|
export const Icons = styled(MaterialCommunityIcons, {
|
||||||
|
color: "$color",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Icons;
|
20
frontend/components/ui/pressable.tsx
Normal file
20
frontend/components/ui/pressable.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Pressable as BasePressable } from "react-native";
|
||||||
|
import { GetProps, styled, ViewStyle } from "tamagui";
|
||||||
|
|
||||||
|
const StyledPressable = styled(BasePressable);
|
||||||
|
export type PressableProps = GetProps<typeof StyledPressable> & {
|
||||||
|
$hover?: ViewStyle;
|
||||||
|
$pressed?: ViewStyle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Pressable = ({
|
||||||
|
$hover,
|
||||||
|
$pressed = { opacity: 0.5 },
|
||||||
|
...props
|
||||||
|
}: PressableProps) => {
|
||||||
|
return (
|
||||||
|
<StyledPressable pressStyle={$pressed} hoverStyle={$hover} {...props} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pressable;
|
65
frontend/components/ui/select.tsx
Normal file
65
frontend/components/ui/select.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { forwardRef } from "react";
|
||||||
|
import { Select as BaseSelect } from "tamagui";
|
||||||
|
|
||||||
|
export type SelectItem = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectProps = React.ComponentPropsWithoutRef<typeof BaseSelect.Trigger> & {
|
||||||
|
items?: SelectItem[] | null;
|
||||||
|
value?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectRef = React.ElementRef<typeof BaseSelect.Trigger>;
|
||||||
|
|
||||||
|
const Select = forwardRef<SelectRef, SelectProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
items,
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
placeholder = "Select...",
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<BaseSelect
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
>
|
||||||
|
<BaseSelect.Trigger ref={ref} {...props}>
|
||||||
|
<BaseSelect.Value placeholder={placeholder} />
|
||||||
|
</BaseSelect.Trigger>
|
||||||
|
|
||||||
|
<BaseSelect.Content>
|
||||||
|
<BaseSelect.ScrollUpButton />
|
||||||
|
<BaseSelect.Viewport>
|
||||||
|
<BaseSelect.Item value="" index={0}>
|
||||||
|
<BaseSelect.ItemText>{placeholder}</BaseSelect.ItemText>
|
||||||
|
</BaseSelect.Item>
|
||||||
|
|
||||||
|
{items?.map((item, idx) => (
|
||||||
|
<BaseSelect.Item
|
||||||
|
key={item.value}
|
||||||
|
value={item.value}
|
||||||
|
index={idx + 1}
|
||||||
|
>
|
||||||
|
<BaseSelect.ItemText>{item.label}</BaseSelect.ItemText>
|
||||||
|
</BaseSelect.Item>
|
||||||
|
))}
|
||||||
|
</BaseSelect.Viewport>
|
||||||
|
<BaseSelect.ScrollDownButton />
|
||||||
|
</BaseSelect.Content>
|
||||||
|
</BaseSelect>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Select;
|
@ -1,2 +1,10 @@
|
|||||||
|
import { ofetch } from "ofetch";
|
||||||
|
|
||||||
export const BASE_API_URL = process.env.EXPO_PUBLIC_API_URL || ""; //"http://10.0.0.100:3000";
|
export const BASE_API_URL = process.env.EXPO_PUBLIC_API_URL || ""; //"http://10.0.0.100:3000";
|
||||||
export const BASE_WS_URL = BASE_API_URL.replace("http", "ws");
|
export const BASE_WS_URL = BASE_API_URL.replace("http", "ws");
|
||||||
|
|
||||||
|
const api = ofetch.create({
|
||||||
|
baseURL: BASE_API_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default api;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"license": "0BSD",
|
"license": "MIT",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -18,8 +18,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
"@novnc/novnc": "^1.5.0",
|
"@novnc/novnc": "^1.5.0",
|
||||||
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-navigation/bottom-tabs": "7.0.0-rc.36",
|
"@react-navigation/bottom-tabs": "7.0.0-rc.36",
|
||||||
"@react-navigation/native": "7.0.0-rc.21",
|
"@react-navigation/native": "7.0.0-rc.21",
|
||||||
|
"@tamagui/config": "^1.116.14",
|
||||||
|
"@tanstack/react-query": "^5.59.20",
|
||||||
"@xterm/addon-attach": "^0.11.0",
|
"@xterm/addon-attach": "^0.11.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
@ -35,6 +38,7 @@
|
|||||||
"expo-symbols": "~0.2.0",
|
"expo-symbols": "~0.2.0",
|
||||||
"expo-system-ui": "~4.0.2",
|
"expo-system-ui": "~4.0.2",
|
||||||
"expo-web-browser": "~14.0.0",
|
"expo-web-browser": "~14.0.0",
|
||||||
|
"ofetch": "^1.4.1",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.1",
|
"react-native": "0.76.1",
|
||||||
@ -44,10 +48,13 @@
|
|||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "4.0.0-beta.16",
|
"react-native-screens": "4.0.0-beta.16",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-webview": "^13.12.2"
|
"react-native-webview": "^13.12.2",
|
||||||
|
"tamagui": "^1.116.14",
|
||||||
|
"zustand": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
"@tamagui/babel-plugin": "^1.116.14",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/novnc__novnc": "^1.5.0",
|
"@types/novnc__novnc": "^1.5.0",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
|
2664
frontend/pnpm-lock.yaml
generated
2664
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
29
frontend/stores/theme.ts
Normal file
29
frontend/stores/theme.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist, createJSONStorage } from "zustand/middleware";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
|
type Store = {
|
||||||
|
theme: "light" | "dark";
|
||||||
|
setTheme: (theme: "light" | "dark") => void;
|
||||||
|
toggle: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useThemeStore = create(
|
||||||
|
persist<Store>(
|
||||||
|
(set) => ({
|
||||||
|
theme: "light",
|
||||||
|
setTheme: (theme: "light" | "dark") => {
|
||||||
|
set({ theme });
|
||||||
|
},
|
||||||
|
toggle: () => {
|
||||||
|
set((state) => ({ theme: state.theme === "light" ? "dark" : "light" }));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "theme",
|
||||||
|
storage: createJSONStorage(() => AsyncStorage),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default useThemeStore;
|
13
frontend/tamagui.config.ts
Normal file
13
frontend/tamagui.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createTamagui } from "@tamagui/core";
|
||||||
|
import { config } from "@tamagui/config/v3";
|
||||||
|
|
||||||
|
// you usually export this from a tamagui.config.ts file
|
||||||
|
const tamaguiConfig = createTamagui(config);
|
||||||
|
|
||||||
|
// TypeScript types across all Tamagui APIs
|
||||||
|
type Conf = typeof tamaguiConfig;
|
||||||
|
declare module "@tamagui/core" {
|
||||||
|
interface TamaguiCustomConfig extends Conf {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tamaguiConfig;
|
Loading…
x
Reference in New Issue
Block a user