Add About and Login pages with responsive design and user authentication

- Created a new About page (index.html) detailing the BotServer platform, its features, and technology stack.
- Developed a Login page (login.html) with sign-in and sign-up functionality, including form validation and user feedback messages.
- Removed the empty style.css file as it is no longer needed.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-10-26 00:02:19 -03:00
parent 32feb58b00
commit dfe7e4e4b6
70 changed files with 17866 additions and 72 deletions

3190
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,6 @@ authors = [
"Arenas.io",
"Atylla L",
"Christopher de Castilho",
"Dario Lima",
"Dario Junior",
"David Lerner",
"Experimentation Garage",
@ -38,10 +37,12 @@ license = "AGPL-3.0"
repository = "https://github.com/GeneralBots/BotServer"
[features]
desktop = ["tauri", "tauri-plugin-opener", "tauri-plugin-dialog"]
default = [ "vectordb"]
vectordb = ["qdrant-client"]
email = ["imap"]
web_automation = ["headless_chrome"]
webapp = ["tauri", "tauri-plugin-opener", "tauri-plugin-dialog"]
[dependencies]
actix-cors = "0.7"
@ -70,7 +71,6 @@ log = "0.4"
mailparse = "0.15"
native-tls = "0.2"
num-format = "0.4"
qdrant-client = { version = "1.12", optional = true }
rhai = { git = "https://github.com/therealprof/rhai.git", branch = "features/use-web-time" }
@ -97,3 +97,17 @@ scraper = "0.20"
sha2 = "0.10.9"
ureq = "3.1.2"
indicatif = "0.18.0"
tauri = { version = "2", features = ["unstable"], optional = true }
tauri-plugin-opener = { version = "2", optional = true }
tauri-plugin-dialog = { version = "2", optional = true }
[build-dependencies]
tauri-build = { version = "2", features = [] }
[profile.release]
lto = true # Enables Link-Time Optimization
opt-level = "z" # Optimizes for size instead of speed
strip = true # Strips debug symbols
panic = "abort" # Reduces size by removing panic unwinding
codegen-units = 1 # More aggressive optimization

View file

@ -1,4 +1,5 @@
#![allow(dead_code)]
#![cfg_attr(feature = "desktop", windows_subsystem = "windows")]
use actix_cors::Cors;
use actix_web::middleware::Logger;
use actix_web::{web, App, HttpServer};
@ -18,6 +19,10 @@ mod context;
mod drive_monitor;
#[cfg(feature = "email")]
mod email;
#[cfg(feature = "desktop")]
mod ui;
mod file;
mod kb;
mod llm;
@ -56,6 +61,7 @@ use crate::web_server::{bot_index, index, static_files};
use crate::whatsapp::whatsapp_webhook_verify;
use crate::whatsapp::WhatsAppAdapter;
#[cfg(not(feature = "desktop"))]
#[tokio::main]
async fn main() -> std::io::Result<()> {
let args: Vec<String> = std::env::args().collect();

39
src/riot_compiler/mod.rs Normal file
View file

@ -0,0 +1,39 @@
use boa_engine::{Context, JsValue, Source};
fn compile_riot_component(riot_code: &str) -> Result<JsValue, Box<dyn std::error::Error>> {
let mut context = Context::default();
let compiler = include_str!("riot_compiler.js"); // Your Riot compiler logic
context.eval(Source::from_bytes(compiler))?;
let result = context.eval(Source::from_bytes(&format!(
"compileRiot(`{}`)",
riot_code.replace('`', "\\`")
)))?;
Ok(result)
}
fn main() {
let riot_component = r#"
<todo-item>
<h3>{ props.title }</h3>
<input if="{ !props.done }" type="checkbox" onclick="{ toggle }">
<span if="{ props.done }"> Done</span>
<script>
export default {
toggle() {
this.props.done = !this.props.done
}
}
</script>
</todo-item>
"#;
match compile_riot_component(riot_component) {
Ok(compiled) => println!("Compiled: {:?}", compiled),
Err(e) => eprintln!("Compilation failed: {}", e),
}
}

101
src/ui/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(())
}

444
src/ui/local-sync.rs Normal file
View file

@ -0,0 +1,444 @@
use dioxus::prelude::*;
use dioxus_desktop::{use_window, LogicalSize};
use std::env;
use std::fs::{File, OpenOptions, create_dir_all};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::process::{Command as ProcCommand, Child, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use notify_rust::Notification;
use serde::{Deserialize, Serialize};
use serde_json::Value;
// App state
#[derive(Debug, Clone)]
struct AppState {
name: String,
access_key: String,
secret_key: String,
status_text: String,
sync_processes: Arc<Mutex<Vec<Child>>>,
sync_active: Arc<Mutex<bool>>,
sync_statuses: Arc<Mutex<Vec<SyncStatus>>>,
show_config_dialog: bool,
show_about_dialog: bool,
current_screen: Screen,
}
#[derive(Debug, Clone)]
enum Screen {
Main,
Status,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RcloneConfig {
name: String,
remote_path: String,
local_path: String,
access_key: String,
secret_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SyncStatus {
name: String,
status: String,
transferred: String,
bytes: String,
errors: usize,
last_updated: String,
}
#[derive(Debug, Clone)]
enum Message {
NameChanged(String),
AccessKeyChanged(String),
SecretKeyChanged(String),
SaveConfig,
StartSync,
StopSync,
UpdateStatus(Vec<SyncStatus>),
ShowConfigDialog(bool),
ShowAboutDialog(bool),
ShowStatusScreen,
BackToMain,
None,
}
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let window = use_window();
window.set_inner_size(LogicalSize::new(800, 600));
let state = use_ref(cx, || AppState {
name: String::new(),
access_key: String::new(),
secret_key: String::new(),
status_text: "Enter credentials to set up sync".to_string(),
sync_processes: Arc::new(Mutex::new(Vec::new())),
sync_active: Arc::new(Mutex::new(false)),
sync_statuses: Arc::new(Mutex::new(Vec::new())),
show_config_dialog: false,
show_about_dialog: false,
current_screen: Screen::Main,
});
// Monitor sync status
use_future( async move {
let state = state.clone();
async move {
let mut last_check = Instant::now();
let check_interval = Duration::from_secs(5);
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
if !*state.read().sync_active.lock().unwrap() {
continue;
}
if last_check.elapsed() < check_interval {
continue;
}
last_check = Instant::now();
match read_rclone_configs() {
Ok(configs) => {
let mut new_statuses = Vec::new();
for config in configs {
match get_rclone_status(&config.name) {
Ok(status) => new_statuses.push(status),
Err(e) => eprintln!("Failed to get status: {}", e),
}
}
*state.write().sync_statuses.lock().unwrap() = new_statuses.clone();
state.write().status_text = format!("Syncing {} repositories...", new_statuses.len());
}
Err(e) => eprintln!("Failed to read configs: {}", e),
}
}
}
});
cx.render(rsx! {
div {
class: "app",
// Main menu bar
div {
class: "menu-bar",
button {
onclick: move |_| state.write().show_config_dialog = true,
"Add Sync Configuration"
}
button {
onclick: move |_| state.write().show_about_dialog = true,
"About"
}
}
// Main content
{match state.read().current_screen {
Screen::Main => rsx! {
div {
class: "main-screen",
h1 { "General Bots" }
p { "{state.read().status_text}" }
button {
onclick: move |_| start_sync(&state),
"Start Sync"
}
button {
onclick: move |_| stop_sync(&state),
"Stop Sync"
}
button {
onclick: move |_| state.write().current_screen = Screen::Status,
"Show Status"
}
}
},
Screen::Status => rsx! {
div {
class: "status-screen",
h1 { "Sync Status" }
div {
class: "status-list",
for status in state.read().sync_statuses.lock().unwrap().iter() {
div {
class: "status-item",
h2 { "{status.name}" }
p { "Status: {status.status}" }
p { "Transferred: {status.transferred}" }
p { "Bytes: {status.bytes}" }
p { "Errors: {status.errors}" }
p { "Last Updated: {status.last_updated}" }
}
}
}
button {
onclick: move |_| state.write().current_screen = Screen::Main,
"Back"
}
}
}
}}
// Config dialog
if state.read().show_config_dialog {
div {
class: "dialog",
h2 { "Add Sync Configuration" }
input {
value: "{state.read().name}",
oninput: move |e| state.write().name = e.value.clone(),
placeholder: "Enter sync name",
}
input {
value: "{state.read().access_key}",
oninput: move |e| state.write().access_key = e.value.clone(),
placeholder: "Enter access key",
}
input {
value: "{state.read().secret_key}",
oninput: move |e| state.write().secret_key = e.value.clone(),
placeholder: "Enter secret key",
}
button {
onclick: move |_| {
save_config(&state);
state.write().show_config_dialog = false;
},
"Save"
}
button {
onclick: move |_| state.write().show_config_dialog = false,
"Cancel"
}
}
}
// About dialog
if state.read().show_about_dialog {
div {
class: "dialog",
h2 { "About General Bots" }
p { "Version: 1.0.0" }
p { "A professional-grade sync tool for OneDrive/Dropbox-like functionality." }
button {
onclick: move |_| state.write().show_about_dialog = false,
"Close"
}
}
}
}
})
}
// Save sync configuration
fn save_config(state: &UseRef<AppState>) {
if state.read().name.is_empty() || state.read().access_key.is_empty() || state.read().secret_key.is_empty() {
state.write_with(|state| state.status_text = "All fields are required!".to_string());
return;
}
let new_config = RcloneConfig {
name: state.read().name.clone(),
remote_path: format!("s3://{}", state.read().name),
local_path: Path::new(&env::var("HOME").unwrap()).join("General Bots").join(&state.read().name).to_string_lossy().to_string(),
access_key: state.read().access_key.clone(),
secret_key: state.read().secret_key.clone(),
};
if let Err(e) = save_rclone_config(&new_config) {
state.write_with(|state| state.status_text = format!("Failed to save config: {}", e));
} else {
state.write_with(|state| state.status_text = "New sync saved!".to_string());
}
}
// Start sync process
fn start_sync(state: &UseRef<AppState>) {
let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap());
processes.clear();
match read_rclone_configs() {
Ok(configs) => {
for config in configs {
match run_sync(&config) {
Ok(child) => processes.push(child),
Err(e) => eprintln!("Failed to start sync: {}", e),
}
}
state.write_with(|state| *state.sync_active.lock().unwrap() = true);
state.write_with(|state| state.status_text = format!("Syncing with {} configurations.", processes.len()));
}
Err(e) => state.write_with(|state| state.status_text = format!("Failed to read configurations: {}", e)),
}
}
// Stop sync process
fn stop_sync(state: &UseRef<AppState>) {
let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap());
for child in processes.iter_mut() {
let _ = child.kill();
}
processes.clear();
state.write_with(|state| *state.sync_active.lock().unwrap() = false);
state.write_with(|state| state.status_text = "Sync stopped.".to_string());
}
// Utility functions (rclone, notifications, etc.)
fn save_rclone_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");
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&config_path)
.map_err(|e| format!("Failed to open config file: {}", e))?;
writeln!(file, "[{}]", config.name)
.and_then(|_| writeln!(file, "type = s3"))
.and_then(|_| writeln!(file, "provider = Other"))
.and_then(|_| writeln!(file, "access_key_id = {}", config.access_key))
.and_then(|_| writeln!(file, "secret_access_key = {}", config.secret_key))
.and_then(|_| writeln!(file, "endpoint = https://drive-api.pragmatismo.com.br"))
.and_then(|_| writeln!(file, "acl = private"))
.map_err(|e| format!("Failed to write config: {}", e))
}
fn read_rclone_configs() -> Result<Vec<RcloneConfig>, 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");
if !config_path.exists() {
return Ok(Vec::new());
}
let file = File::open(&config_path).map_err(|e| format!("Failed to open config file: {}", e))?;
let reader = BufReader::new(file);
let mut configs = Vec::new();
let mut current_config: Option<RcloneConfig> = None;
for line in reader.lines() {
let line = line.map_err(|e| format!("Failed to read line: {}", e))?;
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
if let Some(config) = current_config.take() {
configs.push(config);
}
let name = line[1..line.len()-1].to_string();
current_config = Some(RcloneConfig {
name: name.clone(),
remote_path: format!("s3://{}", name),
local_path: Path::new(&home_dir).join("General Bots").join(&name).to_string_lossy().to_string(),
access_key: String::new(),
secret_key: String::new(),
});
} else if let Some(ref mut config) = current_config {
if let Some(pos) = line.find('=') {
let key = line[..pos].trim().to_string();
let value = line[pos+1..].trim().to_string();
match key.as_str() {
"access_key_id" => config.access_key = value,
"secret_access_key" => config.secret_key = value,
_ => {}
}
}
}
}
if let Some(config) = current_config {
configs.push(config);
}
Ok(configs)
}
fn run_sync(config: &RcloneConfig) -> Result<Child, std::io::Error> {
let local_path = Path::new(&config.local_path);
if !local_path.exists() {
create_dir_all(local_path)?;
}
ProcCommand::new("rclone")
.arg("sync")
.arg(&config.remote_path)
.arg(&config.local_path)
.arg("--no-check-certificate")
.arg("--verbose")
.arg("--rc")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
}
fn get_rclone_status(remote_name: &str) -> Result<SyncStatus, String> {
let output = ProcCommand::new("rclone")
.arg("rc")
.arg("core/stats")
.arg("--json")
.output()
.map_err(|e| format!("Failed to execute rclone rc: {}", e))?;
if !output.status.success() {
return Err(format!("rclone rc failed: {}", String::from_utf8_lossy(&output.stderr)));
}
let json = String::from_utf8_lossy(&output.stdout);
let parsed: Result<Value, _> = serde_json::from_str(&json);
match parsed {
Ok(value) => {
let transferred = value.get("bytes").and_then(|v| v.as_u64()).unwrap_or(0);
let errors = value.get("errors").and_then(|v| v.as_u64()).unwrap_or(0);
let speed = value.get("speed").and_then(|v| v.as_f64()).unwrap_or(0.0);
let status = if errors > 0 {
"Error occurred".to_string()
} else if speed > 0.0 {
"Transferring".to_string()
} else if transferred > 0 {
"Completed".to_string()
} else {
"Initializing".to_string()
};
Ok(SyncStatus {
name: remote_name.to_string(),
status,
transferred: format_bytes(transferred),
bytes: format!("{}/s", format_bytes(speed as u64)),
errors: errors as usize,
last_updated: chrono::Local::now().format("%H:%M:%S").to_string(),
})
}
Err(e) => Err(format!("Failed to parse rclone status: {}", e)),
}
}
fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}

4
src/ui/mod.rs Normal file
View file

@ -0,0 +1,4 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub mod drive;
pub mod sync;

145
src/ui/sync.rs Normal file
View file

@ -0,0 +1,145 @@
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use std::process::{Command, Stdio};
use std::path::Path;
use std::fs::{OpenOptions, create_dir_all};
use std::io::Write;
use std::env;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RcloneConfig {
name: String,
remote_path: String,
local_path: String,
access_key: String,
secret_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncStatus {
name: String,
status: String,
transferred: String,
bytes: String,
errors: usize,
last_updated: String,
}
pub(crate) struct AppState {
pub sync_processes: Mutex<Vec<std::process::Child>>,
pub sync_active: Mutex<bool>,
}
#[tauri::command]
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");
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&config_path)
.map_err(|e| format!("Failed to open config file: {}", e))?;
writeln!(file, "[{}]", config.name)
.and_then(|_| writeln!(file, "type = s3"))
.and_then(|_| writeln!(file, "provider = Other"))
.and_then(|_| writeln!(file, "access_key_id = {}", config.access_key))
.and_then(|_| writeln!(file, "secret_access_key = {}", config.secret_key))
.and_then(|_| writeln!(file, "endpoint = https://drive-api.pragmatismo.com.br"))
.and_then(|_| writeln!(file, "acl = private"))
.map_err(|e| format!("Failed to write config: {}", e))
}
#[tauri::command]
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))?;
}
let child = Command::new("rclone")
.arg("sync")
.arg(&config.remote_path)
.arg(&config.local_path)
.arg("--no-check-certificate")
.arg("--verbose")
.arg("--rc")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start rclone: {}", e))?;
state.sync_processes.lock().unwrap().push(child);
*state.sync_active.lock().unwrap() = true;
Ok(())
}
#[tauri::command]
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))?;
}
processes.clear();
*state.sync_active.lock().unwrap() = false;
Ok(())
}
#[tauri::command]
pub fn get_status(remote_name: String) -> Result<SyncStatus, String> {
let output = Command::new("rclone")
.arg("rc")
.arg("core/stats")
.arg("--json")
.output()
.map_err(|e| format!("Failed to execute rclone rc: {}", e))?;
if !output.status.success() {
return Err(format!("rclone rc failed: {}", String::from_utf8_lossy(&output.stderr)));
}
let json = String::from_utf8_lossy(&output.stdout);
let value: serde_json::Value = serde_json::from_str(&json)
.map_err(|e| format!("Failed to parse rclone status: {}", e))?;
let transferred = value.get("bytes").and_then(|v| v.as_u64()).unwrap_or(0);
let errors = value.get("errors").and_then(|v| v.as_u64()).unwrap_or(0);
let speed = value.get("speed").and_then(|v| v.as_f64()).unwrap_or(0.0);
let status = if errors > 0 {
"Error occurred".to_string()
} else if speed > 0.0 {
"Transferring".to_string()
} else if transferred > 0 {
"Completed".to_string()
} else {
"Initializing".to_string()
};
Ok(SyncStatus {
name: remote_name,
status,
transferred: format_bytes(transferred),
bytes: format!("{}/s", format_bytes(speed as u64)),
errors: errors as usize,
last_updated: chrono::Local::now().format("%H:%M:%S").to_string(),
})
}
pub fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}

