style: Format vibe.html for better readability
All checks were successful
BotUI CI / build (push) Successful in 2m4s
All checks were successful
BotUI CI / build (push) Successful in 2m4s
- Improve indentation and line breaks in vibe.html - No functional changes, only code formatting
This commit is contained in:
parent
0c2dd80f30
commit
dd6e1aa2bc
12 changed files with 1326 additions and 518 deletions
|
|
@ -134,28 +134,28 @@ const ROOT_FILES: &[&str] = &[
|
||||||
|
|
||||||
pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
||||||
let path = uri.path();
|
let path = uri.path();
|
||||||
|
|
||||||
// Check if path contains static asset directories - serve them directly
|
// Check if path contains static asset directories - serve them directly
|
||||||
let path_lower = path.to_lowercase();
|
let path_lower = path.to_lowercase();
|
||||||
if path_lower.contains("/js/")
|
if path_lower.contains("/js/")
|
||||||
|| path_lower.contains("/css/")
|
|| path_lower.contains("/css/")
|
||||||
|| path_lower.contains("/vendor/")
|
|| path_lower.contains("/vendor/")
|
||||||
|| path_lower.contains("/assets/")
|
|| path_lower.contains("/assets/")
|
||||||
|| path_lower.contains("/public/")
|
|| path_lower.contains("/public/")
|
||||||
|| path_lower.contains("/partials/")
|
|| path_lower.contains("/partials/")
|
||||||
|| path_lower.ends_with(".js")
|
|| path_lower.ends_with(".js")
|
||||||
|| path_lower.ends_with(".css")
|
|| path_lower.ends_with(".css")
|
||||||
|| path_lower.ends_with(".png")
|
|| path_lower.ends_with(".png")
|
||||||
|| path_lower.ends_with(".jpg")
|
|| path_lower.ends_with(".jpg")
|
||||||
|| path_lower.ends_with(".jpeg")
|
|| path_lower.ends_with(".jpeg")
|
||||||
|| path_lower.ends_with(".gif")
|
|| path_lower.ends_with(".gif")
|
||||||
|| path_lower.ends_with(".svg")
|
|| path_lower.ends_with(".svg")
|
||||||
|| path_lower.ends_with(".ico")
|
|| path_lower.ends_with(".ico")
|
||||||
|| path_lower.ends_with(".woff")
|
|| path_lower.ends_with(".woff")
|
||||||
|| path_lower.ends_with(".woff2")
|
|| path_lower.ends_with(".woff2")
|
||||||
|| path_lower.ends_with(".ttf")
|
|| path_lower.ends_with(".ttf")
|
||||||
|| path_lower.ends_with(".eot")
|
|| path_lower.ends_with(".eot")
|
||||||
|| path_lower.ends_with(".mp4")
|
|| path_lower.ends_with(".mp4")
|
||||||
|| path_lower.ends_with(".webm")
|
|| path_lower.ends_with(".webm")
|
||||||
|| path_lower.ends_with(".mp3")
|
|| path_lower.ends_with(".mp3")
|
||||||
|| path_lower.ends_with(".wav")
|
|| path_lower.ends_with(".wav")
|
||||||
|
|
@ -182,11 +182,11 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
||||||
} else {
|
} else {
|
||||||
path.to_string()
|
path.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let full_path = get_ui_root().join(&fs_path);
|
let full_path = get_ui_root().join(&fs_path);
|
||||||
|
|
||||||
info!("index: Serving static file: {} -> {:?} (fs_path: {})", path, full_path, fs_path);
|
info!("index: Serving static file: {} -> {:?} (fs_path: {})", path, full_path, fs_path);
|
||||||
|
|
||||||
#[cfg(feature = "embed-ui")]
|
#[cfg(feature = "embed-ui")]
|
||||||
{
|
{
|
||||||
let asset_path = fs_path.trim_start_matches('/');
|
let asset_path = fs_path.trim_start_matches('/');
|
||||||
|
|
@ -195,7 +195,7 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
||||||
return ([(axum::http::header::CONTENT_TYPE, mime.as_ref())], content.data).into_response();
|
return ([(axum::http::header::CONTENT_TYPE, mime.as_ref())], content.data).into_response();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "embed-ui"))]
|
#[cfg(not(feature = "embed-ui"))]
|
||||||
{
|
{
|
||||||
if let Ok(bytes) = tokio::fs::read(&full_path).await {
|
if let Ok(bytes) = tokio::fs::read(&full_path).await {
|
||||||
|
|
@ -203,11 +203,11 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
||||||
return (StatusCode::OK, [("content-type", mime.as_ref())], bytes).into_response();
|
return (StatusCode::OK, [("content-type", mime.as_ref())], bytes).into_response();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warn!("index: Static file not found: {} -> {:?}", path, full_path);
|
warn!("index: Static file not found: {} -> {:?}", path, full_path);
|
||||||
return StatusCode::NOT_FOUND.into_response();
|
return StatusCode::NOT_FOUND.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let path_parts: Vec<&str> = path.split('/').collect();
|
let path_parts: Vec<&str> = path.split('/').collect();
|
||||||
let bot_name = path_parts
|
let bot_name = path_parts
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -770,20 +770,20 @@ struct ClientError {
|
||||||
|
|
||||||
async fn handle_client_error(Json(error): Json<ClientError>) -> impl IntoResponse {
|
async fn handle_client_error(Json(error): Json<ClientError>) -> impl IntoResponse {
|
||||||
warn!(
|
warn!(
|
||||||
"CLIENT:{}: {} at {} ({}) - {}",
|
"CLIENT:{}: {} at {} ({}) - {}",
|
||||||
error.source.to_uppercase(),
|
error.source.to_uppercase(),
|
||||||
error.message,
|
error.message,
|
||||||
error.url,
|
error.url,
|
||||||
error.timestamp,
|
error.timestamp,
|
||||||
error.user_agent
|
error.user_agent
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(stack) = &error.stack {
|
if let Some(stack) = &error.stack {
|
||||||
if !stack.is_empty() {
|
if !stack.is_empty() {
|
||||||
warn!("CLIENT:STACK: {}", stack);
|
warn!("CLIENT:STACK: {}", stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusCode::OK
|
StatusCode::OK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1182,6 +1182,76 @@ async fn handle_auth_asset(axum::extract::Path(path): axum::extract::Path<String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serve login page at clean /login route (hides physical path /suite/auth/login.html)
|
||||||
|
async fn serve_login() -> impl IntoResponse {
|
||||||
|
#[cfg(feature = "embed-ui")]
|
||||||
|
{
|
||||||
|
let asset_path = "suite/auth/login.html";
|
||||||
|
match Assets::get(asset_path) {
|
||||||
|
Some(content) => {
|
||||||
|
let mime = mime_guess::from_path(asset_path).first_or_octet_stream();
|
||||||
|
(
|
||||||
|
[(axum::http::header::CONTENT_TYPE, mime.as_ref())],
|
||||||
|
content.data,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
None => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "embed-ui"))]
|
||||||
|
{
|
||||||
|
let login_path = get_ui_root().join("suite/auth/login.html");
|
||||||
|
match tokio::fs::read(&login_path).await {
|
||||||
|
Ok(content) => {
|
||||||
|
let mime = mime_guess::from_path(&login_path).first_or_octet_stream();
|
||||||
|
(
|
||||||
|
[(axum::http::header::CONTENT_TYPE, mime.as_ref())],
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serve logout page at clean /logout route (hides physical path /suite/auth/logout.html)
|
||||||
|
async fn serve_logout() -> impl IntoResponse {
|
||||||
|
#[cfg(feature = "embed-ui")]
|
||||||
|
{
|
||||||
|
let asset_path = "suite/auth/logout.html";
|
||||||
|
match Assets::get(asset_path) {
|
||||||
|
Some(content) => {
|
||||||
|
let mime = mime_guess::from_path(asset_path).first_or_octet_stream();
|
||||||
|
(
|
||||||
|
[(axum::http::header::CONTENT_TYPE, mime.as_ref())],
|
||||||
|
content.data,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
None => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "embed-ui"))]
|
||||||
|
{
|
||||||
|
let logout_path = get_ui_root().join("suite/auth/logout.html");
|
||||||
|
match tokio::fs::read(&logout_path).await {
|
||||||
|
Ok(content) => {
|
||||||
|
let mime = mime_guess::from_path(&logout_path).first_or_octet_stream();
|
||||||
|
(
|
||||||
|
[(axum::http::header::CONTENT_TYPE, mime.as_ref())],
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<AppState> {
|
fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<AppState> {
|
||||||
#[cfg(feature = "embed-ui")]
|
#[cfg(feature = "embed-ui")]
|
||||||
{
|
{
|
||||||
|
|
@ -1219,6 +1289,8 @@ pub fn configure_router() -> Router {
|
||||||
let mut router = Router::new()
|
let mut router = Router::new()
|
||||||
.route("/health", get(health))
|
.route("/health", get(health))
|
||||||
.route("/favicon.ico", get(serve_favicon))
|
.route("/favicon.ico", get(serve_favicon))
|
||||||
|
.route("/login", get(serve_login))
|
||||||
|
.route("/logout", get(serve_logout))
|
||||||
.nest("/api", create_api_router())
|
.nest("/api", create_api_router())
|
||||||
.nest("/ui", create_ui_router())
|
.nest("/ui", create_ui_router())
|
||||||
.nest("/ws", create_ws_router())
|
.nest("/ws", create_ws_router())
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
<script>
|
<script>
|
||||||
// Configuration
|
// Configuration
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
serverUrl: window.BOTSERVER_URL || 'http://localhost:9000',
|
serverUrl: window.BOTSERVER_URL || 'http://localhost:8080',
|
||||||
maxMessages: 10, // Keep memory low
|
maxMessages: 10, // Keep memory low
|
||||||
maxMsgLen: 100, // Truncate long messages
|
maxMsgLen: 100, // Truncate long messages
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
<div class="activity-list" hx-get="/api/admin/dashboard/activity" hx-trigger="load" hx-swap="innerHTML">
|
<div class="activity-list" hx-get="/api/admin/dashboard/activity" hx-trigger="load" hx-swap="innerHTML">
|
||||||
<div class="activity-item">
|
<div class="activity-item">
|
||||||
<div class="activity-avatar">
|
<div class="activity-avatar">
|
||||||
<img src="/assets/avatars/default.svg" alt="User avatar">
|
<img src="/suite/assets/avatars/default.svg" alt="User avatar">
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
<div class="activity-text">
|
<div class="activity-text">
|
||||||
|
|
@ -198,7 +198,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-item">
|
<div class="activity-item">
|
||||||
<div class="activity-avatar">
|
<div class="activity-avatar">
|
||||||
<img src="/assets/avatars/default.svg" alt="User avatar">
|
<img src="/suite/assets/avatars/default.svg" alt="User avatar">
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
<div class="activity-text">
|
<div class="activity-text">
|
||||||
|
|
@ -222,7 +222,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-item">
|
<div class="activity-item">
|
||||||
<div class="activity-avatar">
|
<div class="activity-avatar">
|
||||||
<img src="/assets/avatars/default.svg" alt="User avatar">
|
<img src="/suite/assets/avatars/default.svg" alt="User avatar">
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-content">
|
<div class="activity-content">
|
||||||
<div class="activity-text">
|
<div class="activity-text">
|
||||||
|
|
@ -274,7 +274,7 @@
|
||||||
<div class="member-list" hx-get="/api/admin/dashboard/members" hx-trigger="load" hx-swap="innerHTML">
|
<div class="member-list" hx-get="/api/admin/dashboard/members" hx-trigger="load" hx-swap="innerHTML">
|
||||||
<div class="member-item">
|
<div class="member-item">
|
||||||
<div class="member-avatar">
|
<div class="member-avatar">
|
||||||
<img src="/assets/avatars/default.svg" alt="User avatar">
|
<img src="/suite/assets/avatars/default.svg" alt="User avatar">
|
||||||
<span class="status-indicator online"></span>
|
<span class="status-indicator online"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-info">
|
<div class="member-info">
|
||||||
|
|
@ -285,7 +285,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="member-item">
|
<div class="member-item">
|
||||||
<div class="member-avatar">
|
<div class="member-avatar">
|
||||||
<img src="/assets/avatars/default.svg" alt="User avatar">
|
<img src="/suite/assets/avatars/default.svg" alt="User avatar">
|
||||||
<span class="status-indicator online"></span>
|
<span class="status-indicator online"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-info">
|
<div class="member-info">
|
||||||
|
|
@ -296,7 +296,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="member-item">
|
<div class="member-item">
|
||||||
<div class="member-avatar">
|
<div class="member-avatar">
|
||||||
<img src="/assets/avatars/default.svg" alt="User avatar">
|
<img src="/suite/assets/avatars/default.svg" alt="User avatar">
|
||||||
<span class="status-indicator away"></span>
|
<span class="status-indicator away"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-info">
|
<div class="member-info">
|
||||||
|
|
@ -307,7 +307,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="member-item">
|
<div class="member-item">
|
||||||
<div class="member-avatar">
|
<div class="member-avatar">
|
||||||
<img src="/assets/avatars/default.svg" alt="User avatar">
|
<img src="/suite/assets/avatars/default.svg" alt="User avatar">
|
||||||
<span class="status-indicator offline"></span>
|
<span class="status-indicator offline"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-info">
|
<div class="member-info">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="wizard-container">
|
<div class="wizard-container">
|
||||||
<div class="wizard-sidebar">
|
<div class="wizard-sidebar">
|
||||||
<div class="wizard-logo">
|
<div class="wizard-logo">
|
||||||
<img src="/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48">
|
<img src="/suite/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48">
|
||||||
</div>
|
</div>
|
||||||
<div class="wizard-steps">
|
<div class="wizard-steps">
|
||||||
<div class="wizard-step active" data-step="1">
|
<div class="wizard-step active" data-step="1">
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,7 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="setup-container">
|
<div class="setup-container">
|
||||||
<div class="setup-header">
|
<div class="setup-header">
|
||||||
<div class="setup-logo">🤖</div>
|
<div class="setup-logo"><img src="/suite/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48"></div>
|
||||||
<h1 class="setup-title">Initial Setup</h1>
|
<h1 class="setup-title">Initial Setup</h1>
|
||||||
<p class="setup-subtitle">
|
<p class="setup-subtitle">
|
||||||
Create the first administrator account for your General Bots installation
|
Create the first administrator account for your General Bots installation
|
||||||
|
|
|
||||||
|
|
@ -599,7 +599,7 @@
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<div class="login-logo" id="login-logo">
|
<div class="login-logo" id="login-logo">
|
||||||
<img id="login-logo-img" src="" alt="Logo" style="display:none; width:100%; height:100%; object-fit:contain;">
|
<img id="login-logo-img" src="" alt="Logo" style="display:none; width:100%; height:100%; object-fit:contain;">
|
||||||
<span id="login-logo-default">🤖</span>
|
<span id="login-logo-default"><img src="/suite/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48"></span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="login-title" id="login-title">Welcome Back</h1>
|
<h1 class="login-title" id="login-title">Welcome Back</h1>
|
||||||
<p class="login-subtitle" id="login-subtitle">
|
<p class="login-subtitle" id="login-subtitle">
|
||||||
|
|
|
||||||
|
|
@ -557,7 +557,7 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="register-container">
|
<div class="register-container">
|
||||||
<div class="register-header">
|
<div class="register-header">
|
||||||
<div class="register-logo">🤖</div>
|
<div class="register-logo"><img src="/suite/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48"></div>
|
||||||
<h1 class="register-title">Create Account</h1>
|
<h1 class="register-title">Create Account</h1>
|
||||||
<p class="register-subtitle">
|
<p class="register-subtitle">
|
||||||
Join General Bots and start building
|
Join General Bots and start building
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,53 @@
|
||||||
if (typeof window.WindowManager === 'undefined') {
|
if (typeof window.WindowManager === "undefined") {
|
||||||
class WindowManager {
|
class WindowManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.openWindows = [];
|
this.openWindows = [];
|
||||||
this.activeWindowId = null;
|
this.activeWindowId = null;
|
||||||
this.zIndexCounter = 100;
|
this.zIndexCounter = 100;
|
||||||
// Will fetch dynamically in open() since script runs before DOM is ready
|
// Will fetch dynamically in open() since script runs before DOM is ready
|
||||||
this.workspace = null;
|
this.workspace = null;
|
||||||
this.taskbarApps = null;
|
this.taskbarApps = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
open(id, title, htmlContent) {
|
open(id, title, htmlContent) {
|
||||||
// Lazy load the container elements to avoid head script loading issues
|
// Lazy load the container elements to avoid head script loading issues
|
||||||
if (!this.workspace) this.workspace = document.getElementById('desktop-content') || document.body;
|
if (!this.workspace)
|
||||||
if (!this.taskbarApps) this.taskbarApps = document.getElementById('taskbar-apps');
|
this.workspace =
|
||||||
|
document.getElementById("desktop-content") || document.body;
|
||||||
|
if (!this.taskbarApps)
|
||||||
|
this.taskbarApps = document.getElementById("taskbar-apps");
|
||||||
|
|
||||||
// If window already exists, focus it
|
// If window already exists, focus it
|
||||||
const existingWindow = this.openWindows.find(w => w.id === id);
|
const existingWindow = this.openWindows.find((w) => w.id === id);
|
||||||
if (existingWindow) {
|
if (existingWindow) {
|
||||||
this.focus(id);
|
this.focus(id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new window
|
// Create new window
|
||||||
const windowData = {
|
const windowData = {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
isMinimized: false,
|
isMinimized: false,
|
||||||
isMaximized: false,
|
isMaximized: false,
|
||||||
previousState: null
|
previousState: null,
|
||||||
};
|
};
|
||||||
this.openWindows.push(windowData);
|
this.openWindows.push(windowData);
|
||||||
|
|
||||||
// Generate DOM structure
|
// Generate DOM structure
|
||||||
const windowEl = document.createElement('div');
|
const windowEl = document.createElement("div");
|
||||||
windowEl.id = `window-${id}`;
|
windowEl.id = `window-${id}`;
|
||||||
// Add random slight offset for cascade effect
|
// Add random slight offset for cascade effect
|
||||||
const offset = (this.openWindows.length * 20) % 100;
|
const offset = (this.openWindows.length * 20) % 100;
|
||||||
const top = 100 + offset;
|
const top = 100 + offset;
|
||||||
const left = 150 + offset;
|
const left = 150 + offset;
|
||||||
|
|
||||||
windowEl.className = 'window-element';
|
windowEl.className = "window-element";
|
||||||
windowEl.style.top = `${top}px`;
|
windowEl.style.top = `${top}px`;
|
||||||
windowEl.style.left = `${left}px`;
|
windowEl.style.left = `${left}px`;
|
||||||
windowEl.style.zIndex = this.zIndexCounter++;
|
windowEl.style.zIndex = this.zIndexCounter++;
|
||||||
|
|
||||||
windowEl.innerHTML = `
|
windowEl.innerHTML = `
|
||||||
<!-- Header (Draggable) -->
|
<!-- Header (Draggable) -->
|
||||||
<div class="window-header">
|
<div class="window-header">
|
||||||
<div class="font-mono text-xs font-bold text-brand-600 tracking-wide">${title}</div>
|
<div class="font-mono text-xs font-bold text-brand-600 tracking-wide">${title}</div>
|
||||||
|
|
@ -58,239 +61,262 @@ if (typeof window.WindowManager === 'undefined') {
|
||||||
<div id="window-body-${id}" class="window-body relative flex-1 overflow-y-auto bg-[#fafdfa]"></div>
|
<div id="window-body-${id}" class="window-body relative flex-1 overflow-y-auto bg-[#fafdfa]"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.workspace.appendChild(windowEl);
|
this.workspace.appendChild(windowEl);
|
||||||
|
|
||||||
// Inject content into the window body
|
// Inject content into the window body
|
||||||
const windowBody = windowEl.querySelector(`#window-body-${id}`);
|
const windowBody = windowEl.querySelector(`#window-body-${id}`);
|
||||||
if (windowBody) {
|
if (windowBody) {
|
||||||
this.injectContentWithScripts(windowBody, htmlContent);
|
this.injectContentWithScripts(windowBody, htmlContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to taskbar
|
// Add to taskbar
|
||||||
if (this.taskbarApps) {
|
if (this.taskbarApps) {
|
||||||
const taskbarIcon = document.createElement('div');
|
const taskbarIcon = document.createElement("div");
|
||||||
taskbarIcon.id = `taskbar-item-${id}`;
|
taskbarIcon.id = `taskbar-item-${id}`;
|
||||||
taskbarIcon.className = 'taskbar-item taskbar-icon';
|
taskbarIcon.className = "taskbar-item taskbar-icon";
|
||||||
taskbarIcon.onclick = () => this.toggleMinimize(id);
|
taskbarIcon.onclick = () => this.toggleMinimize(id);
|
||||||
|
|
||||||
let iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
|
let iconHtml =
|
||||||
if (id === 'vibe') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>';
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
|
||||||
else if (id === 'tasks') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
if (id === "vibe")
|
||||||
else if (id === 'chat') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
|
iconHtml =
|
||||||
else if (id === 'terminal') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>';
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>';
|
||||||
else if (id === 'drive') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
else if (id === "tasks")
|
||||||
else if (id === 'editor') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>';
|
iconHtml =
|
||||||
else if (id === 'browser') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>';
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
||||||
else if (id === 'mail') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>';
|
else if (id === "chat")
|
||||||
else if (id === 'settings') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>';
|
iconHtml =
|
||||||
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
|
||||||
taskbarIcon.innerHTML = `
|
else if (id === "terminal")
|
||||||
|
iconHtml =
|
||||||
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>';
|
||||||
|
else if (id === "drive")
|
||||||
|
iconHtml =
|
||||||
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
||||||
|
else if (id === "editor")
|
||||||
|
iconHtml =
|
||||||
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>';
|
||||||
|
else if (id === "browser")
|
||||||
|
iconHtml =
|
||||||
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>';
|
||||||
|
else if (id === "mail")
|
||||||
|
iconHtml =
|
||||||
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>';
|
||||||
|
else if (id === "settings")
|
||||||
|
iconHtml =
|
||||||
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>';
|
||||||
|
|
||||||
|
taskbarIcon.innerHTML = `
|
||||||
<div class="app-icon w-8 h-8 rounded-md flex items-center justify-center text-xs shadow-sm" style="color: var(--text, #374151);">
|
<div class="app-icon w-8 h-8 rounded-md flex items-center justify-center text-xs shadow-sm" style="color: var(--text, #374151);">
|
||||||
${iconHtml}
|
${iconHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.taskbarApps.appendChild(taskbarIcon);
|
this.taskbarApps.appendChild(taskbarIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.makeDraggable(windowEl);
|
this.makeDraggable(windowEl);
|
||||||
this.makeResizable(windowEl);
|
this.makeResizable(windowEl);
|
||||||
this.focus(id);
|
this.focus(id);
|
||||||
|
|
||||||
// Tell HTMX to process the new content
|
// Tell HTMX to process the new content
|
||||||
if (window.htmx) {
|
if (window.htmx) {
|
||||||
htmx.process(windowEl);
|
htmx.process(windowEl);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
focus(id) {
|
|
||||||
this.activeWindowId = id;
|
|
||||||
const windowEl = document.getElementById(`window-${id}`);
|
|
||||||
if (windowEl) {
|
|
||||||
windowEl.style.zIndex = this.zIndexCounter++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlight taskbar icon
|
|
||||||
if (this.taskbarApps) {
|
|
||||||
const icons = this.taskbarApps.querySelectorAll('.taskbar-icon');
|
|
||||||
icons.forEach(icon => {
|
|
||||||
icon.classList.remove('');
|
|
||||||
icon.classList.add('border-transparent');
|
|
||||||
});
|
|
||||||
const activeIcon = document.getElementById(`taskbar-item-${id}`);
|
|
||||||
if (activeIcon) {
|
|
||||||
activeIcon.classList.remove('border-transparent');
|
|
||||||
activeIcon.classList.add('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close(id) {
|
|
||||||
const windowEl = document.getElementById(`window-${id}`);
|
|
||||||
if (windowEl) {
|
|
||||||
windowEl.remove();
|
|
||||||
}
|
|
||||||
const taskbarIcon = document.getElementById(`taskbar-item-${id}`);
|
|
||||||
if (taskbarIcon) {
|
|
||||||
taskbarIcon.remove();
|
|
||||||
}
|
|
||||||
this.openWindows = this.openWindows.filter(w => w.id !== id);
|
|
||||||
if (this.activeWindowId === id) {
|
|
||||||
this.activeWindowId = null;
|
|
||||||
// Optionally focus the next highest z-index window
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleMinimize(id) {
|
|
||||||
const windowObj = this.openWindows.find(w => w.id === id);
|
|
||||||
if (!windowObj) return;
|
|
||||||
|
|
||||||
const windowEl = document.getElementById(`window-${id}`);
|
|
||||||
if (!windowEl) return;
|
|
||||||
|
|
||||||
if (windowObj.isMinimized) {
|
|
||||||
// Restore
|
|
||||||
windowEl.style.display = 'flex';
|
|
||||||
windowObj.isMinimized = false;
|
|
||||||
this.focus(id);
|
|
||||||
} else {
|
|
||||||
// Minimize
|
|
||||||
windowEl.style.display = 'none';
|
|
||||||
windowObj.isMinimized = true;
|
|
||||||
if (this.activeWindowId === id) {
|
|
||||||
this.activeWindowId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleMaximize(id) {
|
|
||||||
const windowObj = this.openWindows.find(w => w.id === id);
|
|
||||||
if (!windowObj) return;
|
|
||||||
|
|
||||||
const windowEl = document.getElementById(`window-${id}`);
|
|
||||||
if (!windowEl) return;
|
|
||||||
|
|
||||||
if (windowObj.isMaximized) {
|
|
||||||
// Restore
|
|
||||||
windowEl.style.width = windowObj.previousState.width;
|
|
||||||
windowEl.style.height = windowObj.previousState.height;
|
|
||||||
windowEl.style.top = windowObj.previousState.top;
|
|
||||||
windowEl.style.left = windowObj.previousState.left;
|
|
||||||
windowObj.isMaximized = false;
|
|
||||||
} else {
|
|
||||||
// Maximize
|
|
||||||
windowObj.previousState = {
|
|
||||||
width: windowEl.style.width,
|
|
||||||
height: windowEl.style.height,
|
|
||||||
top: windowEl.style.top,
|
|
||||||
left: windowEl.style.left
|
|
||||||
};
|
|
||||||
|
|
||||||
// Adjust for taskbar height (assuming taskbar is at bottom)
|
|
||||||
const taskbarHeight = document.getElementById('taskbar') ? document.getElementById('taskbar').offsetHeight : 0;
|
|
||||||
|
|
||||||
windowEl.style.width = '100%';
|
|
||||||
windowEl.style.height = `calc(100% - ${taskbarHeight}px)`;
|
|
||||||
windowEl.style.top = '0px';
|
|
||||||
windowEl.style.left = '0px';
|
|
||||||
windowObj.isMaximized = true;
|
|
||||||
}
|
|
||||||
this.focus(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
makeDraggable(windowEl) {
|
|
||||||
const header = windowEl.querySelector('.window-header');
|
|
||||||
if (!header) return;
|
|
||||||
|
|
||||||
let isDragging = false;
|
|
||||||
let startX, startY, initialLeft, initialTop;
|
|
||||||
|
|
||||||
const onMouseDown = (e) => {
|
|
||||||
// Don't drag if clicking buttons
|
|
||||||
if (e.target.tagName.toLowerCase() === 'button' || e.target.closest('button')) return;
|
|
||||||
|
|
||||||
isDragging = true;
|
|
||||||
startX = e.clientX;
|
|
||||||
startY = e.clientY;
|
|
||||||
initialLeft = parseInt(windowEl.style.left || 0, 10);
|
|
||||||
initialTop = parseInt(windowEl.style.top || 0, 10);
|
|
||||||
|
|
||||||
this.focus(windowEl.id.replace('window-', ''));
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
|
||||||
document.addEventListener('mouseup', onMouseUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseMove = (e) => {
|
|
||||||
if (!isDragging) return;
|
|
||||||
|
|
||||||
// Allow animation frame optimization here in a real implementation
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const dx = e.clientX - startX;
|
|
||||||
const dy = e.clientY - startY;
|
|
||||||
|
|
||||||
// Add basic boundaries
|
|
||||||
let newLeft = initialLeft + dx;
|
|
||||||
let newTop = initialTop + dy;
|
|
||||||
|
|
||||||
// Prevent dragging completely out
|
|
||||||
newTop = Math.max(0, newTop);
|
|
||||||
|
|
||||||
windowEl.style.left = `${newLeft}px`;
|
|
||||||
windowEl.style.top = `${newTop}px`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseUp = () => {
|
|
||||||
isDragging = false;
|
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
|
||||||
document.removeEventListener('mouseup', onMouseUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
header.addEventListener('mousedown', onMouseDown);
|
|
||||||
|
|
||||||
// Add focus listener to the whole window
|
|
||||||
windowEl.addEventListener('mousedown', () => {
|
|
||||||
this.focus(windowEl.id.replace('window-', ''));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
injectContentWithScripts(container, htmlContent) {
|
|
||||||
// Create a temporary div to parse the HTML
|
|
||||||
const tempDiv = document.createElement('div');
|
|
||||||
tempDiv.innerHTML = htmlContent;
|
|
||||||
|
|
||||||
// Extract all script tags
|
|
||||||
const scripts = tempDiv.querySelectorAll('script');
|
|
||||||
const scriptsToExecute = [];
|
|
||||||
|
|
||||||
scripts.forEach((originalScript) => {
|
|
||||||
const scriptClone = document.createElement('script');
|
|
||||||
Array.from(originalScript.attributes).forEach(attr => {
|
|
||||||
scriptClone.setAttribute(attr.name, attr.value);
|
|
||||||
});
|
|
||||||
scriptClone.textContent = originalScript.textContent;
|
|
||||||
scriptsToExecute.push(scriptClone);
|
|
||||||
originalScript.remove(); // Remove from tempDiv so innerHTML doesn't include it
|
|
||||||
});
|
|
||||||
|
|
||||||
// Inject HTML content without scripts
|
|
||||||
container.innerHTML = tempDiv.innerHTML;
|
|
||||||
|
|
||||||
// Execute each script
|
|
||||||
scriptsToExecute.forEach((script) => {
|
|
||||||
container.appendChild(script);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
makeResizable(windowEl) {
|
|
||||||
// Implement simple bottom-right resize for now
|
|
||||||
// In a full implementation, you'd add invisible handles
|
|
||||||
windowEl.style.resize = 'both';
|
|
||||||
// Note: CSS resize creates conflicts with custom dragging/resizing if not careful.
|
|
||||||
// For a true "WinBox" feel, custom handles (divs) on all 8 edges/corners are needed.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize globally
|
focus(id) {
|
||||||
window.WindowManager = new WindowManager();
|
this.activeWindowId = id;
|
||||||
}
|
const windowEl = document.getElementById(`window-${id}`);
|
||||||
|
if (windowEl) {
|
||||||
|
windowEl.style.zIndex = this.zIndexCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight taskbar icon
|
||||||
|
if (this.taskbarApps) {
|
||||||
|
const icons = this.taskbarApps.querySelectorAll(".taskbar-icon");
|
||||||
|
icons.forEach((icon) => {
|
||||||
|
icon.classList.add("border-transparent");
|
||||||
|
});
|
||||||
|
const activeIcon = document.getElementById(`taskbar-item-${id}`);
|
||||||
|
if (activeIcon) {
|
||||||
|
activeIcon.classList.remove("border-transparent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(id) {
|
||||||
|
const windowEl = document.getElementById(`window-${id}`);
|
||||||
|
if (windowEl) {
|
||||||
|
windowEl.remove();
|
||||||
|
}
|
||||||
|
const taskbarIcon = document.getElementById(`taskbar-item-${id}`);
|
||||||
|
if (taskbarIcon) {
|
||||||
|
taskbarIcon.remove();
|
||||||
|
}
|
||||||
|
this.openWindows = this.openWindows.filter((w) => w.id !== id);
|
||||||
|
if (this.activeWindowId === id) {
|
||||||
|
this.activeWindowId = null;
|
||||||
|
// Optionally focus the next highest z-index window
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMinimize(id) {
|
||||||
|
const windowObj = this.openWindows.find((w) => w.id === id);
|
||||||
|
if (!windowObj) return;
|
||||||
|
|
||||||
|
const windowEl = document.getElementById(`window-${id}`);
|
||||||
|
if (!windowEl) return;
|
||||||
|
|
||||||
|
if (windowObj.isMinimized) {
|
||||||
|
// Restore
|
||||||
|
windowEl.style.display = "flex";
|
||||||
|
windowObj.isMinimized = false;
|
||||||
|
this.focus(id);
|
||||||
|
} else {
|
||||||
|
// Minimize
|
||||||
|
windowEl.style.display = "none";
|
||||||
|
windowObj.isMinimized = true;
|
||||||
|
if (this.activeWindowId === id) {
|
||||||
|
this.activeWindowId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMaximize(id) {
|
||||||
|
const windowObj = this.openWindows.find((w) => w.id === id);
|
||||||
|
if (!windowObj) return;
|
||||||
|
|
||||||
|
const windowEl = document.getElementById(`window-${id}`);
|
||||||
|
if (!windowEl) return;
|
||||||
|
|
||||||
|
if (windowObj.isMaximized) {
|
||||||
|
// Restore
|
||||||
|
windowEl.style.width = windowObj.previousState.width;
|
||||||
|
windowEl.style.height = windowObj.previousState.height;
|
||||||
|
windowEl.style.top = windowObj.previousState.top;
|
||||||
|
windowEl.style.left = windowObj.previousState.left;
|
||||||
|
windowObj.isMaximized = false;
|
||||||
|
} else {
|
||||||
|
// Maximize
|
||||||
|
windowObj.previousState = {
|
||||||
|
width: windowEl.style.width,
|
||||||
|
height: windowEl.style.height,
|
||||||
|
top: windowEl.style.top,
|
||||||
|
left: windowEl.style.left,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adjust for taskbar height (assuming taskbar is at bottom)
|
||||||
|
const taskbarHeight = document.getElementById("taskbar")
|
||||||
|
? document.getElementById("taskbar").offsetHeight
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
windowEl.style.width = "100%";
|
||||||
|
windowEl.style.height = `calc(100% - ${taskbarHeight}px)`;
|
||||||
|
windowEl.style.top = "0px";
|
||||||
|
windowEl.style.left = "0px";
|
||||||
|
windowObj.isMaximized = true;
|
||||||
|
}
|
||||||
|
this.focus(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeDraggable(windowEl) {
|
||||||
|
const header = windowEl.querySelector(".window-header");
|
||||||
|
if (!header) return;
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
let startX, startY, initialLeft, initialTop;
|
||||||
|
|
||||||
|
const onMouseDown = (e) => {
|
||||||
|
// Don't drag if clicking buttons
|
||||||
|
if (
|
||||||
|
e.target.tagName.toLowerCase() === "button" ||
|
||||||
|
e.target.closest("button")
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
isDragging = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
initialLeft = parseInt(windowEl.style.left || 0, 10);
|
||||||
|
initialTop = parseInt(windowEl.style.top || 0, 10);
|
||||||
|
|
||||||
|
this.focus(windowEl.id.replace("window-", ""));
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", onMouseMove);
|
||||||
|
document.addEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
// Allow animation frame optimization here in a real implementation
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const dx = e.clientX - startX;
|
||||||
|
const dy = e.clientY - startY;
|
||||||
|
|
||||||
|
// Add basic boundaries
|
||||||
|
let newLeft = initialLeft + dx;
|
||||||
|
let newTop = initialTop + dy;
|
||||||
|
|
||||||
|
// Prevent dragging completely out
|
||||||
|
newTop = Math.max(0, newTop);
|
||||||
|
|
||||||
|
windowEl.style.left = `${newLeft}px`;
|
||||||
|
windowEl.style.top = `${newTop}px`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDragging = false;
|
||||||
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
|
document.removeEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
header.addEventListener("mousedown", onMouseDown);
|
||||||
|
|
||||||
|
// Add focus listener to the whole window
|
||||||
|
windowEl.addEventListener("mousedown", () => {
|
||||||
|
this.focus(windowEl.id.replace("window-", ""));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
injectContentWithScripts(container, htmlContent) {
|
||||||
|
// Create a temporary div to parse the HTML
|
||||||
|
const tempDiv = document.createElement("div");
|
||||||
|
tempDiv.innerHTML = htmlContent;
|
||||||
|
|
||||||
|
// Extract all script tags
|
||||||
|
const scripts = tempDiv.querySelectorAll("script");
|
||||||
|
const scriptsToExecute = [];
|
||||||
|
|
||||||
|
scripts.forEach((originalScript) => {
|
||||||
|
const scriptClone = document.createElement("script");
|
||||||
|
Array.from(originalScript.attributes).forEach((attr) => {
|
||||||
|
scriptClone.setAttribute(attr.name, attr.value);
|
||||||
|
});
|
||||||
|
scriptClone.textContent = originalScript.textContent;
|
||||||
|
scriptsToExecute.push(scriptClone);
|
||||||
|
originalScript.remove(); // Remove from tempDiv so innerHTML doesn't include it
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inject HTML content without scripts
|
||||||
|
container.innerHTML = tempDiv.innerHTML;
|
||||||
|
|
||||||
|
// Execute each script
|
||||||
|
scriptsToExecute.forEach((script) => {
|
||||||
|
container.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
makeResizable(windowEl) {
|
||||||
|
// Implement simple bottom-right resize for now
|
||||||
|
// In a full implementation, you'd add invisible handles
|
||||||
|
windowEl.style.resize = "both";
|
||||||
|
// Note: CSS resize creates conflicts with custom dragging/resizing if not careful.
|
||||||
|
// For a true "WinBox" feel, custom handles (divs) on all 8 edges/corners are needed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize globally
|
||||||
|
window.WindowManager = new WindowManager();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<header class="meet-header">
|
<header class="meet-header">
|
||||||
<div class="meet-brand">
|
<div class="meet-brand">
|
||||||
<img
|
<img
|
||||||
src="/assets/icons/gb-meet.svg"
|
src="/suite/assets/icons/gb-meet.svg"
|
||||||
alt="Meet"
|
alt="Meet"
|
||||||
class="brand-icon"
|
class="brand-icon"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1075,9 +1075,9 @@
|
||||||
var temp = document.createElement("div");
|
var temp = document.createElement("div");
|
||||||
temp.style.color = hexcolor;
|
temp.style.color = hexcolor;
|
||||||
temp.style.display = "none";
|
temp.style.display = "none";
|
||||||
document.div.appendChild(temp);
|
document.body.appendChild(temp);
|
||||||
var style = window.getComputedStyle(temp).color;
|
var style = window.getComputedStyle(temp).color;
|
||||||
document.div.removeChild(temp);
|
document.body.removeChild(temp);
|
||||||
|
|
||||||
var rgb = style.match(/\d+/g);
|
var rgb = style.match(/\d+/g);
|
||||||
if (!rgb || rgb.length < 3) return '#ffffff';
|
if (!rgb || rgb.length < 3) return '#ffffff';
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,79 @@
|
||||||
</svg>
|
</svg>
|
||||||
<span>Agent Farm</span>
|
<span>Agent Farm</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="minibar-actions">
|
<div class="minibar-actions" style="position: relative;">
|
||||||
<button class="minibar-btn" title="Settings" type="button">⚙</button>
|
<button class="minibar-btn" title="Settings" type="button">⚙</button>
|
||||||
<button class="minibar-btn" title="Account" type="button">👤</button>
|
<div class="user-menu-container" style="position: relative;">
|
||||||
|
<button class="minibar-btn" id="userAvatar" title="Account" type="button"
|
||||||
|
style="border-radius: 50%; width: 22px; height: 22px; background: #ddd; color: #555; font-size: 11px; font-weight: bold; overflow: hidden; display: flex; align-items: center; justify-content: center; margin-left: 4px;"><span>U</span></button>
|
||||||
|
<div id="userDropdown"
|
||||||
|
style="display: none; position: absolute; right: 0; top: 100%; margin-top: 4px; background: white; border: 1px solid #f0f1f2; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 220px; padding: 12px; z-index: 10000; flex-direction: column; gap: 8px;">
|
||||||
|
<div
|
||||||
|
style="display: flex; flex-direction: column; border-bottom: 1px solid #f0f1f2; padding-bottom: 8px;">
|
||||||
|
<span id="userName" style="font-weight: 600; color: #3b3b3b; font-size: 13px;">User</span>
|
||||||
|
<span id="userEmail" style="color: #888; font-size: 11px;">user@example.com</span>
|
||||||
|
</div>
|
||||||
|
<a id="authAction" href="/auth/login.html"
|
||||||
|
style="display: flex; align-items: center; gap: 6px; text-decoration: none; padding: 6px; border-radius: 4px; font-size: 12px; transition: background 0.15s; cursor: pointer; color: var(--primary, #84d669);">
|
||||||
|
<svg id="authIcon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
|
||||||
|
<polyline points="10 17 15 12 21 12"></polyline>
|
||||||
|
<line x1="15" y1="12" x2="3" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
<span id="authText">Sign in</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var avatar = document.getElementById('userAvatar');
|
||||||
|
var dropdown = document.getElementById('userDropdown');
|
||||||
|
|
||||||
|
avatar.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
dropdown.style.display = dropdown.style.display === 'none' ? 'flex' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (!e.target.closest('.user-menu-container')) {
|
||||||
|
dropdown.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger manual load or UI sync since minibar loads asynchronously via HTMX
|
||||||
|
setTimeout(function () {
|
||||||
|
var token = localStorage.getItem("gb-access-token") || sessionStorage.getItem("gb-access-token");
|
||||||
|
if (token) {
|
||||||
|
fetch("/api/auth/me", { headers: { Authorization: "Bearer " + token } })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(user => {
|
||||||
|
document.getElementById('userName').textContent = user.display_name || user.first_name || user.username || "User";
|
||||||
|
document.getElementById('userEmail').textContent = user.email || "";
|
||||||
|
|
||||||
|
var btn = document.getElementById('authAction');
|
||||||
|
btn.href = "#";
|
||||||
|
btn.style.color = "var(--error, #ef4444)";
|
||||||
|
document.getElementById('authText').textContent = "Sign out";
|
||||||
|
document.getElementById('authIcon').innerHTML = '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>';
|
||||||
|
|
||||||
|
btn.onclick = function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
fetch("/api/auth/logout", { method: "POST" }).finally(function () {
|
||||||
|
localStorage.removeItem("gb-access-token");
|
||||||
|
sessionStorage.removeItem("gb-access-token");
|
||||||
|
window.location.href = "/auth/login.html";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var initial = (user.display_name || user.username || "U").charAt(0).toUpperCase();
|
||||||
|
document.getElementById('userAvatar').innerHTML = '<span>' + initial + '</span>';
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue