Compare commits

...

10 commits

Author SHA1 Message Date
Rodrigo Rodriguez (Pragmatismo)
b22d5129c1 feat: add Sheet and Skeleton components with customizable styles and animations 2025-04-02 03:22:55 -03:00
Rodrigo Rodriguez (Pragmatismo)
e55a254e24 feat: enhance mail functionality with new data structure, improved UI components, and additional mail entries 2025-04-02 03:22:32 -03:00
Rodrigo Rodriguez (Pragmatismo)
d3bac607aa feat: enhance mail functionality with new data structure, improved UI components, and additional mail entries 2025-04-02 03:01:08 -03:00
Rodrigo Rodriguez (Pragmatismo)
fa525f6090 refactor: update configuration files and styles, migrate to new CSS structure, and clean up imports 2025-03-30 21:53:05 -03:00
Rodrigo Rodriguez (Pragmatismo)
878ce97c54 refactor: update configuration files and styles, migrate to new CSS structure, and clean up imports 2025-03-30 21:47:18 -03:00
Rodrigo Rodriguez (Pragmatismo)
e8cd469ee4 refactor: update component exports to named exports and enhance UI structure 2025-03-30 19:39:59 -03:00
Rodrigo Rodriguez (Pragmatismo)
661b821f37 refactor: update component exports to named exports and enhance UI structure 2025-03-30 19:28:28 -03:00
Rodrigo Rodriguez (Pragmatismo)
4dc77b7717 refactor: migrate chat layout and styles to CSS, remove unused styles and components 2025-03-30 19:04:24 -03:00
Rodrigo Rodriguez (Pragmatismo)
0972670cf4 refactor: clean up unused imports and components across the project 2025-03-30 17:32:22 -03:00
Rodrigo Rodriguez (Pragmatismo)
9c0afe87db refactor: expose structs and commands in drive and sync modules 2025-03-30 16:53:46 -03:00
154 changed files with 19526 additions and 15701 deletions

1
.gitignore vendored
View file

@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
output.sh

View file

@ -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

View file

@ -22,12 +22,12 @@
}
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./public/images/favicon.png"
},
"plugins": [
"expo-router"
],
"experiments": {
"tsconfigPaths": true,

View file

@ -1,7 +1,7 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['nativewind/babel']
};
};

21
components.json Normal file
View 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"
}

View file

@ -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}

View file

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

View file

@ -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
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
};
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1656
public/output.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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

View file

@ -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"

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 974 B

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 903 B

View file

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

101
src-rust/src/drive.rs Normal file
View 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(())
}

View file

@ -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
View 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");
// }

View file

@ -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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;

View file

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

View file

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

View file

@ -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}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,10 +8,10 @@ export function Chat() {
return (
<SoundInitializer>
<SoundProvider>
<ChatProvider>
<ChatLayout />
</ChatProvider>
</SoundProvider>
<ChatProvider>
<ChatLayout />
</ChatProvider>
</SoundProvider>
</SoundInitializer>
);
}

View file

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

View file

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

View file

@ -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 }}>

View file

@ -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
View 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;
}

View file

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

View 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;
}

View file

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

View 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;
}

View file

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

View 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;
}

View file

@ -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
View 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;
}

View file

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

View 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 }

View 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,
}

View 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 }

View file

@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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,
}

View 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,
}

View 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,
}

View 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
View 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,
}

View 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 }

View 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 }

View 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 }

View 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,
}

View 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,
}

View 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 }

View 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 }

View 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 }

View 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 }

View 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,
}

View 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 }

Some files were not shown because too many files have changed in this diff Show more