View file

@ -4,7 +4,7 @@ use std::fs;
#[actix_web::get("/")]
async fn index() -> Result<HttpResponse> {
match fs::read_to_string("web/index.html") {
match fs::read_to_string("web/app/index.html") {
Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)),
Err(e) => {
error!("Failed to load index page: {}", e);
@ -26,10 +26,10 @@ async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
}
}
#[actix_web::get("/static/{filename:.*}")]
#[actix_web::get("/{filename:.*}")]
async fn static_files(req: HttpRequest) -> Result<HttpResponse> {
let filename = req.match_info().query("filename");
let path = format!("web/static/{}", filename);
let path = format!("web/app/{}", filename);
match fs::read(&path) {
Ok(content) => {
debug!(
@ -39,6 +39,8 @@ async fn static_files(req: HttpRequest) -> Result<HttpResponse> {
);
let content_type = match filename {
f if f.ends_with(".js") => "application/javascript",
f if f.ends_with(".riot") => "application/javascript",
f if f.ends_with(".html") => "application/javascript",
f if f.ends_with(".css") => "text/css",
f if f.ends_with(".png") => "image/png",
f if f.ends_with(".jpg") | f.ends_with(".jpeg") => "image/jpeg",

450
web/app/app.html Normal file
View file

@ -0,0 +1,450 @@
<app>
<script>
export default {
// Component state
data() {
return {
email: '',
password: '',
isLoading: false,
error: '',
};
},
// Configuration (ZITADEL)
onBeforeMount() {
// this.zitadelConfig = {
// authority: 'https://your-zitadel-instance.com',
// clientId: 'your-client-id',
// redirectUri: typeof window !== 'undefined' ? window.location.origin : '',
// scopes: ['openid', 'profile', 'email'],
// };
},
// Methods
methods: {
handleSocialLogin(provider) {
this.isLoading = true;
this.error = '';
try {
const authUrl = `${this.zitadelConfig.authority}/oauth/v2/authorize?` +
`client_id=${this.zitadelConfig.clientId}&` +
`redirect_uri=${encodeURIComponent(this.zitadelConfig.redirectUri)}&` +
`response_type=code&` +
`scope=${encodeURIComponent(this.zitadelConfig.scopes.join(' '))}&` +
`provider=${provider}`;
window.location.href = authUrl;
} catch (err) {
this.error = 'Failed to initiate login';
console.error('Login error:', err);
} finally {
this.isLoading = false;
}
},
handleEmailLogin(e) {
e.preventDefault();
this.isLoading = true;
this.error = '';
try {
// Mock implementation store dummy token
localStorage.setItem('authToken', 'dummy-token');
// Navigate to dashboard (adjust path as needed)
window.location.href = '/dashboard';
} catch (err) {
this.error = 'Login failed. Please check your credentials.';
console.error('Login error:', err);
} finally {
this.isLoading = false;
}
}
}
};
</script>
<div class="auth-screen">
<div class="auth-content">
<div class="auth-left-panel">
<div class="auth-logo">
<h1>Welcome to General Bots Online</h1>
</div>
<div class="auth-quote">
<p>"Errar é Humano."</p>
<p>General Bots</p>
</div>
</div>
<div class="auth-form-container">
<div class="auth-form-header">
<h2>Sign in to your account</h2>
<p>Choose your preferred login method</p>
</div>
<div class="auth-error" if={error}>{error}</div>
<div class="auth-social-buttons">
<button class="auth-social-button google" @click={()=> handleSocialLogin('google')}
disabled={isLoading}>
<svg class="auth-social-icon" viewBox="0 0 24 24">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4" />
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853" />
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05" />
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335" />
</svg>
Continue with Google
</button>
<button class="auth-social-button microsoft" @click={()=> handleSocialLogin('microsoft')}
disabled={isLoading}>
<svg class="auth-social-icon" viewBox="0 0 23 23">
<path d="M0 0h11v11H0zM12 0h11v11H12zM0 12h11v11H0zM12 12h11v11H12z" fill="#F25022" />
<path d="M12 0h11v11H12z" fill="#7FBA00" />
<path d="M0 12h11v11H0z" fill="#00A4EF" />
<path d="M12 12h11v11H12z" fill="#FFB900" />
</svg>
Continue with Microsoft
</button>
<button class="auth-social-button facebook" @click={()=> handleSocialLogin('facebook')}
disabled={isLoading}>
<svg class="auth-social-icon" viewBox="0 0 24 24">
<path
d="M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12c0 4.84 3.44 8.87 8 9.8V15H8v-3h2V9.5C10 7.57 11.57 6 13.5 6H16v3h-2c-.55 0-1 .45-1 1v2h3v3h-3v6.95c5.05-.5 9-4.76 9-9.95z"
fill="#1877F2" />
</svg>
Continue with Facebook
</button>
<button class="auth-social-button pragmatismo" @click={()=> handleSocialLogin('pragmatismo')}
disabled={isLoading}>
<svg class="auth-social-icon" viewBox="0 0 24 24">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
fill="currentColor" />
</svg>
Continue with Pragmatismo
</button>
</div>
<div class="auth-divider">
<span>OR</span>
</div>
<form class="auth-form" @submit={handleEmailLogin}>
<div class="auth-form-group">
<label for="email">Email</label>
<input id="email" type="email" value={email} oninput={e=> this.email = e.target.value}
placeholder="your@email.com" required />
</div>
<div class="auth-form-group">
<label for="password">Password</label>
<input id="password" type="password" value={password} oninput={e=> this.password = e.target.value}
placeholder="••••••••" required />
</div>
<div class="auth-form-options">
<div class="auth-remember-me">
<input type="checkbox" id="remember" />
<label for="remember">Remember me</label>
</div>
<a href="#" class="auth-forgot-password">Forgot password?</a>
</div>
<button type="submit" class="auth-submit-button" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign in with Email'}
</button>
</form>
<div class="auth-signup-link">
Don't have an account? <a href="#">Sign up</a>
</div>
<p class="auth-terms">
By continuing, you agree to our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>.
</p>
</div>
</div>
<style>
.auth-screen {
--background: hsl(var(--background));
--foreground: hsl(var(--foreground));
--card: hsl(var(--card));
--card-foreground: hsl(var(--card-foreground));
--primary: hsl(var(--primary));
--primary-foreground: hsl(var(--primary-foreground));
--secondary: hsl(var(--secondary));
--secondary-foreground: hsl(var(--secondary-foreground));
--muted: hsl(var(--muted));
--muted-foreground: hsl(var(--muted-foreground));
--accent: hsl(var(--accent));
--accent-foreground: hsl(var(--accent-foreground));
--destructive: hsl(var(--destructive));
--destructive-foreground: hsl(var(--destructive-foreground));
--border: hsl(var(--border));
--input: hsl(var(--input));
--ring: hsl(var(--ring));
--radius: var(--radius);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--background);
color: var(--foreground);
padding: 1rem;
}
.auth-content {
display: flex;
width: 100%;
max-width: 1200px;
background-color: var(--card);
border-radius: var(--radius);
overflow: hidden;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
.auth-left-panel {
flex: 1;
padding: 4rem;
background: linear-gradient(135deg, var(--primary), var(--accent));
color: var(--primary-foreground);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.auth-logo h1 {
font-size: 2rem;
margin-bottom: 1rem;
}
.auth-quote {
font-style: italic;
margin-top: auto;
}
.auth-quote p:last-child {
text-align: right;
margin-top: 0.5rem;
}
.auth-form-container {
flex: 1;
padding: 4rem;
max-width: 500px;
}
.auth-form-header {
margin-bottom: 2rem;
text-align: center;
}
.auth-form-header h2 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.auth-form-header p {
color: var(--muted-foreground);
}
.auth-error {
background-color: var(--destructive);
color: var(--destructive-foreground);
padding: 0.75rem;
border-radius: var(--radius);
margin-bottom: 1rem;
text-align: center;
}
.auth-social-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.auth-social-button {
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem;
border-radius: var(--radius);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--border);
background-color: var(--secondary);
color: var(--secondary-foreground);
}
.auth-social-button:hover {
background-color: var(--muted);
}
.auth-social-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.auth-social-icon {
width: 1.25rem;
height: 1.25rem;
margin-right: 0.5rem;
}
.auth-divider {
display: flex;
align-items: center;
margin: 1.5rem 0;
color: var(--muted-foreground);
}
.auth-divider::before,
.auth-divider::after {
content: "";
flex: 1;
border-bottom: 1px solid var(--border);
}
.auth-divider span {
padding: 0 1rem;
}
.auth-form {
margin-top: 1.5rem;
}
.auth-form-group {
margin-bottom: 1rem;
}
.auth-form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.auth-form-group input {
width: 100%;
padding: 0.75rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background-color: var(--input);
color: var(--foreground);
}
.auth-form-group input:focus {
outline: none;
border-color: var(--ring);
box-shadow: 0 0 0 2px var(--ring);
}
.auth-form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin: 1rem 0;
}
.auth-remember-me {
display: flex;
align-items: center;
}
.auth-remember-me input {
margin-right: 0.5rem;
}
.auth-forgot-password {
color: var(--primary);
text-decoration: none;
}
.auth-forgot-password:hover {
text-decoration: underline;
}
.auth-submit-button {
width: 100%;
padding: 0.75rem;
border-radius: var(--radius);
background-color: var(--primary);
color: var(--primary-foreground);
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.auth-submit-button:hover {
background-color: color-mix(in srgb, var(--primary), black 10%);
}
.auth-submit-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.auth-signup-link {
text-align: center;
margin: 1.5rem 0;
color: var(--muted-foreground);
}
.auth-signup-link a {
color: var(--primary);
text-decoration: none;
}
.auth-signup-link a:hover {
text-decoration: underline;
}
.auth-terms {
text-align: center;
font-size: 0.875rem;
color: var(--muted-foreground);
}
.auth-terms a {
color: var(--primary);
text-decoration: none;
}
.auth-terms a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.auth-content {
flex-direction: column;
}
.auth-left-panel {
padding: 2rem;
}
.auth-form-container {
padding: 2rem;
max-width: 100%;
}
.auth-social-buttons {
grid-template-columns: 1fr;
}
}
</style>
</div>
</app>

298
web/app/chat/chat.page.html Normal file
View file

@ -0,0 +1,298 @@
<!-- Riot.js component for the chat page (converted from app/chat/page.tsx) -->
<template>
<div class="flex min-h-[calc(100vh-43px)] bg-background text-foreground">
<!-- Sidebar -->
<div class="{sidebarOpen ? 'w-80' : 'w-0'} transition-all duration-300 ease-in-out bg-card border-r border-border flex flex-col overflow-hidden">
{sidebarOpen && (
<>
<!-- Sidebar Header -->
<div class="p-4 border-b border-border flex-shrink-0">
<button @click={newChat} class="flex items-center gap-3 w-full p-3 rounded-xl border-2 border-dashed border-muted hover:border-accent hover:bg-accent/10 transition-all duration-200 group">
<Plus class="w-5 h-5 text-muted-foreground group-hover:text-accent" />
<span class="font-medium text-foreground group-hover:text-accent">New Chat</span>
</button>
</div>
<!-- Conversations List -->
<div class="flex-1 overflow-hidden">
<div class="h-full overflow-y-auto px-3 py-2 space-y-1">
{conversations.map(conv => (
<div
key={conv.id}
class="group relative flex items-center gap-3 p-3 rounded-xl cursor-pointer transition-all duration-200 {conv.active ? 'bg-primary/10 border border-primary' : 'hover:bg-secondary'}"
@click={() => setActiveConversation(conv.id)}
>
<MessageSquare class="w-4 h-4 flex-shrink-0 {conv.active ? 'text-primary' : 'text-muted-foreground'}" />
<div class="flex-1 min-w-0">
<div class="font-medium truncate {conv.active ? 'text-primary' : 'text-foreground'}">{conv.title}</div>
<div class="text-xs text-muted-foreground truncate">{formatTimestamp(conv.timestamp)}</div>
</div>
{conv.active && <div class="w-2 h-2 rounded-full bg-primary flex-shrink-0"></div>}
</div>
))}
</div>
</div>
<!-- Sidebar Footer -->
<div class="p-4 border-t border-border flex-shrink-0">
<div class="flex items-center gap-3 p-3 rounded-xl hover:bg-secondary cursor-pointer transition-colors duration-200">
<User class="w-5 h-5 text-muted-foreground" />
<User class="w-5 h-5 text-muted-foreground" />
<User class="w-5 h-5 text-muted-foreground" />
<User class="w-5 h-5 text-muted-foreground" />
<User class="w-5 h-5 text-muted-foreground" />
</div>
</div>
</>
)}
</div>
<!-- Main Chat Area -->
<div class="flex-1 flex flex-col min-w-0 bg-background">
<!-- Header -->
<div class="flex-shrink-0 p-4 border-b border-border bg-card">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<button @click={() => sidebarOpen = !sidebarOpen} class="p-2 hover:bg-secondary rounded-lg transition-colors duration-200">
<Menu class="w-5 h-5" />
</button>
<h1 class="text-xl font-semibold text-foreground">{activeConversation?.title || 'New Chat'}</h1>
</div>
<button class="p-2 hover:bg-secondary rounded-lg transition-colors duration-200">
<Search class="w-5 h-5" />
</button>
</div>
</div>
<!-- Messages Container -->
<div class="flex-1 overflow-hidden flex flex-col">
<div class="flex-1 overflow-y-auto px-4 py-6 space-y-6">
{messages.map(message => (
<div key={message.id} class="group flex {message.type === 'user' ? 'justify-end' : 'justify-start'}">
<div class="max-w-[85%] md:max-w-[75%] {message.type === 'user' ? 'bg-primary text-primary-foreground' : 'bg-secondary text-secondary-foreground'} rounded-2xl px-4 py-3 shadow-sm">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
{message.type === 'user' ? <User class="w-4 h-4" /> : <Bot class="w-4 h-4" />}
</div>
<div class="flex-1 min-w-0">
<div class="whitespace-pre-wrap break-words leading-relaxed">{message.content}</div>
<div class="mt-3 flex items-center justify-between text-xs {message.type === 'user' ? 'text-primary-foreground/80' : 'text-muted-foreground'}">
<span>{formatTimestamp(message.timestamp)}</span>
{message.type === 'assistant' && (
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<button class="p-1.5 hover:bg-secondary rounded-md transition-colors">
<Copy class="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button class="p-1.5 hover:bg-secondary rounded-md transition-colors">
<ThumbsUp class="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button class="p-1.5 hover:bg-secondary rounded-md transition-colors">
<ThumbsDown class="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button class="p-1.5 hover:bg-secondary rounded-md transition-colors">
<Share class="w-3.5 h-3.5 text-muted-foreground" />
</button>
</div>
)}
</div>
</div>
</div>
</div>
</div>
))}
{isTyping && (
<div class="flex justify-start">
<div class="max-w-[85%] md:max-w-[75%] bg-secondary rounded-2xl px-4 py-3 shadow-sm">
<div class="flex items-center gap-3">
<Bot class="w-4 h-4 text-muted-foreground" />
<div class="flex gap-1">
<div class="w-2 h-2 rounded-full bg-muted-foreground animate-bounce"></div>
<div class="w-2 h-2 rounded-full bg-muted-foreground animate-bounce" style="animation-delay:0.2s"></div>
<div class="w-2 h-2 rounded-full bg-muted-foreground animate-bounce" style="animation-delay:0.4s"></div>
</div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef}></div>
</div>
</div>
<!-- Mode Carousel -->
<div class="flex-shrink-0 border-t border-border bg-card">
<div class="overflow-x-auto px-4 py-3">
<div class="flex gap-2 min-w-max">
{modeButtons.map(button => (
<button
key={button.id}
@click={() => activeMode = button.id}
class="flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 whitespace-nowrap {activeMode === button.id ? 'bg-primary/10 text-primary border border-primary' : 'bg-secondary text-secondary-foreground hover:bg-secondary/80'}"
>
{button.icon}
<span>{button.label}</span>
</button>
))}
</div>
</div>
</div>
<!-- Input Area -->
<div class="flex-shrink-0 p-4 border-t border-border bg-card">
<div class="relative max-w-4xl mx-auto">
<textarea
ref={textareaRef}
value={input}
@input={e => input = e.target.value}
@keydown={handleKeyDown}
placeholder="Type your message..."
class="w-full p-4 pr-14 rounded-2xl border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent resize-none transition-all duration-200"
rows="1"
style="min-height:56px;max-height:120px"
></textarea>
<button
@click={handleSubmit}
disabled={!input.trim()}
class="absolute right-6 bottom-3 p-2.5 rounded-xl transition-all duration-200 {input.trim() ? 'bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transform hover:scale-105' : 'bg-muted text-muted-foreground cursor-not-allowed'}"
>
<Send class="w-5 h-5" />
</button>
</div>
</div>
</div>
</div>
</template>
<script type="module">
import { useState, useRef } from 'riot';
import {
Send, Plus, Menu, Search,
MessageSquare, User, Bot, Copy, ThumbsUp, ThumbsDown,
Share, Image, Video,
Brain, Globe
} from 'lucide-react';
import './style.css';
export default {
// Reactive state
messages: [],
input: '',
isTyping: false,
sidebarOpen: true,
conversations: [],
activeMode: 'assistant',
modeButtons: [],
activeConversation: null,
textareaRef: null,
messagesEndRef: null,
// Lifecycle
async mounted() {
// Initialize state (mirroring the original React defaults)
this.messages = [
{
id: 1,
type: 'assistant',
content: "Hello! I'm General Bots, a large language model by Pragmatismo. How can I help you today?",
timestamp: new Date().toISOString()
}
];
this.input = '';
this.isTyping = false;
this.sidebarOpen = true;
this.conversations = [
{ id: 1, title: 'Current Chat', timestamp: new Date(), active: true },
{ id: 2, title: 'Previous Conversation', timestamp: new Date(Date.now() - 86400000), active: false },
{ id: 3, title: 'Code Review Discussion', timestamp: new Date(Date.now() - 172800000), active: false },
{ id: 4, title: 'Project Planning', timestamp: new Date(Date.now() - 259200000), active: false },
];
this.activeMode = 'assistant';
this.modeButtons = [
{ id: 'deep-think', icon: <Brain size={16} />, label: 'Deep Think' },
{ id: 'web', icon: <Globe size={16} />, label: 'Web' },
{ id: 'image', icon: <Image size={16} />, label: 'Image' },
{ id: 'video', icon: <Video size={16} />, label: 'Video' },
];
this.setActiveConversation(1);
this.scrollToBottom();
},
// Helpers
formatTimestamp(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diffInHours = (now - date) / (1000 * 60 * 60);
if (diffInHours < 24) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else if (diffInHours < 48) {
return 'Yesterday';
} else {
return date.toLocaleDateString();
}
},
scrollToBottom() {
this.messagesEndRef?.scrollIntoView({ behavior: 'smooth' });
},
// Event handlers
async handleSubmit() {
if (!this.input.trim()) return;
const userMessage = {
id: Date.now(),
type: 'user',
content: this.input.trim(),
timestamp: new Date().toISOString()
};
this.messages = [...this.messages, userMessage];
this.input = '';
this.isTyping = true;
this.update();
// Simulate assistant response
setTimeout(() => {
const assistantMessage = {
id: Date.now() + 1,
type: 'assistant',
content: `I understand you're asking about "${userMessage.content}". This is a simulated response to demonstrate the chat interface. The actual implementation would connect to your chat provider and send real responses.`,
timestamp: new Date().toISOString()
};
this.messages = [...this.messages, assistantMessage];
this.isTyping = false;
this.update();
this.scrollToBottom();
}, 1500);
},
handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.handleSubmit();
}
},
newChat() {
const newConv = {
id: Date.now(),
title: 'New Chat',
timestamp: new Date(),
active: true
};
this.conversations = [newConv, ...this.conversations.map(c => ({ ...c, active: false }))];
this.messages = [
{
id: Date.now(),
type: 'assistant',
content: "Hello! I'm General Bots, a large language model by Pragmatismo. How can I help you today?",
timestamp: new Date().toISOString()
}
];
this.setActiveConversation(newConv.id);
},
setActiveConversation(id) {
this.conversations = this.conversations.map(c => ({ ...c, active: c.id === id }));
this.activeConversation = this.conversations.find(c => c.id === id);
}
};
</script>

414
web/app/client-nav.css Normal file
View file

@ -0,0 +1,414 @@
.nav-container {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 50;
background: hsl(var(--background));
height: auto;
min-height: 40px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
border-bottom: 1px solid hsl(var(--border));
}
.nav-inner {
max-width: 100%;
margin: 0 auto;
padding: 0 16px;
height: 100%;
}
.nav-content {
display: flex;
align-items: center;
height: 100%;
gap: 8px;
}
.auth-controls {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.login-container,
.theme-container {
position: relative;
}
.login-button,
.theme-toggle {
background: hsl(var(--accent));
border: 1px solid hsl(var(--border));
color: hsl(var(--accent-foreground));
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.login-button:hover,
.theme-toggle:hover {
transform: scale(1.1);
box-shadow: 0 0 10px hsla(var(--primary), 0.5);
}
.login-menu,
.theme-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
background: hsl(var(--popover));
border: 1px solid hsl(var(--border));
border-radius: 6px;
min-width: 120px;
z-index: 100;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
padding: 4px;
display: flex;
flex-direction: column;
gap: 2px;
}
.menu-item,
.theme-menu-item {
width: 100%;
padding: 8px 12px;
background: transparent;
border: none;
color: hsl(var(--foreground));
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
border-radius: 4px;
}
.menu-item:hover,
.theme-menu-item:hover {
background: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.active-theme {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.scroll-btn {
background: hsl(var(--accent));
border: 1px solid hsl(var(--border));
color: hsl(var(--accent-foreground));
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
font-weight: bold;
transition: all 0.3s ease;
flex-shrink: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
}
.scroll-btn:hover {
transform: scale(1.1);
box-shadow: 0 0 10px hsla(var(--primary), 0.5);
}
.scroll-btn:active {
transform: scale(0.95);
}
.nav-scroll {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
height: 100%;
-ms-overflow-style: none;
scrollbar-width: none;
scroll-behavior: smooth;
position: relative;
}
.nav-scroll::-webkit-scrollbar {
display: none;
}
.nav-items {
display: flex;
align-items: center;
height: 100%;
white-space: nowrap;
gap: 3px;
padding: 0 8px;
}
.nav-item {
position: relative;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
color: hsl(var(--foreground));
font-size: 13px;
font-weight: 500;
padding: 6px 14px;
cursor: pointer;
border-radius: 6px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
white-space: nowrap;
transition: all 0.3s ease;
overflow: hidden;
min-width: 70px;
}
.nav-item::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(var(--neon-color-rgb, 0, 255, 255), 0.2), transparent);
transition: left 0.5s;
}
.nav-item:hover::before {
left: 100%;
}
.nav-item:hover {
border-color: var(--neon-color, hsl(var(--primary)));
color: var(--neon-color, hsl(var(--primary)));
box-shadow: 0 0 15px rgba(var(--neon-color-rgb, 0, 255, 255), 0.3);
text-shadow: 0 0 6px rgba(var(--neon-color-rgb, 0, 255, 255), 0.4);
}
.nav-item.active {
border-color: var(--neon-color, hsl(var(--primary)));
color: var(--neon-color, hsl(var(--primary)));
box-shadow: 0 0 20px rgba(var(--neon-color-rgb, 0, 255, 255), 0.4);
text-shadow: 0 0 8px rgba(var(--neon-color-rgb, 0, 255, 255), 0.6);
}
.nav-item.active:hover {
box-shadow: 0 0 25px rgba(var(--neon-color-rgb, 0, 255, 255), 0.6);
}
.neon-glow {
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg, transparent, rgba(var(--neon-color-rgb, 0, 255, 255), 0.3), transparent);
border-radius: 8px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: -1;
}
.nav-item:hover .neon-glow,
.nav-item.active .neon-glow {
opacity: 1;
}
.nav-spacer {
height: 40px;
}
/* Set CSS custom properties for each neon color */
.nav-item[style*="--neon-color: #25D366"] {
--neon-color-rgb: 37, 211, 102;
}
.nav-item[style*="--neon-color: #6366F1"] {
--neon-color-rgb: 99, 102, 241;
}
.nav-item[style*="--neon-color: #FFD700"] {
--neon-color-rgb: 255, 215, 0;
}
.nav-item[style*="--neon-color: #10B981"] {
--neon-color-rgb: 16, 185, 129;
}
.nav-item[style*="--neon-color: #2563EB"] {
--neon-color-rgb: 37, 99, 235;
}
.nav-item[style*="--neon-color: #8B5CF6"] {
--neon-color-rgb: 139, 92, 246;
}
.nav-item[style*="--neon-color: #059669"] {
--neon-color-rgb: 5, 150, 105;
}
.nav-item[style*="--neon-color: #DC2626"] {
--neon-color-rgb: 220, 38, 38;
}
.nav-item[style*="--neon-color: #1DB954"] {
--neon-color-rgb: 29, 185, 84;
}
.nav-item[style*="--neon-color: #F59E0B"] {
--neon-color-rgb: 245, 158, 11;
}
.nav-item[style*="--neon-color: #6B7280"] {
--neon-color-rgb: 107, 114, 128;
}
@media (max-width: 768px) {
.nav-container {
height: 44px;
}
.nav-spacer {
height: 44px;
}
.nav-inner {
padding: 0 12px;
}
.nav-content {
gap: 6px;
}
.scroll-btn {
width: 30px;
height: 30px;
font-size: 16px;
}
.theme-toggle,
.login-button {
width: 30px;
height: 30px;
font-size: 14px;
}
.nav-item {
font-size: 13px;
padding: 8px 16px;
height: 36px;
margin: 0 2px;
}
.nav-items {
gap: 6px;
padding: 0 8px;
}
.auth-controls {
gap: 6px;
}
}
@media (max-width: 480px) {
.nav-container {
height: 48px;
}
.nav-spacer {
height: 48px;
}
.nav-inner {
padding: 0 8px;
}
.nav-content {
gap: 6px;
}
.scroll-btn {
width: 28px;
height: 28px;
font-size: 16px;
}
.theme-toggle,
.login-button {
width: 28px;
height: 28px;
font-size: 12px;
}
.nav-item {
font-size: 12px;
padding: 10px 14px;
height: 34px;
margin: 0 2px;
}
.nav-items {
gap: 4px;
padding: 0 6px;
}
.auth-controls {
gap: 4px;
}
}
@media (max-width: 320px) {
.nav-inner {
padding: 0 6px;
}
.nav-content {
gap: 4px;
}
.nav-item {
padding: 8px 12px;
height: 32px;
font-size: 11px;
}
.nav-items {
gap: 3px;
padding: 0 4px;
}
.theme-toggle,
.login-button {
width: 26px;
height: 26px;
font-size: 11px;
}
.scroll-btn {
width: 26px;
height: 26px;
font-size: 14px;
}
}
/* Touch-friendly scrolling for mobile */
@media (hover: none) and (pointer: coarse) {
.nav-scroll {
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
}
.nav-item {
scroll-snap-align: start;
}
}

340
web/app/client-nav.html Normal file
View file

@ -0,0 +1,340 @@
<script type="module">
// Import icons from Lucide (using CDN)
import { HardDrive, Terminal, ChevronLeft, ChevronRight } from 'https://cdn.jsdelivr.net/npm/lucide@0.331.0/+esm';
export default {
// Component state
data() {
return {
examples: [
{ name: "Chat", href: "/chat", color: "#25D366" },
{ name: "Paper", href: "/paper", color: "#6366F1" },
{ name: "Mail", href: "/mail", color: "#FFD700" },
{ name: "Calendar", href: "/calendar", color: "#1DB954" },
{ name: "Meet", href: "/meet", color: "#059669" },
{ name: "Drive", href: "/drive", color: "#10B981" },
{ name: "Editor", href: "/editor", color: "#2563EB" },
{ name: "Player", href: "/player", color: "Yellow" },
{ name: "Tables", href: "/tables", color: "#8B5CF6" },
{ name: "Dashboard", href: "/dashboard", color: "#6366F1" },
{ name: "Sources", href: "/sources", color: "#F59E0B" },
{ name: "Settings", href: "/settings", color: "#6B7280" },
],
pathname: window.location.pathname,
scrollContainer: null,
navItems: [],
isLoggedIn: false,
showLoginMenu: false,
showScrollButtons: false,
loginMenu: null,
showThemeMenu: false,
themeMenu: null,
currentTime: new Date(),
theme: { name: "default", label: "Default", icon: "🎨" },
themes: [
{ name: "retrowave", label: "Retrowave", icon: "🌌" },
{ name: "vapordream", label: "Vapordream", icon: "🌀" },
{ name: "y2kglow", label: "Y2K Glow", icon: "💿" },
{ name: "mellowgold", label: "Mellow Gold", icon: "☮️" },
{ name: "arcadeflash", label: "Arcade Flash", icon: "🕹️" },
{ name: "polaroidmemories", label: "Polaroid Memories", icon: "📸" },
{ name: "midcenturymod", label: "MidCentury Mod", icon: "🪑" },
{ name: "grungeera", label: "Grunge Era", icon: "🎸" },
{ name: "discofever", label: "Disco Fever", icon: "🪩" },
{ name: "saturdaycartoons", label: "Saturday Cartoons", icon: "📺" },
{ name: "oldhollywood", label: "Old Hollywood", icon: "🎬" },
{ name: "cyberpunk", label: "Cyberpunk", icon: "🤖" },
{ name: "seasidepostcard", label: "Seaside Postcard", icon: "🏖️" },
{ name: "typewriter", label: "Typewriter", icon: "⌨️" },
{ name: "jazzage", label: "Jazz Age", icon: "🎷" },
{ name: "xtreegold", label: "XTree Gold", icon: "X" },
],
};
},
// Lifecycle: component mounted
mounted() {
// References
this.scrollContainer = this.root.querySelector('.nav-scroll');
this.loginMenu = this.root.querySelector('.login-menu');
this.themeMenu = this.root.querySelector('.theme-menu');
// Initialize nav item refs
this.navItems = Array.from(this.root.querySelectorAll('.nav-item'));
// Time update interval
this.timeInterval = setInterval(() => {
this.currentTime = new Date();
this.update();
}, 1000);
// Scroll button visibility
this.checkScrollNeeded();
// Resize listener
window.addEventListener('resize', this.checkScrollNeeded);
// Clickoutside handling
document.addEventListener('mousedown', this.handleClickOutside);
},
// Cleanup
unmounted() {
clearInterval(this.timeInterval);
window.removeEventListener('resize', this.checkScrollNeeded);
document.removeEventListener('mousedown', this.handleClickOutside);
},
// Methods
formatTime(date) {
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
},
formatDate(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
},
isActive(href) {
if (href === '/') return this.pathname === href;
return this.pathname.startsWith(href);
},
handleLogin() {
this.isLoggedIn = true;
this.showLoginMenu = false;
this.update();
},
handleLogout() {
this.isLoggedIn = false;
this.showLoginMenu = false;
this.update();
},
checkScrollNeeded() {
if (this.scrollContainer) {
const container = this.scrollContainer;
const isScrollable = container.scrollWidth > container.clientWidth;
this.showScrollButtons = isScrollable;
this.update();
}
},
handleClickOutside(event) {
if (this.loginMenu && !this.loginMenu.contains(event.target)) {
this.showLoginMenu = false;
}
if (this.themeMenu && !this.themeMenu.contains(event.target)) {
this.showThemeMenu = false;
}
this.update();
},
scrollLeft() {
if (this.scrollContainer) {
this.scrollContainer.scrollBy({ left: -150, behavior: 'smooth' });
}
},
scrollRight() {
if (this.scrollContainer) {
this.scrollContainer.scrollBy({ left: 150, behavior: 'smooth' });
}
},
getThemeIcon() {
const found = this.themes.find(t => t.name === this.theme.name);
return found ? found.icon : '🎨';
},
setTheme(name) {
const found = this.themes.find(t => t.name === name);
if (found) {
this.theme = found;
this.showThemeMenu = false;
this.update();
}
},
navigate(href) {
window.location.href = href;
},
};
</script>
<style>
/* Basic styles - the original Tailwind classes are kept as comments for reference */
.fixed { position: fixed; }
.top-0 { top: 0; }
.left-0 { left: 0; }
.right-0 { right: 0; }
.z-50 { z-index: 50; }
.bg-gray-800 { background-color: #2d3748; }
.text-green-400 { color: #68d391; }
.font-mono { font-family: monospace; }
.border-b { border-bottom: 1px solid transparent; }
.border-green-600 { border-color: #38a169; }
.text-xs { font-size: .75rem; }
.flex { display: flex; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.py-1 { padding-top: .25rem; padding-bottom: .25rem; }
.gap-4 > * + * { margin-left: 1rem; }
.gap-2 > * + * { margin-left: .5rem; }
.w-3 { width: .75rem; }
.h-3 { height: .75rem; }
.text-green-300 { color: #9ae6b4; }
.w-2 { width: .5rem; }
.h-2 { height: .5rem; }
.bg-green-500 { background-color: #48bb78; }
.rounded-full { border-radius: 9999px; }
.animate-pulse { animation: pulse 2s infinite; }
.nav-container { position: relative; }
.nav-inner { overflow: hidden; }
.nav-content { display: flex; align-items: center; gap: .5rem; }
.logo-container img { display: block; }
.nav-scroll { overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; }
.nav-scroll::-webkit-scrollbar { display: none; }
.nav-items { display: flex; gap: .25rem; }
.nav-item { padding: .25rem .5rem; border: 1px solid transparent; border-radius: .25rem; cursor: pointer; transition: all .2s; }
.nav-item.active { background-color: #2d3748; border-color: #68d391; color: #68d391; }
.nav-item:hover { border-color: currentColor; }
.auth-controls { display: flex; gap: .5rem; }
.login-button, .theme-toggle { background: none; border: none; color: inherit; cursor: pointer; }
.login-menu, .theme-menu { position: absolute; background: #1a202c; border: 1px solid #4a5568; padding: .5rem; margin-top: .25rem; border-radius: .25rem; }
.menu-item { display: block; width: 100%; text-align: left; padding: .25rem; background: none; border: none; color: #a0aec0; cursor: pointer; }
.menu-item:hover { background: #2d3748; }
.active-theme { font-weight: bold; }
</style>
<!-- Markup -->
<div class="fixed top-0 left-0 right-0 z-50 bg-gray-800 text-green-400 font-mono border-b border-green-600 text-xs">
<div class="flex items-center justify-between px-4 py-1">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<HardDrive class="w-3 h-3 text-green-400" />
<span class="text-green-300">RETRO NAVIGATOR v4.0</span>
</div>
<div class="flex items-center gap-1">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span class="text-green-400">READY</span>
</div>
<div class="flex items-center gap-1">
<span class="text-green-300">THEME:</span>
<span class="text-yellow-400">{theme.label}</span>
</div>
</div>
<div class="flex items-center gap-4">
<span class="text-green-300">{formatDate(currentTime)}</span>
<span class="text-green-300">{formatTime(currentTime)}</span>
<div class="flex items-center gap-1">
<Terminal class="w-3 h-3 text-green-400" />
<span class="text-green-400">SYS</span>
</div>
</div>
</div>
</div>
<div class="nav-container" style="top:24px;">
<div class="nav-inner">
<div class="nav-content">
<div class="logo-container">
<img src="/images/generalbots-logo.svg" alt="Logo" width="64" height="24" />
</div>
{#if showScrollButtons}
<button class="w-8 h-8 bg-gray-800 border border-green-600 text-green-400 rounded hover:bg-green-900/30 hover:border-green-500 hover:text-green-300 transition-all flex items-center justify-center flex-shrink-0 mx-1"
@click="scrollLeft" aria-label="Scroll left">
<ChevronLeft class="w-4 h-4" />
</button>
{/if}
<div class="nav-scroll">
<div class="nav-items">
{#each examples as example, index}
<button class="nav-item {isActive(example.href) ? 'active' : ''}"
@click="{() => navigate(example.href)}"
style="--neon-color:{example.color}"
@mouseenter="{(e) => {
e.target.style.boxShadow = `0 0 15px ${example.color}60`;
e.target.style.borderColor = example.color;
e.target.style.color = example.color;
e.target.style.textShadow = `0 0 8px ${example.color}80`;
}}"
@mouseleave="{(e) => {
if (!isActive(example.href)) {
e.target.style.boxShadow = 'none';
e.target.style.borderColor = '';
e.target.style.color = '';
e.target.style.textShadow = 'none';
}
}}">
{example.name}
<div class="neon-glow"></div>
</button>
{/each}
</div>
</div>
{#if showScrollButtons}
<button class="w-8 h-8 bg-gray-800 border border-green-600 text-green-400 rounded hover:bg-green-900/30 hover:border-green-500 hover:text-green-300 transition-all flex items-center justify-center flex-shrink-0 mx-1"
@click="scrollRight" aria-label="Scroll right">
<ChevronRight class="w-4 h-4" />
</button>
{/if}
<div class="auth-controls">
<div class="login-container" bind:this="{loginMenu}">
<button @click="{() => showLoginMenu = !showLoginMenu}"
class="login-button"
aria-label="{isLoggedIn ? 'User menu' : 'Login'}">
{isLoggedIn ? '👤' : '🔐'}
</button>
{#if showLoginMenu}
<div class="login-menu">
{#if !isLoggedIn}
<button @click="handleLogin" class="menu-item">Login</button>
{:else}
<button @click="handleLogout" class="menu-item">Logout</button>
{/if}
</div>
{/if}
</div>
<div class="theme-container" bind:this="{themeMenu}">
<button @click="{() => showThemeMenu = !showThemeMenu}"
class="theme-toggle"
aria-label="Change theme">
{getThemeIcon()}
</button>
{#if showThemeMenu}
<div class="theme-menu">
{#each themes as t}
<button @click="{() => setTheme(t.name)}"
class="theme-menu-item {theme.name === t.name ? 'active-theme' : ''}">
{t.label}
</button>
{/each}
</div>
{/if}
</div>
</div>
</div>
</div>
</div>
<div class="nav-spacer" style="height:88px;"></div>
</div>

View file

@ -0,0 +1,145 @@
<!-- Riot.js component for the dashboard page (converted from app/dashboard/page.tsx) -->
<template>
<div class="min-h-screen bg-background text-foreground">
<main class="container p-4 space-y-4">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-foreground">Dashboard</h1>
<div class="flex items-center space-x-2">
<calendar-date-range-picker />
<button class="px-4 py-2 bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity">
Download
</button>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div class="p-6 bg-card border rounded-lg border-border" each={card in cards}>
<h3 class="text-sm font-medium text-muted-foreground">{card.title}</h3>
<p class="text-2xl font-bold mt-1 text-card-foreground">{card.value}</p>
<p class="text-xs text-muted-foreground mt-1">{card.subtext}</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="p-6 bg-card border rounded-lg border-border">
<h3 class="text-lg font-medium mb-4 text-card-foreground">Overview</h3>
<overview />
</div>
<div class="p-6 bg-card border rounded-lg space-y-4 border-border">
<h3 class="text-lg font-medium text-card-foreground">Recent Sales</h3>
<p class="text-card-foreground">You made 265 sales this month.</p>
<recent-sales />
</div>
</div>
</main>
</div>
</template>
<script type="module">
import { useState } from 'riot';
import './style.css';
export default {
// Reactive state
dateRange: { startDate: new Date(), endDate: new Date() },
salesData: [
{ name: "Olivia Martin", email: "olivia.martin@email.com", amount: "+$1,999.00" },
{ name: "Jackson Lee", email: "jackson.lee@email.com", amount: "+$39.00" },
{ name: "Isabella Nguyen", email: "isabella.nguyen@email.com", amount: "+$299.00" },
{ name: "William Kim", email: "will@email.com", amount: "+$99.00" },
{ name: "Sofia Davis", email: "sofia.davis@email.com", amount: "+$39.00" },
],
cards: [
{ title: "Total Revenue", value: "$45,231.89", subtext: "+20.1% from last month" },
{ title: "Subscriptions", value: "+2350", subtext: "+180.1% from last month" },
{ title: "Sales", value: "+12,234", subtext: "+19% from last month" },
{ title: "Active Now", value: "+573", subtext: "+201 since last hour" },
],
// Helpers
formatDate(date) {
return date.toLocaleDateString('en-US', {
month: 'short',
day: '2-digit',
year: 'numeric'
});
},
// Lifecycle
async mounted() {
// No additional setup needed
},
// Subcomponents
// CalendarDateRangePicker
components: {
'calendar-date-range-picker': {
template: `
<div class="flex items-center gap-2">
<button class="px-3 py-1 border rounded text-foreground border-border hover:bg-accent hover:text-accent-foreground transition-colors"
@click={setStart}>
Start: {formatDate(parent.dateRange.startDate)}
</button>
<span class="text-foreground">to</span>
<button class="px-3 py-1 border rounded text-foreground border-border hover:bg-accent hover:text-accent-foreground transition-colors"
@click={setEnd}>
End: {formatDate(parent.dateRange.endDate)}
</button>
</div>
`,
methods: {
setStart() {
const input = prompt("Enter start date (YYYY-MM-DD)");
if (input) {
parent.dateRange.startDate = new Date(input);
}
},
setEnd() {
const input = prompt("Enter end date (YYYY-MM-DD)");
if (input) {
parent.dateRange.endDate = new Date(input);
}
},
formatDate(date) {
return parent.formatDate(date);
}
}
},
// Overview
overview: {
template: `
<div class="p-4 border rounded-lg border-border">
<div class="flex justify-between items-end h-40">
<div each={h, i in [100,80,60,40,20]}
class="w-8 opacity-60"
style="height:{h}px;background-color:hsl(var(--chart-{(i%5)+1}))">
</div>
</div>
</div>
`
},
// RecentSales
'recent-sales': {
template: `
<div class="space-y-4">
<div each={item, i in parent.salesData}
class="flex items-center justify-between p-2 border-b border-border">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 rounded-full bg-secondary flex items-center justify-center text-secondary-foreground">
{item.name[0]}
</div>
<div>
<p class="font-medium text-foreground">{item.name}</p>
<p class="text-sm text-muted-foreground">{item.email}</p>
</div>
</div>
<span class="font-medium text-foreground">{item.amount}</span>
</div>
</div>
`
}
}
};
</script>

View file

@ -0,0 +1,227 @@
<!-- Riot.js component for the drive page (converted from app/drive/page.tsx) -->
<template>
<div class="flex flex-col h-[calc(100vh-40px)] bg-background">
<resizable-panel-group direction="horizontal" class="flex-1 min-h-0">
<!-- Folder Tree Panel -->
<resizable-panel
default-size="20"
collapsed-size="4"
collapsible="true"
min-size="15"
max-size="30"
@collapse="{() => isCollapsed = true}"
@resize="{() => isCollapsed = false}"
class="{isCollapsed && 'min-w-[50px] transition-all duration-300'}"
>
<folder-tree
on-select="{setCurrentPath}"
selected-path="{currentPath}"
is-collapsed="{isCollapsed}"
/>
</resizable-panel>
<resizable-handle with-handle class="bg-border" />
<!-- File List Panel -->
<resizable-panel default-size="50" min-size="30" class="bg-background border-r border-border">
<tabs default-value="all" class="flex flex-col h-full">
<div class="flex items-center px-4 py-2 bg-secondary border-b border-border">
<h1 class="text-xl font-bold">{currentItem?.name || 'My Drive'}</h1>
<tabs-list class="ml-auto bg-background">
<tabs-trigger value="all" class="data-[state=active]:bg-accent">All</tabs-trigger>
<tabs-trigger value="starred" class="data-[state=active]:bg-accent">Starred</tabs-trigger>
</tabs-list>
</div>
<separator class="bg-border" />
<div class="bg-secondary p-4">
<div class="flex items-center gap-2">
<div class="relative flex-1">
<search class="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<input placeholder="Search files"
class="pl-8 bg-background border border-border"
bind="{searchTerm}"
@input="{e => searchTerm = e.target.value}" />
</div>
<select value="{filterType}" @change="{e => filterType = e.target.value}">
<option value="all">All items</option>
<option value="folders">Folders</option>
<option value="files">Files</option>
<option value="starred">Starred</option>
</select>
</div>
</div>
<tabs-content value="all" class="m-0 flex-1">
<file-list
path="{currentPath}"
search-term="{searchTerm}"
filter-type="{filterType}"
selected-file="{selectedFile}"
set-selected-file="{file => selectedFile = file}"
on-context-action="{handleContextAction}"
/>
</tabs-content>
<tabs-content value="starred" class="m-0 flex-1">
<file-list
path="{currentPath}"
search-term="{searchTerm}"
filter-type="starred"
selected-file="{selectedFile}"
set-selected-file="{file => selectedFile = file}"
on-context-action="{handleContextAction}"
/>
</tabs-content>
</tabs>
</resizable-panel>
<resizable-handle with-handle class="bg-border" />
<!-- File Details Panel -->
<resizable-panel default-size="30" min-size="25" class="bg-background">
<file-display file="{selectedFile}" />
</resizable-panel>
</resizable-panel-group>
<footer-component shortcuts="{shortcuts}" />
</div>
</template>
<script type="module">
import { useState, useEffect } from 'riot';
import {
Search, Download, Trash2, Share, Star,
MoreVertical, Home, ChevronRight, ChevronLeft,
Folder, File, Image, Video, Music, FileText, Code, Database,
Clock, Users, Eye, Edit3, Copy, Scissors,
FolderPlus, Info, Lock, Menu,
ExternalLink, History, X
} from 'lucide-react';
import { cn } from "@/lib/utils";
import Footer from '../footer';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from '@/components/ui/context-menu';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import './style.css';
export default {
// Reactive state
isCollapsed: false,
currentPath: '',
searchTerm: '',
filterType: 'all',
selectedFile: null,
isMobile: false,
showMobileMenu: false,
activePanel: 'files',
shortcuts: [],
// Lifecycle
async mounted() {
// Initialize shortcuts (same as original component)
this.shortcuts = [
[
{ key: 'Q', label: 'Rename', action: () => console.log('Rename') },
{ key: 'W', label: 'View', action: () => console.log('View') },
{ key: 'E', label: 'Edit', action: () => console.log('Edit') },
{ key: 'R', label: 'Move', action: () => console.log('Move') },
{ key: 'T', label: 'MkDir', action: () => console.log('Make Directory') },
{ key: 'Y', label: 'Delete', action: () => console.log('Delete') },
{ key: 'U', label: 'Copy', action: () => console.log('Copy') },
{ key: 'I', label: 'Cut', action: () => console.log('Cut') },
{ key: 'O', label: 'Paste', action: () => console.log('Paste') },
{ key: 'P', label: 'Duplicate', action: () => console.log('Duplicate') },
],
[
{ key: 'A', label: 'Select', action: () => console.log('Select') },
{ key: 'S', label: 'Select All', action: () => console.log('Select All') },
{ key: 'D', label: 'Deselect', action: () => console.log('Deselect') },
{ key: 'G', label: 'Details', action: () => console.log('Details') },
{ key: 'H', label: 'History', action: () => console.log('History') },
{ key: 'J', label: 'Share', action: () => console.log('Share') },
{ key: 'K', label: 'Star', action: () => console.log('Star') },
{ key: 'L', label: 'Download', action: () => console.log('Download') },
{ key: 'Z', label: 'Upload', action: () => console.log('Upload') },
{ key: 'X', label: 'Refresh', action: () => console.log('Refresh') },
]
];
},
// Helpers
formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
},
formatDateTime(dateString) {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
},
formatDistanceToNow(date) {
const now = new Date();
const diffMs = now.getTime() - new Date(date).getTime();
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMinutes < 1) return 'now';
if (diffMinutes < 60) return `${diffMinutes}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return this.formatDate(date);
},
formatFileSize(bytes) {
if (!bytes) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
},
getFileIcon(item) {
if (item.is_dir) {
return <Folder className="w-4 h-4 text-yellow-500" />;
}
const iconMap = {
pdf: <FileText className="w-4 h-4 text-red-500" />,
xlsx: <Database className="w-4 h-4 text-green-600" />,
json: <Code className="w-4 h-4 text-yellow-600" />,
markdown: <Edit3 className="w-4 h-4 text-purple-500" />,
md: <Edit3 className="w-4 h-4 text-purple-500" />,
jpg: <Image className="w-4 h-4 text-pink-500" />,
jpeg: <Image className="w-4 h-4 text-pink-500" />,
png: <Image className="w-4 h-4 text-pink-500" />,
mp4: <Video className="w-4 h-4 text-red-600" />,
mp3: <Music className="w-4 h-4 text-green-600" />
};
return iconMap[item.type] || <File className="w-4 h-4 text-muted-foreground" />;
},
// Context actions
handleContextAction(action, file) {
console.log(`Context action: ${action}`, file);
// Implement actions as needed
}
};
</script>

1
web/app/drive/prompt.md Normal file
View file

@ -0,0 +1 @@
- The UI shoule look exactly xtree gold but using shadcn with keyborad shortcut well explicit.

View file

@ -0,0 +1,340 @@
<!-- Riot.js component for the editor page (converted from app/editor/page.tsx) -->
<template>
<div class="word-clone">
<!-- Quick Access Toolbar -->
<div class="quick-access">
<button class="quick-access-btn" @click={undo}>
<svg><!-- Undo icon (use same SVG as in React) --></svg>
</button>
<button class="quick-access-btn" @click={redo}>
<svg><!-- Redo icon --></svg>
</button>
<div class="title-controls">
<input
type="text"
class="title-input"
placeholder="Document name"
value={fileName}
@input={e => fileName = e.target.value}
/>
<button class="quick-access-btn" @click={saveDocument}>
<svg><!-- Save icon --></svg>
</button>
</div>
</div>
<!-- Ribbon -->
<div class="ribbon">
<div class="ribbon-tabs">
<button class="ribbon-tab-button {activeTab === 'home' ? 'active' : ''}" @click={() => activeTab = 'home'}>Home</button>
<button class="ribbon-tab-button {activeTab === 'insert' ? 'active' : ''}" @click={() => activeTab = 'insert'}>Insert</button>
<button class="ribbon-tab-button {activeTab === 'layout' ? 'active' : ''}" @click={() => activeTab = 'layout'}>Layout</button>
<button class="ribbon-tab-button {activeTab === 'view' ? 'active' : ''}" @click={() => activeTab = 'view'}>View</button>
</div>
{activeTab === 'home' && (
<div class="ribbon-content">
<ribbon-group title="Clipboard">
<ribbon-button icon="copy" label="Copy" size="large" @click={copy} />
<ribbon-button icon="paste" label="Paste" size="large" @click={paste} />
<ribbon-button icon="cut" label="Cut" size="medium" @click={cut} />
</ribbon-group>
<ribbon-group title="Font">
<div style="display:flex;flex-direction:column;gap:4px">
<div style="display:flex;gap:4px">
<select bind={fontFamily} @change={e => setFontFamily(e.target.value)} class="format-select" style="width:120px">
<option value="Calibri">Calibri</option>
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Georgia">Georgia</option>
<option value="Verdana">Verdana</option>
</select>
<select bind={fontSize} @change={e => setFontSize(e.target.value)} class="format-select" style="width:60px">
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="14">14</option>
<option value="16">16</option>
<option value="18">18</option>
<option value="20">20</option>
<option value="24">24</option>
<option value="28">28</option>
<option value="36">36</option>
</select>
</div>
<div style="display:flex;gap:2px">
<ribbon-button icon="bold" label="Bold" @click={toggleBold} active={editor?.isActive('bold')} />
<ribbon-button icon="italic" label="Italic" @click={toggleItalic} active={editor?.isActive('italic')} />
<ribbon-button icon="underline" label="Underline" @click={toggleUnderline} active={editor?.isActive('underline')} />
<div class="color-picker-wrapper">
<ribbon-button icon="type" label="Text Color" />
<input type="color" bind={textColor} @input={e => setTextColor(e.target.value)} class="color-picker" />
<div class="color-indicator" style="background-color:{textColor}"></div>
</div>
<div class="color-picker-wrapper">
<ribbon-button icon="highlighter" label="Highlight" />
<input type="color" bind={highlightColor} @input={e => setHighlightColor(e.target.value)} class="color-picker" />
<div class="color-indicator" style="background-color:{highlightColor}"></div>
</div>
</div>
</div>
</ribbon-group>
<ribbon-group title="Paragraph">
<div style="display:flex;flex-direction:column;gap:4px">
<div style="display:flex;gap:2px">
<ribbon-button icon="align-left" label="Align Left" @click={alignLeft} active={editor?.isActive({textAlign:'left'})} />
<ribbon-button icon="align-center" label="Center" @click={alignCenter} active={editor?.isActive({textAlign:'center'})} />
<ribbon-button icon="align-right" label="Align Right" @click={alignRight} active={editor?.isActive({textAlign:'right'})} />
<ribbon-button icon="align-justify" label="Justify" @click={alignJustify} active={editor?.isActive({textAlign:'justify'})} />
</div>
</div>
</ribbon-group>
</div>
)}
{activeTab === 'insert' && (
<div class="ribbon-content">
<ribbon-group title="Illustrations">
<ribbon-button icon="image" label="Picture" size="large" @click={addImage} />
</ribbon-group>
<ribbon-group title="Tables">
<ribbon-button icon="table" label="Table" size="large" @click={insertTable} />
</ribbon-group>
<ribbon-group title="Links">
<ribbon-button icon="link" label="Link" size="large" @click={addLink} active={editor?.isActive('link')} />
</ribbon-group>
</div>
)}
{activeTab === 'view' && (
<div class="ribbon-content">
<ribbon-group title="Zoom">
<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
<div style="font-size:14px;font-weight:600">{zoom}%</div>
<input type="range" min="50" max="200" bind={zoom} @input={e => zoom = parseInt(e.target.value)} class="zoom-slider" />
</div>
</ribbon-group>
</div>
)}
</div>
<!-- Editor Area -->
<div class="editor-container">
<div class="editor-main">
<div class="pages-container">
<div each={pageNum in pages} class="page">
<div class="page-number">Page {pageNum}</div>
<div class="page-content">
{pageNum === 1 && <editor-content bind={editor} />}
</div>
</div>
</div>
</div>
</div>
<!-- Bubble Menu -->
{editor && (
<bubble-menu bind={editor}>
<ribbon-button icon="bold" label="Bold" @click={toggleBold} active={editor.isActive('bold')} />
<ribbon-button icon="italic" label="Italic" @click={toggleItalic} active={editor.isActive('italic')} />
<ribbon-button icon="underline" label="Underline" @click={toggleUnderline} active={editor.isActive('underline')} />
<ribbon-button icon="link" label="Link" @click={addLink} active={editor.isActive('link')} />
</bubble-menu>
)}
<!-- Status Bar -->
<div class="status-bar">
<div>Page {pages.length} of {pages.length} | Words: {editor?.storage?.characterCount?.words() || 0}</div>
<div class="zoom-controls">
<button @click={() => zoom = Math.max(50, zoom - 10)}>-</button>
<span>{zoom}%</span>
<button @click={() => zoom = Math.min(200, zoom + 10)}>+</button>
</div>
</div>
<!-- Hidden file input -->
<input type="file" bind={fileInputRef} @change={handleImageUpload} accept="image/*" style="display:none" />
</div>
</template>
<script type="module">
<!-- Removed unused Riot imports -->
import { useEditor, EditorContent, BubbleMenu, AnyExtension } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Bold from '@tiptap/extension-bold';
import Italic from '@tiptap/extension-italic';
import History from '@tiptap/extension-history';
import TextStyle from '@tiptap/extension-text-style';
import FontFamily from '@tiptap/extension-font-family';
import Color from '@tiptap/extension-color';
import Highlight from '@tiptap/extension-highlight';
import TextAlign from '@tiptap/extension-text-align';
import Link from '@tiptap/extension-link';
import Image from '@tiptap/extension-image';
import Underline from '@tiptap/extension-underline';
import Table from '@tiptap/extension-table';
import TableCell from '@tiptap/extension-table-cell';
import TableHeader from '@tiptap/extension-table-header';
import TableRow from '@tiptap/extension-table-row';
import './style.css';
export default {
// Reactive state
fileName: 'Document 1',
fontSize: '12',
fontFamily: 'Calibri',
textColor: '#000000',
highlightColor: '#ffff00',
activeTab: 'home',
zoom: 100,
pages: [1],
editor: null,
fileInputRef: null,
// Lifecycle
async mounted() {
this.editor = useEditor({
extensions: [
StarterKit.configure({ history: false }),
Bold,
Italic,
History,
TextStyle,
FontFamily,
Color,
Highlight.configure({ multicolor: true }),
TextAlign.configure({ types: ['heading', 'paragraph', 'tableCell'] }),
Link.configure({ openOnClick: false }),
Image,
Underline,
Table.configure({
resizable: true,
HTMLAttributes: { class: 'editor-table' },
}),
TableRow,
TableHeader,
TableCell,
],
content: `
<h1 style="text-align: center; font-size: 24px; margin-bottom: 20px;">${this.fileName}</h1>
<p><br></p>
<p>Start typing your document here...</p>
<p><br></p>
`,
onUpdate: ({ editor }) => {
const element = document.querySelector('.ProseMirror');
if (element) {
const contentHeight = element.scrollHeight;
const pageHeight = 1123; // A4 height in pixels at 96 DPI
const neededPages = Math.max(1, Math.ceil(contentHeight / pageHeight));
if (neededPages !== this.pages.length) {
this.pages = Array.from({ length: neededPages }, (_, i) => i + 1);
}
}
},
});
},
// Methods
undo() {
this.editor?.chain().focus().undo().run();
},
redo() {
this.editor?.chain().focus().redo().run();
},
saveDocument() {
if (!this.editor) return;
const content = this.editor.getHTML();
const blob = new Blob([content], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${this.fileName}.html`;
a.click();
URL.revokeObjectURL(url);
},
setFontFamily(value) {
this.fontFamily = value;
this.editor?.chain().focus().setFontFamily(value).run();
},
setFontSize(value) {
this.fontSize = value;
// TipTap does not have a builtin fontsize extension; you may need a custom one.
},
setTextColor(value) {
this.textColor = value;
this.editor?.chain().focus().setColor(value).run();
},
setHighlightColor(value) {
this.highlightColor = value;
this.editor?.chain().focus().setHighlight({ color: value }).run();
},
toggleBold() {
this.editor?.chain().focus().toggleBold().run();
},
toggleItalic() {
this.editor?.chain().focus().toggleItalic().run();
},
toggleUnderline() {
this.editor?.chain().focus().toggleUnderline().run();
},
alignLeft() {
this.editor?.chain().focus().setTextAlign('left').run();
},
alignCenter() {
this.editor?.chain().focus().setTextAlign('center').run();
},
alignRight() {
this.editor?.chain().focus().setTextAlign('right').run();
},
alignJustify() {
this.editor?.chain().focus().setTextAlign('justify').run();
},
addImage() {
if (this.fileInputRef) this.fileInputRef.click();
},
handleImageUpload(e) {
const file = e.target.files?.[0];
if (file && this.editor) {
const reader = new FileReader();
reader.onload = (event) => {
const imageUrl = event.target?.result;
this.editor.chain().focus().setImage({ src: imageUrl }).run();
};
reader.readAsDataURL(file);
}
},
insertTable() {
this.editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
},
addLink() {
const previousUrl = this.editor?.getAttributes('link').href;
const url = window.prompt('URL', previousUrl);
if (url === null) return;
if (url === '') {
this.editor?.chain().focus().extendMarkRange('link').unsetLink().run();
return;
}
this.editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
},
copy() {
document.execCommand('copy');
},
paste() {
document.execCommand('paste');
},
cut() {
document.execCommand('cut');
},
};
</script>
<style>
/* Original styles moved to global CSS */
</style>

423
web/app/editor/style.css Normal file
View file

@ -0,0 +1,423 @@
:root {
/* 3DBevel Theme */
--background: 0 0% 80%;
--foreground: 0 0% 10%;
--card: 0 0% 75%;
--card-foreground: 0 0% 10%;
--popover: 0 0% 80%;
--popover-foreground: 0 0% 10%;
--primary: 210 80% 40%;
--primary-foreground: 0 0% 80%;
--secondary: 0 0% 70%;
--secondary-foreground: 0 0% 10%;
--muted: 0 0% 65%;
--muted-foreground: 0 0% 30%;
--accent: 30 80% 40%;
--accent-foreground: 0 0% 80%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 70%;
--input: 0 0% 70%;
--ring: 210 80% 40%;
--radius: 0.5rem;
}
* {
box-sizing: border-box;
}
.word-clone {
min-height: 100vh;
background: hsl(var(--background));
color: hsl(var(--foreground));
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Title Bar */
.title-bar {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
padding: 8px 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid hsl(var(--border));
}
.title-bar h1 {
font-size: 14px;
font-weight: 600;
margin: 0;
}
.title-controls {
display: flex;
gap: 8px;
}
.title-input {
background: hsl(var(--input));
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
padding: 4px 8px;
font-size: 12px;
color: hsl(var(--foreground));
}
/* Quick Access Toolbar */
.quick-access {
background: hsl(var(--card));
border-bottom: 1px solid hsl(var(--border));
padding: 4px 8px;
display: flex;
align-items: center;
gap: 2px;
}
.quick-access-btn {
background: transparent;
border: 1px solid transparent;
border-radius: 3px;
padding: 4px;
cursor: pointer;
color: hsl(var(--foreground));
transition: all 0.2s;
}
.quick-access-btn:hover {
background: hsl(var(--muted));
border-color: hsl(var(--border));
}
/* Ribbon */
.ribbon {
background: hsl(var(--card));
border-bottom: 2px solid hsl(var(--border));
}
.ribbon-tabs {
display: flex;
background: hsl(var(--muted));
border-bottom: 1px solid hsl(var(--border));
}
.ribbon-tab-button {
background: transparent;
border: none;
padding: 8px 16px;
cursor: pointer;
font-size: 12px;
color: hsl(var(--muted-foreground));
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.ribbon-tab-button:hover {
background: hsl(var(--secondary));
color: hsl(var(--foreground));
}
.ribbon-tab-button.active {
background: hsl(var(--card));
color: hsl(var(--foreground));
border-bottom-color: hsl(var(--primary));
font-weight: 600;
}
.ribbon-content {
display: flex;
padding: 8px;
gap: 2px;
min-height: 80px;
align-items: stretch;
}
.ribbon-group {
display: flex;
flex-direction: column;
border-right: 1px solid hsl(var(--border));
padding-right: 8px;
margin-right: 8px;
}
.ribbon-group:last-child {
border-right: none;
}
.ribbon-group-content {
display: flex;
flex-wrap: wrap;
gap: 2px;
flex: 1;
align-items: flex-start;
padding: 4px 0;
}
.ribbon-group-title {
font-size: 10px;
color: hsl(var(--muted-foreground));
text-align: center;
margin-top: 4px;
border-top: 1px solid hsl(var(--border));
padding-top: 2px;
}
.ribbon-button {
background: transparent;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
color: hsl(var(--foreground));
transition: all 0.2s;
position: relative;
}
.ribbon-button:hover {
background: hsl(var(--muted));
border-color: hsl(var(--border));
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.ribbon-button.active {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-color: hsl(var(--primary));
}
.ribbon-button.medium {
padding: 6px;
min-width: 32px;
min-height: 32px;
}
.ribbon-button.large {
padding: 8px;
min-width: 48px;
min-height: 48px;
flex-direction: column;
}
.ribbon-button-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.ribbon-button-label {
font-size: 10px;
text-align: center;
line-height: 1.1;
}
.dropdown-arrow {
position: absolute;
bottom: 2px;
right: 2px;
}
/* Format Controls */
.format-select {
background: hsl(var(--input));
border: 1px solid hsl(var(--border));
border-radius: 3px;
padding: 4px 6px;
font-size: 11px;
color: hsl(var(--foreground));
margin: 2px;
}
.color-picker-wrapper {
position: relative;
display: inline-block;
}
.color-picker {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.color-indicator {
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
width: 16px;
height: 3px;
border-radius: 1px;
}
/* Editor Area */
.editor-container {
display: flex;
flex: 1;
background: hsl(var(--muted));
}
.editor-sidebar {
width: 200px;
background: hsl(var(--card));
border-right: 1px solid hsl(var(--border));
padding: 16px;
}
.editor-main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
overflow-y: auto;
max-height: calc(100vh - 200px);
}
.pages-container {
display: flex;
flex-direction: column;
gap: 20px;
/* Example: Use a CSS variable for zoom, set --zoom: 1 for 100% */
transform: scale(var(--zoom, 1));
transform-origin: top center;
}
.page {
width: 210mm;
min-height: 297mm;
background: white;
box-shadow:
0 0 0 1px hsl(var(--border)),
0 4px 8px rgba(0, 0, 0, 0.1),
0 8px 16px rgba(0, 0, 0, 0.05);
position: relative;
margin: 0 auto;
}
.page-number {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: hsl(var(--muted-foreground));
background: hsl(var(--background));
padding: 2px 8px;
border-radius: 10px;
}
.page-content {
padding: 25mm;
min-height: 247mm;
}
.ProseMirror {
outline: none;
min-height: 100%;
}
.ProseMirror img {
max-width: 100%;
height: auto;
border-radius: 4px;
}
.ProseMirror a {
color: hsl(var(--primary));
text-decoration: underline;
}
/* Table styles */
.editor-table {
border-collapse: collapse;
margin: 16px 0;
width: 100%;
border: 1px solid hsl(var(--border));
}
.editor-table td,
.editor-table th {
border: 1px solid hsl(var(--border));
padding: 8px 12px;
min-width: 50px;
position: relative;
}
.editor-table th {
background: hsl(var(--muted));
font-weight: 600;
}
/* Bubble Menu */
.bubble-menu {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
padding: 4px;
gap: 2px;
}
.bubble-menu .ribbon-button {
min-width: 28px;
min-height: 28px;
padding: 4px;
}
/* Status Bar */
.status-bar {
background: hsl(var(--card));
border-top: 1px solid hsl(var(--border));
padding: 4px 12px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: hsl(var(--muted-foreground));
}
.zoom-controls {
display: flex;
align-items: center;
gap: 8px;
}
.zoom-slider {
width: 100px;
}
@media print {
.title-bar,
.quick-access,
.ribbon,
.editor-sidebar,
.status-bar {
display: none !important;
}
.editor-main {
padding: 0;
max-height: none;
}
.pages-container {
transform: none;
gap: 0;
}
.page {
box-shadow: none;
margin: 0;
break-after: page;
}
.page-number {
display: none;
}
}

24
web/app/index.html Normal file
View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>General Bots</title>
</head>
<body>
<link rel="stylesheet" href="public/output.css" />
<link rel="stylesheet" href="client-nav.css" />
<app></app>
<script src="app.html" type="riot"></script>
<script src="https://unpkg.com/riot@10/riot+compiler.min.js"></script>
<script>
riot.compile().then(() => {
riot.mount('app', {
title: 'General Bots',
})
})
</script>
</body>
</html>

15
web/app/mail/use-mail.ts Normal file
View file

@ -0,0 +1,15 @@
import { atom, useAtom } from "jotai"
import { Mail, mails } from "./data"
type Config = {
selected: Mail["id"] | null
}
const configAtom = atom<Config>({
selected: mails[0].id,
})
export function useMail() {
return useAtom(configAtom)
}

View file

@ -0,0 +1,14 @@
<!-- Riot.js component for the news page (converted from app/news/page.tsx) -->
<template>
<div class="flex flex-col h-screen bg-gray-50">
<!-- Add news page content here -->
</div>
</template>
<script type="module">
import './style.css';
export default {
// No additional state needed for this simple page
};
</script>

View file

@ -0,0 +1,179 @@
<!-- Riot.js component for the paper page (converted from app/paper/page.tsx) -->
<template>
<div class="min-h-screen bg-background text-foreground">
<div>
<div class="max-w-4xl mx-auto">
<!-- Paper Shadow Effect -->
<div class="mx-4 my-8 bg-card rounded-lg shadow-2xl shadow-black/20 border border-border">
<editor-content bind={editor} class="min-h-[calc(100vh-12rem)]" />
</div>
</div>
<!-- Floating Selection Toolbar -->
{editor && (
<bubble-menu bind={editor}>
<div class="flex items-center bg-card border border-border rounded-lg shadow-lg p-1">
<!-- Text Formatting -->
<button @click={() => editor.chain().focus().toggleBold().run()}
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('bold') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
title="Bold">
<BoldIcon class="h-4 w-4" />
</button>
<button @click={() => editor.chain().focus().toggleItalic().run()}
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('italic') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
title="Italic">
<ItalicIcon class="h-4 w-4" />
</button>
<button @click={() => editor.chain().focus().toggleUnderline().run()}
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('underline') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
title="Underline">
<Underline class="h-4 w-4" />
</button>
<div class="w-px h-6 bg-border mx-1"></div>
<!-- Text Alignment -->
<button @click={() => editor.chain().focus().setTextAlign('left').run()}
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive({textAlign:'left'}) ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
title="Align Left">
<AlignLeft class="h-4 w-4" />
</button>
<button @click={() => editor.chain().focus().setTextAlign('center').run()}
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive({textAlign:'center'}) ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
title="Align Center">
<AlignCenter class="h-4 w-4" />
</button>
<button @click={() => editor.chain().focus().setTextAlign('right').run()}
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive({textAlign:'right'}) ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
title="Align Right">
<AlignRight class="h-4 w-4" />
</button>
<div class="w-px h-6 bg-border mx-1"></div>
<!-- Highlight -->
<button @click={() => editor.chain().focus().toggleHighlight({color:'#ffff00'}).run()}
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('highlight') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
title="Highlight">
<Highlighter class="h-4 w-4" />
</button>
<!-- Link -->
<button @click={addLink}
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('link') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
title="Add Link">
<Link class="h-4 w-4" />
</button>
<div class="w-px h-6 bg-border mx-1"></div>
<!-- Heading -->
<button @click={() => {
if (editor.isActive('heading')) {
editor.chain().focus().setNode('paragraph').run();
} else {
editor.chain().focus().setNode('heading', {level:2}).run();
}
}}
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('heading') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
title="Heading">
<Type class="h-4 w-4" />
</button>
</div>
</bubble-menu>
)}
</div>
<footer-component shortcuts="{shortcuts}" />
</div>
</template>
<script type="module">
import { useState, useRef, useEffect } from 'riot';
import { useEditor, EditorContent, BubbleMenu, AnyExtension } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Bold from '@tiptap/extension-bold';
import Italic from '@tiptap/extension-italic';
import TextStyle from '@tiptap/extension-text-style';
import Color from '@tiptap/extension-color';
import Highlight from '@tiptap/extension-highlight';
import TextAlign from '@tiptap/extension-text-align';
import Link from '@tiptap/extension-link';
import Underline from '@tiptap/extension-underline';
import { AlignLeft, AlignCenter, AlignRight, BoldIcon, ItalicIcon, Highlighter, Type, Link as LinkIcon, Underline as UnderlineIcon } from 'lucide-react';
import Footer from '../footer';
import './style.css';
export default {
// Reactive state
editor: null,
shortcuts: [],
// Lifecycle
async mounted() {
this.editor = useEditor({
extensions: [
StarterKit.configure(),
Bold,
Italic,
TextStyle,
Color,
Highlight.configure({ multicolor: true }),
TextAlign.configure({ types: ['heading', 'paragraph'] }),
Link,
Underline,
],
content: `<p>Start writing your thoughts here...</p>`,
editorProps: {
attributes: {
class: 'prose prose-invert max-w-none focus:outline-none min-h-[calc(100vh-8rem)] p-8 text-foreground',
},
},
});
// Initialize shortcuts (same as original component)
this.shortcuts = [
[
{ key: 'Q', label: 'Resume', action: () => {} },
{ key: 'W', label: 'Write', action: () => {} },
{ key: 'E', label: 'Expand', action: () => {} },
{ key: 'R', label: 'One Word', action: () => {} },
{ key: 'T', label: 'As List', action: () => {} },
{ key: 'Y', label: 'As Mail', action: () => {} },
{ key: 'U', label: 'Copy', action: () => document.execCommand('copy') },
{ key: 'I', label: 'Paste', action: () => document.execCommand('paste') },
{ key: 'O', label: 'Undo', action: () => this.editor?.chain().focus().undo().run() },
{ key: 'P', label: 'Redo', action: () => this.editor?.chain().focus().redo().run() },
],
[
{ key: 'A', label: 'Select', action: () => {} },
{ key: 'S', label: 'Select All', action: () => this.editor?.chain().focus().selectAll().run() },
{ key: 'D', label: 'Deselect', action: () => {} },
{ key: 'G', label: 'Random', action: () => {} },
{ key: 'H', label: 'Idea', action: () => {} },
{ key: 'J', label: 'Insert Link', action: this.addLink },
{ key: 'K', label: 'Highlight', action: () => this.editor?.chain().focus().toggleHighlight({color:'#ffff00'}).run() },
{ key: 'L', label: 'To-Do', action: () => {} },
{ key: 'Z', label: 'Zoom In', action: () => {} },
{ key: 'X', label: 'Zoom Out', action: () => {} },
]
];
},
// Methods
addLink() {
const previousUrl = this.editor?.getAttributes('link').href;
const url = window.prompt('Enter URL:', previousUrl);
if (url === null) return;
if (url === '') {
this.editor?.chain().focus().extendMarkRange('link').unsetLink().run();
return;
}
this.editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
}
};
</script>

103
web/app/paper/style.css Normal file
View file

@ -0,0 +1,103 @@
.ProseMirror {
outline: none;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-size: 16px;
line-height: 1.7;
color: hsl(var(--foreground));
padding: 3rem;
min-height: calc(100vh - 12rem);
}
.ProseMirror h1 {
font-size: 2.5rem;
font-weight: 700;
margin: 2rem 0 1rem 0;
color: hsl(var(--primary));
}
.ProseMirror h2 {
font-size: 2rem;
font-weight: 600;
margin: 1.5rem 0 0.75rem 0;
color: hsl(var(--primary));
}
.ProseMirror h3 {
font-size: 1.5rem;
font-weight: 600;
margin: 1.25rem 0 0.5rem 0;
color: hsl(var(--primary));
}
.ProseMirror p {
margin: 0.75rem 0;
}
.ProseMirror a {
color: hsl(var(--accent));
text-decoration: underline;
text-underline-offset: 2px;
}
.ProseMirror a:hover {
color: hsl(var(--primary));
}
.ProseMirror mark {
background-color: #ffff0040;
border-radius: 2px;
padding: 0 2px;
}
.ProseMirror ul, .ProseMirror ol {
margin: 1rem 0;
padding-left: 1.5rem;
}
.ProseMirror li {
margin: 0.25rem 0;
}
.ProseMirror blockquote {
border-left: 4px solid hsl(var(--primary));
padding-left: 1rem;
margin: 1rem 0;
font-style: italic;
color: hsl(var(--muted-foreground));
}
.ProseMirror code {
background-color: hsl(var(--muted));
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
}
.ProseMirror pre {
background-color: hsl(var(--muted));
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
margin: 1rem 0;
}
.ProseMirror pre code {
background: none;
padding: 0;
}
/* Selection highlighting */
.ProseMirror ::selection {
background-color: hsl(var(--primary) / 0.2);
}
/* Placeholder styling */
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: hsl(var(--muted-foreground));
pointer-events: none;
height: 0;
}

32
web/app/player/style.css Normal file
View file

@ -0,0 +1,32 @@
.slider::-webkit-slider-thumb {
appearance: none;
height: 12px;
width: 12px;
border-radius: 50%;
background: hsl(var(--primary));
cursor: pointer;
border: 2px solid hsl(var(--primary-foreground));
}
.slider::-moz-range-thumb {
height: 12px;
width: 12px;
border-radius: 50%;
background: hsl(var(--primary));
cursor: pointer;
border: 2px solid hsl(var(--primary-foreground));
}
.slider::-webkit-slider-track {
height: 4px;
cursor: pointer;
background: hsl(var(--muted));
border-radius: 2px;
}
.slider::-moz-range-track {
height: 4px;
cursor: pointer;
background: hsl(var(--muted));
border-radius: 2px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

4751
web/app/public/output.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

View file

@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

View file

@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

View file

@ -0,0 +1,13 @@
export const soundAssets = {
send: '/assets/sounds/send.mp3',
receive: '/assets/sounds/receive.mp3',
typing: '/assets/sounds/typing.mp3',
notification: '/assets/sounds/notification.mp3',
click: '/assets/sounds/click.mp3',
hover: '/assets/sounds/hover.mp3',
success: '/assets/sounds/success.mp3',
error: '/assets/sounds/error.mp3'
} as const;
// Type for sound names
export type SoundName = keyof typeof soundAssets;

View file

@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

View file

@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

View file

@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

View file

@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

View file

@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,66 @@
body, .card, .popover, .input, .button, .menu, .dialog {
font-family: 'IBM Plex Mono', 'Courier New', monospace !important;
background: #c0c0c0 !important;
color: #000 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.card, .popover, .menu, .dialog {
border: 2px solid #fff !important;
border-bottom: 2px solid #404040 !important;
border-right: 2px solid #404040 !important;
padding: 8px !important;
background: #e0e0e0 !important;
}
.button, button, input[type="button"], input[type="submit"] {
background: #e0e0e0 !important;
color: #000 !important;
border: 2px solid #fff !important;
border-bottom: 2px solid #404040 !important;
border-right: 2px solid #404040 !important;
padding: 4px 12px !important;
font-weight: bold !important;
box-shadow: none !important;
outline: none !important;
}
input, textarea, select {
background: #fff !important;
color: #000 !important;
border: 2px solid #fff !important;
border-bottom: 2px solid #404040 !important;
border-right: 2px solid #404040 !important;
font-family: inherit !important;
box-shadow: none !important;
}
.menu {
background: #d0d0d0 !important;
border: 2px solid #fff !important;
border-bottom: 2px solid #404040 !important;
border-right: 2px solid #404040 !important;
}
::-webkit-scrollbar {
width: 16px !important;
background: #c0c0c0 !important;
}
::-webkit-scrollbar-thumb {
background: #404040 !important;
border: 2px solid #fff !important;
border-bottom: 2px solid #404040 !important;
border-right: 2px solid #404040 !important;
}
a {
color: #0000aa !important;
text-decoration: underline !important;
}
hr {
border: none !important;
border-top: 2px solid #404040 !important;
margin: 8px 0 !important;
}

View file

@ -0,0 +1,28 @@
:root {
/* ArcadeFlash Theme */
--background: 0 0% 5%;
--foreground: 0 0% 98%;
--card: 0 0% 8%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 5%;
--popover-foreground: 0 0% 98%;
--primary: 120 100% 50%;
--primary-foreground: 0 0% 5%;
--secondary: 0 0% 15%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 10%;
--muted-foreground: 0 0% 60%;
--accent: 240 100% 50%;
--accent-foreground: 0 0% 98%;
--destructive: 0 100% 50%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 15%;
--input: 0 0% 15%;
--ring: 120 100% 50%;
--radius: 0.5rem;
--chart-1: 120 100% 50%;
--chart-2: 240 100% 50%;
--chart-3: 60 100% 50%;
--chart-4: 0 100% 50%;
--chart-5: 300 100% 50%;
}

View file

@ -0,0 +1,28 @@
:root {
/* CyberPunk Theme */
--background: 240 30% 5%;
--foreground: 60 100% 80%;
--card: 240 30% 8%;
--card-foreground: 60 100% 80%;
--popover: 240 30% 5%;
--popover-foreground: 60 100% 80%;
--primary: 330 100% 60%;
--primary-foreground: 240 30% 5%;
--secondary: 240 30% 15%;
--secondary-foreground: 60 100% 80%;
--muted: 240 30% 10%;
--muted-foreground: 60 100% 60%;
--accent: 180 100% 60%;
--accent-foreground: 240 30% 5%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 240 30% 15%;
--input: 240 30% 15%;
--ring: 330 100% 60%;
--radius: 0.5rem;
--chart-1: 330 100% 60%;
--chart-2: 180 100% 60%;
--chart-3: 60 100% 60%;
--chart-4: 0 100% 60%;
--chart-5: 270 100% 60%;
}

View file

@ -0,0 +1,28 @@
:root {
/* DiscoFever Theme */
--background: 270 20% 10%;
--foreground: 0 0% 98%;
--card: 270 20% 15%;
--card-foreground: 0 0% 98%;
--popover: 270 20% 10%;
--popover-foreground: 0 0% 98%;
--primary: 330 100% 60%;
--primary-foreground: 0 0% 98%;
--secondary: 270 20% 20%;
--secondary-foreground: 0 0% 98%;
--muted: 270 20% 25%;
--muted-foreground: 270 10% 60%;
--accent: 60 100% 60%;
--accent-foreground: 270 20% 10%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 270 20% 20%;
--input: 270 20% 20%;
--ring: 330 100% 60%;
--radius: 0.5rem;
--chart-1: 330 100% 60%;
--chart-2: 60 100% 60%;
--chart-3: 120 100% 60%;
--chart-4: 240 100% 60%;
--chart-5: 0 100% 60%;
}

View file

@ -0,0 +1,28 @@
:root {
/* GrungeEra Theme */
--background: 30 10% 10%;
--foreground: 30 30% 80%;
--card: 30 10% 15%;
--card-foreground: 30 30% 80%;
--popover: 30 10% 10%;
--popover-foreground: 30 30% 80%;
--primary: 10 70% 50%;
--primary-foreground: 30 30% 80%;
--secondary: 30 10% 20%;
--secondary-foreground: 30 30% 80%;
--muted: 30 10% 25%;
--muted-foreground: 30 30% 60%;
--accent: 200 70% 50%;
--accent-foreground: 30 30% 80%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 30 10% 20%;
--input: 30 10% 20%;
--ring: 10 70% 50%;
--radius: 0.5rem;
--chart-1: 10 70% 50%;
--chart-2: 200 70% 50%;
--chart-3: 90 70% 50%;
--chart-4: 300 70% 50%;
--chart-5: 30 70% 50%;
}

View file

@ -0,0 +1,28 @@
:root {
/* JazzAge Theme */
--background: 30 20% 10%;
--foreground: 40 30% 85%;
--card: 30 20% 15%;
--card-foreground: 40 30% 85%;
--popover: 30 20% 10%;
--popover-foreground: 40 30% 85%;
--primary: 20 80% 50%;
--primary-foreground: 40 30% 85%;
--secondary: 30 20% 20%;
--secondary-foreground: 40 30% 85%;
--muted: 30 20% 25%;
--muted-foreground: 40 30% 60%;
--accent: 200 80% 50%;
--accent-foreground: 40 30% 85%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 30 20% 20%;
--input: 30 20% 20%;
--ring: 20 80% 50%;
--radius: 0.5rem;
--chart-1: 20 80% 50%;
--chart-2: 200 80% 50%;
--chart-3: 350 80% 50%;
--chart-4: 140 80% 50%;
--chart-5: 260 80% 50%;
}

View file

@ -0,0 +1,28 @@
:root {
/* MellowGold Theme */
--background: 45 30% 90%;
--foreground: 30 20% 20%;
--card: 45 30% 85%;
--card-foreground: 30 20% 20%;
--popover: 45 30% 90%;
--popover-foreground: 30 20% 20%;
--primary: 35 80% 50%;
--primary-foreground: 45 30% 90%;
--secondary: 45 30% 80%;
--secondary-foreground: 30 20% 20%;
--muted: 45 30% 75%;
--muted-foreground: 30 20% 40%;
--accent: 25 80% 50%;
--accent-foreground: 45 30% 90%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 45 30% 80%;
--input: 45 30% 80%;
--ring: 35 80% 50%;
--radius: 0.5rem;
--chart-1: 35 80% 50%;
--chart-2: 25 80% 50%;
--chart-3: 15 80% 50%;
--chart-4: 5 80% 50%;
--chart-5: 55 80% 50%;
}

View file

@ -0,0 +1,28 @@
:root {
/* MidCenturyMod Theme */
--background: 40 30% 95%;
--foreground: 30 20% 20%;
--card: 40 30% 90%;
--card-foreground: 30 20% 20%;
--popover: 40 30% 95%;
--popover-foreground: 30 20% 20%;
--primary: 180 60% 40%;
--primary-foreground: 40 30% 95%;
--secondary: 40 30% 85%;
--secondary-foreground: 30 20% 20%;
--muted: 40 30% 80%;
--muted-foreground: 30 20% 40%;
--accent: 350 60% 40%;
--accent-foreground: 40 30% 95%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 40 30% 85%;
--input: 40 30% 85%;
--ring: 180 60% 40%;
--radius: 0.5rem;
--chart-1: 180 60% 40%;
--chart-2: 350 60% 40%;
--chart-3: 40 60% 40%;
--chart-4: 220 60% 40%;
--chart-5: 300 60% 40%;
}

View file

@ -0,0 +1,27 @@
:root {
--background: 0 0% 100%; /* White */
--foreground: 0 0% 13%; /* #212121 - near black */
--card: 0 0% 98%; /* #faf9f8 - light gray */
--card-foreground: 0 0% 13%; /* #212121 */
--popover: 0 0% 100%; /* White */
--popover-foreground: 0 0% 13%; /* #212121 */
--primary: 24 90% 54%; /* #d83b01 - Office orange */
--primary-foreground: 0 0% 100%; /* White */
--secondary: 210 36% 96%; /* #f3f2f1 - light blue-gray */
--secondary-foreground: 0 0% 13%; /* #212121 */
--muted: 0 0% 90%; /* #e1dfdd - muted gray */
--muted-foreground: 0 0% 40%; /* #666666 */
--accent: 207 90% 54%; /* #0078d4 - Office blue */
--accent-foreground: 0 0% 100%; /* White */
--destructive: 0 85% 60%; /* #e81123 - Office red */
--destructive-foreground: 0 0% 100%; /* White */
--border: 0 0% 85%; /* #d2d0ce - light border */
--input: 0 0% 100%; /* White */
--ring: 207 90% 54%; /* #0078d4 */
--radius: 0.25rem; /* Slightly less rounded */
--chart-1: 24 90% 54%; /* Office orange */
--chart-2: 207 90% 54%; /* Office blue */
--chart-3: 120 60% 40%; /* Office green */
--chart-4: 340 82% 52%; /* Office magenta */
--chart-5: 44 100% 50%; /* Office yellow */
}

View file

@ -0,0 +1,28 @@
:root {
/* PolaroidMemories Theme */
--background: 50 30% 95%;
--foreground: 30 20% 20%;
--card: 50 30% 90%;
--card-foreground: 30 20% 20%;
--popover: 50 30% 95%;
--popover-foreground: 30 20% 20%;
--primary: 200 80% 50%;
--primary-foreground: 50 30% 95%;
--secondary: 50 30% 85%;
--secondary-foreground: 30 20% 20%;
--muted: 50 30% 80%;
--muted-foreground: 30 20% 40%;
--accent: 350 80% 50%;
--accent-foreground: 50 30% 95%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 50 30% 85%;
--input: 50 30% 85%;
--ring: 200 80% 50%;
--radius: 0.5rem;
--chart-1: 200 80% 50%;
--chart-2: 350 80% 50%;
--chart-3: 50 80% 50%;
--chart-4: 140 80% 50%;
--chart-5: 260 80% 50%;
}

View file

@ -0,0 +1,28 @@
:root {
/* RetroWave Theme */
--background: 240 21% 15%;
--foreground: 0 0% 98%;
--card: 240 21% 18%;
--card-foreground: 0 0% 98%;
--popover: 240 21% 15%;
--popover-foreground: 0 0% 98%;
--primary: 334 89% 62%;
--primary-foreground: 0 0% 100%;
--secondary: 240 21% 25%;
--secondary-foreground: 0 0% 98%;
--muted: 240 21% 20%;
--muted-foreground: 240 5% 65%;
--accent: 41 99% 60%;
--accent-foreground: 240 21% 15%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 240 21% 25%;
--input: 240 21% 25%;
--ring: 334 89% 62%;
--radius: 0.5rem;
--chart-1: 334 89% 62%;
--chart-2: 41 99% 60%;
--chart-3: 190 90% 50%;
--chart-4: 280 89% 65%;
--chart-5: 80 75% 55%;
}

View file

@ -0,0 +1,28 @@
:root {
/* SaturdayCartoons Theme */
--background: 220 50% 95%;
--foreground: 220 50% 20%;
--card: 220 50% 90%;
--card-foreground: 220 50% 20%;
--popover: 220 50% 95%;
--popover-foreground: 220 50% 20%;
--primary: 30 100% 55%;
--primary-foreground: 220 50% 95%;
--secondary: 220 50% 85%;
--secondary-foreground: 220 50% 20%;
--muted: 220 50% 80%;
--muted-foreground: 220 50% 40%;
--accent: 120 100% 55%;
--accent-foreground: 220 50% 95%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 220 50% 85%;
--input: 220 50% 85%;
--ring: 30 100% 55%;
--radius: 0.5rem;
--chart-1: 30 100% 55%;
--chart-2: 120 100% 55%;
--chart-3: 240 100% 55%;
--chart-4: 330 100% 55%;
--chart-5: 60 100% 55%;
}

View file

@ -0,0 +1,28 @@
:root {
/* SeasidePostcard Theme */
--background: 200 50% 95%;
--foreground: 200 50% 20%;
--card: 200 50% 90%;
--card-foreground: 200 50% 20%;
--popover: 200 50% 95%;
--popover-foreground: 200 50% 20%;
--primary: 30 100% 55%;
--primary-foreground: 200 50% 95%;
--secondary: 200 50% 85%;
--secondary-foreground: 200 50% 20%;
--muted: 200 50% 80%;
--muted-foreground: 200 50% 40%;
--accent: 350 100% 55%;
--accent-foreground: 200 50% 95%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 200 50% 85%;
--input: 200 50% 85%;
--ring: 30 100% 55%;
--radius: 0.5rem;
--chart-1: 30 100% 55%;
--chart-2: 350 100% 55%;
--chart-3: 200 100% 55%;
--chart-4: 140 100% 55%;
--chart-5: 260 100% 55%;
}

View file

@ -0,0 +1,28 @@
:root {
/* Typewriter Theme */
--background: 0 0% 95%;
--foreground: 0 0% 10%;
--card: 0 0% 90%;
--card-foreground: 0 0% 10%;
--popover: 0 0% 95%;
--popover-foreground: 0 0% 10%;
--primary: 0 0% 20%;
--primary-foreground: 0 0% 95%;
--secondary: 0 0% 85%;
--secondary-foreground: 0 0% 10%;
--muted: 0 0% 80%;
--muted-foreground: 0 0% 40%;
--accent: 0 0% 70%;
--accent-foreground: 0 0% 10%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 85%;
--input: 0 0% 85%;
--ring: 0 0% 20%;
--radius: 0.5rem;
--chart-1: 0 0% 20%;
--chart-2: 0 0% 40%;
--chart-3: 0 0% 60%;
--chart-4: 0 0% 30%;
--chart-5: 0 0% 50%;
}

View file

@ -0,0 +1,28 @@
:root {
/* VaporDream Theme */
--background: 260 20% 10%;
--foreground: 0 0% 98%;
--card: 260 20% 13%;
--card-foreground: 0 0% 98%;
--popover: 260 20% 10%;
--popover-foreground: 0 0% 98%;
--primary: 300 100% 70%;
--primary-foreground: 260 20% 10%;
--secondary: 260 20% 20%;
--secondary-foreground: 0 0% 98%;
--muted: 260 20% 15%;
--muted-foreground: 260 10% 60%;
--accent: 200 100% 70%;
--accent-foreground: 260 20% 10%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 260 20% 20%;
--input: 260 20% 20%;
--ring: 300 100% 70%;
--radius: 0.5rem;
--chart-1: 300 100% 70%;
--chart-2: 200 100% 70%;
--chart-3: 50 100% 60%;
--chart-4: 330 100% 70%;
--chart-5: 150 100% 60%;
}

View file

@ -0,0 +1,71 @@
:root {
/* Windows 3.1 White & Blue Theme */
--background: 0 0% 100%; /* Pure white */
--foreground: 0 0% 0%; /* Black text */
--card: 0 0% 98%; /* Slightly off-white for cards */
--card-foreground: 0 0% 0%; /* Black text */
--popover: 0 0% 100%; /* White */
--popover-foreground: 0 0% 0%; /* Black */
--primary: 240 100% 27%; /* Windows blue */
--primary-foreground: 0 0% 100%; /* White text on blue */
--secondary: 0 0% 90%; /* Light gray for secondary */
--secondary-foreground: 0 0% 0%; /* Black text */
--muted: 0 0% 85%; /* Muted gray */
--muted-foreground: 240 10% 40%; /* Muted blue-gray */
--accent: 60 100% 50%; /* Classic yellow accent */
--accent-foreground: 240 100% 27%; /* Blue */
--destructive: 0 100% 50%; /* Red for destructive */
--destructive-foreground: 0 0% 100%; /* White */
--border: 240 100% 27%; /* Blue borders */
--input: 0 0% 100%; /* White input */
--ring: 240 100% 27%; /* Blue ring/focus */
--radius: 0.125rem; /* Small radius, almost square */
--chart-1: 240 100% 27%; /* Blue */
--chart-2: 0 0% 60%; /* Gray */
--chart-3: 60 100% 50%; /* Yellow */
--chart-4: 0 100% 50%; /* Red */
--chart-5: 120 100% 25%; /* Green */
--border-light: 0 0% 100%; /* White for top/left border */
--border-dark: 240 100% 20%; /* Dark blue for bottom/right border */
}
/* Windows 3.11 style border */
.win311-border {
border-top: 2px solid hsl(var(--border-light));
border-left: 2px solid hsl(var(--border-light));
border-bottom: 2px solid hsl(var(--border-dark));
border-right: 2px solid hsl(var(--border-dark));
background: hsl(var(--background));
}
/* Titles */
.win311-title {
color: hsl(var(--primary));
border-bottom: 2px solid hsl(var(--primary));
font-weight: bold;
padding: 0.25em 0.5em;
background: hsl(var(--background));
}
/* General text */
body, .filemanager, .filemanager * {
color: hsl(var(--foreground));
background: hsl(var(--background));
}
button, .win311-button {
font-family: inherit;
font-size: 1em;
padding: 0.25em 1.5em;
background: #c0c0c0; /* classic light gray */
color: #000;
border-top: 2px solid #fff; /* light bevel */
border-left: 2px solid #fff; /* light bevel */
border-bottom: 2px solid #808080;/* dark bevel */
border-right: 2px solid #808080; /* dark bevel */
border-radius: 0;
box-shadow: inset 1px 1px 0 #fff, inset -1px -1px 0 #808080 !important;
outline: none !important;
cursor: pointer !important;
transition: none !important;
}

View file

@ -0,0 +1,228 @@
:root {
/* XTree Gold DOS File Manager Theme - Authentic 1980s Interface */
/* Core XTree Gold Palette - Exact Match */
--background: 240 100% 16%; /* Classic XTree blue background */
--foreground: 60 100% 88%; /* Bright yellow text */
/* Card Elements - File Panels */
--card: 240 100% 16%; /* Same blue as main background */
--card-foreground: 60 100% 88%; /* Bright yellow panel text */
/* Popover Elements - Context Menus */
--popover: 240 100% 12%; /* Slightly darker blue for menus */
--popover-foreground: 60 100% 90%; /* Bright yellow menu text */
/* Primary - XTree Gold Highlight (Cyan Selection) */
--primary: 180 100% 70%; /* Bright cyan for selections */
--primary-foreground: 240 100% 10%; /* Dark blue text on cyan */
/* Secondary - Directory Highlights */
--secondary: 180 100% 50%; /* Pure cyan for directories */
--secondary-foreground: 240 100% 10%; /* Dark blue on cyan */
/* Muted - Status Areas */
--muted: 240 100% 14%; /* Slightly darker blue */
--muted-foreground: 60 80% 75%; /* Dimmed yellow */
/* Accent - Function Keys & Highlights */
--accent: 60 100% 50%; /* Pure yellow for F-keys */
--accent-foreground: 240 100% 10%; /* Dark blue on yellow */
/* Destructive - Delete/Error */
--destructive: 0 100% 60%; /* Bright red for warnings */
--destructive-foreground: 60 90% 95%; /* Light yellow on red */
/* Interactive Elements */
--border: 60 100% 70%; /* Yellow border lines */
--input: 240 100% 14%; /* Dark blue input fields */
--ring: 180 100% 70%; /* Cyan focus ring */
/* Border Radius - Sharp DOS aesthetic */
--radius: 0rem; /* No rounding - pure DOS */
/* Chart Colors - Authentic DOS 16-color palette */
--chart-1: 180 100% 70%; /* Bright cyan */
--chart-2: 60 100% 50%; /* Yellow */
--chart-3: 120 100% 50%; /* Green */
--chart-4: 300 100% 50%; /* Magenta */
--chart-5: 0 100% 60%; /* Red */
/* Authentic XTree Gold Colors */
--xtree-blue: 240 100% 16%; /* Main background blue */
--xtree-yellow: 60 100% 88%; /* Text yellow */
--xtree-cyan: 180 100% 70%; /* Selection cyan */
--xtree-white: 0 0% 100%; /* Pure white */
--xtree-green: 120 100% 50%; /* DOS green */
--xtree-magenta: 300 100% 50%; /* DOS magenta */
--xtree-red: 0 100% 60%; /* DOS red */
/* File Type Colors - Authentic XTree */
--executable-color: 0 0% 100%; /* White for executables */
--directory-color: 180 100% 70%; /* Cyan for directories */
--archive-color: 300 100% 50%; /* Magenta for archives */
--text-color: 60 100% 88%; /* Yellow for text */
--system-color: 0 100% 60%; /* Red for system files */
/* Menu Bar Colors */
--menu-bar: 240 100% 8%; /* Dark blue menu bar */
--menu-text: 60 100% 88%; /* Yellow menu text */
--menu-highlight: 180 100% 50%; /* Cyan menu highlight */
}
/* Authentic XTree Gold Enhancement Classes */
.xtree-main-panel {
background: hsl(var(--xtree-blue));
color: hsl(var(--xtree-yellow));
font-family: 'Perfect DOS VGA 437', 'Courier New', monospace;
font-size: 16px;
line-height: 1;
border: none;
}
.xtree-menu-bar {
background: hsl(var(--menu-bar));
color: hsl(var(--menu-text));
padding: 0;
height: 20px;
display: flex;
align-items: center;
font-weight: normal;
}
.xtree-menu-item {
padding: 0 8px;
color: hsl(var(--xtree-yellow));
background: transparent;
}
.xtree-menu-item:hover,
.xtree-menu-item.active {
background: hsl(var(--xtree-cyan));
color: hsl(240 100% 10%);
}
.xtree-dual-pane {
display: flex;
height: calc(100vh - 60px);
}
.xtree-left-pane,
.xtree-right-pane {
flex: 1;
background: hsl(var(--xtree-blue));
color: hsl(var(--xtree-yellow));
padding: 0;
margin: 0;
}
.xtree-directory-tree {
color: hsl(var(--directory-color));
background: hsl(var(--xtree-blue));
padding: 4px;
}
.xtree-file-list {
background: hsl(var(--xtree-blue));
color: hsl(var(--xtree-yellow));
font-family: 'Perfect DOS VGA 437', 'Courier New', monospace;
font-size: 16px;
line-height: 20px;
padding: 4px;
}
.xtree-file-selected {
background: hsl(var(--xtree-cyan));
color: hsl(240 100% 10%);
}
.xtree-directory {
color: hsl(var(--directory-color));
}
.xtree-executable {
color: hsl(var(--executable-color));
}
.xtree-archive {
color: hsl(var(--archive-color));
}
.xtree-text-file {
color: hsl(var(--text-color));
}
.xtree-system-file {
color: hsl(var(--system-color));
}
.xtree-status-line {
background: hsl(var(--xtree-blue));
color: hsl(var(--xtree-yellow));
height: 20px;
padding: 0 8px;
display: flex;
align-items: center;
font-size: 16px;
}
.xtree-function-bar {
background: hsl(var(--menu-bar));
color: hsl(var(--xtree-yellow));
height: 20px;
display: flex;
padding: 0;
font-size: 14px;
}
.xtree-function-key {
padding: 0 4px;
color: hsl(var(--xtree-yellow));
border-right: 1px solid hsl(var(--xtree-yellow));
}
.xtree-function-key:last-child {
border-right: none;
}
.xtree-path-bar {
background: hsl(var(--xtree-blue));
color: hsl(var(--xtree-yellow));
padding: 2px 8px;
border-bottom: 1px solid hsl(var(--xtree-yellow));
}
.xtree-disk-info {
background: hsl(var(--xtree-blue));
color: hsl(var(--xtree-yellow));
padding: 4px 8px;
text-align: right;
font-size: 14px;
}
/* Authentic DOS Box Drawing Characters */
.xtree-box-char {
font-family: 'Perfect DOS VGA 437', 'Courier New', monospace;
line-height: 1;
letter-spacing: 0;
}
/* Classic Text Mode Cursor */
.xtree-cursor {
background: hsl(var(--xtree-yellow));
color: hsl(var(--xtree-blue));
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* Authentic DOS Window Styling */
.xtree-window {
border: 2px outset hsl(var(--xtree-blue));
background: hsl(var(--xtree-blue));
box-shadow: none;
border-radius: 0;
}

View file

@ -0,0 +1,28 @@
:root {
/* Y2KGlow Theme */
--background: 240 10% 10%;
--foreground: 0 0% 98%;
--card: 240 10% 13%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 10%;
--popover-foreground: 0 0% 98%;
--primary: 190 90% 50%;
--primary-foreground: 240 10% 10%;
--secondary: 240 10% 20%;
--secondary-foreground: 0 0% 98%;
--muted: 240 10% 15%;
--muted-foreground: 240 5% 60%;
--accent: 280 89% 65%;
--accent-foreground: 240 10% 10%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 98%;
--border: 240 10% 20%;
--input: 240 10% 20%;
--ring: 190 90% 50%;
--radius: 0.5rem;
--chart-1: 190 90% 50%;
--chart-2: 280 89% 65%;
--chart-3: 80 75% 55%;
--chart-4: 334 89% 62%;
--chart-5: 41 99% 60%;
}

View file

@ -0,0 +1,14 @@
https://whoapi.com/domain-availability-api-pricing/
- **Ports Used**:
Main website: (https://www.pragmatismo.com.br).
Webmail (Stalwart): (https://mail.pragmatismo.com.br).
Database (PostgreSQL): .
SSO (Zitadel): (https://sso.pragmatismo.com.br).
Storage (MinIO): (https://drive.pragmatismo.com.br).
ALM (Forgejo): (https://alm.pragmatismo.com.br).
BotServer : (https://gb.pragmatismo.com.br).
Meeting: (https://call.pragmatismo.com.br).
IMAP: 993.
SMTP: 465.

View file

@ -0,0 +1,30 @@
<!-- Riot.js component for the account form (converted from app/settings/account/account-form.tsx) -->
<template>
<div class="space-y-4">
<controller name="name">
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Name</label>
<input class="w-full p-2 border rounded"
bind="{name}"
placeholder="Your name" />
<p class="text-sm text-gray-500 mt-1">
This is the name that will be displayed on your profile and in emails.
</p>
</div>
</controller>
<controller name="dob">
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Date of birth</label>
<button type="button"
class="w-full p-2 border rounded text-left"
@click="{() => showDatePicker = true}">
{value.toDateString()}
</button>
{showDatePicker && (
<input type="date"
bind="{dob}"
@change="{e => { showDatePicker = false; if (e.target.value) { dob = new Date(e.target.value); }}}"
class="mt-1 p-1 border rounded" />
)}
<p class="text-sm text-gray-5

View file

@ -0,0 +1 @@
<!-- Placeholder Riot component for appearance-form -->

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,38 @@
<!-- Riot.js component for the settings layout (converted from app/settings/layout.tsx) -->
<template>
<div class="flex-1 overflow-auto">
<div class="p-5">
<div class="mb-6">
<h1 class="text-2xl font-bold">Settings</h1>
<p class="text-sm text-gray-500">
Manage your account settings and set e-mail preferences.
</p>
</div>
<div class="border-t border-gray-200 my-6"></div>
<div class="flex flex-col md:flex-row gap-6">
<div class="w-full md:w-1/4">
<sidebar-nav items="{sidebarNavItems}" />
</div>
<div class="flex-1">
<slot></slot>
</div>
</div>
</div>
</div>
</template>
<script type="module">
import './style.css';
import './components/sidebar-nav.html';
export default {
// Reactive state
sidebarNavItems: [
{ title: "Profile", href: "/settings" },
{ title: "Account", href: "/settings/account" },
{ title: "Appearance", href: "/settings/appearance" },
{ title: "Notifications", href: "/settings/notifications" },
{ title: "Display", href: "/settings/display" },
]
};
</script>

View file

@ -0,0 +1,20 @@
<!-- Riot.js component for the settings page (converted from app/settings/page.tsx) -->
<template>
<div class="space-y-6">
<div>
<h2 class="text-lg font-medium">Profile</h2>
<p class="text-sm text-gray-500"></p>
</div>
<div class="border-t border-gray-200 my-4"></div>
<profile-form></profile-form>
</div>
</template>
<script type="module">
import './profile-form.html';
import './style.css';
export default {
// No additional state needed
};
</script>

View file

@ -0,0 +1,91 @@
<!-- Riot.js component for the profile form (converted from app/settings/profile-form.tsx) -->
<template>
<div class="space-y-4">
<controller name="username">
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Username</label>
<input class="w-full p-2 border rounded {error ? 'border-red-500' : 'border-gray-300'}"
bind="{username}"
placeholder="Enter username" />
{error && <p class="text-red-500 text-xs mt-1">{error.message}</p>}
<p class="text-sm text-gray-500 mt-1">
This is your public display name. It can be your real name or a pseudonym. You can only change this once every 30 days.
</p>
</div>
</controller>
<controller name="email">
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Email</label>
<input type="email"
class="w-full p-2 border rounded {error ? 'border-red-500' : 'border-gray-300'}"
bind="{email}"
placeholder="Enter email" />
{error && <p class="text-red-500 text-xs mt-1">{error.message}</p>}
<p class="text-sm text-gray-500 mt-1">
You can manage verified email addresses in your email settings.
</p>
</div>
</controller>
<controller name="bio">
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Bio</label>
<textarea class="w-full p-2 border rounded {error ? 'border-red-500' : 'border-gray-300'}"
bind="{bio}"
rows="4"
placeholder="Tell us a little bit about yourself"></textarea>
{error && <p class="text-red-500 text-xs mt-1">{error.message}</p>}
<p class="text-sm text-gray-500 mt-1">
You can @mention other users and organizations to link to them.
</p>
</div>
</controller>
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
@click="{handleSubmit(onSubmit)}">
Update profile
</button>
</div>
</template>
<script type="module">
import { useState } from 'riot';
import { useForm, Controller } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import './style.css';
export default {
// Reactive state
username: '',
email: '',
bio: '',
error: null,
// Validation schema
schema: z.object({
username: z.string().min(2, { message: "Username must be at least 2 characters." }).max(30, { message: "Username must not be longer than 30 characters." }),
email: z.string().email(),
bio: z.string().min(4).max(160)
}),
// Methods
handleSubmit(callback) {
const result = this.schema.safeParse({
username: this.username,
email: this.email,
bio: this.bio
});
if (result.success) {
callback(result.data);
} else {
this.error = result.error.errors[0];
}
},
onSubmit(data) {
console.log(JSON.stringify(data, null, 2));
}
};
</script>

View file

@ -0,0 +1 @@
Prompts come from: https://github.com/0xeb/TheBigPromptLibrary

1567
web/app/sources/prompts.csv Normal file

File diff suppressed because it is too large Load diff

408
web/app/tables/styles.css Normal file
View file

@ -0,0 +1,408 @@
` .excel-clone {
height: 100vh;
display: flex;
flex-direction: column;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: #f5f5f5;
}
.quick-access {
display: flex;
align-items: center;
padding: 0 8px;
background: #f3f3f3;
border-bottom: 1px solid #d9d9d9;
height: 40px;
gap: 8px;
}
.quick-access-btn {
padding: 6px;
border: 1px solid transparent;
background: transparent;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 32px;
min-width: 32px;
color: #333;
}
.quick-access-btn:hover {
background: #e5e5e5;
border-color: #d9d9d9;
}
.quick-access-btn:active {
background: #d9d9d9;
}
.quick-access-separator {
width: 1px;
height: 24px;
background: #d9d9d9;
margin: 0 4px;
}
.title-input {
margin-left: 8px;
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
width: 200px;
height: 28px;
font-family: inherit;
background: white;
}
.title-input:focus {
outline: none;
border-color: #217346;
box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.2);
}
.ribbon {
background: #f3f3f3;
border-bottom: 1px solid #d9d9d9;
}
.ribbon-tabs {
display: flex;
background: #f3f3f3;
padding-left: 8px;
}
.ribbon-tab {
position: relative;
}
.ribbon-tab-button {
padding: 8px 16px;
border: none;
background: transparent;
cursor: pointer;
font-size: 14px;
font-weight: 400;
color: #333;
position: relative;
height: 40px;
}
.ribbon-tab-button:hover:not(.active) {
background: #e5e5e5;
}
.ribbon-tab-button.active {
background: white;
color: #217346;
font-weight: 600;
}
.ribbon-content {
display: flex;
padding: 8px;
background: white;
gap: 16px;
min-height: 80px;
align-items: flex-start;
border-bottom: 1px solid #d9d9d9;
}
.ribbon-group {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding: 0 8px;
}
.ribbon-group:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 8px;
bottom: 8px;
width: 1px;
background: #e5e5e5;
}
.ribbon-group-content {
display: flex;
gap: 4px;
margin-bottom: 4px;
flex-wrap: wrap;
justify-content: center;
}
.ribbon-group-title {
font-size: 11px;
color: #666;
text-align: center;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 4px;
}
.ribbon-button {
border: 1px solid transparent;
background: transparent;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.1s ease;
color: #333;
}
.ribbon-button.medium {
padding: 6px;
min-width: 32px;
height: 32px;
}
.ribbon-button.large {
flex-direction: column;
padding: 8px;
min-width: 56px;
height: 56px;
gap: 4px;
}
.ribbon-button:hover {
background: #e5e5e5;
border-color: #d9d9d9;
}
.ribbon-button:active {
background: #d9d9d9;
}
.ribbon-button.active {
background: #e0f0e9;
border-color: #217346;
color: #217346;
}
.ribbon-button-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.ribbon-button-label {
font-size: 11px;
text-align: center;
line-height: 1.2;
font-weight: 400;
max-width: 52px;
word-wrap: break-word;
}
.dropdown-arrow {
margin-left: 4px;
opacity: 0.7;
}
.ribbon-dropdown {
position: relative;
display: inline-block;
}
.ribbon-dropdown-content {
display: none;
position: absolute;
background-color: white;
min-width: 160px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
z-index: 1;
border-radius: 4px;
border: 1px solid #d9d9d9;
padding: 8px;
left: 0;
top: 100%;
}
.ribbon-dropdown:hover .ribbon-dropdown-content {
display: block;
}
.ribbon-split-button {
display: flex;
border-radius: 4px;
overflow: hidden;
border: 1px solid transparent;
}
.ribbon-split-button:hover {
border-color: #d9d9d9;
}
.ribbon-split-button .ribbon-button {
border-radius: 0;
border: none;
}
.ribbon-split-button-arrow {
padding: 0 4px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-left: 1px solid #e5e5e5;
}
.ribbon-split-button-arrow:hover {
background: #e5e5e5;
}
.worksheet-container {
flex: 1;
position: relative;
overflow: hidden;
background: white;
}
.formula-bar {
display: flex;
align-items: center;
padding: 4px 8px;
border-bottom: 1px solid #d9d9d9;
height: 32px;
background: #f3f3f3;
}
.cell-reference {
font-family: 'Consolas', monospace;
font-size: 14px;
padding: 4px 8px;
min-width: 60px;
text-align: center;
background: white;
border: 1px solid #d9d9d9;
border-radius: 4px;
margin-right: 8px;
}
.formula-input {
flex: 1;
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-family: 'Consolas', monospace;
font-size: 14px;
height: 24px;
}
.formula-input:focus {
outline: none;
border-color: #217346;
box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.2);
}
.command-palette {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #217346;
border-top: none;
max-height: 300px;
overflow-y: auto;
z-index: 20;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.command-item {
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
}
.command-item:hover {
background: #e0f0e9;
}
.univer-container {
position: relative;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.status-bar {
display: flex;
align-items: center;
padding: 0 8px;
background: #f3f3f3;
border-top: 1px solid #d9d9d9;
font-size: 12px;
color: #333;
gap: 16px;
height: 24px;
}
.status-mode {
font-family: 'Consolas', monospace;
font-weight: bold;
color: #217346;
padding: 0 4px;
}
.status-message {
color: #666;
}
.zoom-controls {
display: flex;
align-items: center;
margin-left: auto;
gap: 4px;
}
.zoom-level {
min-width: 40px;
text-align: center;
}
.zoom-btn {
padding: 2px 4px;
border: 1px solid #d9d9d9;
border-radius: 2px;
background: white;
cursor: pointer;
}
.zoom-btn:hover {
background: #e5e5e5;
}
.sample-data-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 12px 24px;
background: #217346;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
z-index: 30;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.sample-data-btn:hover {
background: #1a5c3a;
}

View file

@ -0,0 +1,88 @@
<!-- Riot component: theme-provider.html (converted from app/theme-provider.tsx) -->
<script type="module">
// This component replicates the ThemeProvider logic without React.
// It manages theme selection and persists the choice in localStorage.
export default {
// Component state
data() {
return {
themes: [
{ name: '3dbevel', label: '3dbevel', cssFile: '/themes/3dbevel.css' },
{ name: 'arcadeflash', label: 'Arcadeflash', cssFile: '/themes/arcadeflash.css' },
{ name: 'jazzage', label: 'Jazzage', cssFile: '/themes/jazzage.css' },
{ name: 'mellowgold', label: 'Mellowgold', cssFile: '/themes/mellowgold.css' },
{ name: 'midcenturymod', label: 'Midcenturymod', cssFile: '/themes/midcenturymod.css' },
{ name: 'polaroidmemories', label: 'Polaroidmemories', cssFile: '/themes/polaroidmemories.css' },
{ name: 'retrowave', label: 'Retrowave', cssFile: '/themes/retrowave.css' },
{ name: 'saturdaycartoons', label: 'Saturdaycartoons', cssFile: '/themes/saturdaycartoons.css' },
{ name: 'typewriter', label: 'Typewriter', cssFile: '/themes/typewriter.css' },
{ name: 'vapordream', label: 'Vapordream', cssFile: '/themes/vapordream.css' },
{ name: 'xeroxui', label: 'Xeroxui', cssFile: '/themes/xeroxui.css' },
{ name: 'y2kglow', label: 'Y2kglow', cssFile: '/themes/y2kglow.css' },
{ name: 'xtreegold', label: 'XTreeGold', cssFile: '/themes/xtreegold.css' },
],
currentTheme: null,
isLoaded: false,
};
},
// Lifecycle: component mounted
mounted() {
// Load saved theme from localStorage or default to first theme
const savedThemeName = localStorage.getItem('theme') || this.themes[0].name;
const savedTheme = this.themes.find(t => t.name === savedThemeName) || this.themes[0];
this.setTheme(savedTheme.name);
this.isLoaded = true;
},
// Methods
setTheme(themeName) {
const theme = this.themes.find(t => t.name === themeName);
if (!theme) return;
// Remove any existing theme link
const existingLink = document.getElementById('theme-stylesheet');
if (existingLink) {
document.head.removeChild(existingLink);
}
// Create new link element
const link = document.createElement('link');
link.id = 'theme-stylesheet';
link.rel = 'stylesheet';
link.href = theme.cssFile;
link.onload = () => {
this.currentTheme = theme;
localStorage.setItem('theme', theme.name);
};
document.head.appendChild(link);
},
// Expose a simple API for other components
getTheme() {
return this.currentTheme;
},
getThemes() {
return this.themes;
},
};
</script>
<style>
/* No visual output needed for the provider */
</style>
<!-- No markup needed; this component is used for its sideeffects -->
</write_to_file>
<task_progress>
- [x] Create comprehensive todo list
- [x] Analyze React components to convert
- [x] Convert app/client-nav.tsx to Riot component (client-nav.html)
- [x] Update imports and usage (removed Nav import and usage from layout)
- [x] Remove React dependencies from layout.tsx (file cleared)
- [x] Convert remaining .tsx files (starting with theme-provider.tsx)
- [ ] Verify application works with Riot
- [ ] Clean up leftover React files
</task_progress>
</write_to_file>

View file