Compare commits
10 commits
85a5e1d0de
...
b22d5129c1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b22d5129c1 | ||
![]() |
e55a254e24 | ||
![]() |
d3bac607aa | ||
![]() |
fa525f6090 | ||
![]() |
878ce97c54 | ||
![]() |
e8cd469ee4 | ||
![]() |
661b821f37 | ||
![]() |
4dc77b7717 | ||
![]() |
0972670cf4 | ||
![]() |
9c0afe87db |
1
.gitignore
vendored
|
@ -22,3 +22,4 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
output.sh
|
11
README.md
|
@ -1,11 +0,0 @@
|
|||
# Tauri + React + Typescript
|
||||
|
||||
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
|
||||
|
||||
|
||||
upx --best --lzma /home/rodriguez/Sources/my-tauri-app/src-tauri/target/release/my-tauri-app
|
4
app.json
|
@ -22,12 +22,12 @@
|
|||
}
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
|
||||
"output": "static",
|
||||
"favicon": "./public/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router"
|
||||
|
||||
],
|
||||
"experiments": {
|
||||
"tsconfigPaths": true,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
|
||||
plugins: ['nativewind/babel']
|
||||
};
|
||||
};
|
21
components.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
|
@ -16,7 +16,7 @@ export function Collapsible({ children, title }: PropsWithChildren & { title: st
|
|||
style={globalStylesheading}
|
||||
onPress={() => setIsOpen((value) => !value)}
|
||||
activeOpacity={0.8}>
|
||||
<Ionicons
|
||||
|
||||
name={isOpen ? 'chevron-down' : 'chevron-forward-outline'}
|
||||
size={18}
|
||||
color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Link } from 'expo-router';
|
||||
import { openBrowserAsync } from 'expo-web-browser';
|
||||
import { type ComponentProps } from 'react';
|
||||
|
||||
import { Link } from 'lucide-react';
|
||||
import React, { type ComponentProps } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: string };
|
||||
|
@ -11,14 +11,7 @@ export function ExternalLink({ href, ...rest }: Props) {
|
|||
target="_blank"
|
||||
{...rest}
|
||||
href={href}
|
||||
onPress={async (event) => {
|
||||
if (Platform.OS !== 'web') {
|
||||
// Prevent the default behavior of linking to the default browser on native.
|
||||
event.preventDefault();
|
||||
// Open the link in an in-app browser.
|
||||
await openBrowserAsync(href);
|
||||
}
|
||||
}}
|
||||
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@ import { type IconProps } from '@expo/vector-icons/build/createIconSet';
|
|||
import { type ComponentProps } from 'react';
|
||||
|
||||
export function TabBarIcon({ style, ...rest }: IconProps<ComponentProps<typeof Ionicons>['name']>) {
|
||||
return <Ionicons size={28} style={[{ marginBottom: -3 }, style]} {...rest} />;
|
||||
return
|
||||
}
|
||||
|
|
27
index.html
|
@ -1,14 +1,19 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + React + Typescript</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>General Bots</title>
|
||||
<link href="/output.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
12787
package-lock.json
generated
86
package.json
|
@ -1,66 +1,80 @@
|
|||
{
|
||||
"name": "my-tauri-app",
|
||||
"name": "gbclient",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "npx tailwindcss -i ./src/styles/globals.css -o ./public/output.css;ESBUILD_RUNNER=true vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.0",
|
||||
"@hookform/resolvers": "3.9.1",
|
||||
"@react-native-async-storage/async-storage": "2.1.0",
|
||||
"@react-native-community/datetimepicker": "8.2.0",
|
||||
"@react-native-community/slider": "4.5.5",
|
||||
"@react-native-picker/picker": "2.9.0",
|
||||
"@react-navigation/native": "6.x",
|
||||
"@react-navigation/stack": "6.x",
|
||||
"@tauri-apps/api": "2",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@tailwindcss/vite": "4.0.17",
|
||||
"@tauri-apps/api": "2.4.0",
|
||||
"@tauri-apps/plugin-opener": "2",
|
||||
"@zitadel/react": "1.0.5",
|
||||
"autoprefixer": "10.4.17",
|
||||
"botframework-directlinejs": "0.15.1",
|
||||
"botframework-webchat": "4.15.7",
|
||||
"date-fns": "2.30.0",
|
||||
"lucide-react-native": "0.469.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"lucide-react": "0.454.0",
|
||||
"nativewind": "2.0.10",
|
||||
"postcss": "8.4.35",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "7.53.2",
|
||||
"react-native-chart-kit": "6.12.0",
|
||||
"react-native-elements": "3.4.3",
|
||||
"react-native-gesture-handler": "2.16.1",
|
||||
"react-native-linear-gradient": "2.8.3",
|
||||
"react-native-markdown-display": "7.0.0-alpha.2",
|
||||
"react-native-reanimated": "3.10.1",
|
||||
"react-native-safe-area-context": "4.10.5",
|
||||
"react-native-screens": "3.31.1",
|
||||
"react-native-web": "0.19.6",
|
||||
"react-native": "0.74.5",
|
||||
"react": "18.3.1",
|
||||
"tailwindcss": "3.4.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router-dom": "7.4.1",
|
||||
"tailwind-merge": "3.0.2",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"uuid": "11.0.3",
|
||||
"zod": "3.21.4"
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.18.6",
|
||||
"@tauri-apps/cli": "2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/react-test-renderer": "18.0.7",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"jest": "29.2.1",
|
||||
"jest-expo": "51.0.3",
|
||||
"postcss": "8.4.23",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"tailwindcss": "3.1.8",
|
||||
"@types/node": "22.13.14",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-test-renderer": "18.0.7",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"esbuild-runner": "2.2.2",
|
||||
"jest": "29.2.1",
|
||||
"postcss": "8.4.23",
|
||||
"postcss-load-config": "6.0.1",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"tailwindcss": "3.1.8",
|
||||
"typescript": "5.6.2",
|
||||
"vite": "6.0.3",
|
||||
"@tauri-apps/cli": "2"
|
||||
"vite": "6.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
11557
pnpm-lock.yaml
generated
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
};
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
1656
public/output.css
Normal file
0
src-tauri/.gitignore → src-rust/.gitignore
vendored
156
src-tauri/Cargo.lock → src-rust/Cargo.lock
generated
|
@ -62,6 +62,24 @@ version = "1.0.97"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.9.0",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
"url",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
|
@ -750,6 +768,18 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block2 0.6.0",
|
||||
"libc",
|
||||
"objc2 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
|
@ -1095,6 +1125,20 @@ dependencies = [
|
|||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gbclient"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk"
|
||||
version = "0.18.2"
|
||||
|
@ -2046,19 +2090,6 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "my-tauri-app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-opener",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
|
@ -2812,6 +2843,17 @@ dependencies = [
|
|||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
|
@ -2832,6 +2874,16 @@ dependencies = [
|
|||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
|
@ -2850,6 +2902,15 @@ dependencies = [
|
|||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
|
@ -2961,6 +3022,31 @@ dependencies = [
|
|||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2 0.6.0",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.6.0",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.0",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
|
@ -3645,6 +3731,47 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b59fd750551b1066744ab956a1cd6b1ea3e1b3763b0b9153ac27a044d596426"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.12",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a1edf18000f02903a7c2e5997fb89aca455ecbc0acc15c6535afbb883be223"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"percent-encoding",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.2.6"
|
||||
|
@ -3888,6 +4015,7 @@ dependencies = [
|
|||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
|
@ -5059,6 +5187,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_repr",
|
||||
"static_assertions",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.59.0",
|
||||
|
@ -5169,6 +5298,7 @@ dependencies = [
|
|||
"enumflags2",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"url",
|
||||
"winnow 0.7.4",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "my-tauri-app"
|
||||
name = "gbclient"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
description = "General Bots Client"
|
||||
authors = ["Pragmatismo", "OER"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
@ -18,12 +18,13 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri = { version = "2", features = ["unstable"] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
chrono = "0.4"
|
||||
tauri-plugin-dialog = "2"
|
||||
|
||||
[profile.release]
|
||||
lto = true # Enables Link-Time Optimization
|
|
@ -2,7 +2,6 @@
|
|||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 974 B |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 903 B |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
101
src-rust/src/drive.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::{Emitter, Window};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FileItem {
|
||||
name: String,
|
||||
path: String,
|
||||
is_dir: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||
let base_path = Path::new(path);
|
||||
let mut files = Vec::new();
|
||||
|
||||
if !base_path.exists() {
|
||||
return Err("Path does not exist".into());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(base_path).map_err(|e| e.to_string())? {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let path = entry.path();
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
files.push(FileItem {
|
||||
name,
|
||||
path: path.to_str().unwrap_or("").to_string(),
|
||||
is_dir: path.is_dir(),
|
||||
});
|
||||
}
|
||||
|
||||
// Sort directories first, then files
|
||||
files.sort_by(|a, b| {
|
||||
if a.is_dir && !b.is_dir {
|
||||
std::cmp::Ordering::Less
|
||||
} else if !a.is_dir && b.is_dir {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
a.name.cmp(&b.name)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn upload_file(window: Window, src_path: String, dest_path: String) -> Result<(), String> {
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
let src = PathBuf::from(&src_path);
|
||||
let dest_dir = PathBuf::from(&dest_path);
|
||||
let dest = dest_dir.join(src.file_name().ok_or("Invalid source file")?);
|
||||
|
||||
// Create destination directory if it doesn't exist
|
||||
if !dest_dir.exists() {
|
||||
fs::create_dir_all(&dest_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let mut source_file = File::open(&src).map_err(|e| e.to_string())?;
|
||||
let mut dest_file = File::create(&dest).map_err(|e| e.to_string())?;
|
||||
|
||||
let file_size = source_file.metadata().map_err(|e| e.to_string())?.len();
|
||||
let mut buffer = [0; 8192];
|
||||
let mut total_read = 0;
|
||||
|
||||
loop {
|
||||
let bytes_read = source_file.read(&mut buffer).map_err(|e| e.to_string())?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
dest_file
|
||||
.write_all(&buffer[..bytes_read])
|
||||
.map_err(|e| e.to_string())?;
|
||||
total_read += bytes_read as u64;
|
||||
|
||||
let progress = (total_read as f64 / file_size as f64) * 100.0;
|
||||
window
|
||||
.emit("upload_progress", progress)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
||||
let full_path = Path::new(&path).join(&name);
|
||||
if full_path.exists() {
|
||||
return Err("Folder already exists".into());
|
||||
}
|
||||
fs::create_dir(full_path).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,8 +1,4 @@
|
|||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
pub mod drive;
|
||||
|
||||
fn main() {
|
||||
my_tauri_app_lib::run()
|
||||
}
|
||||
pub mod sync;
|
68
src-rust/src/main.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
pub mod drive;
|
||||
pub mod sync;
|
||||
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use sync::AppState;
|
||||
use tauri::{Manager, WebviewUrl};
|
||||
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn main() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let window = app.get_window("main").unwrap();
|
||||
|
||||
// Hide window initially
|
||||
window.hide()?;
|
||||
|
||||
// Remove decorations initially
|
||||
window.set_decorations(false)?;
|
||||
|
||||
// Create a handle to the main window for the delayed show operation
|
||||
let window_handle = window.clone();
|
||||
|
||||
// Use a delayed show operation without JS
|
||||
std::thread::spawn(move || {
|
||||
// Give time for the UI to initialize (adjust duration as needed)
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
// Execute on main thread since window operations are not thread-safe
|
||||
let window_for_closure = window_handle.clone();
|
||||
let _ = window_handle.run_on_main_thread(move || {
|
||||
window_for_closure.show().expect("Failed to show window");
|
||||
window_for_closure
|
||||
.set_decorations(true)
|
||||
.expect("Failed to set decorations");
|
||||
});
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Remove the now-unnecessary JS-dependent commands
|
||||
sync::save_config,
|
||||
drive::list_files,
|
||||
// ... other commands
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("Failed to run app");
|
||||
}
|
||||
|
||||
// These commands are no longer needed as we're handling the window display
|
||||
// directly in the setup function
|
||||
// #[tauri::command]
|
||||
// fn show_main_window(window: tauri::Window) {
|
||||
// window.show().expect("Failed to show window");
|
||||
// }
|
||||
//
|
||||
// #[tauri::command]
|
||||
// fn set_decorations(window: tauri::Window, value: bool) {
|
||||
// window.set_decorations(value).expect("Failed to set decorations");
|
||||
// }
|
|
@ -1,15 +1,14 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{Manager, Window};
|
||||
use std::sync::Mutex;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::path::Path;
|
||||
use std::fs::{File, OpenOptions, create_dir_all};
|
||||
use std::fs::{OpenOptions, create_dir_all};
|
||||
use std::io::Write;
|
||||
use std::env;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct RcloneConfig {
|
||||
pub struct RcloneConfig {
|
||||
name: String,
|
||||
remote_path: String,
|
||||
local_path: String,
|
||||
|
@ -18,7 +17,7 @@ struct RcloneConfig {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct SyncStatus {
|
||||
pub struct SyncStatus {
|
||||
name: String,
|
||||
status: String,
|
||||
transferred: String,
|
||||
|
@ -27,13 +26,13 @@ struct SyncStatus {
|
|||
last_updated: String,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
sync_processes: Mutex<Vec<std::process::Child>>,
|
||||
sync_active: Mutex<bool>,
|
||||
pub(crate) struct AppState {
|
||||
pub sync_processes: Mutex<Vec<std::process::Child>>,
|
||||
pub sync_active: Mutex<bool>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn save_config(config: RcloneConfig) -> Result<(), String> {
|
||||
pub fn save_config(config: RcloneConfig) -> Result<(), String> {
|
||||
let home_dir = env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?;
|
||||
let config_path = Path::new(&home_dir).join(".config/rclone/rclone.conf");
|
||||
|
||||
|
@ -54,7 +53,7 @@ fn save_config(config: RcloneConfig) -> Result<(), String> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_sync(config: RcloneConfig, state: tauri::State<AppState>) -> Result<(), String> {
|
||||
pub fn start_sync(config: RcloneConfig, state: tauri::State<AppState>) -> Result<(), String> {
|
||||
let local_path = Path::new(&config.local_path);
|
||||
if !local_path.exists() {
|
||||
create_dir_all(local_path).map_err(|e| format!("Failed to create local path: {}", e))?;
|
||||
|
@ -78,7 +77,7 @@ fn start_sync(config: RcloneConfig, state: tauri::State<AppState>) -> Result<(),
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_sync(state: tauri::State<AppState>) -> Result<(), String> {
|
||||
pub fn stop_sync(state: tauri::State<AppState>) -> Result<(), String> {
|
||||
let mut processes = state.sync_processes.lock().unwrap();
|
||||
for child in processes.iter_mut() {
|
||||
child.kill().map_err(|e| format!("Failed to kill process: {}", e))?;
|
||||
|
@ -89,7 +88,7 @@ fn stop_sync(state: tauri::State<AppState>) -> Result<(), String> {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_status(remote_name: String) -> Result<SyncStatus, String> {
|
||||
pub fn get_status(remote_name: String) -> Result<SyncStatus, String> {
|
||||
let output = Command::new("rclone")
|
||||
.arg("rc")
|
||||
.arg("core/stats")
|
||||
|
@ -129,7 +128,7 @@ fn get_status(remote_name: String) -> Result<SyncStatus, String> {
|
|||
})
|
||||
}
|
||||
|
||||
fn format_bytes(bytes: u64) -> String {
|
||||
pub fn format_bytes(bytes: u64) -> String {
|
||||
const KB: u64 = 1024;
|
||||
const MB: u64 = KB * 1024;
|
||||
const GB: u64 = MB * 1024;
|
|
@ -1,23 +1,31 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "my-tauri-app",
|
||||
"version": "0.1.0",
|
||||
"productName": "General Bots",
|
||||
"version": "6.0.0",
|
||||
"identifier": "online.generalbots",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"frontendDist": "../dist"
|
||||
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "my-tauri-app",
|
||||
"label": "main",
|
||||
"title": "General Bots",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"visible": false,
|
||||
"decorations": false,
|
||||
"skipTaskbar": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
|
@ -1,119 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::{Manager, Window};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct FileItem {
|
||||
name: String,
|
||||
path: String,
|
||||
is_dir: bool,
|
||||
}
|
||||
|
||||
impl Drive {
|
||||
#[tauri::command]
|
||||
fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||
let base_path = Path::new(path);
|
||||
let mut files = Vec::new();
|
||||
|
||||
if !base_path.exists() {
|
||||
return Err("Path does not exist".into());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(base_path).map_err(|e| e.to_string())? {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let path = entry.path();
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
files.push(FileItem {
|
||||
name,
|
||||
path: path.to_str().unwrap_or("").to_string(),
|
||||
is_dir: path.is_dir(),
|
||||
});
|
||||
}
|
||||
|
||||
// Sort directories first, then files
|
||||
files.sort_by(|a, b| {
|
||||
if a.is_dir && !b.is_dir {
|
||||
std::cmp::Ordering::Less
|
||||
} else if !a.is_dir && b.is_dir {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
a.name.cmp(&b.name)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn upload_file(
|
||||
window: Window,
|
||||
src_path: String,
|
||||
dest_path: String,
|
||||
) -> Result<(), String> {
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use tauri::api::path::home_dir;
|
||||
|
||||
let src = PathBuf::from(&src_path);
|
||||
let dest_dir = PathBuf::from(&dest_path);
|
||||
let dest = dest_dir.join(src.file_name().ok_or("Invalid source file")?);
|
||||
|
||||
// Create destination directory if it doesn't exist
|
||||
if !dest_dir.exists() {
|
||||
fs::create_dir_all(&dest_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let mut source_file = File::open(&src).map_err(|e| e.to_string())?;
|
||||
let mut dest_file = File::create(&dest).map_err(|e| e.to_string())?;
|
||||
|
||||
let file_size = source_file.metadata().map_err(|e| e.to_string())?.len();
|
||||
let mut buffer = [0; 8192];
|
||||
let mut total_read = 0;
|
||||
|
||||
loop {
|
||||
let bytes_read = source_file.read(&mut buffer).map_err(|e| e.to_string())?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
dest_file
|
||||
.write_all(&buffer[..bytes_read])
|
||||
.map_err(|e| e.to_string())?;
|
||||
total_read += bytes_read as u64;
|
||||
|
||||
let progress = (total_read as f64 / file_size as f64) * 100.0;
|
||||
window
|
||||
.emit("upload_progress", progress)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn create_folder(path: String, name: String) -> Result<(), String> {
|
||||
let full_path = Path::new(&path).join(&name);
|
||||
if full_path.exists() {
|
||||
return Err("Folder already exists".into());
|
||||
}
|
||||
fs::create_dir(full_path).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
list_files,
|
||||
upload_file,
|
||||
create_folder
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::fs::{create_dir_all, File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{Manager, Window};
|
||||
|
||||
pub mod drive;
|
||||
use drive::Drive::;
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.manage(AppState {
|
||||
sync_processes: Mutex::new(Vec::new()),
|
||||
sync_active: Mutex::new(false),
|
||||
})
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
save_config,
|
||||
list_files,
|
||||
start_sync,
|
||||
stop_sync,
|
||||
get_status
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
165
src/App.css
|
@ -1,165 +0,0 @@
|
|||
.app {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.menu-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.main-screen, .status-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
background-color: #f0f0f0;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Existing styles below */
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafb);
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, ActivityIndicator, StyleSheet } from 'react-native';
|
||||
|
||||
interface UserAuthFormProps {
|
||||
// Add any props you need
|
||||
|
@ -17,90 +16,50 @@ export function UserAuthForm({ }: UserAuthFormProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="name@example.com"
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
autoCorrect={false}
|
||||
editable={!isLoading}
|
||||
<div className="auth-container">
|
||||
<div className="auth-form">
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="email"
|
||||
className="auth-input"
|
||||
placeholder="name@example.com"
|
||||
autoCapitalize="off"
|
||||
autoComplete="email"
|
||||
autoCorrect="off"
|
||||
disabled={isLoading}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={onSubmit}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="auth-button"
|
||||
onClick={onSubmit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <ActivityIndicator style={styles.spinner} color="#ffffff" />}
|
||||
<Text style={styles.buttonText}>Sign In with Email</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.divider}>
|
||||
<View style={styles.dividerLine} />
|
||||
<Text style={styles.dividerText}>Or continue with</Text>
|
||||
<View style={styles.dividerLine} />
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.githubButton}
|
||||
onPress={() => {/* Add GitHub sign in logic */}}
|
||||
{isLoading ? (
|
||||
<span className="auth-spinner" />
|
||||
) : (
|
||||
'Sign In with Email'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="auth-divider">
|
||||
<div className="divider-line" />
|
||||
<span className="divider-text">Or continue with</span>
|
||||
<div className="divider-line" />
|
||||
</div>
|
||||
<button
|
||||
className="github-button"
|
||||
onClick={() => {}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator style={styles.spinner} color="#000000" />
|
||||
<span className="auth-spinner" />
|
||||
) : (
|
||||
<Text>GitHub Icon</Text> // Replace with actual GitHub icon
|
||||
<span>GitHub Icon</span>
|
||||
)}
|
||||
<Text style={styles.githubButtonText}>GitHub</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<span>GitHub</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
gap: 20,
|
||||
},
|
||||
form: {
|
||||
gap: 10,
|
||||
},
|
||||
inputContainer: {
|
||||
// Add styles for input container
|
||||
},
|
||||
input: {
|
||||
// Add styles for input
|
||||
},
|
||||
button: {
|
||||
// Add styles for button
|
||||
},
|
||||
buttonText: {
|
||||
// Add styles for button text
|
||||
},
|
||||
spinner: {
|
||||
marginRight: 8,
|
||||
},
|
||||
divider: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: '#e0e0e0',
|
||||
},
|
||||
dividerText: {
|
||||
paddingHorizontal: 10,
|
||||
// Add styles for divider text
|
||||
},
|
||||
githubButton: {
|
||||
// Add styles for GitHub button
|
||||
},
|
||||
githubButtonText: {
|
||||
// Add styles for GitHub button text
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,159 +1,66 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, Image, StyleSheet, TouchableOpacity, ScrollView, Alert } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { ZitadelAuth } from '@zitadel/react';
|
||||
import { UserAuthForm } from './components/user-auth-form';
|
||||
|
||||
const AuthenticationScreen = () => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const auth = new ZitadelAuth({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
issuer: 'YOUR_ZITADEL_ISSUER_URL',
|
||||
redirectUri: 'YOUR_REDIRECT_URI',
|
||||
scopes: ['openid', 'profile', 'email'],
|
||||
});
|
||||
|
||||
const result = await auth.authorize();
|
||||
if (result?.accessToken) {
|
||||
await AsyncStorage.setItem('authToken', result.accessToken);
|
||||
setIsAuthenticated(true);
|
||||
Alert.alert('Login Successful', 'You are now authenticated.');
|
||||
} else {
|
||||
Alert.alert('Login Failed', 'Unable to retrieve access token.');
|
||||
}
|
||||
localStorage.setItem('authToken', 'dummy-token');
|
||||
setIsAuthenticated(true);
|
||||
alert('Login Successful');
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
Alert.alert('Login Error', 'An error occurred during login.');
|
||||
alert('Login Error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await AsyncStorage.removeItem('authToken');
|
||||
localStorage.removeItem('authToken');
|
||||
setIsAuthenticated(false);
|
||||
Alert.alert('Logout Successful', 'You are now logged out.');
|
||||
alert('Logout Successful');
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
Alert.alert('Logout Error', 'An error occurred during logout.');
|
||||
alert('Logout Error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.imageContainer}>
|
||||
</View>
|
||||
<div className="auth-screen">
|
||||
<div className="auth-content">
|
||||
<button
|
||||
className="auth-login-button"
|
||||
onClick={isAuthenticated ? handleLogout : handleLogin}
|
||||
>
|
||||
{isAuthenticated ? 'Logout' : 'Login'}
|
||||
</button>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
<TouchableOpacity style={styles.loginButton} onPress={isAuthenticated ? handleLogout : handleLogin}>
|
||||
<Text style={styles.loginButtonText}>{isAuthenticated ? 'Logout' : 'Login'}</Text>
|
||||
</TouchableOpacity>
|
||||
<div className="auth-left-panel">
|
||||
<div className="auth-logo">
|
||||
<h1>Welcome to General Bots Online</h1>
|
||||
</div>
|
||||
<div className="auth-quote">
|
||||
<p>"Errar é Humano."</p>
|
||||
<p>General Bots</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<View style={styles.leftPanel}>
|
||||
<View style={styles.logoContainer}>
|
||||
{/* Replace with your logo component */}
|
||||
<Text style={styles.logoText}>Welcome to General Bots Online</Text>
|
||||
</View>
|
||||
<View style={styles.quoteContainer}>
|
||||
<Text style={styles.quoteText}>
|
||||
"Errar é Humano."
|
||||
</Text>
|
||||
<Text style={styles.quoteAuthor}>General Bots</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.formContainer}>
|
||||
<View style={styles.formHeader}>
|
||||
<Text style={styles.formTitle}>Create an account</Text>
|
||||
<Text style={styles.formSubtitle}>
|
||||
Enter your email below to create your account
|
||||
</Text>
|
||||
</View>
|
||||
<div className="auth-form-container">
|
||||
<div className="auth-form-header">
|
||||
<h2>Create an account</h2>
|
||||
<p>Enter your email below to create your account</p>
|
||||
</div>
|
||||
|
||||
<Text style={styles.termsText}>
|
||||
By clicking continue, you agree to our Terms of Service and Privacy Policy.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
<UserAuthForm />
|
||||
|
||||
<p className="auth-terms">
|
||||
By clicking continue, you agree to our Terms of Service and Privacy Policy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
imageContainer: {
|
||||
// ... styles for image container
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: 300,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
loginButton: {
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
padding: 8,
|
||||
},
|
||||
loginButtonText: {
|
||||
color: '#000',
|
||||
},
|
||||
leftPanel: {
|
||||
// ... styles for left panel
|
||||
},
|
||||
logoContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
logoText: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
quoteContainer: {
|
||||
marginTop: 'auto',
|
||||
},
|
||||
quoteText: {
|
||||
fontSize: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
quoteAuthor: {
|
||||
fontSize: 14,
|
||||
},
|
||||
formContainer: {
|
||||
// ... styles for form container
|
||||
},
|
||||
formHeader: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
formTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
formSubtitle: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
},
|
||||
termsText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
marginTop: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default AuthenticationScreen;
|
||||
export default AuthenticationScreen;
|
||||
|
|
|
@ -1,27 +1,23 @@
|
|||
import React from 'react';
|
||||
import { View, Platform } from 'react-native';
|
||||
import { PersonSelector } from './selector/person-selector';
|
||||
import { ProjectorView } from './projector/projector-view';
|
||||
import { ChatWindow } from './chat/chat-window';
|
||||
import { layoutStyles } from '../styles/layout.styles';
|
||||
import '../styles/layout.css';
|
||||
|
||||
export function ChatLayout() {
|
||||
return (
|
||||
<View style={[
|
||||
layoutStyles.container,
|
||||
Platform.OS === 'web' && { height: '100vh' }
|
||||
]}>
|
||||
<View style={layoutStyles.sidebar}>
|
||||
<div className="chat-layout">
|
||||
<div className="sidebar">
|
||||
<PersonSelector />
|
||||
</View>
|
||||
<View style={layoutStyles.mainContent}>
|
||||
<View style={layoutStyles.projector}>
|
||||
</div>
|
||||
<div className="main-content">
|
||||
<div className="projector">
|
||||
<ProjectorView />
|
||||
</View>
|
||||
<View style={layoutStyles.chatArea}>
|
||||
</div>
|
||||
<div className="chat-area">
|
||||
<ChatWindow />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { MoreVertical } from 'lucide-react-native';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { chatStyles } from '../../styles/chat.styles';
|
||||
import '../../styles/chat.css';
|
||||
|
||||
export function ChatHeader() {
|
||||
const { instance } = useChat();
|
||||
|
||||
return (
|
||||
<View style={chatStyles.header}>
|
||||
<View style={chatStyles.headerContent}>
|
||||
<Text style={chatStyles.headerTitle}>
|
||||
<div className="chat-header">
|
||||
<div className="header-content">
|
||||
<h2 className="header-title">
|
||||
{instance?.name || 'Chat'}
|
||||
</Text>
|
||||
<Text style={chatStyles.headerSubtitle}>
|
||||
Online
|
||||
</Text>
|
||||
</View>
|
||||
</h2>
|
||||
<span className="header-subtitle">Online</span>
|
||||
</div>
|
||||
|
||||
<TouchableOpacity style={chatStyles.headerButton}>
|
||||
<MoreVertical color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<button className="header-button">
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm14 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import React from 'react';
|
||||
import { View, TextInput, TouchableOpacity, Animated } from 'react-native';
|
||||
import { Send, Paperclip, Mic, Smile } from 'lucide-react-native';
|
||||
import { EmojiPicker } from '../ui/emoji-picker';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { useSound } from '../../providers/sound-provider';
|
||||
import { chatStyles } from '../../styles/chat.styles';
|
||||
import '../../styles/chat.css';
|
||||
|
||||
export function ChatInput() {
|
||||
const [message, setMessage] = React.useState('');
|
||||
const [showEmoji, setShowEmoji] = React.useState(false);
|
||||
const pulseAnim = React.useRef(new Animated.Value(1)).current;
|
||||
const { sendActivity } = useChat();
|
||||
const { playSound } = useSound();
|
||||
const typingTimeout = React.useRef<NodeJS.Timeout>();
|
||||
|
||||
const handleKeyPress = () => {
|
||||
if (typingTimeout.current) {
|
||||
clearTimeout(typingTimeout.current);
|
||||
}
|
||||
typingTimeout.current = setTimeout(() => {
|
||||
playSound('typing');
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
if (!message.trim()) return;
|
||||
|
@ -40,55 +27,40 @@ export function ChatInput() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<View style={chatStyles.inputContainer}>
|
||||
<TouchableOpacity
|
||||
style={chatStyles.iconButton}
|
||||
onPress={() => playSound('click')}
|
||||
>
|
||||
<Paperclip color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
<div className="input-container">
|
||||
<button className="icon-button" onClick={() => playSound('click')}>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<TouchableOpacity
|
||||
style={chatStyles.iconButton}
|
||||
onPress={() => {
|
||||
playSound('click');
|
||||
setShowEmoji(true);
|
||||
}}
|
||||
>
|
||||
<Smile color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
<button className="icon-button" onClick={() => setShowEmoji(true)}>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<TextInput
|
||||
<textarea
|
||||
value={message}
|
||||
onChangeText={setMessage}
|
||||
onKeyPress={handleKeyPress}
|
||||
style={[
|
||||
chatStyles.input,
|
||||
{ borderColor: message ? '#00f3ff' : '#333' }
|
||||
]}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="chat-input"
|
||||
placeholder="Type a message..."
|
||||
placeholderTextColor="#666"
|
||||
multiline
|
||||
/>
|
||||
|
||||
{message.trim().length > 0 ? (
|
||||
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
||||
<TouchableOpacity
|
||||
style={[chatStyles.iconButton, chatStyles.sendButton]}
|
||||
onPress={handleSend}
|
||||
>
|
||||
<Send color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
<button className="send-button" onClick={handleSend}>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={chatStyles.iconButton}
|
||||
onPress={() => playSound('click')}
|
||||
>
|
||||
<Mic color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
<button className="icon-button" onClick={() => playSound('click')}>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</View>
|
||||
</div>
|
||||
|
||||
<EmojiPicker
|
||||
visible={showEmoji}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { MessageList } from './message-list';
|
||||
import { ChatInput } from './chat-input';
|
||||
import { ChatHeader } from './chat-header';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { Message } from '../../types';
|
||||
import { chatStyles } from '../../styles/chat.styles';
|
||||
import '../../styles/chat.css';
|
||||
|
||||
export function ChatWindow() {
|
||||
const { line } = useChat();
|
||||
|
@ -14,7 +13,7 @@ export function ChatWindow() {
|
|||
React.useEffect(() => {
|
||||
if (!line) return;
|
||||
|
||||
const subscription = line.activity$.subscribe(activity => {
|
||||
const subscription = line.activity$.subscribe((activity: any) => {
|
||||
if (activity.type === 'message') {
|
||||
setMessages(prev => [...prev, activity as Message]);
|
||||
}
|
||||
|
@ -24,10 +23,10 @@ export function ChatWindow() {
|
|||
}, [line]);
|
||||
|
||||
return (
|
||||
<View style={chatStyles.window}>
|
||||
<div className="chat-window">
|
||||
<ChatHeader />
|
||||
<MessageList messages={messages} />
|
||||
<ChatInput />
|
||||
</View>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import React from 'react';
|
||||
import { ScrollView, View, Text } from 'react-native';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { useSound } from '../../providers/sound-provider';
|
||||
import { Message } from '../../types';
|
||||
import { chatStyles } from '../../styles/chat.styles';
|
||||
import '../../styles/chat.css';
|
||||
|
||||
interface MessageListProps {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export function MessageList({ messages }: MessageListProps) {
|
||||
const scrollViewRef = React.useRef<ScrollView>(null);
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const { user } = useChat();
|
||||
const { playSound } = useSound();
|
||||
const prevMessagesLength = React.useRef(messages.length);
|
||||
|
@ -21,33 +20,26 @@ export function MessageList({ messages }: MessageListProps) {
|
|||
if (lastMessage.from.id !== user.id) {
|
||||
playSound('receive');
|
||||
}
|
||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
prevMessagesLength.current = messages.length;
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={chatStyles.messageList}
|
||||
contentContainerStyle={chatStyles.messageListContent}
|
||||
>
|
||||
<div className="message-list" ref={scrollRef}>
|
||||
{messages.map((message, index) => (
|
||||
<View
|
||||
<div
|
||||
key={`${message.id}-${index}`}
|
||||
style={[
|
||||
chatStyles.messageContainer,
|
||||
message.from.id === user.id
|
||||
? chatStyles.userMessage
|
||||
: chatStyles.botMessage
|
||||
]}
|
||||
className={`message-container ${
|
||||
message.from.id === user.id ? 'user-message' : 'bot-message'
|
||||
}`}
|
||||
>
|
||||
<Text style={chatStyles.messageText}>{message.text}</Text>
|
||||
<Text style={chatStyles.messageTime}>
|
||||
<p className="message-text">{message.text}</p>
|
||||
<span className="message-time">
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</Text>
|
||||
</View>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</ScrollView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View, Image } from 'react-native';
|
||||
import { projectorStyles } from '../../styles/projector.styles';
|
||||
import '../../styles/projector.css';
|
||||
|
||||
interface ImageViewerProps {
|
||||
url: string;
|
||||
|
@ -8,12 +7,8 @@ interface ImageViewerProps {
|
|||
|
||||
export function ImageViewer({ url }: ImageViewerProps) {
|
||||
return (
|
||||
<View style={projectorStyles.imageContainer}>
|
||||
<Image
|
||||
source={{ uri: url }}
|
||||
style={projectorStyles.image}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<div className="image-container">
|
||||
<img src={url} className="projector-image" alt="Projected content" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import Markdown from 'react-native-markdown-display';
|
||||
import { projectorStyles } from '../../styles/projector.styles';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import '../../styles/projector.css';
|
||||
|
||||
interface MarkdownViewerProps {
|
||||
content: string;
|
||||
|
@ -9,10 +8,8 @@ interface MarkdownViewerProps {
|
|||
|
||||
export function MarkdownViewer({ content }: MarkdownViewerProps) {
|
||||
return (
|
||||
<ScrollView style={projectorStyles.markdownContainer}>
|
||||
<Markdown style={projectorStyles.markdown}>
|
||||
{content}
|
||||
</Markdown>
|
||||
</ScrollView>
|
||||
<div className="markdown-container">
|
||||
<ReactMarkdown>{content}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { VideoPlayer } from './video-player';
|
||||
import { ImageViewer } from './image-viewer';
|
||||
import { MarkdownViewer } from './markdown-viewer';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { projectorStyles } from '../../styles/projector.styles';
|
||||
import '../../styles/projector.css';
|
||||
|
||||
export function ProjectorView() {
|
||||
const { line } = useChat();
|
||||
|
@ -14,9 +13,10 @@ export function ProjectorView() {
|
|||
if (!line) return;
|
||||
|
||||
const subscription = line.activity$
|
||||
.filter(activity => activity.type === 'event' && activity.name === 'project')
|
||||
.subscribe(activity => {
|
||||
setContent(activity.value);
|
||||
.subscribe((activity: any) => {
|
||||
if (activity.type === 'event' && activity.name === 'project') {
|
||||
setContent(activity.value);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
|
@ -38,8 +38,8 @@ export function ProjectorView() {
|
|||
};
|
||||
|
||||
return (
|
||||
<View style={projectorStyles.container}>
|
||||
<div className="projector-container">
|
||||
{renderContent()}
|
||||
</View>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
import React from 'react';
|
||||
import { View, Image } from 'react-native';
|
||||
import { projectorStyles } from '../../styles/projector.styles';
|
||||
import '../../styles/projector.css';
|
||||
|
||||
interface ImageViewerProps {
|
||||
interface VideoPlayerProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export function VideoViewer({ url }: ImageViewerProps) {
|
||||
export function VideoPlayer({ url }: VideoPlayerProps) {
|
||||
return (
|
||||
<View style={projectorStyles.imageContainer}>
|
||||
<iframe
|
||||
src={url}
|
||||
|
||||
|
||||
/>
|
||||
</View>
|
||||
<div className="video-container">
|
||||
<video controls src={url} className="projector-video" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,50 +1,44 @@
|
|||
import React from 'react';
|
||||
import { View, Text, Image, TextInput, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Search } from 'lucide-react-native';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { selectorStyles } from '../../styles/selector.styles';
|
||||
import '../../styles/selector.css';
|
||||
|
||||
export function PersonSelector() {
|
||||
const [search, setSearch] = React.useState('');
|
||||
const { instance } = useChat();
|
||||
|
||||
return (
|
||||
<View style={selectorStyles.container}>
|
||||
<View style={selectorStyles.header}>
|
||||
<Image
|
||||
source={{ uri: instance?.logo }}
|
||||
style={selectorStyles.logo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<div className="selector-container">
|
||||
<div className="selector-header">
|
||||
{instance?.logo && (
|
||||
<img src={instance.logo} className="selector-logo" alt="Logo" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<View style={selectorStyles.searchContainer}>
|
||||
<Search size={20} color="#00f3ff" />
|
||||
<TextInput
|
||||
<div className="search-container">
|
||||
<svg className="search-icon" viewBox="0 0 24 24">
|
||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<input
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="search-input"
|
||||
placeholder="Search conversations..."
|
||||
placeholderTextColor="#666"
|
||||
style={selectorStyles.searchInput}
|
||||
/>
|
||||
</View>
|
||||
</div>
|
||||
|
||||
<ScrollView style={selectorStyles.list}>
|
||||
<div className="selector-list">
|
||||
{['FAQ', 'Support', 'Sales'].map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item}
|
||||
style={selectorStyles.item}
|
||||
>
|
||||
<View style={selectorStyles.avatar}>
|
||||
<Text style={selectorStyles.avatarText}>{item[0]}</Text>
|
||||
</View>
|
||||
<View style={selectorStyles.itemContent}>
|
||||
<Text style={selectorStyles.itemTitle}>{item}</Text>
|
||||
<Text style={selectorStyles.itemSubtitle}>Start a conversation</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<div key={item} className="selector-item">
|
||||
<div className="selector-avatar">
|
||||
<span>{item[0]}</span>
|
||||
</div>
|
||||
<div className="item-content">
|
||||
<h3 className="item-title">{item}</h3>
|
||||
<p className="item-subtitle">Start a conversation</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { soundAssets } from '../../../public/sounds/manifest';
|
||||
import { cacheAssets } from '../lib/asset-loader';
|
||||
|
||||
//import { cacheAssets } from '../lib/asset-loader';
|
||||
|
||||
export function SoundInitializer({ children }: { children: React.ReactNode }) {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
@ -11,31 +9,29 @@ export function SoundInitializer({ children }: { children: React.ReactNode }) {
|
|||
useEffect(() => {
|
||||
const initializeSounds = async () => {
|
||||
try {
|
||||
await cacheAssets(Object.values(soundAssets));
|
||||
// await cacheAssets(Object.values(soundAssets));
|
||||
setIsReady(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to initialize sounds');
|
||||
}
|
||||
};
|
||||
|
||||
//initializeSounds();
|
||||
setIsReady(true);
|
||||
initializeSounds();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'red' }}>Error: {error}</Text>
|
||||
</View>
|
||||
<div className="error-container">
|
||||
<p className="error-text">Error: {error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Text>Loading sounds...</Text>
|
||||
</View>
|
||||
<div className="loading-container">
|
||||
<p>Loading sounds...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, Modal } from 'react-native';
|
||||
import { X } from 'lucide-react-native';
|
||||
import { emojiStyles } from '../../styles/ui.styles';
|
||||
import '../../styles/ui.css';
|
||||
|
||||
const EMOJI_CATEGORIES = {
|
||||
"😀 🎮": ["😀", "😎", "🤖", "👾", "🎮", "✨", "🚀", "💫"],
|
||||
|
@ -9,43 +7,45 @@ const EMOJI_CATEGORIES = {
|
|||
"🤖 🎯": ["🤖", "🎯", "🎲", "🎮", "🕹️", "👾", "💻", "⌨️"]
|
||||
};
|
||||
|
||||
export function EmojiPicker({ visible, onClose, onEmojiSelect }) {
|
||||
export function EmojiPicker({ visible, onClose, onEmojiSelect }: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onEmojiSelect: (emoji: string) => void;
|
||||
}) {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="slide"
|
||||
>
|
||||
<View style={emojiStyles.container}>
|
||||
<View style={emojiStyles.header}>
|
||||
<Text style={emojiStyles.title}>Select Emoji</Text>
|
||||
<TouchableOpacity onPress={onClose}>
|
||||
<X color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={emojiStyles.content}>
|
||||
{Object.entries(EMOJI_CATEGORIES).map(([category, emojis]) => (
|
||||
<View key={category} style={emojiStyles.category}>
|
||||
<Text style={emojiStyles.categoryTitle}>{category}</Text>
|
||||
<View style={emojiStyles.emojiGrid}>
|
||||
{emojis.map(emoji => (
|
||||
<TouchableOpacity
|
||||
key={emoji}
|
||||
style={emojiStyles.emojiButton}
|
||||
onPress={() => {
|
||||
onEmojiSelect(emoji);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Text style={emojiStyles.emoji}>{emoji}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Modal>
|
||||
<div className="emoji-picker-modal">
|
||||
<div className="emoji-picker-header">
|
||||
<h3>Select Emoji</h3>
|
||||
<button onClick={onClose} className="close-button">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="emoji-picker-content">
|
||||
{Object.entries(EMOJI_CATEGORIES).map(([category, emojis]) => (
|
||||
<div key={category} className="emoji-category">
|
||||
<h4 className="category-title">{category}</h4>
|
||||
<div className="emoji-grid">
|
||||
{emojis.map(emoji => (
|
||||
<button
|
||||
key={emoji}
|
||||
className="emoji-button"
|
||||
onClick={() => {
|
||||
onEmojiSelect(emoji);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React from 'react';
|
||||
import { View, Text, Switch, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import { Moon, Sun, Volume2, VolumeX, Zap, Settings } from 'lucide-react-native';
|
||||
import { useSound } from '../../providers/sound-provider';
|
||||
import { settingsStyles } from '../../styles/ui.styles';
|
||||
import '../../styles/ui.css';
|
||||
|
||||
export function SettingsPanel() {
|
||||
const [theme, setTheme] = React.useState('dark');
|
||||
|
@ -29,21 +27,27 @@ export function SettingsPanel() {
|
|||
};
|
||||
|
||||
return (
|
||||
// ... rest of the settings panel code ...
|
||||
<View style={settingsStyles.option}>
|
||||
{sound ? (
|
||||
<Volume2 color="#00f3ff" size={20} />
|
||||
) : (
|
||||
<VolumeX color="#666" size={20} />
|
||||
)}
|
||||
<Text style={settingsStyles.optionText}>Sound Effects</Text>
|
||||
<Switch
|
||||
value={sound}
|
||||
onValueChange={handleSoundToggle}
|
||||
trackColor={{ false: '#333', true: '#00f3ff44' }}
|
||||
thumbColor={sound ? '#00f3ff' : '#666'}
|
||||
/>
|
||||
</View>
|
||||
// ... rest of the settings panel code ...
|
||||
<div className="settings-panel">
|
||||
<div className="settings-option">
|
||||
{sound ? (
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M3 15a1 1 0 011-1h2.83a1 1 0 00.8-.4l1.12-1.5a1 1 0 01.8-.4h3.55a1 1 0 00.8-.4l1.12-1.5a1 1 0 01.8-.4H19a1 1 0 011 1v6a1 1 0 01-1 1h-2.83a1 1 0 00-.8-.4l-1.12-1.5a1 1 0 00-.8-.4H9.72a1 1 0 01-.8-.4l-1.12-1.5a1 1 0 00-.8-.4H4a1 1 0 01-1-1v-2zM12 6a1 1 0 011-1h6a1 1 0 011 1v3"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M3 15a1 1 0 011-1h2.83a1 1 0 00.8-.4l1.12-1.5a1 1 0 01.8-.4h3.55a1 1 0 00.8-.4l1.12-1.5a1 1 0 01.8-.4H19a1 1 0 011 1v6a1 1 0 01-1 1h-2.83a1 1 0 00-.8-.4l-1.12-1.5a1 1 0 00-.8-.4H9.72a1 1 0 01-.8-.4l-1.12-1.5a1 1 0 00-.8-.4H4a1 1 0 01-1-1v-2zM12 6a1 1 0 011-1h6a1 1 0 011 1v3M3 3l18 18"/>
|
||||
</svg>
|
||||
)}
|
||||
<span className="option-text">Sound Effects</span>
|
||||
<label className="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sound}
|
||||
onChange={(e) => handleSoundToggle(e.target.checked)}
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,10 +8,10 @@ export function Chat() {
|
|||
return (
|
||||
<SoundInitializer>
|
||||
<SoundProvider>
|
||||
<ChatProvider>
|
||||
<ChatLayout />
|
||||
</ChatProvider>
|
||||
</SoundProvider>
|
||||
<ChatProvider>
|
||||
<ChatLayout />
|
||||
</ChatProvider>
|
||||
</SoundProvider>
|
||||
</SoundInitializer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import { Asset } from 'expo-asset';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
export async function ensureAssetLoaded(assetPath: string): Promise<string> {
|
||||
try {
|
||||
const asset = Asset.fromModule(assetPath);
|
||||
if (!asset.localUri) {
|
||||
await asset.downloadAsync();
|
||||
}
|
||||
return asset.localUri;
|
||||
} catch (error) {
|
||||
console.error('Failed to load asset:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cacheAssets(assets: string[]): Promise<void> {
|
||||
try {
|
||||
const cacheDirectory = `${FileSystem.cacheDirectory}sounds/`;
|
||||
await FileSystem.makeDirectoryAsync(cacheDirectory, { intermediates: true });
|
||||
|
||||
await Promise.all(
|
||||
assets.map(async (asset) => {
|
||||
const assetName = asset.split('/').pop();
|
||||
const cachedPath = `${cacheDirectory}${assetName}`;
|
||||
|
||||
const fileInfo = await FileSystem.getInfoAsync(cachedPath);
|
||||
if (!fileInfo.exists) {
|
||||
await FileSystem.copyAsync({
|
||||
from: asset,
|
||||
to: cachedPath,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to cache assets:', error);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
import React, { useState } from 'react';
|
||||
import { DirectLine } from 'botframework-directlinejs';
|
||||
import { ChatInstance, User } from '../types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { core } from '@tauri-apps/api';
|
||||
import { User, ChatInstance } from '../types';
|
||||
|
||||
interface ChatContextType {
|
||||
line: DirectLine | null;
|
||||
line: any;
|
||||
user: User;
|
||||
instance: ChatInstance | null;
|
||||
sendActivity: (activity: any) => void;
|
||||
|
@ -12,92 +11,66 @@ interface ChatContextType {
|
|||
setVoice: (voice: any) => void;
|
||||
}
|
||||
|
||||
const generateUserId = () => {
|
||||
return 'usergb@gb';
|
||||
};
|
||||
|
||||
export const ChatContext = React.createContext<ChatContextType | undefined>(undefined);
|
||||
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
||||
|
||||
export function ChatProvider({ children }: { children: React.ReactNode }) {
|
||||
const [line, setLine] = React.useState<DirectLine | null>(null);
|
||||
const [instance, setInstance] = React.useState<ChatInstance | null>(null);
|
||||
const [line, setLine] = useState<any>(null);
|
||||
const [instance, setInstance] = useState<ChatInstance | null>(null);
|
||||
const [selectedVoice, setSelectedVoice] = useState(null);
|
||||
const [user] = React.useState<User>(() => ({
|
||||
const [user] = useState<User>({
|
||||
id: `user_${Math.random().toString(36).slice(2)}`,
|
||||
name: 'You'
|
||||
}));
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const initializeChat = async () => {
|
||||
try {
|
||||
const botId = window.location.pathname.split('/')[1] || 'default';
|
||||
const instanceData = await core.invoke('get_chat_instance', { botId });
|
||||
setInstance(instanceData as ChatInstance);
|
||||
|
||||
// Initialize DirectLine or other chat service
|
||||
const directLine = {
|
||||
activity$: { subscribe: () => {} },
|
||||
postActivity: () => ({ subscribe: () => {} })
|
||||
};
|
||||
setLine(directLine);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize chat:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeChat();
|
||||
}, []);
|
||||
|
||||
const initializeChat = async () => {
|
||||
const sendActivity = async (activity: any) => {
|
||||
try {
|
||||
var botId = window.location.href.split('/')[3];
|
||||
if (botId.indexOf('#') !== -1) {
|
||||
botId = botId.split('#')[0];
|
||||
}
|
||||
|
||||
if (!botId || botId === '') {
|
||||
botId = '[default]';
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
'http://localhost:4242/instances/' + botId,
|
||||
)
|
||||
const data = await response.json();
|
||||
const userId = generateUserId();
|
||||
|
||||
const directLine = data.webchatToken
|
||||
? new DirectLine({
|
||||
token: data.token,
|
||||
webSocket: true
|
||||
})
|
||||
: new DirectLine({
|
||||
domain: data.domain,
|
||||
secret: null,
|
||||
token: null,
|
||||
webSocket: false
|
||||
});
|
||||
directLine.setUserId(userId);
|
||||
setLine(directLine);
|
||||
setInstance(data.instance);
|
||||
console.info (`DirectLine for user:` + userId);
|
||||
await core.invoke('send_chat_activity', {
|
||||
activity: {
|
||||
...activity,
|
||||
from: user,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
line?.postActivity(activity).subscribe();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize chat:', error);
|
||||
console.error('Failed to send activity:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const sendActivity = (activity: any) => {
|
||||
line?.postActivity({
|
||||
...activity,
|
||||
from: user,
|
||||
timestamp: new Date().toISOString()
|
||||
}).subscribe();
|
||||
};
|
||||
|
||||
const setVoice = (voice: any) => {
|
||||
setSelectedVoice(voice);
|
||||
};
|
||||
|
||||
const contextValue: ChatContextType = {
|
||||
line,
|
||||
user,
|
||||
instance,
|
||||
sendActivity,
|
||||
selectedVoice,
|
||||
setVoice
|
||||
};
|
||||
|
||||
return (
|
||||
<ChatContext.Provider value={contextValue}>
|
||||
<ChatContext.Provider value={{ line, user, instance, sendActivity, selectedVoice, setVoice }}>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useChat() {
|
||||
const context = React.useContext(ChatContext);
|
||||
const context = useContext(ChatContext);
|
||||
if (!context) {
|
||||
throw new Error('useChat must be used within ChatProvider');
|
||||
}
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
import React from 'react';
|
||||
import React, { createContext, useContext, useCallback } from 'react';
|
||||
import { core } from '@tauri-apps/api';
|
||||
|
||||
interface SoundContextType {
|
||||
playSound: (sound: string) => void;
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const SoundContext = React.createContext<SoundContextType | undefined>(undefined);
|
||||
const SoundContext = createContext<SoundContextType | undefined>(undefined);
|
||||
|
||||
export function SoundProvider({ children }: { children: React.ReactNode }) {
|
||||
const playSound = React.useCallback((sound: string) => {
|
||||
// soundManager.play(sound as any);
|
||||
}, []);
|
||||
const [enabled, setEnabled] = React.useState(true);
|
||||
|
||||
const setEnabled = React.useCallback((enabled: boolean) => {
|
||||
// soundManager.setEnabled(enabled);
|
||||
}, []);
|
||||
const playSound = useCallback(async (sound: string) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
await core.invoke('play_sound', { sound });
|
||||
} catch (error) {
|
||||
console.error('Failed to play sound:', error);
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
return (
|
||||
<SoundContext.Provider value={{ playSound, setEnabled }}>
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const audioStyles = StyleSheet.create({
|
||||
volumeContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
volumeButton: {
|
||||
padding: 8,
|
||||
},
|
||||
sliderContainer: {
|
||||
flex: 1,
|
||||
marginLeft: 16,
|
||||
},
|
||||
slider: {
|
||||
flex: 1,
|
||||
height: 40,
|
||||
},
|
||||
visualizerContainer: {
|
||||
flexDirection: 'row',
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
visualizerBar: {
|
||||
width: 3,
|
||||
height: 20,
|
||||
backgroundColor: '#00f3ff',
|
||||
borderRadius: 2,
|
||||
marginHorizontal: 1,
|
||||
},
|
||||
modal: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.95)',
|
||||
margin: 20,
|
||||
marginTop: 100,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#00f3ff',
|
||||
shadowColor: '#00f3ff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 10,
|
||||
},
|
||||
voiceList: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
voiceOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
selectedVoice: {
|
||||
borderColor: '#00f3ff',
|
||||
backgroundColor: '#00f3ff11',
|
||||
},
|
||||
voiceInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
voiceName: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
voiceAccent: {
|
||||
color: '#666',
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#333',
|
||||
},
|
||||
title: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
closeButton: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
trigger: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 8,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
triggerText: {
|
||||
color: '#ffffff',
|
||||
marginLeft: 8,
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export const voiceStyles = StyleSheet.create({
|
||||
// ... copy from audioStyles the modal-related styles ...
|
||||
// Add voice-specific styles here
|
||||
});
|
112
src/chat/styles/chat.css
Normal file
|
@ -0,0 +1,112 @@
|
|||
.chat-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background-color: #111;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
color: white;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: #00f3ff;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
max-width: 70%;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
align-self: flex-end;
|
||||
background-color: rgba(0, 243, 255, 0.1);
|
||||
border: 1px solid #00f3ff;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
align-self: flex-start;
|
||||
background-color: rgba(191, 0, 255, 0.1);
|
||||
border: 1px solid #bf00ff;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: #666;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #333;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
min-height: 2.5rem;
|
||||
max-height: 6rem;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0.5rem;
|
||||
background-color: #1a1a1a;
|
||||
color: white;
|
||||
border: 1px solid #333;
|
||||
border-radius: 1.25rem;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #00f3ff;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
background-color: rgba(0, 243, 255, 0.1);
|
||||
border: 1px solid #00f3ff;
|
||||
border-radius: 50%;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
fill: currentColor;
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const chatStyles = StyleSheet.create({
|
||||
window: {
|
||||
flex: 1,
|
||||
backgroundColor: '#111111',
|
||||
},
|
||||
messageList: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
messageListContent: {
|
||||
paddingBottom: 16,
|
||||
},
|
||||
messageContainer: {
|
||||
maxWidth: '70%',
|
||||
marginVertical: 4,
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
},
|
||||
userMessage: {
|
||||
alignSelf: 'flex-end',
|
||||
backgroundColor: '#00f3ff22',
|
||||
borderColor: '#00f3ff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
botMessage: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#bf00ff22',
|
||||
borderColor: '#bf00ff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
messageText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
},
|
||||
messageTime: {
|
||||
color: '#666666',
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#1a1a1a',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
padding: 12,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 24,
|
||||
color: '#ffffff',
|
||||
maxHeight: 100,
|
||||
},
|
||||
iconButton: {
|
||||
padding: 8,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#1a1a1a',
|
||||
},
|
||||
headerContent: {
|
||||
flex: 1,
|
||||
},
|
||||
headerTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
headerSubtitle: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 14,
|
||||
marginTop: 2,
|
||||
},
|
||||
headerButton: {
|
||||
padding: 8,
|
||||
},
|
||||
sendButton: {
|
||||
backgroundColor: '#00f3ff22',
|
||||
borderRadius: 20,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: '#00f3ff',
|
||||
}
|
||||
});
|
24
src/chat/styles/layout.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
.chat-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 18rem;
|
||||
border-right: 1px solid #333;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.projector {
|
||||
width: 40%;
|
||||
border-right: 1px solid #333;
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
flex: 1;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
// layout.styles.ts
|
||||
import { Colors } from '../../../constants/Colors';
|
||||
import { StyleSheet } from 'react-native';
|
||||
export const layoutStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: Colors.dark.background,
|
||||
},
|
||||
sidebar: {
|
||||
width: 300,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: Colors.dark.icon,
|
||||
},
|
||||
mainContent: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
projector: {
|
||||
width: '40%',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: Colors.dark.icon,
|
||||
},
|
||||
chatArea: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
45
src/chat/styles/projector.css
Normal file
|
@ -0,0 +1,45 @@
|
|||
.projector-container {
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.image-container, .video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #111;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.projector-image, .projector-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.markdown-container {
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
color: white;
|
||||
background-color: #111;
|
||||
border-radius: 0.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.markdown-container h1,
|
||||
.markdown-container h2,
|
||||
.markdown-container h3 {
|
||||
color: #00f3ff;
|
||||
}
|
||||
|
||||
.markdown-container a {
|
||||
color: #00f3ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-container pre {
|
||||
background-color: #1a1a1a;
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
// projector.styles.ts
|
||||
import { Colors } from '../../../constants/Colors';
|
||||
import { StyleSheet } from 'react-native';
|
||||
export const projectorStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.dark.background,
|
||||
padding: 16,
|
||||
},
|
||||
videoContainer: {
|
||||
aspectRatio: 16/9,
|
||||
backgroundColor: Colors.dark.background,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
imageContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.dark.background,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
markdownContainer: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
backgroundColor: Colors.dark.background,
|
||||
borderRadius: 8,
|
||||
},
|
||||
body: {
|
||||
color: Colors.dark.text,
|
||||
fontSize: 16,
|
||||
},
|
||||
heading1: {
|
||||
color: Colors.dark.tint,
|
||||
fontSize: 24,
|
||||
marginBottom: 16,
|
||||
},
|
||||
heading2: {
|
||||
color: Colors.dark.tint,
|
||||
fontSize: 20,
|
||||
marginBottom: 12,
|
||||
},
|
||||
link: {
|
||||
color: Colors.dark.tint,
|
||||
},
|
||||
code_block: {
|
||||
backgroundColor: Colors.dark.background,
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
}
|
||||
});
|
87
src/chat/styles/selector.css
Normal file
|
@ -0,0 +1,87 @@
|
|||
.selector-container {
|
||||
height: 100%;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.selector-logo {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.5rem;
|
||||
color: #00f3ff;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.selector-list {
|
||||
height: calc(100% - 7rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.selector-item {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selector-item:hover {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.selector-avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background-color: #1a1a1a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #00f3ff;
|
||||
color: #00f3ff;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
margin-left: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-subtitle {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// selector.styles.ts
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from '../../../constants/Colors';
|
||||
|
||||
export const selectorStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.dark.background,
|
||||
},
|
||||
header: {
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: Colors.dark.icon,
|
||||
},
|
||||
logo: {
|
||||
width: 150,
|
||||
height: 50,
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: Colors.dark.icon,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
marginLeft: 8,
|
||||
color: Colors.dark.text,
|
||||
fontSize: 16,
|
||||
},
|
||||
list: {
|
||||
flex: 1,
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: Colors.dark.icon,
|
||||
},
|
||||
avatar: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: Colors.dark.background,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.dark.tint,
|
||||
},
|
||||
avatarText: {
|
||||
color: Colors.dark.tint,
|
||||
fontSize: 20,
|
||||
},
|
||||
itemContent: {
|
||||
marginLeft: 12,
|
||||
flex: 1,
|
||||
},
|
||||
itemTitle: {
|
||||
color: Colors.dark.text,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
itemSubtitle: {
|
||||
color: Colors.dark.icon,
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
137
src/chat/styles/ui.css
Normal file
|
@ -0,0 +1,137 @@
|
|||
.emoji-picker-modal {
|
||||
position: fixed;
|
||||
bottom: 5rem;
|
||||
right: 2rem;
|
||||
width: 20rem;
|
||||
max-height: 25rem;
|
||||
background-color: rgba(0, 0, 0, 0.95);
|
||||
border: 1px solid #00f3ff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 0 1rem rgba(0, 243, 255, 0.5);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emoji-picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid rgba(0, 243, 255, 0.2);
|
||||
}
|
||||
|
||||
.emoji-picker-header h3 {
|
||||
color: #00f3ff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #00f3ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.emoji-picker-content {
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
max-height: calc(25rem - 3.5rem);
|
||||
}
|
||||
|
||||
.emoji-category {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
color: #bf00ff;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.emoji-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.emoji-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.emoji-button:hover {
|
||||
background-color: rgba(0, 243, 255, 0.1);
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
background-color: #111;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.settings-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background-color: #1a1a1a;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
color: white;
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #333;
|
||||
transition: .4s;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1.1rem;
|
||||
width: 1.1rem;
|
||||
left: 0.2rem;
|
||||
bottom: 0.2rem;
|
||||
background-color: #666;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: rgba(0, 243, 255, 0.2);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(1.5rem);
|
||||
background-color: #00f3ff;
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const emojiStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.95)',
|
||||
margin: 20,
|
||||
marginTop: 100,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#00f3ff',
|
||||
shadowColor: '#00f3ff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 10,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#00f3ff33',
|
||||
},
|
||||
title: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
category: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
categoryTitle: {
|
||||
color: '#bf00ff',
|
||||
fontSize: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
emojiGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
emojiButton: {
|
||||
width: '12.5%',
|
||||
aspectRatio: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 8,
|
||||
},
|
||||
emoji: {
|
||||
fontSize: 24,
|
||||
},
|
||||
});
|
||||
|
||||
export const settingsStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#111111',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#00f3ff33',
|
||||
},
|
||||
title: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 12,
|
||||
},
|
||||
section: {
|
||||
padding: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: '#bf00ff',
|
||||
fontSize: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
option: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 12,
|
||||
marginBottom: 12,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
optionText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
marginLeft: 12,
|
||||
flex: 1,
|
||||
},
|
||||
activeIndicator: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginLeft: 8,
|
||||
},
|
||||
effectPreview: {
|
||||
padding: 20,
|
||||
},
|
||||
previewTitle: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
previewContent: {
|
||||
padding: 20,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#00f3ff33',
|
||||
},
|
||||
previewText: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
// Add animation helpers
|
||||
export const pulseAnimation = {
|
||||
0: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
},
|
||||
0.5: {
|
||||
opacity: 0.7,
|
||||
scale: 1.05,
|
||||
},
|
||||
1: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const neonGlow = {
|
||||
shadowColor: '#00f3ff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.8,
|
||||
shadowRadius: 15,
|
||||
};
|
55
src/components/ui/accordion.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
139
src/components/ui/alert-dialog.tsx
Normal file
|
@ -0,0 +1,139 @@
|
|||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
59
src/components/ui/alert.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
5
src/components/ui/aspect-ratio.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
48
src/components/ui/avatar.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
36
src/components/ui/badge.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
57
src/components/ui/button.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
74
src/components/ui/calendar.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
month: "space-y-4",
|
||||
caption: "flex justify-center pt-1 relative items-center",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "space-x-1 flex items-center",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
day_range_start: "day-range-start",
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Calendar.displayName = "Calendar"
|
||||
|
||||
export { Calendar }
|
76
src/components/ui/card.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
28
src/components/ui/checkbox.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
9
src/components/ui/collapsible.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
151
src/components/ui/command.tsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
198
src/components/ui/context-menu.tsx
Normal file
|
@ -0,0 +1,198 @@
|
|||
import * as React from "react"
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root
|
||||
|
||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
||||
|
||||
const ContextMenuGroup = ContextMenuPrimitive.Group
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
||||
|
||||
const ContextMenuSub = ContextMenuPrimitive.Sub
|
||||
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
||||
|
||||
const ContextMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
))
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const ContextMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
||||
|
||||
const ContextMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
))
|
||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
||||
|
||||
const ContextMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
||||
|
||||
const ContextMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
ContextMenuCheckboxItem.displayName =
|
||||
ContextMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const ContextMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-4 w-4 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
))
|
||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const ContextMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
||||
|
||||
const ContextMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
}
|
120
src/components/ui/dialog.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
199
src/components/ui/dropdown-menu.tsx
Normal file
|
@ -0,0 +1,199 @@
|
|||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
176
src/components/ui/form.tsx
Normal file
|
@ -0,0 +1,176 @@
|
|||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
27
src/components/ui/hover-card.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
22
src/components/ui/input.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
24
src/components/ui/label.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
254
src/components/ui/menubar.tsx
Normal file
|
@ -0,0 +1,254 @@
|
|||
import * as React from "react"
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function MenubarMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||
return <MenubarPrimitive.Menu {...props} />
|
||||
}
|
||||
|
||||
function MenubarGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||
return <MenubarPrimitive.Group {...props} />
|
||||
}
|
||||
|
||||
function MenubarPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||
return <MenubarPrimitive.Portal {...props} />
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||
return <MenubarPrimitive.RadioGroup {...props} />
|
||||
}
|
||||
|
||||
function MenubarSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
||||
}
|
||||
|
||||
const Menubar = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName
|
||||
|
||||
const MenubarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
))
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
||||
|
||||
const MenubarSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
||||
|
||||
const MenubarContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||
>(
|
||||
(
|
||||
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
||||
ref
|
||||
) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPrimitive.Portal>
|
||||
)
|
||||
)
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
||||
|
||||
const MenubarItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
))
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
||||
|
||||
const MenubarRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Circle className="h-4 w-4 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
))
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
||||
|
||||
const MenubarLabel = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
||||
|
||||
const MenubarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
||||
|
||||
const MenubarShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
MenubarShortcut.displayname = "MenubarShortcut"
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
}
|
128
src/components/ui/navigation-menu.tsx
Normal file
|
@ -0,0 +1,128 @@
|
|||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
))
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
||||
)
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
))
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
))
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName
|
||||
|
||||
export {
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
}
|
31
src/components/ui/popover.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
26
src/components/ui/progress.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
42
src/components/ui/radio-group.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-3.5 w-3.5 fill-primary" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
})
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
46
src/components/ui/scroll-area.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
157
src/components/ui/select.tsx
Normal file
|
@ -0,0 +1,157 @@
|
|||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
29
src/components/ui/separator.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|