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 {
|
||||
let path = uri.path();
|
||||
|
||||
|
||||
// Check if path contains static asset directories - serve them directly
|
||||
let path_lower = path.to_lowercase();
|
||||
if path_lower.contains("/js/")
|
||||
if path_lower.contains("/js/")
|
||||
|| path_lower.contains("/css/")
|
||||
|| path_lower.contains("/vendor/")
|
||||
|| path_lower.contains("/assets/")
|
||||
|| path_lower.contains("/public/")
|
||||
|| path_lower.contains("/partials/")
|
||||
|| path_lower.ends_with(".js")
|
||||
|| path_lower.ends_with(".css")
|
||||
|| path_lower.ends_with(".png")
|
||||
|| path_lower.ends_with(".jpg")
|
||||
|| path_lower.ends_with(".jpeg")
|
||||
|| path_lower.ends_with(".gif")
|
||||
|| path_lower.ends_with(".svg")
|
||||
|| path_lower.ends_with(".ico")
|
||||
|| path_lower.ends_with(".woff")
|
||||
|| path_lower.ends_with(".woff2")
|
||||
|| path_lower.ends_with(".ttf")
|
||||
|| path_lower.ends_with(".js")
|
||||
|| path_lower.ends_with(".css")
|
||||
|| path_lower.ends_with(".png")
|
||||
|| path_lower.ends_with(".jpg")
|
||||
|| path_lower.ends_with(".jpeg")
|
||||
|| path_lower.ends_with(".gif")
|
||||
|| path_lower.ends_with(".svg")
|
||||
|| path_lower.ends_with(".ico")
|
||||
|| path_lower.ends_with(".woff")
|
||||
|| path_lower.ends_with(".woff2")
|
||||
|| path_lower.ends_with(".ttf")
|
||||
|| path_lower.ends_with(".eot")
|
||||
|| path_lower.ends_with(".mp4")
|
||||
|| path_lower.ends_with(".mp4")
|
||||
|| path_lower.ends_with(".webm")
|
||||
|| path_lower.ends_with(".mp3")
|
||||
|| path_lower.ends_with(".wav")
|
||||
|
|
@ -182,11 +182,11 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
|||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
|
||||
|
||||
let full_path = get_ui_root().join(&fs_path);
|
||||
|
||||
|
||||
info!("index: Serving static file: {} -> {:?} (fs_path: {})", path, full_path, fs_path);
|
||||
|
||||
|
||||
#[cfg(feature = "embed-ui")]
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(feature = "embed-ui"))]
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
warn!("index: Static file not found: {} -> {:?}", path, full_path);
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
|
||||
|
||||
let path_parts: Vec<&str> = path.split('/').collect();
|
||||
let bot_name = path_parts
|
||||
.iter()
|
||||
|
|
@ -770,20 +770,20 @@ struct ClientError {
|
|||
|
||||
async fn handle_client_error(Json(error): Json<ClientError>) -> impl IntoResponse {
|
||||
warn!(
|
||||
"CLIENT:{}: {} at {} ({}) - {}",
|
||||
"CLIENT:{}: {} at {} ({}) - {}",
|
||||
error.source.to_uppercase(),
|
||||
error.message,
|
||||
error.url,
|
||||
error.timestamp,
|
||||
error.user_agent
|
||||
);
|
||||
|
||||
|
||||
if let Some(stack) = &error.stack {
|
||||
if !stack.is_empty() {
|
||||
warn!("CLIENT:STACK: {}", stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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> {
|
||||
#[cfg(feature = "embed-ui")]
|
||||
{
|
||||
|
|
@ -1219,6 +1289,8 @@ pub fn configure_router() -> Router {
|
|||
let mut router = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/favicon.ico", get(serve_favicon))
|
||||
.route("/login", get(serve_login))
|
||||
.route("/logout", get(serve_logout))
|
||||
.nest("/api", create_api_router())
|
||||
.nest("/ui", create_ui_router())
|
||||
.nest("/ws", create_ws_router())
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@
|
|||
<script>
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
serverUrl: window.BOTSERVER_URL || 'http://localhost:9000',
|
||||
serverUrl: window.BOTSERVER_URL || 'http://localhost:8080',
|
||||
maxMessages: 10, // Keep memory low
|
||||
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-item">
|
||||
<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 class="activity-content">
|
||||
<div class="activity-text">
|
||||
|
|
@ -198,7 +198,7 @@
|
|||
</div>
|
||||
<div class="activity-item">
|
||||
<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 class="activity-content">
|
||||
<div class="activity-text">
|
||||
|
|
@ -222,7 +222,7 @@
|
|||
</div>
|
||||
<div class="activity-item">
|
||||
<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 class="activity-content">
|
||||
<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-item">
|
||||
<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>
|
||||
</div>
|
||||
<div class="member-info">
|
||||
|
|
@ -285,7 +285,7 @@
|
|||
</div>
|
||||
<div class="member-item">
|
||||
<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>
|
||||
</div>
|
||||
<div class="member-info">
|
||||
|
|
@ -296,7 +296,7 @@
|
|||
</div>
|
||||
<div class="member-item">
|
||||
<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>
|
||||
</div>
|
||||
<div class="member-info">
|
||||
|
|
@ -307,7 +307,7 @@
|
|||
</div>
|
||||
<div class="member-item">
|
||||
<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>
|
||||
</div>
|
||||
<div class="member-info">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="wizard-container">
|
||||
<div class="wizard-sidebar">
|
||||
<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 class="wizard-steps">
|
||||
<div class="wizard-step active" data-step="1">
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@
|
|||
<body>
|
||||
<div class="setup-container">
|
||||
<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>
|
||||
<p class="setup-subtitle">
|
||||
Create the first administrator account for your General Bots installation
|
||||
|
|
|
|||
|
|
@ -599,7 +599,7 @@
|
|||
<div class="login-header">
|
||||
<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;">
|
||||
<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>
|
||||
<h1 class="login-title" id="login-title">Welcome Back</h1>
|
||||
<p class="login-subtitle" id="login-subtitle">
|
||||
|
|
|
|||
|
|
@ -557,7 +557,7 @@
|
|||
<body>
|
||||
<div class="register-container">
|
||||
<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>
|
||||
<p class="register-subtitle">
|
||||
Join General Bots and start building
|
||||
|
|
|
|||
|
|
@ -1,50 +1,53 @@
|
|||
if (typeof window.WindowManager === 'undefined') {
|
||||
class WindowManager {
|
||||
constructor() {
|
||||
this.openWindows = [];
|
||||
this.activeWindowId = null;
|
||||
this.zIndexCounter = 100;
|
||||
// Will fetch dynamically in open() since script runs before DOM is ready
|
||||
this.workspace = null;
|
||||
this.taskbarApps = null;
|
||||
}
|
||||
if (typeof window.WindowManager === "undefined") {
|
||||
class WindowManager {
|
||||
constructor() {
|
||||
this.openWindows = [];
|
||||
this.activeWindowId = null;
|
||||
this.zIndexCounter = 100;
|
||||
// Will fetch dynamically in open() since script runs before DOM is ready
|
||||
this.workspace = null;
|
||||
this.taskbarApps = null;
|
||||
}
|
||||
|
||||
open(id, title, htmlContent) {
|
||||
// Lazy load the container elements to avoid head script loading issues
|
||||
if (!this.workspace) this.workspace = document.getElementById('desktop-content') || document.body;
|
||||
if (!this.taskbarApps) this.taskbarApps = document.getElementById('taskbar-apps');
|
||||
open(id, title, htmlContent) {
|
||||
// Lazy load the container elements to avoid head script loading issues
|
||||
if (!this.workspace)
|
||||
this.workspace =
|
||||
document.getElementById("desktop-content") || document.body;
|
||||
if (!this.taskbarApps)
|
||||
this.taskbarApps = document.getElementById("taskbar-apps");
|
||||
|
||||
// If window already exists, focus it
|
||||
const existingWindow = this.openWindows.find(w => w.id === id);
|
||||
if (existingWindow) {
|
||||
this.focus(id);
|
||||
return;
|
||||
}
|
||||
// If window already exists, focus it
|
||||
const existingWindow = this.openWindows.find((w) => w.id === id);
|
||||
if (existingWindow) {
|
||||
this.focus(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new window
|
||||
const windowData = {
|
||||
id,
|
||||
title,
|
||||
isMinimized: false,
|
||||
isMaximized: false,
|
||||
previousState: null
|
||||
};
|
||||
this.openWindows.push(windowData);
|
||||
// Create new window
|
||||
const windowData = {
|
||||
id,
|
||||
title,
|
||||
isMinimized: false,
|
||||
isMaximized: false,
|
||||
previousState: null,
|
||||
};
|
||||
this.openWindows.push(windowData);
|
||||
|
||||
// Generate DOM structure
|
||||
const windowEl = document.createElement('div');
|
||||
windowEl.id = `window-${id}`;
|
||||
// Add random slight offset for cascade effect
|
||||
const offset = (this.openWindows.length * 20) % 100;
|
||||
const top = 100 + offset;
|
||||
const left = 150 + offset;
|
||||
// Generate DOM structure
|
||||
const windowEl = document.createElement("div");
|
||||
windowEl.id = `window-${id}`;
|
||||
// Add random slight offset for cascade effect
|
||||
const offset = (this.openWindows.length * 20) % 100;
|
||||
const top = 100 + offset;
|
||||
const left = 150 + offset;
|
||||
|
||||
windowEl.className = 'window-element';
|
||||
windowEl.style.top = `${top}px`;
|
||||
windowEl.style.left = `${left}px`;
|
||||
windowEl.style.zIndex = this.zIndexCounter++;
|
||||
windowEl.className = "window-element";
|
||||
windowEl.style.top = `${top}px`;
|
||||
windowEl.style.left = `${left}px`;
|
||||
windowEl.style.zIndex = this.zIndexCounter++;
|
||||
|
||||
windowEl.innerHTML = `
|
||||
windowEl.innerHTML = `
|
||||
<!-- Header (Draggable) -->
|
||||
<div class="window-header">
|
||||
<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>
|
||||
`;
|
||||
|
||||
this.workspace.appendChild(windowEl);
|
||||
this.workspace.appendChild(windowEl);
|
||||
|
||||
// Inject content into the window body
|
||||
const windowBody = windowEl.querySelector(`#window-body-${id}`);
|
||||
if (windowBody) {
|
||||
this.injectContentWithScripts(windowBody, htmlContent);
|
||||
}
|
||||
// Inject content into the window body
|
||||
const windowBody = windowEl.querySelector(`#window-body-${id}`);
|
||||
if (windowBody) {
|
||||
this.injectContentWithScripts(windowBody, htmlContent);
|
||||
}
|
||||
|
||||
// Add to taskbar
|
||||
if (this.taskbarApps) {
|
||||
const taskbarIcon = document.createElement('div');
|
||||
taskbarIcon.id = `taskbar-item-${id}`;
|
||||
taskbarIcon.className = 'taskbar-item taskbar-icon';
|
||||
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>';
|
||||
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>';
|
||||
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>';
|
||||
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>';
|
||||
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 = `
|
||||
// Add to taskbar
|
||||
if (this.taskbarApps) {
|
||||
const taskbarIcon = document.createElement("div");
|
||||
taskbarIcon.id = `taskbar-item-${id}`;
|
||||
taskbarIcon.className = "taskbar-item taskbar-icon";
|
||||
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>';
|
||||
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>';
|
||||
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>';
|
||||
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>';
|
||||
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);">
|
||||
${iconHtml}
|
||||
</div>
|
||||
`;
|
||||
this.taskbarApps.appendChild(taskbarIcon);
|
||||
}
|
||||
this.taskbarApps.appendChild(taskbarIcon);
|
||||
}
|
||||
|
||||
this.makeDraggable(windowEl);
|
||||
this.makeResizable(windowEl);
|
||||
this.focus(id);
|
||||
this.makeDraggable(windowEl);
|
||||
this.makeResizable(windowEl);
|
||||
this.focus(id);
|
||||
|
||||
// Tell HTMX to process the new content
|
||||
if (window.htmx) {
|
||||
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.
|
||||
}
|
||||
// Tell HTMX to process the new content
|
||||
if (window.htmx) {
|
||||
htmx.process(windowEl);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize globally
|
||||
window.WindowManager = new WindowManager();
|
||||
}
|
||||
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.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">
|
||||
<div class="meet-brand">
|
||||
<img
|
||||
src="/assets/icons/gb-meet.svg"
|
||||
src="/suite/assets/icons/gb-meet.svg"
|
||||
alt="Meet"
|
||||
class="brand-icon"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1075,9 +1075,9 @@
|
|||
var temp = document.createElement("div");
|
||||
temp.style.color = hexcolor;
|
||||
temp.style.display = "none";
|
||||
document.div.appendChild(temp);
|
||||
document.body.appendChild(temp);
|
||||
var style = window.getComputedStyle(temp).color;
|
||||
document.div.removeChild(temp);
|
||||
document.body.removeChild(temp);
|
||||
|
||||
var rgb = style.match(/\d+/g);
|
||||
if (!rgb || rgb.length < 3) return '#ffffff';
|
||||
|
|
|
|||
|
|
@ -66,8 +66,79 @@
|
|||
</svg>
|
||||
<span>Agent Farm</span>
|
||||
</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="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>
|
||||
|
||||
<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