Compare commits
10 commits
08469ecbf6
...
5657e33006
| Author | SHA1 | Date | |
|---|---|---|---|
| 5657e33006 | |||
| fee8b3d2a3 | |||
| 74a2122827 | |||
| 97b96faa89 | |||
| e3219d8a4f | |||
| 55947a20b9 | |||
| 1435d1016f | |||
| e32e9b793a | |||
| eb785b9a69 | |||
| e3b5929b99 |
60 changed files with 13958 additions and 8586 deletions
83
Cargo.toml
83
Cargo.toml
|
|
@ -9,47 +9,60 @@ keywords = ["chatbot", "ui", "web-interface", "general-bots"]
|
|||
categories = ["web-programming", "gui"]
|
||||
|
||||
[dependencies.botlib]
|
||||
path = "../botlib"
|
||||
workspace = true
|
||||
features = ["http-client"]
|
||||
|
||||
[features]
|
||||
default = ["ui-server"]
|
||||
default = ["ui-server", "chat", "drive", "tasks"]
|
||||
ui-server = []
|
||||
|
||||
# App Features
|
||||
chat = []
|
||||
mail = []
|
||||
calendar = []
|
||||
drive = []
|
||||
tasks = []
|
||||
docs = []
|
||||
paper = []
|
||||
sheet = []
|
||||
slides = []
|
||||
meet = []
|
||||
research = []
|
||||
analytics = []
|
||||
monitoring = []
|
||||
admin = []
|
||||
settings = []
|
||||
sources = []
|
||||
attendant = []
|
||||
tools = []
|
||||
video = []
|
||||
learn = []
|
||||
social = []
|
||||
dashboards = []
|
||||
designer = []
|
||||
workspace = []
|
||||
project = []
|
||||
goals = []
|
||||
player = []
|
||||
canvas = []
|
||||
people = []
|
||||
billing = []
|
||||
products = []
|
||||
editor = []
|
||||
tickets = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
axum = { version = "0.7.5", features = ["ws", "multipart", "macros"] }
|
||||
base64 = "0.22"
|
||||
bytes = "1.8"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
diesel = { version = "2.1", features = ["sqlite"] }
|
||||
dirs = "5.0"
|
||||
env_logger = "0.11"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
hostname = "0.4"
|
||||
jsonwebtoken = "9.3"
|
||||
local-ip-address = "0.6.5"
|
||||
log = "0.4"
|
||||
mime_guess = "2.0"
|
||||
native-tls = "0.2"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
time = "0.3"
|
||||
tokio = { version = "1.41", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["cors", "fs", "trace"] }
|
||||
tower-cookies = "0.10"
|
||||
tracing = "0.1"
|
||||
urlencoding = "2.1"
|
||||
uuid = { version = "1.11", features = ["serde", "v4"] }
|
||||
webbrowser = "0.8"
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
log = { workspace = true }
|
||||
native-tls = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-tungstenite = { workspace = true, features = ["native-tls", "connect"] }
|
||||
tower-http = { workspace = true, features = ["cors", "fs", "trace"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use log::{debug, error, info};
|
|||
use serde::Deserialize;
|
||||
use std::{fs, path::Path, path::PathBuf};
|
||||
use tokio_tungstenite::{
|
||||
connect_async_tls_with_config, tungstenite::protocol::Message as TungsteniteMessage,
|
||||
connect_async_tls_with_config, tungstenite, tungstenite::protocol::Message as TungsteniteMessage,
|
||||
};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
|
|
@ -25,37 +25,94 @@ const SUITE_DIRS: &[&str] = &[
|
|||
"js",
|
||||
"css",
|
||||
"public",
|
||||
"drive",
|
||||
"chat",
|
||||
"mail",
|
||||
"tasks",
|
||||
"calendar",
|
||||
"meet",
|
||||
"paper",
|
||||
"sheet",
|
||||
"slides",
|
||||
"docs",
|
||||
"research",
|
||||
"analytics",
|
||||
"monitoring",
|
||||
"admin",
|
||||
"auth",
|
||||
"settings",
|
||||
"sources",
|
||||
"attendant",
|
||||
"tools",
|
||||
"assets",
|
||||
"partials",
|
||||
"video",
|
||||
// Core & Support
|
||||
"settings",
|
||||
"auth",
|
||||
"about",
|
||||
|
||||
// Core Apps
|
||||
#[cfg(feature = "drive")]
|
||||
"drive",
|
||||
#[cfg(feature = "chat")]
|
||||
"chat",
|
||||
#[cfg(feature = "mail")]
|
||||
"mail",
|
||||
#[cfg(feature = "tasks")]
|
||||
"tasks",
|
||||
#[cfg(feature = "calendar")]
|
||||
"calendar",
|
||||
#[cfg(feature = "meet")]
|
||||
"meet",
|
||||
|
||||
// Document Apps
|
||||
#[cfg(feature = "paper")]
|
||||
"paper",
|
||||
#[cfg(feature = "sheet")]
|
||||
"sheet",
|
||||
#[cfg(feature = "slides")]
|
||||
"slides",
|
||||
#[cfg(feature = "docs")]
|
||||
"docs",
|
||||
|
||||
// Research & Learning
|
||||
#[cfg(feature = "research")]
|
||||
"research",
|
||||
#[cfg(feature = "sources")]
|
||||
"sources",
|
||||
#[cfg(feature = "learn")]
|
||||
"learn",
|
||||
"social",
|
||||
|
||||
// Analytics
|
||||
#[cfg(feature = "analytics")]
|
||||
"analytics",
|
||||
#[cfg(feature = "dashboards")]
|
||||
"dashboards",
|
||||
"designer",
|
||||
"workspace",
|
||||
"project",
|
||||
"goals",
|
||||
#[cfg(feature = "monitoring")]
|
||||
"monitoring",
|
||||
|
||||
// Admin & Tools
|
||||
#[cfg(feature = "admin")]
|
||||
"admin",
|
||||
#[cfg(feature = "attendant")]
|
||||
"attendant",
|
||||
#[cfg(feature = "tools")]
|
||||
"tools",
|
||||
|
||||
// Media
|
||||
#[cfg(feature = "video")]
|
||||
"video",
|
||||
#[cfg(feature = "player")]
|
||||
"player",
|
||||
#[cfg(feature = "canvas")]
|
||||
"canvas",
|
||||
|
||||
// Social
|
||||
#[cfg(feature = "social")]
|
||||
"social",
|
||||
#[cfg(feature = "people")]
|
||||
"people",
|
||||
#[cfg(feature = "people")]
|
||||
"crm",
|
||||
#[cfg(feature = "tickets")]
|
||||
"tickets",
|
||||
|
||||
// Business
|
||||
#[cfg(feature = "billing")]
|
||||
"billing",
|
||||
#[cfg(feature = "products")]
|
||||
"products",
|
||||
|
||||
// Development
|
||||
#[cfg(feature = "designer")]
|
||||
"designer",
|
||||
#[cfg(feature = "workspace")]
|
||||
"workspace",
|
||||
#[cfg(feature = "project")]
|
||||
"project",
|
||||
#[cfg(feature = "goals")]
|
||||
"goals",
|
||||
];
|
||||
|
||||
pub async fn index() -> impl IntoResponse {
|
||||
|
|
@ -78,7 +135,69 @@ pub async fn serve_minimal() -> impl IntoResponse {
|
|||
|
||||
pub async fn serve_suite() -> impl IntoResponse {
|
||||
match fs::read_to_string("ui/suite/index.html") {
|
||||
Ok(html) => (StatusCode::OK, [("content-type", "text/html")], Html(html)),
|
||||
Ok(raw_html) => {
|
||||
#[allow(unused_mut)] // Mutable required for feature-gated blocks
|
||||
let mut html = raw_html;
|
||||
|
||||
// Core Apps
|
||||
#[cfg(not(feature = "chat"))] { html = remove_section(&html, "chat"); }
|
||||
#[cfg(not(feature = "mail"))] { html = remove_section(&html, "mail"); }
|
||||
#[cfg(not(feature = "calendar"))] { html = remove_section(&html, "calendar"); }
|
||||
#[cfg(not(feature = "drive"))] { html = remove_section(&html, "drive"); }
|
||||
#[cfg(not(feature = "tasks"))] { html = remove_section(&html, "tasks"); }
|
||||
#[cfg(not(feature = "meet"))] { html = remove_section(&html, "meet"); }
|
||||
|
||||
// Documents
|
||||
#[cfg(not(feature = "docs"))] { html = remove_section(&html, "docs"); }
|
||||
#[cfg(not(feature = "sheet"))] { html = remove_section(&html, "sheet"); }
|
||||
#[cfg(not(feature = "slides"))] { html = remove_section(&html, "slides"); }
|
||||
#[cfg(not(feature = "paper"))] { html = remove_section(&html, "paper"); }
|
||||
|
||||
// Research
|
||||
#[cfg(not(feature = "research"))] { html = remove_section(&html, "research"); }
|
||||
#[cfg(not(feature = "sources"))] { html = remove_section(&html, "sources"); }
|
||||
#[cfg(not(feature = "learn"))] { html = remove_section(&html, "learn"); }
|
||||
|
||||
// Analytics
|
||||
#[cfg(not(feature = "analytics"))] { html = remove_section(&html, "analytics"); }
|
||||
#[cfg(not(feature = "dashboards"))] { html = remove_section(&html, "dashboards"); }
|
||||
#[cfg(not(feature = "monitoring"))] { html = remove_section(&html, "monitoring"); }
|
||||
|
||||
// Business
|
||||
#[cfg(not(feature = "people"))] {
|
||||
html = remove_section(&html, "people");
|
||||
html = remove_section(&html, "crm");
|
||||
}
|
||||
#[cfg(not(feature = "billing"))] { html = remove_section(&html, "billing"); }
|
||||
#[cfg(not(feature = "products"))] { html = remove_section(&html, "products"); }
|
||||
#[cfg(not(feature = "tickets"))] { html = remove_section(&html, "tickets"); }
|
||||
|
||||
// Media
|
||||
#[cfg(not(feature = "video"))] { html = remove_section(&html, "video"); }
|
||||
#[cfg(not(feature = "player"))] { html = remove_section(&html, "player"); }
|
||||
#[cfg(not(feature = "canvas"))] { html = remove_section(&html, "canvas"); }
|
||||
|
||||
// Social & Project
|
||||
#[cfg(not(feature = "social"))] { html = remove_section(&html, "social"); }
|
||||
#[cfg(not(feature = "project"))] { html = remove_section(&html, "project"); }
|
||||
#[cfg(not(feature = "goals"))] { html = remove_section(&html, "goals"); }
|
||||
#[cfg(not(feature = "workspace"))] { html = remove_section(&html, "workspace"); }
|
||||
|
||||
// Admin/Tools
|
||||
#[cfg(not(feature = "admin"))] {
|
||||
html = remove_section(&html, "admin");
|
||||
}
|
||||
// Mapped security to tools feature
|
||||
#[cfg(not(feature = "tools"))] {
|
||||
html = remove_section(&html, "security");
|
||||
}
|
||||
#[cfg(not(feature = "attendant"))] { html = remove_section(&html, "attendant"); }
|
||||
#[cfg(not(feature = "designer"))] { html = remove_section(&html, "designer"); }
|
||||
#[cfg(not(feature = "editor"))] { html = remove_section(&html, "editor"); }
|
||||
#[cfg(not(feature = "settings"))] { html = remove_section(&html, "settings"); }
|
||||
|
||||
(StatusCode::OK, [("content-type", "text/html")], Html(html))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to load suite UI: {e}");
|
||||
(
|
||||
|
|
@ -90,6 +209,38 @@ pub async fn serve_suite() -> impl IntoResponse {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn remove_section(html: &str, section: &str) -> String {
|
||||
let start_marker = format!("<!-- SECTION:{} -->", section);
|
||||
let end_marker = format!("<!-- ENDSECTION:{} -->", section);
|
||||
|
||||
let mut result = String::with_capacity(html.len());
|
||||
let mut current_pos = 0;
|
||||
|
||||
// Process multiple occurrences of the section
|
||||
while let Some(start_idx) = html[current_pos..].find(&start_marker) {
|
||||
let abs_start = current_pos + start_idx;
|
||||
// Append content up to the marker
|
||||
result.push_str(&html[current_pos..abs_start]);
|
||||
|
||||
// Find end marker
|
||||
if let Some(end_idx) = html[abs_start..].find(&end_marker) {
|
||||
// Skip past the end marker
|
||||
current_pos = abs_start + end_idx + end_marker.len();
|
||||
} else {
|
||||
// No end marker? This shouldn't happen with our script,
|
||||
// but if it does, just skip the start marker and continue
|
||||
// or consume everything?
|
||||
// Safety: Skip start marker only
|
||||
current_pos = abs_start + start_marker.len();
|
||||
}
|
||||
}
|
||||
|
||||
// Append remaining content
|
||||
result.push_str(&html[current_pos..]);
|
||||
result
|
||||
}
|
||||
|
||||
async fn health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_json::Value>) {
|
||||
if state.health_check().await {
|
||||
(
|
||||
|
|
@ -312,7 +463,7 @@ async fn handle_task_progress_ws_proxy(
|
|||
let backend_result =
|
||||
connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await;
|
||||
|
||||
let backend_socket = match backend_result {
|
||||
let backend_socket: tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> = match backend_result {
|
||||
Ok((socket, _)) => socket,
|
||||
Err(e) => {
|
||||
error!("Failed to connect to backend task-progress WebSocket: {e}");
|
||||
|
|
@ -329,38 +480,34 @@ async fn handle_task_progress_ws_proxy(
|
|||
while let Some(msg) = client_rx.next().await {
|
||||
match msg {
|
||||
Ok(AxumMessage::Text(text)) => {
|
||||
if backend_tx
|
||||
let res: Result<(), tungstenite::Error> = backend_tx
|
||||
.send(TungsteniteMessage::Text(text))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
.await;
|
||||
if res.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(AxumMessage::Binary(data)) => {
|
||||
if backend_tx
|
||||
let res: Result<(), tungstenite::Error> = backend_tx
|
||||
.send(TungsteniteMessage::Binary(data))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
.await;
|
||||
if res.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(AxumMessage::Ping(data)) => {
|
||||
if backend_tx
|
||||
let res: Result<(), tungstenite::Error> = backend_tx
|
||||
.send(TungsteniteMessage::Ping(data))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
.await;
|
||||
if res.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(AxumMessage::Pong(data)) => {
|
||||
if backend_tx
|
||||
let res: Result<(), tungstenite::Error> = backend_tx
|
||||
.send(TungsteniteMessage::Pong(data))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
.await;
|
||||
if res.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -370,7 +517,7 @@ async fn handle_task_progress_ws_proxy(
|
|||
};
|
||||
|
||||
let backend_to_client = async {
|
||||
while let Some(msg) = backend_rx.next().await {
|
||||
while let Some(msg) = backend_rx.next().await as Option<Result<TungsteniteMessage, tungstenite::Error>> {
|
||||
match msg {
|
||||
Ok(TungsteniteMessage::Text(text)) => {
|
||||
// Log manifest_update messages for debugging
|
||||
|
|
@ -448,7 +595,7 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
|
|||
let backend_result =
|
||||
connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await;
|
||||
|
||||
let backend_socket = match backend_result {
|
||||
let backend_socket: tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> = match backend_result {
|
||||
Ok((socket, _)) => socket,
|
||||
Err(e) => {
|
||||
error!("Failed to connect to backend WebSocket: {e}");
|
||||
|
|
@ -465,38 +612,34 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
|
|||
while let Some(msg) = client_rx.next().await {
|
||||
match msg {
|
||||
Ok(AxumMessage::Text(text)) => {
|
||||
if backend_tx
|
||||
let res: Result<(), tungstenite::Error> = backend_tx
|
||||
.send(TungsteniteMessage::Text(text))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
.await;
|
||||
if res.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(AxumMessage::Binary(data)) => {
|
||||
if backend_tx
|
||||
let res: Result<(), tungstenite::Error> = backend_tx
|
||||
.send(TungsteniteMessage::Binary(data))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
.await;
|
||||
if res.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(AxumMessage::Ping(data)) => {
|
||||
if backend_tx
|
||||
let res: Result<(), tungstenite::Error> = backend_tx
|
||||
.send(TungsteniteMessage::Ping(data))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
.await;
|
||||
if res.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(AxumMessage::Pong(data)) => {
|
||||
if backend_tx
|
||||
let res: Result<(), tungstenite::Error> = backend_tx
|
||||
.send(TungsteniteMessage::Pong(data))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
.await;
|
||||
if res.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -598,9 +741,6 @@ pub fn configure_router() -> Router {
|
|||
router = add_static_routes(router, &suite_path);
|
||||
|
||||
router
|
||||
.fallback_service(
|
||||
ServeDir::new(suite_path.clone())
|
||||
.fallback(ServeDir::new(suite_path).append_index_html_on_directories(true)),
|
||||
)
|
||||
.fallback(get(index))
|
||||
.with_state(state)
|
||||
}
|
||||
|
|
|
|||
2
ui/suite/about/about.css
Normal file
2
ui/suite/about/about.css
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/* About page styles - most styles are inline in about.html */
|
||||
/* This file is for additional customization if needed */
|
||||
571
ui/suite/about/about.html
Normal file
571
ui/suite/about/about.html
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
<script>
|
||||
CSSLoader.loadMultiple(['about.css']);
|
||||
|
||||
(async function loadAboutData() {
|
||||
try {
|
||||
const response = await fetch('/api/product');
|
||||
const config = await response.json();
|
||||
|
||||
// Update version info
|
||||
const versionEl = document.getElementById('about-version');
|
||||
if (versionEl) versionEl.textContent = config.version || '6.1.0';
|
||||
|
||||
// Update product name
|
||||
const nameEl = document.getElementById('about-product-name');
|
||||
if (nameEl) nameEl.textContent = config.name || 'General Bots';
|
||||
|
||||
// Populate compiled features
|
||||
const compiledContainer = document.getElementById('compiled-features');
|
||||
if (compiledContainer && config.compiled_features) {
|
||||
compiledContainer.innerHTML = config.compiled_features
|
||||
.sort()
|
||||
.map(f => `<span class="feature-badge enabled">${f}</span>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// Populate enabled apps
|
||||
const enabledContainer = document.getElementById('enabled-apps');
|
||||
if (enabledContainer && config.apps) {
|
||||
enabledContainer.innerHTML = config.apps
|
||||
.sort()
|
||||
.map(a => `<span class="feature-badge active">${a}</span>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// Calculate disabled features
|
||||
const disabledContainer = document.getElementById('disabled-features');
|
||||
if (disabledContainer && config.compiled_features && config.apps) {
|
||||
const compiled = new Set(config.compiled_features);
|
||||
const enabled = new Set(config.apps.map(a => a.toLowerCase()));
|
||||
const disabled = [...compiled].filter(f => !enabled.has(f.toLowerCase()));
|
||||
|
||||
if (disabled.length > 0) {
|
||||
disabledContainer.innerHTML = disabled
|
||||
.sort()
|
||||
.map(f => `<span class="feature-badge disabled">${f}</span>`)
|
||||
.join('');
|
||||
} else {
|
||||
disabledContainer.innerHTML = '<span class="text-muted">All compiled features are enabled</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Update copyright
|
||||
const copyrightEl = document.getElementById('about-copyright');
|
||||
if (copyrightEl) copyrightEl.textContent = config.copyright || '© 2026 General Bots. All rights reserved.';
|
||||
|
||||
} catch (e) {
|
||||
console.warn('Failed to load product config:', e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div class="about-container">
|
||||
<!-- Header with Logo and Branding -->
|
||||
<header class="about-header">
|
||||
<div class="about-logo">
|
||||
<svg class="logo-svg" width="80" height="50" viewBox="0 0 140 80" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Left antenna -->
|
||||
<line x1="5" y1="40" x2="25" y2="40"/>
|
||||
<line x1="25" y1="28" x2="25" y2="52"/>
|
||||
<!-- Left gear -->
|
||||
<circle cx="50" cy="40" r="16"/>
|
||||
<circle cx="50" cy="40" r="6"/>
|
||||
<line x1="50" y1="20" x2="50" y2="26"/>
|
||||
<line x1="50" y1="54" x2="50" y2="60"/>
|
||||
<line x1="30" y1="40" x2="36" y2="40"/>
|
||||
<line x1="64" y1="40" x2="70" y2="40"/>
|
||||
<!-- Right gear -->
|
||||
<circle cx="90" cy="40" r="16"/>
|
||||
<circle cx="90" cy="40" r="6"/>
|
||||
<line x1="90" y1="20" x2="90" y2="26"/>
|
||||
<line x1="90" y1="54" x2="90" y2="60"/>
|
||||
<line x1="70" y1="40" x2="76" y2="40"/>
|
||||
<line x1="104" y1="40" x2="110" y2="40"/>
|
||||
<!-- Right antenna -->
|
||||
<line x1="115" y1="40" x2="135" y2="40"/>
|
||||
<line x1="115" y1="28" x2="115" y2="52"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 id="about-product-name">General Bots</h1>
|
||||
<p class="tagline">AI-Powered Agentic Office Suite</p>
|
||||
<div class="version-badge">
|
||||
<span class="version-label">Version</span>
|
||||
<span id="about-version" class="version-number">6.1.0</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Key Information Cards -->
|
||||
<div class="about-cards">
|
||||
<div class="about-card">
|
||||
<div class="card-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||||
<path d="M2 17l10 5 10-5"/>
|
||||
<path d="M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Architecture</h3>
|
||||
<p>Rust-based high-performance server with HTMX-powered reactive UI</p>
|
||||
</div>
|
||||
|
||||
<div class="about-card">
|
||||
<div class="card-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v6"/>
|
||||
<path d="M12 17v6"/>
|
||||
<path d="M4.22 4.22l4.24 4.24"/>
|
||||
<path d="M15.54 15.54l4.24 4.24"/>
|
||||
<path d="M1 12h6"/>
|
||||
<path d="M17 12h6"/>
|
||||
<path d="M4.22 19.78l4.24-4.24"/>
|
||||
<path d="M15.54 8.46l4.24-4.24"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Multi-Agent AI</h3>
|
||||
<p>Collaborative AI agents with specialized roles for complex task automation</p>
|
||||
</div>
|
||||
|
||||
<div class="about-card">
|
||||
<div class="card-icon">
|
||||
<svg width="24" height="24" 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"/>
|
||||
<line x1="3" y1="9" x2="21" y2="9"/>
|
||||
<line x1="9" y1="21" x2="9" y2="9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Modular Design</h3>
|
||||
<p>Feature-gated compilation for minimal binary size and resource usage</p>
|
||||
</div>
|
||||
|
||||
<div class="about-card">
|
||||
<div class="card-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
<polyline points="9,12 12,15 15,9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Enterprise Security</h3>
|
||||
<p>RBAC, SOC 2 compliance ready, multi-tenant architecture</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature Matrix Section -->
|
||||
<section class="feature-matrix">
|
||||
<h2>Feature Matrix</h2>
|
||||
|
||||
<div class="feature-section">
|
||||
<h3>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
Compiled Features
|
||||
</h3>
|
||||
<p class="section-description">Features included in this build</p>
|
||||
<div id="compiled-features" class="feature-badges">
|
||||
<span class="feature-badge enabled">chat</span>
|
||||
<span class="feature-badge enabled">drive</span>
|
||||
<span class="feature-badge enabled">tasks</span>
|
||||
<span class="feature-badge enabled">automation</span>
|
||||
<span class="feature-badge enabled">cache</span>
|
||||
<span class="feature-badge enabled">directory</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-section">
|
||||
<h3>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
Enabled Apps
|
||||
</h3>
|
||||
<p class="section-description">Applications active for this instance</p>
|
||||
<div id="enabled-apps" class="feature-badges">
|
||||
<span class="feature-badge active">chat</span>
|
||||
<span class="feature-badge active">drive</span>
|
||||
<span class="feature-badge active">tasks</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-section">
|
||||
<h3>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
Not Compiled / Disabled
|
||||
</h3>
|
||||
<p class="section-description">Features not included or disabled in configuration</p>
|
||||
<div id="disabled-features" class="feature-badges">
|
||||
<span class="text-muted">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature Dependency Tree -->
|
||||
<section class="dependency-tree-section">
|
||||
<h2>Feature Dependency Tree</h2>
|
||||
<p class="section-description">How features relate to each other</p>
|
||||
<div class="tree-container">
|
||||
<img src="/suite/about/feature-tree.svg" alt="Feature Dependency Tree" class="dependency-tree-svg" onerror="this.parentElement.innerHTML='<p class=\'text-muted\'>Dependency tree not available</p>'"/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- System Information -->
|
||||
<section class="system-info">
|
||||
<h2>System Information</h2>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Platform</span>
|
||||
<span class="info-value">Rust + Axum + HTMX</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Database</span>
|
||||
<span class="info-value">PostgreSQL + Diesel ORM</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Cache</span>
|
||||
<span class="info-value">Redis</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Vector DB</span>
|
||||
<span class="info-value">Qdrant (optional)</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">License</span>
|
||||
<span class="info-value">AGPL-3.0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Documentation</span>
|
||||
<a href="https://docs.pragmatismo.com.br" target="_blank" class="info-link">docs.pragmatismo.com.br</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Links and Resources -->
|
||||
<section class="resources">
|
||||
<h2>Resources</h2>
|
||||
<div class="resource-links">
|
||||
<a href="https://github.com/GeneralBots" target="_blank" class="resource-link">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
<a href="https://docs.pragmatismo.com.br" target="_blank" class="resource-link">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
||||
</svg>
|
||||
Documentation
|
||||
</a>
|
||||
<a href="mailto:support@pragmatismo.cloud" class="resource-link">
|
||||
<svg width="20" height="20" 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"/>
|
||||
<polyline points="22,6 12,13 2,6"/>
|
||||
</svg>
|
||||
Support
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="about-footer">
|
||||
<p id="about-copyright">© 2026 General Bots. All rights reserved.</p>
|
||||
<p class="built-with">Built with ❤️ by <a href="https://pragmatismo.com.br" target="_blank">Pragmatismo</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.about-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.about-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, var(--surface) 0%, var(--surface-hover) 100%);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.about-logo {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.logo-svg {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.about-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, var(--primary), var(--text));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
margin: 0.5rem 0 1.5rem;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary);
|
||||
color: var(--primary-contrast, #000);
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.version-label {
|
||||
opacity: 0.8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.version-number {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.about-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.about-card {
|
||||
padding: 1.5rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.about-card:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--primary-light, rgba(212, 245, 5, 0.1));
|
||||
border-radius: 12px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.about-card h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.about-card p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Feature Matrix */
|
||||
.feature-matrix {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.feature-matrix h2,
|
||||
.dependency-tree-section h2,
|
||||
.system-info h2,
|
||||
.resources h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
.feature-section {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--surface);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.feature-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.feature-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-badge {
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.feature-badge.enabled {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.feature-badge.active {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.feature-badge.disabled {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Dependency Tree */
|
||||
.dependency-tree-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
padding: 1.5rem;
|
||||
background: var(--surface);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
text-align: center;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.dependency-tree-svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* System Info */
|
||||
.system-info {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-link {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.info-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Resources */
|
||||
.resources {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.resource-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.resource-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.resource-link:hover {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.about-footer {
|
||||
text-align: center;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.about-footer p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.built-with a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.built-with a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
314
ui/suite/about/feature-tree.svg
Normal file
314
ui/suite/about/feature-tree.svg
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 700" style="background: transparent;">
|
||||
<defs>
|
||||
<!-- Gradients -->
|
||||
<linearGradient id="nodeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#22c55e;stop-opacity:0.2"/>
|
||||
<stop offset="100%" style="stop-color:#22c55e;stop-opacity:0.05"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="coreGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#d4f505;stop-opacity:0.3"/>
|
||||
<stop offset="100%" style="stop-color:#d4f505;stop-opacity:0.1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bundleGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:0.2"/>
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:0.05"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Arrow marker -->
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto" fill="#888">
|
||||
<polygon points="0 0, 10 3.5, 0 7"/>
|
||||
</marker>
|
||||
|
||||
<!-- Glow filter -->
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<style>
|
||||
.title { font: bold 24px sans-serif; fill: #e0e0e0; }
|
||||
.subtitle { font: 14px sans-serif; fill: #888; }
|
||||
.node-label { font: bold 11px monospace; fill: #e0e0e0; text-anchor: middle; }
|
||||
.category-label { font: bold 12px sans-serif; fill: #888; text-anchor: middle; }
|
||||
.dep-text { font: 10px sans-serif; fill: #666; }
|
||||
.legend-text { font: 11px sans-serif; fill: #888; }
|
||||
.node { stroke-width: 2; }
|
||||
.node-core { fill: url(#coreGradient); stroke: #d4f505; }
|
||||
.node-app { fill: url(#nodeGradient); stroke: #22c55e; }
|
||||
.node-bundle { fill: url(#bundleGradient); stroke: #3b82f6; }
|
||||
.edge { stroke: #555; stroke-width: 1.5; fill: none; marker-end: url(#arrowhead); }
|
||||
.edge-deps { stroke: #444; stroke-width: 1; stroke-dasharray: 4,2; }
|
||||
</style>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="450" y="35" class="title" text-anchor="middle">General Bots Feature Dependency Tree</text>
|
||||
<text x="450" y="55" class="subtitle" text-anchor="middle">Cargo.toml Feature Flags and Dependencies</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<g transform="translate(20, 80)">
|
||||
<rect x="0" y="0" width="130" height="90" rx="8" fill="#1a1a2e" stroke="#333"/>
|
||||
<text x="10" y="20" class="legend-text" font-weight="bold">Legend</text>
|
||||
<rect x="10" y="30" width="16" height="16" rx="4" class="node-core"/>
|
||||
<text x="32" y="42" class="legend-text">Core Technology</text>
|
||||
<rect x="10" y="50" width="16" height="16" rx="4" class="node-app"/>
|
||||
<text x="32" y="62" class="legend-text">Application</text>
|
||||
<rect x="10" y="70" width="16" height="16" rx="4" class="node-bundle"/>
|
||||
<text x="32" y="82" class="legend-text">Bundle (group)</text>
|
||||
</g>
|
||||
|
||||
<!-- ================ CORE TECHNOLOGIES (Bottom Layer) ================ -->
|
||||
<text x="450" y="650" class="category-label">CORE TECHNOLOGIES</text>
|
||||
|
||||
<!-- cache -->
|
||||
<g transform="translate(150, 590)">
|
||||
<rect x="-40" y="-15" width="80" height="30" rx="6" class="node node-core"/>
|
||||
<text y="4" class="node-label">cache</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">redis</text>
|
||||
</g>
|
||||
|
||||
<!-- automation -->
|
||||
<g transform="translate(280, 590)">
|
||||
<rect x="-50" y="-15" width="100" height="30" rx="6" class="node node-core"/>
|
||||
<text y="4" class="node-label">automation</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">rhai, cron</text>
|
||||
</g>
|
||||
|
||||
<!-- llm -->
|
||||
<g transform="translate(410, 590)">
|
||||
<rect x="-30" y="-15" width="60" height="30" rx="6" class="node node-core"/>
|
||||
<text y="4" class="node-label">llm</text>
|
||||
</g>
|
||||
|
||||
<!-- vectordb -->
|
||||
<g transform="translate(520, 590)">
|
||||
<rect x="-40" y="-15" width="80" height="30" rx="6" class="node node-core"/>
|
||||
<text y="4" class="node-label">vectordb</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">qdrant</text>
|
||||
</g>
|
||||
|
||||
<!-- monitoring -->
|
||||
<g transform="translate(650, 590)">
|
||||
<rect x="-50" y="-15" width="100" height="30" rx="6" class="node node-core"/>
|
||||
<text y="4" class="node-label">monitoring</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">sysinfo</text>
|
||||
</g>
|
||||
|
||||
<!-- directory -->
|
||||
<g transform="translate(780, 590)">
|
||||
<rect x="-45" y="-15" width="90" height="30" rx="6" class="node node-core"/>
|
||||
<text y="4" class="node-label">directory</text>
|
||||
</g>
|
||||
|
||||
<!-- ================ PRODUCTIVITY APPS (Layer 2) ================ -->
|
||||
<text x="200" y="480" class="category-label">PRODUCTIVITY</text>
|
||||
|
||||
<!-- tasks (depends on automation, cron) -->
|
||||
<g transform="translate(100, 510)">
|
||||
<rect x="-35" y="-15" width="70" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">tasks</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">cron</text>
|
||||
</g>
|
||||
<path d="M 100 540 Q 100 565 150 575 L 150 575" class="edge"/>
|
||||
<path d="M 100 540 Q 180 560 230 575" class="edge"/>
|
||||
|
||||
<!-- calendar -->
|
||||
<g transform="translate(200, 510)">
|
||||
<rect x="-45" y="-15" width="90" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">calendar</text>
|
||||
</g>
|
||||
|
||||
<!-- project (depends on quick-xml) -->
|
||||
<g transform="translate(300, 510)">
|
||||
<rect x="-40" y="-15" width="80" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">project</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">quick-xml</text>
|
||||
</g>
|
||||
|
||||
<!-- goals -->
|
||||
<g transform="translate(400, 510)">
|
||||
<rect x="-35" y="-15" width="70" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">goals</text>
|
||||
</g>
|
||||
|
||||
<!-- ================ COMMUNICATION APPS (Layer 2) ================ -->
|
||||
<text x="700" y="480" class="category-label">COMMUNICATION</text>
|
||||
|
||||
<!-- chat -->
|
||||
<g transform="translate(550, 510)">
|
||||
<rect x="-30" y="-15" width="60" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">chat</text>
|
||||
</g>
|
||||
|
||||
<!-- mail (has deps) -->
|
||||
<g transform="translate(650, 510)">
|
||||
<rect x="-30" y="-15" width="60" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">mail</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">lettre,imap</text>
|
||||
</g>
|
||||
|
||||
<!-- meet (has deps) -->
|
||||
<g transform="translate(750, 510)">
|
||||
<rect x="-30" y="-15" width="60" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">meet</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">livekit</text>
|
||||
</g>
|
||||
|
||||
<!-- people -->
|
||||
<g transform="translate(850, 510)">
|
||||
<rect x="-40" y="-15" width="80" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">people</text>
|
||||
</g>
|
||||
|
||||
<!-- ================ DOCUMENT APPS (Layer 3) ================ -->
|
||||
<text x="200" y="360" class="category-label">DOCUMENTS</text>
|
||||
|
||||
<!-- drive (has deps) -->
|
||||
<g transform="translate(100, 395)">
|
||||
<rect x="-35" y="-15" width="70" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">drive</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">aws-s3</text>
|
||||
</g>
|
||||
|
||||
<!-- docs (has deps) -->
|
||||
<g transform="translate(200, 395)">
|
||||
<rect x="-30" y="-15" width="60" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">docs</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">docx-rs</text>
|
||||
</g>
|
||||
|
||||
<!-- sheet (has deps) -->
|
||||
<g transform="translate(300, 395)">
|
||||
<rect x="-35" y="-15" width="70" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">sheet</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">calamine</text>
|
||||
</g>
|
||||
|
||||
<!-- slides -->
|
||||
<g transform="translate(400, 395)">
|
||||
<rect x="-40" y="-15" width="80" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">slides</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">ooxmlsdk</text>
|
||||
</g>
|
||||
|
||||
<!-- paper (depends on docs, pdf-extract) -->
|
||||
<g transform="translate(150, 320)">
|
||||
<rect x="-35" y="-15" width="70" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">paper</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">pdf-extract</text>
|
||||
</g>
|
||||
<path d="M 150 350 L 172 380" class="edge"/>
|
||||
|
||||
<!-- ================ RESEARCH / LEARNING (Layer 3) ================ -->
|
||||
<text x="600" y="360" class="category-label">RESEARCH & LEARNING</text>
|
||||
|
||||
<!-- research (depends on llm, vectordb) -->
|
||||
<g transform="translate(550, 395)">
|
||||
<rect x="-50" y="-15" width="100" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">research</text>
|
||||
</g>
|
||||
<path d="M 520 410 Q 470 530 440 575" class="edge"/>
|
||||
<path d="M 580 410 L 510 575" class="edge"/>
|
||||
|
||||
<!-- learn -->
|
||||
<g transform="translate(680, 395)">
|
||||
<rect x="-35" y="-15" width="70" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">learn</text>
|
||||
</g>
|
||||
|
||||
<!-- sources -->
|
||||
<g transform="translate(780, 395)">
|
||||
<rect x="-45" y="-15" width="90" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">sources</text>
|
||||
</g>
|
||||
|
||||
<!-- ================ ADMIN / ANALYTICS (Layer 4) ================ -->
|
||||
<text x="200" y="240" class="category-label">ADMIN & ANALYTICS</text>
|
||||
|
||||
<!-- analytics -->
|
||||
<g transform="translate(100, 275)">
|
||||
<rect x="-50" y="-15" width="100" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">analytics</text>
|
||||
</g>
|
||||
|
||||
<!-- dashboards -->
|
||||
<g transform="translate(220, 275)">
|
||||
<rect x="-55" y="-15" width="110" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">dashboards</text>
|
||||
</g>
|
||||
|
||||
<!-- admin -->
|
||||
<g transform="translate(340, 275)">
|
||||
<rect x="-35" y="-15" width="70" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">admin</text>
|
||||
</g>
|
||||
|
||||
<!-- settings -->
|
||||
<g transform="translate(440, 275)">
|
||||
<rect x="-45" y="-15" width="90" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">settings</text>
|
||||
</g>
|
||||
|
||||
<!-- ================ DEVELOPMENT (Layer 4) ================ -->
|
||||
<text x="650" y="240" class="category-label">DEVELOPMENT</text>
|
||||
|
||||
<!-- designer -->
|
||||
<g transform="translate(580, 275)">
|
||||
<rect x="-45" y="-15" width="90" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">designer</text>
|
||||
</g>
|
||||
|
||||
<!-- editor -->
|
||||
<g transform="translate(700, 275)">
|
||||
<rect x="-40" y="-15" width="80" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">editor</text>
|
||||
</g>
|
||||
|
||||
<!-- console (depends on monitoring) -->
|
||||
<g transform="translate(820, 275)">
|
||||
<rect x="-45" y="-15" width="90" height="30" rx="6" class="node node-app"/>
|
||||
<text y="4" class="node-label">console</text>
|
||||
<text y="25" class="dep-text" text-anchor="middle">crossterm</text>
|
||||
</g>
|
||||
<path d="M 780 290 Q 720 450 670 575" class="edge"/>
|
||||
|
||||
<!-- ================ BUNDLE FEATURES (Top Layer) ================ -->
|
||||
<text x="450" y="120" class="category-label">BUNDLE FEATURES</text>
|
||||
|
||||
<!-- communications bundle -->
|
||||
<g transform="translate(200, 160)">
|
||||
<rect x="-80" y="-20" width="160" height="40" rx="8" class="node node-bundle"/>
|
||||
<text y="5" class="node-label">communications</text>
|
||||
</g>
|
||||
<path d="M 200 180 Q 200 350 550 495" class="edge-deps"/>
|
||||
<path d="M 200 180 Q 350 350 650 495" class="edge-deps"/>
|
||||
<path d="M 200 180 Q 500 350 750 495" class="edge-deps"/>
|
||||
|
||||
<!-- productivity bundle -->
|
||||
<g transform="translate(400, 160)">
|
||||
<rect x="-65" y="-20" width="130" height="40" rx="8" class="node node-bundle"/>
|
||||
<text y="5" class="node-label">productivity</text>
|
||||
</g>
|
||||
<path d="M 340 180 Q 200 350 100 495" class="edge-deps"/>
|
||||
<path d="M 380 180 Q 260 350 200 495" class="edge-deps"/>
|
||||
<path d="M 420 180 Q 350 350 300 495" class="edge-deps"/>
|
||||
|
||||
<!-- documents bundle -->
|
||||
<g transform="translate(580, 160)">
|
||||
<rect x="-60" y="-20" width="120" height="40" rx="8" class="node node-bundle"/>
|
||||
<text y="5" class="node-label">documents</text>
|
||||
</g>
|
||||
<path d="M 530 180 Q 200 300 100 380" class="edge-deps"/>
|
||||
<path d="M 560 180 Q 320 300 200 380" class="edge-deps"/>
|
||||
<path d="M 590 180 Q 400 300 300 380" class="edge-deps"/>
|
||||
<path d="M 620 180 Q 480 300 400 380" class="edge-deps"/>
|
||||
|
||||
<!-- full bundle -->
|
||||
<g transform="translate(760, 160)">
|
||||
<rect x="-45" y="-20" width="90" height="40" rx="8" class="node node-bundle"/>
|
||||
<text y="5" class="node-label">full</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
1563
ui/suite/admin/admin-functions.js
Normal file
1563
ui/suite/admin/admin-functions.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="admin/admin.css" />
|
||||
|
||||
<div class="admin-layout">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="admin-sidebar">
|
||||
|
|
@ -716,5 +718,4 @@
|
|||
</div>
|
||||
</dialog>
|
||||
|
||||
<link rel="stylesheet" href="admin.css" />
|
||||
<script src="admin.js"></script>
|
||||
<link rel="stylesheet" href="admin/admin.css" />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="analytics/analytics.css" />
|
||||
|
||||
<div class="analytics-container" id="analytics-app">
|
||||
<header class="analytics-header">
|
||||
<div class="header-title">
|
||||
|
|
@ -534,6 +536,6 @@
|
|||
</aside>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="analytics.css" />
|
||||
<script src="analytics.js"></script>
|
||||
<link rel="stylesheet" href="analytics/analytics.css" />
|
||||
<script src="analytics/analytics.js"></script>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- Attendant Console - General Bots -->
|
||||
<link rel="stylesheet" href="attendant.css" />
|
||||
<link rel="stylesheet" href="attendant/attendant.css" />
|
||||
|
||||
<!-- CRM Disabled State -->
|
||||
<!-- CRM Disabled State - Hidden by default, CRM is now enabled by default -->
|
||||
|
|
@ -572,4 +572,4 @@
|
|||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<script src="attendant.js"></script>
|
||||
<script src="attendant/attendant.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,192 +0,0 @@
|
|||
/* =============================================================================
|
||||
BOTUI SUITE - BASE LAYOUT JAVASCRIPT
|
||||
Sentient Theme with AI Assistant Panel
|
||||
============================================================================= */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// =============================================================================
|
||||
// STATE
|
||||
// =============================================================================
|
||||
|
||||
const state = {
|
||||
aiPanelOpen: true,
|
||||
currentApp: 'dashboard',
|
||||
messages: []
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// AI PANEL
|
||||
// =============================================================================
|
||||
|
||||
window.toggleAIPanel = function() {
|
||||
const panel = document.getElementById('ai-panel');
|
||||
if (panel) {
|
||||
panel.classList.toggle('open');
|
||||
state.aiPanelOpen = panel.classList.contains('open');
|
||||
}
|
||||
};
|
||||
|
||||
window.sendAIMessage = function() {
|
||||
const input = document.getElementById('ai-input');
|
||||
if (!input || !input.value.trim()) return;
|
||||
|
||||
const message = input.value.trim();
|
||||
input.value = '';
|
||||
|
||||
addMessage('user', message);
|
||||
showTypingIndicator();
|
||||
|
||||
// Simulate AI response
|
||||
setTimeout(() => {
|
||||
hideTypingIndicator();
|
||||
addMessage('assistant', `Entendido! Vou processar sua solicitação: "${message}"`);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
function addMessage(type, content, action) {
|
||||
const container = document.getElementById('ai-messages');
|
||||
if (!container) return;
|
||||
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = `ai-message ${type}`;
|
||||
|
||||
let html = `<div class="ai-message-bubble">${content}</div>`;
|
||||
if (action) {
|
||||
html += `<span class="ai-message-action">${action}</span>`;
|
||||
}
|
||||
|
||||
messageEl.innerHTML = html;
|
||||
container.appendChild(messageEl);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
state.messages.push({ type, content, action });
|
||||
}
|
||||
|
||||
function showTypingIndicator() {
|
||||
const container = document.getElementById('ai-messages');
|
||||
if (!container) return;
|
||||
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'ai-message assistant';
|
||||
indicator.id = 'typing-indicator';
|
||||
indicator.innerHTML = `
|
||||
<div class="ai-typing-indicator">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(indicator);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function hideTypingIndicator() {
|
||||
const indicator = document.getElementById('typing-indicator');
|
||||
if (indicator) indicator.remove();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// APP LAUNCHER
|
||||
// =============================================================================
|
||||
|
||||
function initAppLauncher() {
|
||||
document.querySelectorAll('.app-icon').forEach(icon => {
|
||||
icon.addEventListener('click', function() {
|
||||
const app = this.dataset.app;
|
||||
switchApp(app);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function switchApp(appName) {
|
||||
document.querySelectorAll('.app-icon').forEach(icon => {
|
||||
icon.classList.toggle('active', icon.dataset.app === appName);
|
||||
});
|
||||
state.currentApp = appName;
|
||||
|
||||
// Dispatch custom event for app switching
|
||||
document.dispatchEvent(new CustomEvent('app-switch', { detail: { app: appName } }));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NAVIGATION TABS
|
||||
// =============================================================================
|
||||
|
||||
function initTabs() {
|
||||
document.querySelectorAll('.topbar-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
document.querySelectorAll('.topbar-tab').forEach(t => t.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// QUICK ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
function initQuickActions() {
|
||||
document.querySelectorAll('.quick-action-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const action = this.textContent;
|
||||
addMessage('user', action);
|
||||
showTypingIndicator();
|
||||
|
||||
setTimeout(() => {
|
||||
hideTypingIndicator();
|
||||
addMessage('assistant', `Ação "${action}" executada com sucesso!`, 'Ver alterações');
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// KEYBOARD SHORTCUTS
|
||||
// =============================================================================
|
||||
|
||||
function initKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Enter to send message in AI input
|
||||
if (e.key === 'Enter' && document.activeElement.id === 'ai-input') {
|
||||
e.preventDefault();
|
||||
sendAIMessage();
|
||||
}
|
||||
|
||||
// Escape to close AI panel on mobile
|
||||
if (e.key === 'Escape' && window.innerWidth <= 1024) {
|
||||
const panel = document.getElementById('ai-panel');
|
||||
if (panel && panel.classList.contains('open')) {
|
||||
toggleAIPanel();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL MESSAGES
|
||||
// =============================================================================
|
||||
|
||||
function loadInitialMessages() {
|
||||
addMessage('assistant', 'Olá! Sou o AI Developer. Como posso ajudar você hoje?');
|
||||
addMessage('assistant', 'Você pode me pedir para modificar campos, alterar cores, adicionar validações ou qualquer outra mudança no sistema.');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INITIALIZE
|
||||
// =============================================================================
|
||||
|
||||
function init() {
|
||||
initAppLauncher();
|
||||
initTabs();
|
||||
initQuickActions();
|
||||
initKeyboardShortcuts();
|
||||
loadInitialMessages();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- Billing - Invoices, Payments & Quotes -->
|
||||
<!-- Dynamics nomenclature: Quote → Invoice → Payment -->
|
||||
|
||||
<link rel="stylesheet" href="/suite/billing/billing.css">
|
||||
<link rel="stylesheet" href="billing/billing.css" />
|
||||
|
||||
<div class="billing-container">
|
||||
<!-- Header -->
|
||||
|
|
@ -9,26 +9,62 @@
|
|||
<div class="billing-header-left">
|
||||
<h1 data-i18n="billing-title">Billing</h1>
|
||||
<nav class="billing-tabs">
|
||||
<button class="billing-tab active" data-view="invoices" data-i18n="billing-invoices">Invoices</button>
|
||||
<button class="billing-tab" data-view="payments" data-i18n="billing-payments">Payments</button>
|
||||
<button class="billing-tab" data-view="quotes" data-i18n="billing-quotes">Quotes</button>
|
||||
<button
|
||||
class="billing-tab active"
|
||||
data-view="invoices"
|
||||
data-i18n="billing-invoices"
|
||||
>
|
||||
Invoices
|
||||
</button>
|
||||
<button
|
||||
class="billing-tab"
|
||||
data-view="payments"
|
||||
data-i18n="billing-payments"
|
||||
>
|
||||
Payments
|
||||
</button>
|
||||
<button
|
||||
class="billing-tab"
|
||||
data-view="quotes"
|
||||
data-i18n="billing-quotes"
|
||||
>
|
||||
Quotes
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="billing-header-right">
|
||||
<div class="billing-search">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.35-4.35" />
|
||||
</svg>
|
||||
<input type="text"
|
||||
placeholder="Search invoices, quotes..."
|
||||
data-i18n-placeholder="billing-search-placeholder"
|
||||
hx-get="/api/billing/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#billing-search-results">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search invoices, quotes..."
|
||||
data-i18n-placeholder="billing-search-placeholder"
|
||||
hx-get="/api/billing/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#billing-search-results"
|
||||
/>
|
||||
</div>
|
||||
<button class="btn-primary" id="billing-new-invoice">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
<span data-i18n="billing-new-invoice">New Invoice</span>
|
||||
</button>
|
||||
|
|
@ -42,47 +78,111 @@
|
|||
<div class="billing-summary">
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon pending">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="billing-pending">Pending</span>
|
||||
<span class="summary-value" hx-get="/api/billing/stats/pending" hx-trigger="load">$0</span>
|
||||
<span class="summary-label" data-i18n="billing-pending"
|
||||
>Pending</span
|
||||
>
|
||||
<span
|
||||
class="summary-value"
|
||||
hx-get="/api/billing/stats/pending"
|
||||
hx-trigger="load"
|
||||
>$0</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon overdue">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="billing-overdue">Overdue</span>
|
||||
<span class="summary-value overdue" hx-get="/api/billing/stats/overdue" hx-trigger="load">$0</span>
|
||||
<span class="summary-label" data-i18n="billing-overdue"
|
||||
>Overdue</span
|
||||
>
|
||||
<span
|
||||
class="summary-value overdue"
|
||||
hx-get="/api/billing/stats/overdue"
|
||||
hx-trigger="load"
|
||||
>$0</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon paid">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="billing-paid-month">Paid This Month</span>
|
||||
<span class="summary-value paid" hx-get="/api/billing/stats/paid-month" hx-trigger="load">$0</span>
|
||||
<span class="summary-label" data-i18n="billing-paid-month"
|
||||
>Paid This Month</span
|
||||
>
|
||||
<span
|
||||
class="summary-value paid"
|
||||
hx-get="/api/billing/stats/paid-month"
|
||||
hx-trigger="load"
|
||||
>$0</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon total">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="12" y1="1" x2="12" y2="23" />
|
||||
<path
|
||||
d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="billing-revenue-month">Revenue This Month</span>
|
||||
<span class="summary-value" hx-get="/api/billing/stats/revenue-month" hx-trigger="load">$0</span>
|
||||
<span class="summary-label" data-i18n="billing-revenue-month"
|
||||
>Revenue This Month</span
|
||||
>
|
||||
<span
|
||||
class="summary-value"
|
||||
hx-get="/api/billing/stats/revenue-month"
|
||||
hx-trigger="load"
|
||||
>$0</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -91,23 +191,64 @@
|
|||
<div id="billing-invoices-view" class="billing-view active">
|
||||
<div class="billing-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" hx-include="this" name="status">
|
||||
<option value="all" data-i18n="billing-filter-all">All Invoices</option>
|
||||
<option value="draft" data-i18n="billing-filter-draft">Draft</option>
|
||||
<option value="sent" data-i18n="billing-filter-sent">Sent</option>
|
||||
<option value="paid" data-i18n="billing-filter-paid">Paid</option>
|
||||
<option value="overdue" data-i18n="billing-filter-overdue">Overdue</option>
|
||||
<option value="cancelled" data-i18n="billing-filter-cancelled">Cancelled</option>
|
||||
<select
|
||||
hx-get="/api/billing/invoices"
|
||||
hx-trigger="change"
|
||||
hx-target="#invoices-table-body"
|
||||
hx-include="this"
|
||||
name="status"
|
||||
>
|
||||
<option value="all" data-i18n="billing-filter-all">
|
||||
All Invoices
|
||||
</option>
|
||||
<option value="draft" data-i18n="billing-filter-draft">
|
||||
Draft
|
||||
</option>
|
||||
<option value="sent" data-i18n="billing-filter-sent">
|
||||
Sent
|
||||
</option>
|
||||
<option value="paid" data-i18n="billing-filter-paid">
|
||||
Paid
|
||||
</option>
|
||||
<option value="overdue" data-i18n="billing-filter-overdue">
|
||||
Overdue
|
||||
</option>
|
||||
<option
|
||||
value="cancelled"
|
||||
data-i18n="billing-filter-cancelled"
|
||||
>
|
||||
Cancelled
|
||||
</option>
|
||||
</select>
|
||||
<select hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" hx-include="this" name="period">
|
||||
<option value="all" data-i18n="billing-period-all">All Time</option>
|
||||
<option value="month" data-i18n="billing-period-month">This Month</option>
|
||||
<option value="quarter" data-i18n="billing-period-quarter">This Quarter</option>
|
||||
<option value="year" data-i18n="billing-period-year">This Year</option>
|
||||
<select
|
||||
hx-get="/api/billing/invoices"
|
||||
hx-trigger="change"
|
||||
hx-target="#invoices-table-body"
|
||||
hx-include="this"
|
||||
name="period"
|
||||
>
|
||||
<option value="all" data-i18n="billing-period-all">
|
||||
All Time
|
||||
</option>
|
||||
<option value="month" data-i18n="billing-period-month">
|
||||
This Month
|
||||
</option>
|
||||
<option value="quarter" data-i18n="billing-period-quarter">
|
||||
This Quarter
|
||||
</option>
|
||||
<option value="year" data-i18n="billing-period-year">
|
||||
This Year
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="list-actions">
|
||||
<button class="action-btn" hx-get="/api/billing/invoices/export" data-i18n="billing-export">Export</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
hx-get="/api/billing/invoices/export"
|
||||
data-i18n="billing-export"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
|
|
@ -122,7 +263,11 @@
|
|||
<th data-i18n="billing-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="invoices-table-body" hx-get="/api/billing/invoices" hx-trigger="load">
|
||||
<tbody
|
||||
id="invoices-table-body"
|
||||
hx-get="/api/billing/invoices"
|
||||
hx-trigger="load"
|
||||
>
|
||||
<!-- Invoices loaded via HTMX -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -132,18 +277,49 @@
|
|||
<div id="billing-payments-view" class="billing-view">
|
||||
<div class="billing-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/billing/payments" hx-trigger="change" hx-target="#payments-table-body" hx-include="this" name="method">
|
||||
<option value="all" data-i18n="billing-method-all">All Methods</option>
|
||||
<option value="bank" data-i18n="billing-method-bank">Bank Transfer</option>
|
||||
<option value="card" data-i18n="billing-method-card">Credit Card</option>
|
||||
<option value="pix" data-i18n="billing-method-pix">PIX</option>
|
||||
<option value="boleto" data-i18n="billing-method-boleto">Boleto</option>
|
||||
<option value="cash" data-i18n="billing-method-cash">Cash</option>
|
||||
<select
|
||||
hx-get="/api/billing/payments"
|
||||
hx-trigger="change"
|
||||
hx-target="#payments-table-body"
|
||||
hx-include="this"
|
||||
name="method"
|
||||
>
|
||||
<option value="all" data-i18n="billing-method-all">
|
||||
All Methods
|
||||
</option>
|
||||
<option value="bank" data-i18n="billing-method-bank">
|
||||
Bank Transfer
|
||||
</option>
|
||||
<option value="card" data-i18n="billing-method-card">
|
||||
Credit Card
|
||||
</option>
|
||||
<option value="pix" data-i18n="billing-method-pix">
|
||||
PIX
|
||||
</option>
|
||||
<option value="boleto" data-i18n="billing-method-boleto">
|
||||
Boleto
|
||||
</option>
|
||||
<option value="cash" data-i18n="billing-method-cash">
|
||||
Cash
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" hx-get="/suite/billing/partials/payment-form.html" hx-target="#billing-modal-content" hx-on::after-request="openBillingModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<button
|
||||
class="btn-primary"
|
||||
hx-get="/suite/billing/partials/payment-form.html"
|
||||
hx-target="#billing-modal-content"
|
||||
hx-on::after-request="openBillingModal()"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
<span data-i18n="billing-record-payment">Record Payment</span>
|
||||
</button>
|
||||
|
|
@ -160,8 +336,11 @@
|
|||
<th data-i18n="billing-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="payments-table-body" hx-get="/api/billing/payments" hx-trigger="load">
|
||||
</tbody>
|
||||
<tbody
|
||||
id="payments-table-body"
|
||||
hx-get="/api/billing/payments"
|
||||
hx-trigger="load"
|
||||
></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
|
@ -169,18 +348,49 @@
|
|||
<div id="billing-quotes-view" class="billing-view">
|
||||
<div class="billing-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/billing/quotes" hx-trigger="change" hx-target="#quotes-table-body" hx-include="this" name="status">
|
||||
<option value="all" data-i18n="billing-quote-all">All Quotes</option>
|
||||
<option value="draft" data-i18n="billing-quote-draft">Draft</option>
|
||||
<option value="sent" data-i18n="billing-quote-sent">Sent</option>
|
||||
<option value="accepted" data-i18n="billing-quote-accepted">Accepted</option>
|
||||
<option value="rejected" data-i18n="billing-quote-rejected">Rejected</option>
|
||||
<option value="expired" data-i18n="billing-quote-expired">Expired</option>
|
||||
<select
|
||||
hx-get="/api/billing/quotes"
|
||||
hx-trigger="change"
|
||||
hx-target="#quotes-table-body"
|
||||
hx-include="this"
|
||||
name="status"
|
||||
>
|
||||
<option value="all" data-i18n="billing-quote-all">
|
||||
All Quotes
|
||||
</option>
|
||||
<option value="draft" data-i18n="billing-quote-draft">
|
||||
Draft
|
||||
</option>
|
||||
<option value="sent" data-i18n="billing-quote-sent">
|
||||
Sent
|
||||
</option>
|
||||
<option value="accepted" data-i18n="billing-quote-accepted">
|
||||
Accepted
|
||||
</option>
|
||||
<option value="rejected" data-i18n="billing-quote-rejected">
|
||||
Rejected
|
||||
</option>
|
||||
<option value="expired" data-i18n="billing-quote-expired">
|
||||
Expired
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" hx-get="/suite/billing/partials/quote-form.html" hx-target="#billing-modal-content" hx-on::after-request="openBillingModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<button
|
||||
class="btn-primary"
|
||||
hx-get="/suite/billing/partials/quote-form.html"
|
||||
hx-target="#billing-modal-content"
|
||||
hx-on::after-request="openBillingModal()"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
<span data-i18n="billing-new-quote">New Quote</span>
|
||||
</button>
|
||||
|
|
@ -198,8 +408,11 @@
|
|||
<th data-i18n="billing-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="quotes-table-body" hx-get="/api/billing/quotes" hx-trigger="load">
|
||||
</tbody>
|
||||
<tbody
|
||||
id="quotes-table-body"
|
||||
hx-get="/api/billing/quotes"
|
||||
hx-trigger="load"
|
||||
></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -213,44 +426,56 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Tab switching
|
||||
document.querySelectorAll('.billing-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
document.querySelectorAll('.billing-tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.billing-view').forEach(v => v.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
const view = this.dataset.view;
|
||||
document.getElementById(`billing-${view}-view`).classList.add('active');
|
||||
(function () {
|
||||
// Tab switching
|
||||
document.querySelectorAll(".billing-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", function () {
|
||||
document
|
||||
.querySelectorAll(".billing-tab")
|
||||
.forEach((t) => t.classList.remove("active"));
|
||||
document
|
||||
.querySelectorAll(".billing-view")
|
||||
.forEach((v) => v.classList.remove("active"));
|
||||
this.classList.add("active");
|
||||
const view = this.dataset.view;
|
||||
document
|
||||
.getElementById(`billing-${view}-view`)
|
||||
.classList.add("active");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// New Invoice button
|
||||
document.getElementById('billing-new-invoice').addEventListener('click', function() {
|
||||
htmx.ajax('GET', '/suite/billing/partials/invoice-form.html', '#billing-modal-content').then(() => {
|
||||
openBillingModal();
|
||||
// New Invoice button
|
||||
document
|
||||
.getElementById("billing-new-invoice")
|
||||
.addEventListener("click", function () {
|
||||
htmx.ajax(
|
||||
"GET",
|
||||
"/suite/billing/partials/invoice-form.html",
|
||||
"#billing-modal-content",
|
||||
).then(() => {
|
||||
openBillingModal();
|
||||
});
|
||||
});
|
||||
|
||||
// Modal functions
|
||||
window.openBillingModal = function () {
|
||||
document.getElementById("billing-modal").classList.add("open");
|
||||
};
|
||||
|
||||
window.closeBillingModal = function () {
|
||||
document.getElementById("billing-modal").classList.remove("open");
|
||||
};
|
||||
|
||||
// Keyboard shortcut: Escape to close modal
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") {
|
||||
closeBillingModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Modal functions
|
||||
window.openBillingModal = function() {
|
||||
document.getElementById('billing-modal').classList.add('open');
|
||||
};
|
||||
|
||||
window.closeBillingModal = function() {
|
||||
document.getElementById('billing-modal').classList.remove('open');
|
||||
};
|
||||
|
||||
// Keyboard shortcut: Escape to close modal
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeBillingModal();
|
||||
// Initialize i18n if available
|
||||
if (window.i18n && window.i18n.translatePage) {
|
||||
window.i18n.translatePage();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize i18n if available
|
||||
if (window.i18n && window.i18n.translatePage) {
|
||||
window.i18n.translatePage();
|
||||
}
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="calendar/calendar.css" />
|
||||
|
||||
<!-- Calendar - Event Management -->
|
||||
<div class="calendar-container" id="calendar-app">
|
||||
<!-- Sidebar -->
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
1391
ui/suite/canvas/canvas.js
Normal file
1391
ui/suite/canvas/canvas.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -110,12 +110,97 @@
|
|||
|
||||
/* Messages Area */
|
||||
#messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24);
|
||||
}
|
||||
|
||||
/* Custom scrollbar for markers */
|
||||
#messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
#messages::-webkit-scrollbar-track {
|
||||
background: var(--surface, #1a1a24);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#messages::-webkit-scrollbar-thumb {
|
||||
background: var(--accent, #3b82f6);
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--surface, #1a1a24);
|
||||
}
|
||||
|
||||
#messages::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-hover, #2563eb);
|
||||
}
|
||||
|
||||
/* Scrollbar markers container */
|
||||
.scrollbar-markers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.scrollbar-marker {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--accent, #3b82f6);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.scrollbar-marker:hover {
|
||||
transform: scale(1.5);
|
||||
background: var(--accent-hover, #2563eb);
|
||||
box-shadow: 0 0 8px var(--accent, #3b82f6);
|
||||
}
|
||||
|
||||
.scrollbar-marker.user-marker {
|
||||
background: var(--accent, #3b82f6);
|
||||
}
|
||||
|
||||
.scrollbar-marker.bot-marker {
|
||||
background: var(--success, #22c55e);
|
||||
}
|
||||
|
||||
.scrollbar-marker-tooltip {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
transform: translateY(-50%);
|
||||
background: var(--surface, #1a1a24);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text, #ffffff);
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
z-index: 12;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.scrollbar-marker:hover .scrollbar-marker-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
|
|
@ -314,8 +399,28 @@ footer {
|
|||
}
|
||||
|
||||
.mention-results {
|
||||
overflow-y: auto;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
max-height: 250px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--accent, #3b82f6) transparent;
|
||||
}
|
||||
|
||||
.mention-results::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.mention-results::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.mention-results::-webkit-scrollbar-thumb {
|
||||
background: var(--accent, #3b82f6);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.mention-results::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-hover, #2563eb);
|
||||
}
|
||||
|
||||
.mention-item {
|
||||
|
|
|
|||
|
|
@ -740,6 +740,33 @@ function parseMarkdown(text) {
|
|||
.replace(/`([^`]+)`/gim, "<code>$1</code>")
|
||||
.replace(/\n/gim, "<br>");
|
||||
}
|
||||
// Export projector functions for onclick handlers in projector.html
|
||||
window.openProjector = openProjector;
|
||||
window.closeProjector = closeProjector;
|
||||
window.closeProjectorOnOverlay = closeProjectorOnOverlay;
|
||||
window.toggleFullscreen = toggleFullscreen;
|
||||
window.downloadContent = downloadContent;
|
||||
window.shareContent = shareContent;
|
||||
window.togglePlayPause = togglePlayPause;
|
||||
window.mediaSeekBack = mediaSeekBack;
|
||||
window.mediaSeekForward = mediaSeekForward;
|
||||
window.toggleMute = toggleMute;
|
||||
window.setVolume = setVolume;
|
||||
window.toggleLoop = toggleLoop;
|
||||
window.prevSlide = prevSlide;
|
||||
window.nextSlide = nextSlide;
|
||||
window.goToSlide = goToSlide;
|
||||
window.zoomIn = zoomIn;
|
||||
window.zoomOut = zoomOut;
|
||||
window.prevImage = prevImage;
|
||||
window.nextImage = nextImage;
|
||||
window.rotateImage = rotateImage;
|
||||
window.fitToScreen = fitToScreen;
|
||||
window.toggleLineNumbers = toggleLineNumbers;
|
||||
window.toggleWordWrap = toggleWordWrap;
|
||||
window.setCodeTheme = setCodeTheme;
|
||||
window.copyCode = copyCode;
|
||||
|
||||
if (window.htmx) {
|
||||
htmx.on("htmx:wsMessage", function (event) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- CRM - Customer Relationship Management -->
|
||||
<!-- Dynamics nomenclature: Lead → Opportunity → Account/Contact -->
|
||||
|
||||
<link rel="stylesheet" href="/suite/crm/crm.css">
|
||||
<link rel="stylesheet" href="crm/crm.css">
|
||||
|
||||
<div class="crm-container">
|
||||
<!-- Header -->
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="dashboards/dashboards.css" />
|
||||
|
||||
<div class="dashboards-container" id="dashboards-app">
|
||||
<header class="dashboards-header">
|
||||
<div class="header-title">
|
||||
|
|
|
|||
744
ui/suite/dashboards/dashboards.js
Normal file
744
ui/suite/dashboards/dashboards.js
Normal file
|
|
@ -0,0 +1,744 @@
|
|||
/* =============================================================================
|
||||
DASHBOARDS MODULE - Business Intelligence Dashboards
|
||||
============================================================================= */
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// =============================================================================
|
||||
// STATE
|
||||
// =============================================================================
|
||||
|
||||
const state = {
|
||||
dashboards: [],
|
||||
dataSources: [],
|
||||
currentDashboard: null,
|
||||
selectedWidgetType: null,
|
||||
isEditing: false,
|
||||
widgets: [],
|
||||
filters: {
|
||||
category: "all",
|
||||
search: "",
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// INITIALIZATION
|
||||
// =============================================================================
|
||||
|
||||
function init() {
|
||||
loadDashboards();
|
||||
loadDataSources();
|
||||
bindEvents();
|
||||
console.log("Dashboards module initialized");
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
// Search input
|
||||
const searchInput = document.getElementById("dashboard-search");
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", (e) => {
|
||||
state.filters.search = e.target.value;
|
||||
filterDashboards();
|
||||
});
|
||||
}
|
||||
|
||||
// Category filter
|
||||
const categorySelect = document.getElementById("category-filter");
|
||||
if (categorySelect) {
|
||||
categorySelect.addEventListener("change", (e) => {
|
||||
state.filters.category = e.target.value;
|
||||
filterDashboards();
|
||||
});
|
||||
}
|
||||
|
||||
// Dashboard card clicks
|
||||
document.addEventListener("click", (e) => {
|
||||
const card = e.target.closest(".dashboard-card");
|
||||
if (card && !e.target.closest("button")) {
|
||||
const dashboardId = card.dataset.id;
|
||||
if (dashboardId) {
|
||||
openDashboard(dashboardId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DASHBOARD CRUD
|
||||
// =============================================================================
|
||||
|
||||
async function loadDashboards() {
|
||||
try {
|
||||
const response = await fetch("/api/dashboards");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
state.dashboards = data.dashboards || [];
|
||||
renderDashboardList();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load dashboards:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDashboardList() {
|
||||
const container = document.getElementById("dashboards-grid");
|
||||
if (!container) return;
|
||||
|
||||
const filtered = state.dashboards.filter((d) => {
|
||||
const matchesSearch =
|
||||
!state.filters.search ||
|
||||
d.name.toLowerCase().includes(state.filters.search.toLowerCase());
|
||||
const matchesCategory =
|
||||
state.filters.category === "all" ||
|
||||
d.category === state.filters.category;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<span class="empty-icon">📊</span>
|
||||
<h3>No dashboards found</h3>
|
||||
<p>Create your first dashboard to visualize your data</p>
|
||||
<button class="btn-primary" onclick="showCreateDashboardModal()">
|
||||
Create Dashboard
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered
|
||||
.map(
|
||||
(dashboard) => `
|
||||
<div class="dashboard-card" data-id="${dashboard.id}">
|
||||
<div class="dashboard-preview">
|
||||
<div class="preview-placeholder">📊</div>
|
||||
</div>
|
||||
<div class="dashboard-info">
|
||||
<h3>${escapeHtml(dashboard.name)}</h3>
|
||||
<p>${escapeHtml(dashboard.description || "No description")}</p>
|
||||
<div class="dashboard-meta">
|
||||
<span class="category">${escapeHtml(dashboard.category || "General")}</span>
|
||||
<span class="updated">Updated ${formatRelativeTime(dashboard.updated_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-actions">
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); editDashboardById('${dashboard.id}')" title="Edit">✏️</button>
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); duplicateDashboard('${dashboard.id}')" title="Duplicate">📋</button>
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); deleteDashboard('${dashboard.id}')" title="Delete">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function filterDashboards() {
|
||||
renderDashboardList();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CREATE DASHBOARD MODAL
|
||||
// =============================================================================
|
||||
|
||||
function showCreateDashboardModal() {
|
||||
const modal = document.getElementById("createDashboardModal");
|
||||
if (modal) {
|
||||
modal.style.display = "flex";
|
||||
// Reset form
|
||||
const form = modal.querySelector("form");
|
||||
if (form) form.reset();
|
||||
}
|
||||
}
|
||||
|
||||
function closeCreateDashboardModal() {
|
||||
const modal = document.getElementById("createDashboardModal");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
async function createDashboard(formData) {
|
||||
const data = {
|
||||
name: formData.get("name") || "Untitled Dashboard",
|
||||
description: formData.get("description") || "",
|
||||
category: formData.get("category") || "general",
|
||||
is_public: formData.get("is_public") === "on",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/dashboards", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
showNotification("Dashboard created", "success");
|
||||
closeCreateDashboardModal();
|
||||
loadDashboards();
|
||||
|
||||
// Open the new dashboard for editing
|
||||
if (result.id) {
|
||||
openDashboard(result.id);
|
||||
}
|
||||
} else {
|
||||
showNotification("Failed to create dashboard", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to create dashboard:", e);
|
||||
showNotification("Failed to create dashboard", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
document.addEventListener("submit", (e) => {
|
||||
if (e.target.id === "createDashboardForm") {
|
||||
e.preventDefault();
|
||||
createDashboard(new FormData(e.target));
|
||||
} else if (e.target.id === "addDataSourceForm") {
|
||||
e.preventDefault();
|
||||
addDataSource(new FormData(e.target));
|
||||
} else if (e.target.id === "addWidgetForm") {
|
||||
e.preventDefault();
|
||||
addWidget(new FormData(e.target));
|
||||
}
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// DASHBOARD VIEWER
|
||||
// =============================================================================
|
||||
|
||||
async function openDashboard(dashboardId) {
|
||||
try {
|
||||
const response = await fetch(`/api/dashboards/${dashboardId}`);
|
||||
if (response.ok) {
|
||||
const dashboard = await response.json();
|
||||
state.currentDashboard = dashboard;
|
||||
state.widgets = dashboard.widgets || [];
|
||||
showDashboardViewer(dashboard);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to open dashboard:", e);
|
||||
showNotification("Failed to load dashboard", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function showDashboardViewer(dashboard) {
|
||||
const viewer = document.getElementById("dashboard-viewer");
|
||||
const list = document.getElementById("dashboards-list");
|
||||
|
||||
if (viewer) viewer.classList.remove("hidden");
|
||||
if (list) list.classList.add("hidden");
|
||||
|
||||
// Update title
|
||||
const titleEl = document.getElementById("viewer-dashboard-name");
|
||||
if (titleEl) titleEl.textContent = dashboard.name;
|
||||
|
||||
// Render widgets
|
||||
renderWidgets(dashboard.widgets || []);
|
||||
}
|
||||
|
||||
function closeDashboardViewer() {
|
||||
const viewer = document.getElementById("dashboard-viewer");
|
||||
const list = document.getElementById("dashboards-list");
|
||||
|
||||
if (viewer) viewer.classList.add("hidden");
|
||||
if (list) list.classList.remove("hidden");
|
||||
|
||||
state.currentDashboard = null;
|
||||
state.isEditing = false;
|
||||
}
|
||||
|
||||
function renderWidgets(widgets) {
|
||||
const container = document.getElementById("widgets-grid");
|
||||
if (!container) return;
|
||||
|
||||
if (widgets.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-widgets">
|
||||
<span class="empty-icon">📈</span>
|
||||
<h3>No widgets yet</h3>
|
||||
<p>Add widgets to visualize your data</p>
|
||||
<button class="btn-primary" onclick="showAddWidgetModal()">
|
||||
Add Widget
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = widgets
|
||||
.map(
|
||||
(widget) => `
|
||||
<div class="widget" data-id="${widget.id}" style="grid-column: span ${widget.width || 1}; grid-row: span ${widget.height || 1};">
|
||||
<div class="widget-header">
|
||||
<h4>${escapeHtml(widget.title)}</h4>
|
||||
<div class="widget-actions">
|
||||
<button class="btn-icon btn-sm" onclick="editWidget('${widget.id}')" title="Edit">⚙️</button>
|
||||
<button class="btn-icon btn-sm" onclick="removeWidget('${widget.id}')" title="Remove">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="widget-content" id="widget-content-${widget.id}">
|
||||
${renderWidgetContent(widget)}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderWidgetContent(widget) {
|
||||
// Placeholder rendering - in production, this would render actual charts
|
||||
const icons = {
|
||||
line_chart: "📈",
|
||||
bar_chart: "📊",
|
||||
pie_chart: "🥧",
|
||||
area_chart: "📉",
|
||||
scatter_plot: "⚬",
|
||||
kpi: "🎯",
|
||||
table: "📋",
|
||||
gauge: "⏲️",
|
||||
map: "🗺️",
|
||||
text: "📝",
|
||||
};
|
||||
|
||||
return `
|
||||
<div class="widget-placeholder">
|
||||
<span class="widget-icon">${icons[widget.widget_type] || "📊"}</span>
|
||||
<span class="widget-type">${widget.widget_type}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DASHBOARD ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
async function refreshDashboard() {
|
||||
if (!state.currentDashboard) return;
|
||||
showNotification("Refreshing dashboard...", "info");
|
||||
await openDashboard(state.currentDashboard.id);
|
||||
showNotification("Dashboard refreshed", "success");
|
||||
}
|
||||
|
||||
function editDashboard() {
|
||||
if (!state.currentDashboard) return;
|
||||
state.isEditing = true;
|
||||
|
||||
const viewer = document.getElementById("dashboard-viewer");
|
||||
if (viewer) {
|
||||
viewer.classList.add("editing");
|
||||
}
|
||||
|
||||
showNotification("Edit mode enabled", "info");
|
||||
}
|
||||
|
||||
function editDashboardById(dashboardId) {
|
||||
openDashboard(dashboardId).then(() => {
|
||||
editDashboard();
|
||||
});
|
||||
}
|
||||
|
||||
function shareDashboard() {
|
||||
if (!state.currentDashboard) return;
|
||||
|
||||
const shareUrl = `${window.location.origin}/dashboards/${state.currentDashboard.id}`;
|
||||
navigator.clipboard.writeText(shareUrl).then(() => {
|
||||
showNotification("Share link copied to clipboard", "success");
|
||||
});
|
||||
}
|
||||
|
||||
function exportDashboard() {
|
||||
if (!state.currentDashboard) return;
|
||||
|
||||
// Export as JSON
|
||||
const data = JSON.stringify(state.currentDashboard, null, 2);
|
||||
const blob = new Blob([data], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.download = `${state.currentDashboard.name}.json`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showNotification("Dashboard exported", "success");
|
||||
}
|
||||
|
||||
async function duplicateDashboard(dashboardId) {
|
||||
try {
|
||||
const response = await fetch(`/api/dashboards/${dashboardId}/duplicate`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showNotification("Dashboard duplicated", "success");
|
||||
loadDashboards();
|
||||
} else {
|
||||
showNotification("Failed to duplicate dashboard", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to duplicate dashboard:", e);
|
||||
showNotification("Failed to duplicate dashboard", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDashboard(dashboardId) {
|
||||
if (!confirm("Delete this dashboard? This cannot be undone.")) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/dashboards/${dashboardId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showNotification("Dashboard deleted", "success");
|
||||
loadDashboards();
|
||||
if (state.currentDashboard?.id === dashboardId) {
|
||||
closeDashboardViewer();
|
||||
}
|
||||
} else {
|
||||
showNotification("Failed to delete dashboard", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to delete dashboard:", e);
|
||||
showNotification("Failed to delete dashboard", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DATA SOURCES
|
||||
// =============================================================================
|
||||
|
||||
async function loadDataSources() {
|
||||
try {
|
||||
const response = await fetch("/api/dashboards/data-sources");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
state.dataSources = data.data_sources || [];
|
||||
renderDataSourcesList();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load data sources:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDataSourcesList() {
|
||||
const container = document.getElementById("data-sources-list");
|
||||
if (!container) return;
|
||||
|
||||
if (state.dataSources.length === 0) {
|
||||
container.innerHTML = `
|
||||
<p class="empty-message">No data sources configured</p>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = state.dataSources
|
||||
.map(
|
||||
(source) => `
|
||||
<div class="data-source-item" data-id="${source.id}">
|
||||
<span class="source-icon">${getSourceIcon(source.source_type)}</span>
|
||||
<div class="source-info">
|
||||
<span class="source-name">${escapeHtml(source.name)}</span>
|
||||
<span class="source-type">${source.source_type}</span>
|
||||
</div>
|
||||
<button class="btn-icon btn-sm" onclick="removeDataSource('${source.id}')" title="Remove">✕</button>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function getSourceIcon(sourceType) {
|
||||
const icons = {
|
||||
postgresql: "🐘",
|
||||
mysql: "🐬",
|
||||
api: "🔌",
|
||||
csv: "📄",
|
||||
json: "📋",
|
||||
elasticsearch: "🔍",
|
||||
mongodb: "🍃",
|
||||
};
|
||||
return icons[sourceType] || "📊";
|
||||
}
|
||||
|
||||
function showAddDataSourceModal() {
|
||||
const modal = document.getElementById("addDataSourceModal");
|
||||
if (modal) {
|
||||
modal.style.display = "flex";
|
||||
const form = modal.querySelector("form");
|
||||
if (form) form.reset();
|
||||
}
|
||||
}
|
||||
|
||||
function closeAddDataSourceModal() {
|
||||
const modal = document.getElementById("addDataSourceModal");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
async function testDataSourceConnection() {
|
||||
const form = document.getElementById("addDataSourceForm");
|
||||
if (!form) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
showNotification("Testing connection...", "info");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/dashboards/data-sources/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
source_type: formData.get("source_type"),
|
||||
connection_string: formData.get("connection_string"),
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showNotification("Connection successful!", "success");
|
||||
} else {
|
||||
showNotification("Connection failed", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification("Connection test failed", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function addDataSource(formData) {
|
||||
const data = {
|
||||
name: formData.get("name"),
|
||||
source_type: formData.get("source_type"),
|
||||
connection_string: formData.get("connection_string"),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/dashboards/data-sources", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showNotification("Data source added", "success");
|
||||
closeAddDataSourceModal();
|
||||
loadDataSources();
|
||||
} else {
|
||||
showNotification("Failed to add data source", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to add data source:", e);
|
||||
showNotification("Failed to add data source", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDataSource(sourceId) {
|
||||
if (!confirm("Remove this data source?")) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/dashboards/data-sources/${sourceId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showNotification("Data source removed", "success");
|
||||
loadDataSources();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to remove data source:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WIDGETS
|
||||
// =============================================================================
|
||||
|
||||
function showAddWidgetModal() {
|
||||
const modal = document.getElementById("addWidgetModal");
|
||||
if (modal) {
|
||||
modal.style.display = "flex";
|
||||
state.selectedWidgetType = null;
|
||||
updateWidgetTypeSelection();
|
||||
}
|
||||
}
|
||||
|
||||
function closeAddWidgetModal() {
|
||||
const modal = document.getElementById("addWidgetModal");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function selectWidgetType(widgetType) {
|
||||
state.selectedWidgetType = widgetType;
|
||||
updateWidgetTypeSelection();
|
||||
}
|
||||
|
||||
function updateWidgetTypeSelection() {
|
||||
document.querySelectorAll(".widget-option").forEach((btn) => {
|
||||
btn.classList.toggle(
|
||||
"selected",
|
||||
btn.dataset.type === state.selectedWidgetType,
|
||||
);
|
||||
});
|
||||
|
||||
// Show/hide configuration section
|
||||
const configSection = document.getElementById("widget-config-section");
|
||||
if (configSection) {
|
||||
configSection.style.display = state.selectedWidgetType ? "block" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
async function addWidget(formData) {
|
||||
if (!state.currentDashboard || !state.selectedWidgetType) {
|
||||
showNotification("Please select a widget type", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
dashboard_id: state.currentDashboard.id,
|
||||
widget_type: state.selectedWidgetType,
|
||||
title: formData.get("widget_title") || "Untitled Widget",
|
||||
data_source_id: formData.get("data_source_id"),
|
||||
config: {
|
||||
width: parseInt(formData.get("width")) || 1,
|
||||
height: parseInt(formData.get("height")) || 1,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/dashboards/${state.currentDashboard.id}/widgets`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
showNotification("Widget added", "success");
|
||||
closeAddWidgetModal();
|
||||
openDashboard(state.currentDashboard.id);
|
||||
} else {
|
||||
showNotification("Failed to add widget", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to add widget:", e);
|
||||
showNotification("Failed to add widget", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function editWidget(widgetId) {
|
||||
// TODO: Implement widget editing modal
|
||||
showNotification("Widget editing coming soon", "info");
|
||||
}
|
||||
|
||||
async function removeWidget(widgetId) {
|
||||
if (!confirm("Remove this widget?")) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/dashboards/widgets/${widgetId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showNotification("Widget removed", "success");
|
||||
if (state.currentDashboard) {
|
||||
openDashboard(state.currentDashboard.id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to remove widget:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateString) {
|
||||
if (!dateString) return "Never";
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHour = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHour / 24);
|
||||
|
||||
if (diffMin < 1) return "just now";
|
||||
if (diffMin < 60) return `${diffMin}m ago`;
|
||||
if (diffHour < 24) return `${diffHour}h ago`;
|
||||
if (diffDay < 7) return `${diffDay}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
if (typeof window.showNotification === "function") {
|
||||
window.showNotification(message, type);
|
||||
} else if (typeof window.GBAlerts !== "undefined") {
|
||||
if (type === "success") window.GBAlerts.success("Dashboards", message);
|
||||
else if (type === "error") window.GBAlerts.error("Dashboards", message);
|
||||
else window.GBAlerts.info("Dashboards", message);
|
||||
} else {
|
||||
console.log(`[${type}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT TO WINDOW
|
||||
// =============================================================================
|
||||
|
||||
// Create Dashboard Modal
|
||||
window.showCreateDashboardModal = showCreateDashboardModal;
|
||||
window.closeCreateDashboardModal = closeCreateDashboardModal;
|
||||
|
||||
// Dashboard Viewer
|
||||
window.openDashboard = openDashboard;
|
||||
window.closeDashboardViewer = closeDashboardViewer;
|
||||
window.refreshDashboard = refreshDashboard;
|
||||
window.editDashboard = editDashboard;
|
||||
window.editDashboardById = editDashboardById;
|
||||
window.shareDashboard = shareDashboard;
|
||||
window.exportDashboard = exportDashboard;
|
||||
window.duplicateDashboard = duplicateDashboard;
|
||||
window.deleteDashboard = deleteDashboard;
|
||||
|
||||
// Data Sources
|
||||
window.showAddDataSourceModal = showAddDataSourceModal;
|
||||
window.closeAddDataSourceModal = closeAddDataSourceModal;
|
||||
window.testDataSourceConnection = testDataSourceConnection;
|
||||
window.removeDataSource = removeDataSource;
|
||||
|
||||
// Widgets
|
||||
window.showAddWidgetModal = showAddWidgetModal;
|
||||
window.closeAddWidgetModal = closeAddWidgetModal;
|
||||
window.selectWidgetType = selectWidgetType;
|
||||
window.editWidget = editWidget;
|
||||
window.removeWidget = removeWidget;
|
||||
|
||||
// =============================================================================
|
||||
// INITIALIZE
|
||||
// =============================================================================
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
|
@ -1747,6 +1747,11 @@
|
|||
const toolboxItems = document.querySelectorAll('.toolbox-item');
|
||||
const canvas = document.getElementById('canvas-inner');
|
||||
|
||||
if (!canvas) {
|
||||
console.warn('initDragAndDrop: canvas-inner not found');
|
||||
return;
|
||||
}
|
||||
|
||||
toolboxItems.forEach(item => {
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('nodeType', item.dataset.nodeType);
|
||||
|
|
@ -1779,6 +1784,11 @@
|
|||
const canvas = document.getElementById('canvas');
|
||||
const container = document.getElementById('canvas-container');
|
||||
|
||||
if (!canvas || !container) {
|
||||
console.warn('initCanvasInteraction: canvas or canvas-container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Pan with middle mouse or space+drag
|
||||
let isPanning = false;
|
||||
let panStart = { x: 0, y: 0 };
|
||||
|
|
@ -2295,6 +2305,11 @@
|
|||
const canvas = document.getElementById('canvas');
|
||||
const contextMenu = document.getElementById('context-menu');
|
||||
|
||||
if (!canvas || !contextMenu) {
|
||||
console.warn('initContextMenu: canvas or context-menu not found');
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
const nodeEl = e.target.closest('.node');
|
||||
|
|
@ -2312,7 +2327,10 @@
|
|||
}
|
||||
|
||||
function hideContextMenu() {
|
||||
document.getElementById('context-menu').classList.remove('visible');
|
||||
const menu = document.getElementById('context-menu');
|
||||
if (menu) {
|
||||
menu.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
// Context Menu Actions
|
||||
|
|
@ -2353,14 +2371,20 @@
|
|||
|
||||
// Modal Management
|
||||
function showModal(id) {
|
||||
document.getElementById(id).classList.add('visible');
|
||||
if (id === 'open-modal') {
|
||||
htmx.trigger('#file-list-content', 'load');
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) {
|
||||
modal.classList.add('visible');
|
||||
if (id === 'open-modal') {
|
||||
htmx.trigger('#file-list-content', 'load');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideModal(id) {
|
||||
document.getElementById(id).classList.remove('visible');
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) {
|
||||
modal.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
// Save Design
|
||||
|
|
@ -2579,7 +2603,10 @@
|
|||
}
|
||||
|
||||
function hideMagicPanel() {
|
||||
document.getElementById('magic-panel').classList.remove('visible');
|
||||
const panel = document.getElementById('magic-panel');
|
||||
if (panel) {
|
||||
panel.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
async function analyzeMagicSuggestions() {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,21 @@
|
|||
<link rel="stylesheet" href="docs/docs.css" />
|
||||
|
||||
<div class="docs-app" id="docs-app">
|
||||
<div class="docs-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<input
|
||||
type="text"
|
||||
class="doc-name-input"
|
||||
id="docName"
|
||||
value="Untitled Document"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<input type="text" class="doc-name-input" id="docName" value="Untitled Document" spellcheck="false" />
|
||||
</div>
|
||||
<div class="toolbar-center">
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="printPreviewBtn"
|
||||
title="Print Preview"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="printPreviewBtn" title="Print Preview">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 6 2 18 2 18 9"></polyline>
|
||||
<path
|
||||
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
|
||||
></path>
|
||||
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
|
||||
<rect x="6" y="14" width="12" height="8"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="findReplaceBtn"
|
||||
title="Find & Replace (Ctrl+H)"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="findReplaceBtn" title="Find & Replace (Ctrl+H)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
|
|
@ -52,42 +24,21 @@
|
|||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button class="btn-icon" id="undoBtn" title="Undo (Ctrl+Z)">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 7v6h6"></path>
|
||||
<path
|
||||
d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"
|
||||
></path>
|
||||
<path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="redoBtn" title="Redo (Ctrl+Y)">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 7v6h-6"></path>
|
||||
<path
|
||||
d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"
|
||||
></path>
|
||||
<path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<select
|
||||
class="toolbar-select heading-select"
|
||||
id="headingSelect"
|
||||
>
|
||||
<select class="toolbar-select heading-select" id="headingSelect">
|
||||
<option value="p">Normal</option>
|
||||
<option value="h1">Heading 1</option>
|
||||
<option value="h2">Heading 2</option>
|
||||
|
|
@ -118,11 +69,7 @@
|
|||
<button class="btn-icon" id="italicBtn" title="Italic (Ctrl+I)">
|
||||
<em>I</em>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="underlineBtn"
|
||||
title="Underline (Ctrl+U)"
|
||||
>
|
||||
<button class="btn-icon" id="underlineBtn" title="Underline (Ctrl+U)">
|
||||
<u>U</u>
|
||||
</button>
|
||||
<button class="btn-icon" id="strikeBtn" title="Strikethrough">
|
||||
|
|
@ -131,107 +78,45 @@
|
|||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="btn-icon color-btn"
|
||||
id="textColorBtn"
|
||||
title="Text Color"
|
||||
>
|
||||
<button class="btn-icon color-btn" id="textColorBtn" title="Text Color">
|
||||
<span class="color-letter">A</span>
|
||||
<span
|
||||
class="color-indicator"
|
||||
id="textColorIndicator"
|
||||
></span>
|
||||
<input
|
||||
type="color"
|
||||
class="color-input"
|
||||
id="textColorInput"
|
||||
value="#000000"
|
||||
/>
|
||||
<span class="color-indicator" id="textColorIndicator"></span>
|
||||
<input type="color" class="color-input" id="textColorInput" value="#000000" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon color-btn"
|
||||
id="highlightBtn"
|
||||
title="Highlight"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<button class="btn-icon color-btn" id="highlightBtn" title="Highlight">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M15.243 4.515l-6.738 6.737-.707 2.121-1.04 1.041 2.828 2.828 1.04-1.04 2.122-.707 6.737-6.738-4.242-4.242zm6.364 3.536a1 1 0 0 1 0 1.414l-7.778 7.778-2.122.707-1.414 1.414a1 1 0 0 1-1.414 0l-4.243-4.243a1 1 0 0 1 0-1.414l1.414-1.414.707-2.121 7.778-7.778a1 1 0 0 1 1.414 0l5.657 5.657z"
|
||||
/>
|
||||
d="M15.243 4.515l-6.738 6.737-.707 2.121-1.04 1.041 2.828 2.828 1.04-1.04 2.122-.707 6.737-6.738-4.242-4.242zm6.364 3.536a1 1 0 0 1 0 1.414l-7.778 7.778-2.122.707-1.414 1.414a1 1 0 0 1-1.414 0l-4.243-4.243a1 1 0 0 1 0-1.414l1.414-1.414.707-2.121 7.778-7.778a1 1 0 0 1 1.414 0l5.657 5.657z" />
|
||||
</svg>
|
||||
<span
|
||||
class="color-indicator bg-indicator"
|
||||
id="highlightIndicator"
|
||||
></span>
|
||||
<input
|
||||
type="color"
|
||||
class="color-input"
|
||||
id="highlightInput"
|
||||
value="#ffff00"
|
||||
/>
|
||||
<span class="color-indicator bg-indicator" id="highlightIndicator"></span>
|
||||
<input type="color" class="color-input" id="highlightInput" value="#ffff00" />
|
||||
</button>
|
||||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button class="btn-icon" id="alignLeftBtn" title="Align Left">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="12" x2="15" y2="12"></line>
|
||||
<line x1="3" y1="18" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="alignCenterBtn"
|
||||
title="Align Center"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="alignCenterBtn" title="Align Center">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="6" y1="12" x2="18" y2="12"></line>
|
||||
<line x1="4" y1="18" x2="20" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="alignRightBtn" title="Align Right">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="9" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="6" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="justifyBtn" title="Justify">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
|
|
@ -241,110 +126,41 @@
|
|||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button class="btn-icon" id="bulletListBtn" title="Bullet List">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="9" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="9" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="9" y1="18" x2="21" y2="18"></line>
|
||||
<circle
|
||||
cx="4"
|
||||
cy="6"
|
||||
r="2"
|
||||
fill="currentColor"
|
||||
></circle>
|
||||
<circle
|
||||
cx="4"
|
||||
cy="12"
|
||||
r="2"
|
||||
fill="currentColor"
|
||||
></circle>
|
||||
<circle
|
||||
cx="4"
|
||||
cy="18"
|
||||
r="2"
|
||||
fill="currentColor"
|
||||
></circle>
|
||||
<circle cx="4" cy="6" r="2" fill="currentColor"></circle>
|
||||
<circle cx="4" cy="12" r="2" fill="currentColor"></circle>
|
||||
<circle cx="4" cy="18" r="2" fill="currentColor"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="numberListBtn"
|
||||
title="Numbered List"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="numberListBtn" title="Numbered List">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="10" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="10" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="10" y1="18" x2="21" y2="18"></line>
|
||||
<text
|
||||
x="2"
|
||||
y="8"
|
||||
font-size="8"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
>
|
||||
<text x="2" y="8" font-size="8" fill="currentColor" stroke="none">
|
||||
1
|
||||
</text>
|
||||
<text
|
||||
x="2"
|
||||
y="14"
|
||||
font-size="8"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
>
|
||||
<text x="2" y="14" font-size="8" fill="currentColor" stroke="none">
|
||||
2
|
||||
</text>
|
||||
<text
|
||||
x="2"
|
||||
y="20"
|
||||
font-size="8"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
>
|
||||
<text x="2" y="20" font-size="8" fill="currentColor" stroke="none">
|
||||
3
|
||||
</text>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="indentBtn" title="Increase Indent">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="9" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="9" y1="18" x2="21" y2="18"></line>
|
||||
<polyline points="3 10 6 12 3 14"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="outdentBtn"
|
||||
title="Decrease Indent"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="outdentBtn" title="Decrease Indent">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="9" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="9" y1="18" x2="21" y2="18"></line>
|
||||
|
|
@ -354,87 +170,33 @@
|
|||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="headerFooterBtn"
|
||||
title="Edit Header & Footer"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="headerFooterBtn" title="Edit Header & Footer">
|
||||
<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"></rect>
|
||||
<line x1="3" y1="7" x2="21" y2="7"></line>
|
||||
<line x1="3" y1="17" x2="21" y2="17"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="pageBreakBtn"
|
||||
title="Insert Page Break"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="pageBreakBtn" title="Insert Page Break">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h4l3-9 4 18 3-9h4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="linkBtn" title="Insert Link">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"
|
||||
></path>
|
||||
<path
|
||||
d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
|
||||
></path>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="imageBtn" title="Insert Image">
|
||||
<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 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>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="tableBtn" title="Insert Table">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<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"></rect>
|
||||
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||
<line x1="3" y1="15" x2="21" y2="15"></line>
|
||||
|
|
@ -447,14 +209,7 @@
|
|||
<div class="toolbar-right">
|
||||
<div class="collaborators" id="collaborators"></div>
|
||||
<button class="btn-primary" id="shareBtn">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="18" cy="5" r="3"></circle>
|
||||
<circle cx="6" cy="12" r="3"></circle>
|
||||
<circle cx="18" cy="19" r="3"></circle>
|
||||
|
|
@ -463,139 +218,19 @@
|
|||
</svg>
|
||||
<span>Share</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon chat-toggle"
|
||||
id="chatToggle"
|
||||
title="Toggle AI Chat"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
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"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="docs-main">
|
||||
<div class="docs-canvas">
|
||||
<div class="editor-page" id="editorPage">
|
||||
<div
|
||||
class="editor-content"
|
||||
id="editorContent"
|
||||
contenteditable="true"
|
||||
spellcheck="true"
|
||||
data-placeholder="Start typing..."
|
||||
></div>
|
||||
<div class="editor-content" id="editorContent" contenteditable="true" spellcheck="true"
|
||||
data-placeholder="Start typing..."></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="chat-panel" id="chatPanel">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">
|
||||
<div class="chat-avatar">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"
|
||||
/>
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
<path
|
||||
d="M12 15c-3 0-5 1.5-5 3v1h10v-1c0-1.5-2-3-5-3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3>AI Assistant</h3>
|
||||
<span class="chat-status">Ready to help</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="chat-close" id="chatClose" title="Close">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
<div class="chat-message assistant">
|
||||
<div class="message-bubble">
|
||||
<p>
|
||||
Hi! I can help you edit your document. Try asking me
|
||||
to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Make text shorter or longer</li>
|
||||
<li>Fix grammar and spelling</li>
|
||||
<li>Translate to another language</li>
|
||||
<li>Change the tone (formal/casual)</li>
|
||||
<li>Add a summary</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-suggestions" id="chatSuggestions">
|
||||
<button class="suggestion-btn" data-action="shorter">
|
||||
Make shorter
|
||||
</button>
|
||||
<button class="suggestion-btn" data-action="grammar">
|
||||
Fix grammar
|
||||
</button>
|
||||
<button class="suggestion-btn" data-action="formal">
|
||||
Make formal
|
||||
</button>
|
||||
<button class="suggestion-btn" data-action="summarize">
|
||||
Summarize
|
||||
</button>
|
||||
</div>
|
||||
<form class="chat-input-container" id="chatForm">
|
||||
<input
|
||||
type="text"
|
||||
class="chat-input"
|
||||
id="chatInput"
|
||||
placeholder="Ask me to edit your document..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="chat-send"
|
||||
id="chatSend"
|
||||
title="Send"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="docs-status-bar">
|
||||
|
|
@ -626,11 +261,7 @@
|
|||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="share-input-group">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter email address"
|
||||
id="shareEmail"
|
||||
/>
|
||||
<input type="email" placeholder="Enter email address" id="shareEmail" />
|
||||
<select id="sharePermission">
|
||||
<option value="view">Can view</option>
|
||||
<option value="comment">Can comment</option>
|
||||
|
|
@ -643,24 +274,10 @@
|
|||
<div class="share-link-group">
|
||||
<input type="text" readonly id="shareLink" />
|
||||
<button class="btn-icon" id="copyLinkBtn" title="Copy link">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="9"
|
||||
y="9"
|
||||
width="13"
|
||||
height="13"
|
||||
rx="2"
|
||||
></rect>
|
||||
<path
|
||||
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
|
||||
></path>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -678,11 +295,7 @@
|
|||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>URL</label>
|
||||
<input
|
||||
type="url"
|
||||
id="linkUrl"
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
<input type="url" id="linkUrl" placeholder="https://example.com" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Text (optional)</label>
|
||||
|
|
@ -705,19 +318,11 @@
|
|||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Image URL</label>
|
||||
<input
|
||||
type="url"
|
||||
id="imageUrl"
|
||||
placeholder="https://example.com/image.jpg"
|
||||
/>
|
||||
<input type="url" id="imageUrl" placeholder="https://example.com/image.jpg" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Alt text</label>
|
||||
<input
|
||||
type="text"
|
||||
id="imageAlt"
|
||||
placeholder="Image description"
|
||||
/>
|
||||
<input type="text" id="imageAlt" placeholder="Image description" />
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-secondary" id="cancelImageBtn">
|
||||
|
|
@ -740,23 +345,11 @@
|
|||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Rows</label>
|
||||
<input
|
||||
type="number"
|
||||
id="tableRows"
|
||||
value="3"
|
||||
min="1"
|
||||
max="20"
|
||||
/>
|
||||
<input type="number" id="tableRows" value="3" min="1" max="20" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Columns</label>
|
||||
<input
|
||||
type="number"
|
||||
id="tableCols"
|
||||
value="3"
|
||||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
<input type="number" id="tableCols" value="3" min="1" max="10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -780,19 +373,9 @@
|
|||
<div class="export-options">
|
||||
<button class="export-option" data-format="pdf">
|
||||
<div class="export-icon">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<path d="M9 15v-2h1.5a1.5 1.5 0 0 1 0 3H9" />
|
||||
<path d="M13 13h1.5a1.5 1.5 0 0 1 0 3H13v-3z" />
|
||||
|
|
@ -802,19 +385,9 @@
|
|||
</button>
|
||||
<button class="export-option" data-format="docx">
|
||||
<div class="export-icon">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="8" y1="13" x2="16" y2="13" />
|
||||
<line x1="8" y1="17" x2="14" y2="17" />
|
||||
|
|
@ -824,40 +397,21 @@
|
|||
</button>
|
||||
<button class="export-option" data-format="html">
|
||||
<div class="export-icon">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="2" y1="12" x2="22" y2="12" />
|
||||
<path
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||
/>
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="export-label">HTML</div>
|
||||
</button>
|
||||
<button class="export-option" data-format="txt">
|
||||
<div class="export-icon">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
</div>
|
||||
|
|
@ -865,19 +419,9 @@
|
|||
</button>
|
||||
<button class="export-option" data-format="md">
|
||||
<div class="export-icon">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<path d="M7 15l2-2 2 2" />
|
||||
<line x1="9" y1="13" x2="9" y2="17" />
|
||||
|
|
@ -901,20 +445,11 @@
|
|||
<div class="modal-body">
|
||||
<div class="find-replace-group">
|
||||
<label>Find:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="findInput"
|
||||
placeholder="Search text..."
|
||||
autofocus
|
||||
/>
|
||||
<input type="text" id="findInput" placeholder="Search text..." autofocus />
|
||||
</div>
|
||||
<div class="find-replace-group">
|
||||
<label>Replace:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="replaceInput"
|
||||
placeholder="Replace with..."
|
||||
/>
|
||||
<input type="text" id="replaceInput" placeholder="Replace with..." />
|
||||
</div>
|
||||
<div class="find-replace-options">
|
||||
<label class="checkbox-label">
|
||||
|
|
@ -974,18 +509,9 @@
|
|||
<div class="modal-actions">
|
||||
<button class="btn-secondary" id="cancelPrintBtn">Cancel</button>
|
||||
<button class="btn-primary" id="printBtn">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 6 2 18 2 18 9"></polyline>
|
||||
<path
|
||||
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
|
||||
></path>
|
||||
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
|
||||
<rect x="6" y="14" width="12" height="8"></rect>
|
||||
</svg>
|
||||
Print
|
||||
|
|
@ -1008,12 +534,8 @@
|
|||
<div class="hf-tab-content active" id="hfHeaderTab">
|
||||
<div class="form-group">
|
||||
<label>Header Content:</label>
|
||||
<div
|
||||
class="hf-editor"
|
||||
id="headerEditor"
|
||||
contenteditable="true"
|
||||
data-placeholder="Enter header text..."
|
||||
></div>
|
||||
<div class="hf-editor" id="headerEditor" contenteditable="true"
|
||||
data-placeholder="Enter header text..."></div>
|
||||
</div>
|
||||
<div class="hf-options">
|
||||
<label class="checkbox-label">
|
||||
|
|
@ -1034,10 +556,7 @@
|
|||
<button class="btn-secondary btn-sm" id="insertDate">
|
||||
Date
|
||||
</button>
|
||||
<button
|
||||
class="btn-secondary btn-sm"
|
||||
id="insertDocTitle"
|
||||
>
|
||||
<button class="btn-secondary btn-sm" id="insertDocTitle">
|
||||
Document Title
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1046,12 +565,8 @@
|
|||
<div class="hf-tab-content" id="hfFooterTab">
|
||||
<div class="form-group">
|
||||
<label>Footer Content:</label>
|
||||
<div
|
||||
class="hf-editor"
|
||||
id="footerEditor"
|
||||
contenteditable="true"
|
||||
data-placeholder="Enter footer text..."
|
||||
></div>
|
||||
<div class="hf-editor" id="footerEditor" contenteditable="true"
|
||||
data-placeholder="Enter footer text..."></div>
|
||||
</div>
|
||||
<div class="hf-options">
|
||||
<label class="checkbox-label">
|
||||
|
|
@ -1066,22 +581,13 @@
|
|||
<div class="hf-insert-options">
|
||||
<label>Insert:</label>
|
||||
<div class="hf-insert-btns">
|
||||
<button
|
||||
class="btn-secondary btn-sm"
|
||||
id="insertFooterPageNum"
|
||||
>
|
||||
<button class="btn-secondary btn-sm" id="insertFooterPageNum">
|
||||
Page Number
|
||||
</button>
|
||||
<button
|
||||
class="btn-secondary btn-sm"
|
||||
id="insertFooterDate"
|
||||
>
|
||||
<button class="btn-secondary btn-sm" id="insertFooterDate">
|
||||
Date
|
||||
</button>
|
||||
<button
|
||||
class="btn-secondary btn-sm"
|
||||
id="insertFooterDocTitle"
|
||||
>
|
||||
<button class="btn-secondary btn-sm" id="insertFooterDocTitle">
|
||||
Document Title
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1102,4 +608,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="docs/docs.js"></script>
|
||||
<script src="docs/docs.js"></script>
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
autoSaveTimer: null,
|
||||
ws: null,
|
||||
collaborators: [],
|
||||
chatPanelOpen: true,
|
||||
|
||||
driveSource: null,
|
||||
zoom: 100,
|
||||
findMatches: [],
|
||||
|
|
@ -47,10 +47,7 @@
|
|||
elements.charCount = document.getElementById("charCount");
|
||||
elements.saveStatus = document.getElementById("saveStatus");
|
||||
elements.zoomLevel = document.getElementById("zoomLevel");
|
||||
elements.chatPanel = document.getElementById("chatPanel");
|
||||
elements.chatMessages = document.getElementById("chatMessages");
|
||||
elements.chatInput = document.getElementById("chatInput");
|
||||
elements.chatForm = document.getElementById("chatForm");
|
||||
|
||||
elements.shareModal = document.getElementById("shareModal");
|
||||
elements.linkModal = document.getElementById("linkModal");
|
||||
elements.imageModal = document.getElementById("imageModal");
|
||||
|
|
@ -161,19 +158,7 @@
|
|||
document.getElementById("zoomInBtn")?.addEventListener("click", zoomIn);
|
||||
document.getElementById("zoomOutBtn")?.addEventListener("click", zoomOut);
|
||||
|
||||
document
|
||||
.getElementById("chatToggle")
|
||||
?.addEventListener("click", toggleChatPanel);
|
||||
document
|
||||
.getElementById("chatClose")
|
||||
?.addEventListener("click", toggleChatPanel);
|
||||
elements.chatForm?.addEventListener("submit", handleChatSubmit);
|
||||
|
||||
document.querySelectorAll(".suggestion-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () =>
|
||||
handleSuggestionClick(btn.dataset.action),
|
||||
);
|
||||
});
|
||||
|
||||
document.querySelectorAll(".btn-close, .modal").forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
|
|
|
|||
296
ui/suite/drive/drive-sentient.js
Normal file
296
ui/suite/drive/drive-sentient.js
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
(function () {
|
||||
"use strict";
|
||||
|
||||
let currentView = "grid";
|
||||
let selectedFile = null;
|
||||
let aiPanelOpen = true;
|
||||
|
||||
function toggleView(type) {
|
||||
currentView = type;
|
||||
const fileView = document.getElementById("file-view");
|
||||
if (fileView) {
|
||||
fileView.classList.remove("grid-view", "list-view");
|
||||
fileView.classList.add(type + "-view");
|
||||
}
|
||||
document.querySelectorAll(".app-btn-secondary").forEach((btn) => {
|
||||
btn.classList.remove("active");
|
||||
});
|
||||
event.target.classList.add("active");
|
||||
}
|
||||
|
||||
function openFolder(el) {
|
||||
const folderName = el.querySelector(".file-name").textContent;
|
||||
const breadcrumb = document.querySelector(".breadcrumb");
|
||||
if (breadcrumb) {
|
||||
const separator = document.createElement("span");
|
||||
separator.className = "breadcrumb-separator";
|
||||
separator.textContent = "›";
|
||||
breadcrumb.appendChild(separator);
|
||||
|
||||
const item = document.createElement("span");
|
||||
item.className = "breadcrumb-item current";
|
||||
item.textContent = folderName;
|
||||
breadcrumb.appendChild(item);
|
||||
|
||||
breadcrumb.querySelectorAll(".breadcrumb-item").forEach((i) => {
|
||||
i.classList.remove("current");
|
||||
});
|
||||
item.classList.add("current");
|
||||
}
|
||||
addAIMessage("assistant", `Opened folder: ${folderName}`);
|
||||
}
|
||||
|
||||
function selectFile(el) {
|
||||
document.querySelectorAll(".file-item").forEach((item) => {
|
||||
item.classList.remove("selected");
|
||||
});
|
||||
el.classList.add("selected");
|
||||
selectedFile = {
|
||||
name: el.querySelector(".file-name").textContent,
|
||||
meta: el.querySelector(".file-meta").textContent,
|
||||
};
|
||||
}
|
||||
|
||||
function toggleAIPanel() {
|
||||
const panel = document.getElementById("ai-panel");
|
||||
if (panel) {
|
||||
aiPanelOpen = !aiPanelOpen;
|
||||
panel.classList.toggle("hidden", !aiPanelOpen);
|
||||
}
|
||||
const toggle = document.querySelector(".ai-toggle");
|
||||
if (toggle) {
|
||||
toggle.classList.toggle("active", aiPanelOpen);
|
||||
}
|
||||
}
|
||||
|
||||
function aiAction(action) {
|
||||
const actions = {
|
||||
organize: "Analyzing folder structure to suggest organization...",
|
||||
find: "What file are you looking for?",
|
||||
analyze: "Select a file and I'll analyze its content.",
|
||||
share: "Select a file to set up sharing options.",
|
||||
};
|
||||
addAIMessage("assistant", actions[action] || "How can I help?");
|
||||
}
|
||||
|
||||
function sendAIMessage() {
|
||||
const input = document.getElementById("ai-input");
|
||||
if (!input || !input.value.trim()) return;
|
||||
|
||||
const message = input.value.trim();
|
||||
addAIMessage("user", message);
|
||||
input.value = "";
|
||||
|
||||
setTimeout(() => {
|
||||
processAIQuery(message);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function addAIMessage(type, content) {
|
||||
const container = document.getElementById("ai-messages");
|
||||
if (!container) return;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "ai-message " + type;
|
||||
div.innerHTML = '<div class="ai-message-bubble">' + escapeHtml(content) + "</div>";
|
||||
container.appendChild(div);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function processAIQuery(query) {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
let response = "I can help you manage your files. Try asking me to find, organize, or analyze files.";
|
||||
|
||||
if (lowerQuery.includes("find") || lowerQuery.includes("search") || lowerQuery.includes("buscar")) {
|
||||
response = "I'll search for files matching your query. What type of file are you looking for?";
|
||||
} else if (lowerQuery.includes("organize") || lowerQuery.includes("organizar")) {
|
||||
response = "I can help organize your files by type, date, or project. Which method would you prefer?";
|
||||
} else if (lowerQuery.includes("share") || lowerQuery.includes("compartilhar")) {
|
||||
if (selectedFile) {
|
||||
response = `Setting up sharing for "${selectedFile.name}". Who would you like to share it with?`;
|
||||
} else {
|
||||
response = "Please select a file first, then I can help you share it.";
|
||||
}
|
||||
} else if (lowerQuery.includes("delete") || lowerQuery.includes("excluir")) {
|
||||
if (selectedFile) {
|
||||
response = `Are you sure you want to delete "${selectedFile.name}"? This action cannot be undone.`;
|
||||
} else {
|
||||
response = "Please select a file first before deleting.";
|
||||
}
|
||||
} else if (lowerQuery.includes("storage") || lowerQuery.includes("space") || lowerQuery.includes("espaço")) {
|
||||
response = "You're using 12.4 GB of your 50 GB storage. Would you like me to find large files to free up space?";
|
||||
}
|
||||
|
||||
addAIMessage("assistant", response);
|
||||
}
|
||||
|
||||
function uploadFile() {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.onchange = function (e) {
|
||||
const files = Array.from(e.target.files);
|
||||
if (files.length > 0) {
|
||||
const names = files.map((f) => f.name).join(", ");
|
||||
addAIMessage("assistant", `Uploading ${files.length} file(s): ${names}`);
|
||||
simulateUpload(files);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
function simulateUpload(files) {
|
||||
setTimeout(() => {
|
||||
addAIMessage("assistant", `Successfully uploaded ${files.length} file(s)!`);
|
||||
files.forEach((file) => {
|
||||
addFileToView(file.name, formatFileSize(file.size));
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function addFileToView(name, size) {
|
||||
const fileView = document.getElementById("file-view");
|
||||
if (!fileView) return;
|
||||
|
||||
const icon = getFileIcon(name);
|
||||
const div = document.createElement("div");
|
||||
div.className = "file-item";
|
||||
div.onclick = function () {
|
||||
selectFile(this);
|
||||
};
|
||||
div.innerHTML = `
|
||||
<div class="file-icon">${icon}</div>
|
||||
<div class="file-name">${escapeHtml(name)}</div>
|
||||
<div class="file-meta">${size}</div>
|
||||
`;
|
||||
fileView.appendChild(div);
|
||||
}
|
||||
|
||||
function getFileIcon(filename) {
|
||||
const ext = filename.split(".").pop().toLowerCase();
|
||||
const icons = {
|
||||
pdf: "📄",
|
||||
doc: "📝",
|
||||
docx: "📝",
|
||||
xls: "📊",
|
||||
xlsx: "📊",
|
||||
ppt: "📽️",
|
||||
pptx: "📽️",
|
||||
jpg: "🖼️",
|
||||
jpeg: "🖼️",
|
||||
png: "🖼️",
|
||||
gif: "🖼️",
|
||||
mp4: "🎬",
|
||||
mov: "🎬",
|
||||
mp3: "🎵",
|
||||
wav: "🎵",
|
||||
zip: "📦",
|
||||
rar: "📦",
|
||||
txt: "📝",
|
||||
md: "📝",
|
||||
js: "💻",
|
||||
ts: "💻",
|
||||
rs: "💻",
|
||||
py: "💻",
|
||||
};
|
||||
return icons[ext] || "📄";
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + " GB";
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function initKeyboardShortcuts() {
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
|
||||
|
||||
if (e.key === "Delete" && selectedFile) {
|
||||
addAIMessage("assistant", `Delete "${selectedFile.name}"? Press Delete again to confirm.`);
|
||||
}
|
||||
if (e.ctrlKey && e.key === "u") {
|
||||
e.preventDefault();
|
||||
uploadFile();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
document.querySelectorAll(".file-item").forEach((item) => {
|
||||
item.classList.remove("selected");
|
||||
});
|
||||
selectedFile = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initSearch() {
|
||||
const searchInput = document.querySelector(".search-input");
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", function (e) {
|
||||
const query = e.target.value.toLowerCase();
|
||||
document.querySelectorAll(".file-item").forEach((item) => {
|
||||
const name = item.querySelector(".file-name").textContent.toLowerCase();
|
||||
item.style.display = name.includes(query) ? "" : "none";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
document.querySelectorAll(".topbar-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", function () {
|
||||
document.querySelectorAll(".topbar-tab").forEach((t) => t.classList.remove("active"));
|
||||
this.classList.add("active");
|
||||
const tabName = this.textContent.trim();
|
||||
addAIMessage("assistant", `Switched to ${tabName} view.`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initAppLauncher() {
|
||||
document.querySelectorAll(".app-icon").forEach((icon) => {
|
||||
icon.addEventListener("click", function () {
|
||||
const app = this.dataset.app;
|
||||
if (app && app !== "drive") {
|
||||
window.location.href = "/suite/" + app + "/";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
initKeyboardShortcuts();
|
||||
initSearch();
|
||||
initTabs();
|
||||
initAppLauncher();
|
||||
|
||||
const aiInput = document.getElementById("ai-input");
|
||||
if (aiInput) {
|
||||
aiInput.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
sendAIMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.toggleView = toggleView;
|
||||
window.openFolder = openFolder;
|
||||
window.selectFile = selectFile;
|
||||
window.toggleAIPanel = toggleAIPanel;
|
||||
window.aiAction = aiAction;
|
||||
window.sendAIMessage = sendAIMessage;
|
||||
window.uploadFile = uploadFile;
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
|
@ -1214,6 +1214,140 @@
|
|||
loadFiles(currentPath, currentBucket);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MISSING FUNCTIONS FOR HTML ONCLICK HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
function toggleView(type) {
|
||||
setView(type);
|
||||
}
|
||||
|
||||
function setView(type) {
|
||||
const gridBtn = document.getElementById("grid-view-btn");
|
||||
const listBtn = document.getElementById("list-view-btn");
|
||||
const fileGrid = document.getElementById("file-grid");
|
||||
const fileList = document.getElementById("file-list");
|
||||
const fileView = document.getElementById("file-view");
|
||||
|
||||
if (type === "grid") {
|
||||
gridBtn?.classList.add("active");
|
||||
listBtn?.classList.remove("active");
|
||||
if (fileGrid) fileGrid.style.display = "grid";
|
||||
if (fileList) fileList.style.display = "none";
|
||||
if (fileView) fileView.className = "file-grid";
|
||||
} else {
|
||||
gridBtn?.classList.remove("active");
|
||||
listBtn?.classList.add("active");
|
||||
if (fileGrid) fileGrid.style.display = "none";
|
||||
if (fileList) fileList.style.display = "block";
|
||||
if (fileView) fileView.className = "file-list";
|
||||
}
|
||||
}
|
||||
|
||||
function openFolder(el) {
|
||||
const path =
|
||||
el?.dataset?.path || el?.querySelector(".file-name")?.textContent;
|
||||
if (path) {
|
||||
currentPath = path.startsWith("/") ? path : currentPath + "/" + path;
|
||||
loadFiles(currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
function selectFile(el) {
|
||||
const path = el?.dataset?.path;
|
||||
if (path) {
|
||||
toggleSelection(path);
|
||||
el.classList.toggle("selected", selectedFiles.has(path));
|
||||
} else {
|
||||
// Toggle visual selection
|
||||
document.querySelectorAll(".file-item.selected").forEach((item) => {
|
||||
if (item !== el) item.classList.remove("selected");
|
||||
});
|
||||
el.classList.toggle("selected");
|
||||
}
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
function setActiveNav(el) {
|
||||
document.querySelectorAll(".nav-item").forEach((item) => {
|
||||
item.classList.remove("active");
|
||||
});
|
||||
el.classList.add("active");
|
||||
}
|
||||
|
||||
function toggleInfoPanel() {
|
||||
const panel =
|
||||
document.getElementById("info-panel") ||
|
||||
document.getElementById("details-panel");
|
||||
if (panel) {
|
||||
panel.classList.toggle("open");
|
||||
panel.classList.toggle("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAIPanel() {
|
||||
const panel =
|
||||
document.getElementById("ai-panel") ||
|
||||
document.querySelector(".ai-panel");
|
||||
if (panel) {
|
||||
panel.classList.toggle("open");
|
||||
}
|
||||
}
|
||||
|
||||
function aiAction(action) {
|
||||
const messages = {
|
||||
organize:
|
||||
"I'll help you organize your files. What folder would you like to organize?",
|
||||
find: "What file are you looking for?",
|
||||
analyze: "Select a file and I'll analyze its contents.",
|
||||
share: "Select files to share. Who would you like to share with?",
|
||||
};
|
||||
addAIMessage("assistant", messages[action] || "How can I help you?");
|
||||
}
|
||||
|
||||
function sendAIMessage() {
|
||||
const input = document.getElementById("ai-input");
|
||||
if (!input || !input.value.trim()) return;
|
||||
|
||||
const message = input.value.trim();
|
||||
input.value = "";
|
||||
|
||||
addAIMessage("user", message);
|
||||
// Simulate AI response
|
||||
setTimeout(() => {
|
||||
addAIMessage("assistant", `Processing your request: "${message}"`);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function addAIMessage(type, content) {
|
||||
const container =
|
||||
document.getElementById("ai-messages") ||
|
||||
document.querySelector(".ai-messages");
|
||||
if (!container) return;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = `ai-message ${type}`;
|
||||
div.innerHTML = `<div class="ai-message-bubble">${content}</div>`;
|
||||
container.appendChild(div);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function updateSelectionUI() {
|
||||
const count = selectedFiles.size;
|
||||
const bulkActions = document.getElementById("bulk-actions");
|
||||
if (bulkActions) {
|
||||
bulkActions.style.display = count > 0 ? "flex" : "none";
|
||||
}
|
||||
const countEl = document.getElementById("selection-count");
|
||||
if (countEl) {
|
||||
countEl.textContent = `${count} selected`;
|
||||
}
|
||||
}
|
||||
|
||||
function uploadFile() {
|
||||
triggerUpload();
|
||||
}
|
||||
|
||||
window.DriveModule = {
|
||||
init,
|
||||
loadFiles,
|
||||
|
|
@ -1238,6 +1372,18 @@
|
|||
navigateUp,
|
||||
};
|
||||
|
||||
// Export functions for HTML onclick handlers
|
||||
window.toggleView = toggleView;
|
||||
window.setView = setView;
|
||||
window.openFolder = openFolder;
|
||||
window.selectFile = selectFile;
|
||||
window.setActiveNav = setActiveNav;
|
||||
window.toggleInfoPanel = toggleInfoPanel;
|
||||
window.toggleAIPanel = toggleAIPanel;
|
||||
window.aiAction = aiAction;
|
||||
window.sendAIMessage = sendAIMessage;
|
||||
window.uploadFile = uploadFile;
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
|
|
|
|||
445
ui/suite/goals/goals.js
Normal file
445
ui/suite/goals/goals.js
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
/* =============================================================================
|
||||
GOALS/OKR MODULE - Objectives & Key Results
|
||||
============================================================================= */
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// =============================================================================
|
||||
// STATE
|
||||
// =============================================================================
|
||||
|
||||
const state = {
|
||||
currentView: "dashboard",
|
||||
objectives: [],
|
||||
selectedObjective: null,
|
||||
filters: {
|
||||
status: "all",
|
||||
owner: "all",
|
||||
period: "current",
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// INITIALIZATION
|
||||
// =============================================================================
|
||||
|
||||
function init() {
|
||||
loadObjectives();
|
||||
bindEvents();
|
||||
console.log("Goals module initialized");
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
// View toggle buttons
|
||||
document.querySelectorAll(".view-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", function () {
|
||||
const view = this.dataset.view;
|
||||
if (view) {
|
||||
switchGoalsView(view);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Objective cards
|
||||
document.addEventListener("click", (e) => {
|
||||
const card = e.target.closest(".objective-card");
|
||||
if (card) {
|
||||
const objectiveId = card.dataset.id;
|
||||
if (objectiveId) {
|
||||
selectObjective(objectiveId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VIEW SWITCHING
|
||||
// =============================================================================
|
||||
|
||||
function switchGoalsView(view) {
|
||||
state.currentView = view;
|
||||
|
||||
// Update button states
|
||||
document.querySelectorAll(".view-btn").forEach((btn) => {
|
||||
btn.classList.toggle("active", btn.dataset.view === view);
|
||||
});
|
||||
|
||||
// Update view panels
|
||||
document.querySelectorAll(".goals-view").forEach((panel) => {
|
||||
panel.classList.toggle("active", panel.id === `${view}-view`);
|
||||
});
|
||||
|
||||
// Load view-specific data if using HTMX
|
||||
if (typeof htmx !== "undefined") {
|
||||
const viewContainer = document.getElementById("goals-content");
|
||||
if (viewContainer) {
|
||||
htmx.ajax("GET", `/api/ui/goals/${view}`, { target: viewContainer });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DETAILS PANEL
|
||||
// =============================================================================
|
||||
|
||||
function toggleGoalsPanel() {
|
||||
const panel = document.getElementById("details-panel");
|
||||
if (panel) {
|
||||
panel.classList.toggle("collapsed");
|
||||
}
|
||||
}
|
||||
|
||||
function openGoalsPanel() {
|
||||
const panel = document.getElementById("details-panel");
|
||||
if (panel) {
|
||||
panel.classList.remove("collapsed");
|
||||
}
|
||||
}
|
||||
|
||||
function closeGoalsPanel() {
|
||||
const panel = document.getElementById("details-panel");
|
||||
if (panel) {
|
||||
panel.classList.add("collapsed");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// OBJECTIVES
|
||||
// =============================================================================
|
||||
|
||||
async function loadObjectives() {
|
||||
try {
|
||||
const response = await fetch("/api/goals/objectives");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
state.objectives = data.objectives || [];
|
||||
renderObjectives();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load objectives:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderObjectives() {
|
||||
const container = document.getElementById("objectives-list");
|
||||
if (!container) return;
|
||||
|
||||
if (state.objectives.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<span class="empty-icon">🎯</span>
|
||||
<h3>No objectives yet</h3>
|
||||
<p>Create your first objective to start tracking goals</p>
|
||||
<button class="btn-primary" onclick="showCreateObjectiveModal()">
|
||||
Create Objective
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = state.objectives
|
||||
.map(
|
||||
(obj) => `
|
||||
<div class="objective-card ${state.selectedObjective?.id === obj.id ? "selected" : ""}"
|
||||
data-id="${obj.id}">
|
||||
<div class="objective-header">
|
||||
<h3>${escapeHtml(obj.title)}</h3>
|
||||
<span class="status-badge ${obj.status}">${obj.status}</span>
|
||||
</div>
|
||||
<div class="objective-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${obj.progress || 0}%"></div>
|
||||
</div>
|
||||
<span class="progress-text">${obj.progress || 0}%</span>
|
||||
</div>
|
||||
<div class="objective-meta">
|
||||
<span class="owner">${escapeHtml(obj.owner_name || "Unassigned")}</span>
|
||||
<span class="due-date">${formatDate(obj.end_date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function selectObjective(objectiveId) {
|
||||
const objective = state.objectives.find((o) => o.id === objectiveId);
|
||||
if (!objective) return;
|
||||
|
||||
state.selectedObjective = objective;
|
||||
renderObjectives();
|
||||
renderObjectiveDetails(objective);
|
||||
openGoalsPanel();
|
||||
}
|
||||
|
||||
function renderObjectiveDetails(objective) {
|
||||
const container = document.getElementById("objective-details");
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="detail-header">
|
||||
<h2>${escapeHtml(objective.title)}</h2>
|
||||
<div class="detail-actions">
|
||||
<button class="btn-icon" onclick="editObjective('${objective.id}')" title="Edit">✏️</button>
|
||||
<button class="btn-icon" onclick="deleteObjective('${objective.id}')" title="Delete">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<div class="detail-section">
|
||||
<label>Status</label>
|
||||
<span class="status-badge ${objective.status}">${objective.status}</span>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<label>Progress</label>
|
||||
<div class="progress-bar large">
|
||||
<div class="progress-fill" style="width: ${objective.progress || 0}%"></div>
|
||||
</div>
|
||||
<span>${objective.progress || 0}%</span>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<label>Description</label>
|
||||
<p>${escapeHtml(objective.description || "No description")}</p>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<label>Owner</label>
|
||||
<p>${escapeHtml(objective.owner_name || "Unassigned")}</p>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<label>Timeline</label>
|
||||
<p>${formatDate(objective.start_date)} - ${formatDate(objective.end_date)}</p>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<label>Key Results</label>
|
||||
<div id="key-results-list">
|
||||
${renderKeyResults(objective.key_results || [])}
|
||||
</div>
|
||||
<button class="btn-secondary btn-sm" onclick="addKeyResult('${objective.id}')">
|
||||
+ Add Key Result
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderKeyResults(keyResults) {
|
||||
if (keyResults.length === 0) {
|
||||
return '<p class="empty-message">No key results defined</p>';
|
||||
}
|
||||
|
||||
return keyResults
|
||||
.map(
|
||||
(kr) => `
|
||||
<div class="key-result-item">
|
||||
<div class="kr-header">
|
||||
<span class="kr-title">${escapeHtml(kr.title)}</span>
|
||||
<span class="kr-progress">${kr.current_value || 0} / ${kr.target_value || 100}</span>
|
||||
</div>
|
||||
<div class="progress-bar small">
|
||||
<div class="progress-fill" style="width: ${((kr.current_value || 0) / (kr.target_value || 100)) * 100}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CRUD OPERATIONS
|
||||
// =============================================================================
|
||||
|
||||
function showCreateObjectiveModal() {
|
||||
const modal = document.getElementById("create-objective-modal");
|
||||
if (modal) {
|
||||
if (modal.showModal) {
|
||||
modal.showModal();
|
||||
} else {
|
||||
modal.classList.add("open");
|
||||
}
|
||||
} else {
|
||||
// Fallback: create a simple prompt-based flow
|
||||
const title = prompt("Enter objective title:");
|
||||
if (title) {
|
||||
createObjective({ title });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeCreateObjectiveModal() {
|
||||
const modal = document.getElementById("create-objective-modal");
|
||||
if (modal) {
|
||||
if (modal.close) {
|
||||
modal.close();
|
||||
} else {
|
||||
modal.classList.remove("open");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createObjective(data) {
|
||||
try {
|
||||
const response = await fetch("/api/goals/objectives", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (response.ok) {
|
||||
showNotification("Objective created", "success");
|
||||
loadObjectives();
|
||||
closeCreateObjectiveModal();
|
||||
} else {
|
||||
showNotification("Failed to create objective", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to create objective:", e);
|
||||
showNotification("Failed to create objective", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function editObjective(objectiveId) {
|
||||
const objective = state.objectives.find((o) => o.id === objectiveId);
|
||||
if (!objective) return;
|
||||
|
||||
const newTitle = prompt("Edit objective title:", objective.title);
|
||||
if (newTitle && newTitle !== objective.title) {
|
||||
updateObjective(objectiveId, { title: newTitle });
|
||||
}
|
||||
}
|
||||
|
||||
async function updateObjective(objectiveId, data) {
|
||||
try {
|
||||
const response = await fetch(`/api/goals/objectives/${objectiveId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (response.ok) {
|
||||
showNotification("Objective updated", "success");
|
||||
loadObjectives();
|
||||
} else {
|
||||
showNotification("Failed to update objective", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to update objective:", e);
|
||||
showNotification("Failed to update objective", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteObjective(objectiveId) {
|
||||
if (!confirm("Delete this objective? This cannot be undone.")) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/goals/objectives/${objectiveId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
showNotification("Objective deleted", "success");
|
||||
state.selectedObjective = null;
|
||||
closeGoalsPanel();
|
||||
loadObjectives();
|
||||
} else {
|
||||
showNotification("Failed to delete objective", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to delete objective:", e);
|
||||
showNotification("Failed to delete objective", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function addKeyResult(objectiveId) {
|
||||
const title = prompt("Enter key result title:");
|
||||
if (!title) return;
|
||||
|
||||
const targetValue = prompt("Enter target value:", "100");
|
||||
if (!targetValue) return;
|
||||
|
||||
createKeyResult(objectiveId, {
|
||||
title,
|
||||
target_value: parseFloat(targetValue) || 100,
|
||||
current_value: 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function createKeyResult(objectiveId, data) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/goals/objectives/${objectiveId}/key-results`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
if (response.ok) {
|
||||
showNotification("Key result added", "success");
|
||||
loadObjectives();
|
||||
} else {
|
||||
showNotification("Failed to add key result", "error");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to create key result:", e);
|
||||
showNotification("Failed to add key result", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return "Not set";
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString();
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
if (typeof window.showNotification === "function") {
|
||||
window.showNotification(message, type);
|
||||
} else if (typeof window.GBAlerts !== "undefined") {
|
||||
if (type === "success") window.GBAlerts.success("Goals", message);
|
||||
else if (type === "error") window.GBAlerts.error("Goals", message);
|
||||
else window.GBAlerts.info("Goals", message);
|
||||
} else {
|
||||
console.log(`[${type}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT TO WINDOW
|
||||
// =============================================================================
|
||||
|
||||
window.switchGoalsView = switchGoalsView;
|
||||
window.toggleGoalsPanel = toggleGoalsPanel;
|
||||
window.openGoalsPanel = openGoalsPanel;
|
||||
window.closeGoalsPanel = closeGoalsPanel;
|
||||
window.selectObjective = selectObjective;
|
||||
window.showCreateObjectiveModal = showCreateObjectiveModal;
|
||||
window.closeCreateObjectiveModal = closeCreateObjectiveModal;
|
||||
window.createObjective = createObjective;
|
||||
window.editObjective = editObjective;
|
||||
window.updateObjective = updateObjective;
|
||||
window.deleteObjective = deleteObjective;
|
||||
window.addKeyResult = addKeyResult;
|
||||
|
||||
// =============================================================================
|
||||
// INITIALIZE
|
||||
// =============================================================================
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
4940
ui/suite/index.html
4940
ui/suite/index.html
File diff suppressed because it is too large
Load diff
|
|
@ -42,7 +42,25 @@ function applyProductConfig(config) {
|
|||
|
||||
// Filter apps based on enabled list
|
||||
if (config.apps && Array.isArray(config.apps)) {
|
||||
filterAppsByConfig(config.apps);
|
||||
let effectiveApps = config.apps;
|
||||
|
||||
// Check if we have compiled_features info to filter even further
|
||||
// This ensures we don't show apps that are enabled in config but not compiled in binary
|
||||
if (config.compiled_features && Array.isArray(config.compiled_features)) {
|
||||
const compiledSet = new Set(config.compiled_features.map(f => f.toLowerCase()));
|
||||
effectiveApps = effectiveApps.filter(app =>
|
||||
compiledSet.has(app.toLowerCase()) ||
|
||||
app.toLowerCase() === 'settings' ||
|
||||
app.toLowerCase() === 'auth' ||
|
||||
app.toLowerCase() === 'admin' // Admin usually contains settings which is always there
|
||||
);
|
||||
|
||||
// Also call a helper to hide UI elements for non-compiled features explicitly
|
||||
// This handles features that might not be "apps" but are UI sections
|
||||
hideNonCompiledUI(compiledSet);
|
||||
}
|
||||
|
||||
filterAppsByConfig(effectiveApps);
|
||||
}
|
||||
|
||||
// Apply custom logo
|
||||
|
|
@ -74,6 +92,28 @@ function applyProductConfig(config) {
|
|||
}
|
||||
}
|
||||
|
||||
// Hide UI elements that require features not compiled in the binary
|
||||
function hideNonCompiledUI(compiledSet) {
|
||||
// Hide elements with data-feature attribute that aren't in compiled set
|
||||
document.querySelectorAll('[data-feature]').forEach(el => {
|
||||
const feature = el.getAttribute('data-feature').toLowerCase();
|
||||
// Allow settings/admin as they are usually core
|
||||
if (!compiledSet.has(feature) && feature !== 'settings' && feature !== 'admin') {
|
||||
el.style.display = 'none';
|
||||
el.classList.add('hidden-uncompiled');
|
||||
}
|
||||
});
|
||||
|
||||
// Also look for specific sections that might map to features
|
||||
// e.g. .feature-mail, .feature-meet classes
|
||||
compiledSet.forEach(feature => {
|
||||
// This loop defines what IS available.
|
||||
// Logic should be inverse: find all feature- classes and hide if not in set
|
||||
// But scanning all classes is expensive.
|
||||
// Better to rely on data-feature or explicit app hiding which filterAppsByConfig does.
|
||||
});
|
||||
}
|
||||
|
||||
// Filter visible apps based on enabled list
|
||||
function filterAppsByConfig(enabledApps) {
|
||||
const enabledSet = new Set(enabledApps.map((a) => a.toLowerCase()));
|
||||
|
|
|
|||
212
ui/suite/js/css-loader.js
Normal file
212
ui/suite/js/css-loader.js
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
/**
|
||||
* CSS Lazy Loader - Efficient on-demand stylesheet loading
|
||||
* Prevents duplicate loads and handles caching automatically
|
||||
*/
|
||||
const CSSLoader = (function () {
|
||||
const loadedStyles = new Set();
|
||||
const loadingPromises = new Map();
|
||||
|
||||
function getAbsoluteUrl(href) {
|
||||
if (href.startsWith("http://") || href.startsWith("https://")) {
|
||||
return href;
|
||||
}
|
||||
const base = window.location.pathname.includes("/suite/")
|
||||
? "/suite/"
|
||||
: "/";
|
||||
if (href.startsWith("/")) {
|
||||
return href;
|
||||
}
|
||||
return base + href;
|
||||
}
|
||||
|
||||
function load(href, options = {}) {
|
||||
const absoluteUrl = getAbsoluteUrl(href);
|
||||
|
||||
if (loadedStyles.has(absoluteUrl)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (loadingPromises.has(absoluteUrl)) {
|
||||
return loadingPromises.get(absoluteUrl);
|
||||
}
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const existingLink = document.querySelector(
|
||||
`link[href="${href}"], link[href="${absoluteUrl}"]`
|
||||
);
|
||||
if (existingLink) {
|
||||
loadedStyles.add(absoluteUrl);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
|
||||
if (options.media) {
|
||||
link.media = options.media;
|
||||
}
|
||||
|
||||
if (options.crossOrigin) {
|
||||
link.crossOrigin = options.crossOrigin;
|
||||
}
|
||||
|
||||
link.onload = function () {
|
||||
loadedStyles.add(absoluteUrl);
|
||||
loadingPromises.delete(absoluteUrl);
|
||||
resolve();
|
||||
};
|
||||
|
||||
link.onerror = function () {
|
||||
loadingPromises.delete(absoluteUrl);
|
||||
reject(new Error(`Failed to load CSS: ${href}`));
|
||||
};
|
||||
|
||||
const insertPoint =
|
||||
options.insertAfter ||
|
||||
document.querySelector('link[rel="stylesheet"]:last-of-type') ||
|
||||
document.head.lastChild;
|
||||
|
||||
if (insertPoint && insertPoint.parentNode) {
|
||||
insertPoint.parentNode.insertBefore(
|
||||
link,
|
||||
insertPoint.nextSibling
|
||||
);
|
||||
} else {
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
});
|
||||
|
||||
loadingPromises.set(absoluteUrl, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function loadMultiple(hrefs, options = {}) {
|
||||
return Promise.all(hrefs.map((href) => load(href, options)));
|
||||
}
|
||||
|
||||
function preload(href) {
|
||||
const absoluteUrl = getAbsoluteUrl(href);
|
||||
|
||||
if (loadedStyles.has(absoluteUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingPreload = document.querySelector(
|
||||
`link[rel="preload"][href="${href}"]`
|
||||
);
|
||||
if (existingPreload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const link = document.createElement("link");
|
||||
link.rel = "preload";
|
||||
link.as = "style";
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
function isLoaded(href) {
|
||||
const absoluteUrl = getAbsoluteUrl(href);
|
||||
return loadedStyles.has(absoluteUrl);
|
||||
}
|
||||
|
||||
function unload(href) {
|
||||
const absoluteUrl = getAbsoluteUrl(href);
|
||||
const link = document.querySelector(
|
||||
`link[href="${href}"], link[href="${absoluteUrl}"]`
|
||||
);
|
||||
if (link) {
|
||||
link.remove();
|
||||
loadedStyles.delete(absoluteUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function loadForApp(appName) {
|
||||
const appCssMap = {
|
||||
admin: ["admin/admin.css"],
|
||||
analytics: ["analytics/analytics.css"],
|
||||
attendant: ["attendant/attendant.css"],
|
||||
auth: ["auth/auth.css"],
|
||||
billing: ["billing/billing.css"],
|
||||
calendar: ["calendar/calendar.css"],
|
||||
chat: ["chat/chat.css"],
|
||||
crm: ["crm/crm.css"],
|
||||
dashboards: ["dashboards/dashboards.css"],
|
||||
docs: ["docs/docs.css"],
|
||||
drive: ["drive/drive.css"],
|
||||
learn: ["learn/learn.css"],
|
||||
mail: ["mail/mail.css"],
|
||||
meet: ["meet/meet.css"],
|
||||
monitoring: ["monitoring/monitoring.css"],
|
||||
paper: ["paper/paper.css"],
|
||||
people: ["people/people.css"],
|
||||
products: ["products/products.css"],
|
||||
research: ["research/research.css"],
|
||||
settings: ["settings/settings.css"],
|
||||
sheet: ["sheet/sheet.css"],
|
||||
slides: ["slides/slides.css"],
|
||||
social: ["social/social.css"],
|
||||
sources: ["sources/sources.css"],
|
||||
tasks: ["tasks/tasks.css"],
|
||||
tickets: ["tickets/tickets.css"],
|
||||
tools: ["tools/tools.css"],
|
||||
};
|
||||
|
||||
const cssFiles = appCssMap[appName];
|
||||
if (cssFiles && cssFiles.length > 0) {
|
||||
return loadMultiple(cssFiles);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function init() {
|
||||
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
||||
if (link.href) {
|
||||
loadedStyles.add(link.href);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("htmx:beforeSwap", function (event) {
|
||||
const content = event.detail.serverResponse;
|
||||
if (content && typeof content === "string") {
|
||||
const cssMatches = content.match(
|
||||
/<link[^>]+rel=["']stylesheet["'][^>]*>/gi
|
||||
);
|
||||
if (cssMatches) {
|
||||
cssMatches.forEach((match) => {
|
||||
const hrefMatch = match.match(/href=["']([^"']+)["']/i);
|
||||
if (hrefMatch && hrefMatch[1]) {
|
||||
load(hrefMatch[1]).catch((err) => {
|
||||
console.warn(
|
||||
"CSS preload failed:",
|
||||
err.message
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return {
|
||||
load: load,
|
||||
loadMultiple: loadMultiple,
|
||||
preload: preload,
|
||||
isLoaded: isLoaded,
|
||||
unload: unload,
|
||||
loadForApp: loadForApp,
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.CSSLoader = CSSLoader;
|
||||
}
|
||||
1387
ui/suite/js/suite_app.js
Normal file
1387
ui/suite/js/suite_app.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,19 @@
|
|||
<link rel="stylesheet" href="learn/learn.css" />
|
||||
|
||||
<div class="learn-container">
|
||||
<!-- Sidebar -->
|
||||
<aside class="learn-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2 data-i18n="learn-title">📚 Learn</h2>
|
||||
<button class="btn-icon-sm" onclick="toggleLearnSidebar()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
|
@ -19,19 +28,27 @@
|
|||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="statCoursesCompleted">0</span>
|
||||
<span class="stat-label" data-i18n="learn-completed">Concluídos</span>
|
||||
<span class="stat-label" data-i18n="learn-completed"
|
||||
>Concluídos</span
|
||||
>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="statCoursesInProgress">0</span>
|
||||
<span class="stat-label" data-i18n="learn-in-progress">Em Andamento</span>
|
||||
<span class="stat-label" data-i18n="learn-in-progress"
|
||||
>Em Andamento</span
|
||||
>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="statCertificates">0</span>
|
||||
<span class="stat-label" data-i18n="learn-certificates">Certificados</span>
|
||||
<span class="stat-label" data-i18n="learn-certificates"
|
||||
>Certificados</span
|
||||
>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="statTimeSpent">0h</span>
|
||||
<span class="stat-label" data-i18n="learn-time-spent">Tempo Total</span>
|
||||
<span class="stat-label" data-i18n="learn-time-spent"
|
||||
>Tempo Total</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -78,15 +95,21 @@
|
|||
<h3 data-i18n="learn-difficulty">Dificuldade</h3>
|
||||
<div class="difficulty-filter">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked data-difficulty="beginner">
|
||||
<input type="checkbox" checked data-difficulty="beginner" />
|
||||
<span class="difficulty-badge beginner">Iniciante</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked data-difficulty="intermediate">
|
||||
<span class="difficulty-badge intermediate">Intermediário</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked
|
||||
data-difficulty="intermediate"
|
||||
/>
|
||||
<span class="difficulty-badge intermediate"
|
||||
>Intermediário</span
|
||||
>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked data-difficulty="advanced">
|
||||
<input type="checkbox" checked data-difficulty="advanced" />
|
||||
<span class="difficulty-badge advanced">Avançado</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -103,7 +126,9 @@
|
|||
<div class="certificates-preview" id="certificatesPreview">
|
||||
<div class="empty-state-small">
|
||||
<span>🏆</span>
|
||||
<p data-i18n="learn-no-certificates">Nenhum certificado ainda</p>
|
||||
<p data-i18n="learn-no-certificates">
|
||||
Nenhum certificado ainda
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -115,34 +140,84 @@
|
|||
<div class="learn-header">
|
||||
<div class="header-left">
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="catalog" onclick="switchTab('catalog')">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="tab active"
|
||||
data-tab="catalog"
|
||||
onclick="switchTab('catalog')"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
|
||||
<path
|
||||
d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"
|
||||
></path>
|
||||
</svg>
|
||||
<span data-i18n="learn-catalog">Catálogo</span>
|
||||
</button>
|
||||
<button class="tab" data-tab="my-courses" onclick="switchTab('my-courses')">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="tab"
|
||||
data-tab="my-courses"
|
||||
onclick="switchTab('my-courses')"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M22 10v6M2 10l10-5 10 5-10 5z"></path>
|
||||
<path d="M6 12v5c3 3 9 3 12 0v-5"></path>
|
||||
</svg>
|
||||
<span data-i18n="learn-my-courses">Meus Cursos</span>
|
||||
<span class="badge" id="myCoursesCount">0</span>
|
||||
</button>
|
||||
<button class="tab" data-tab="mandatory" onclick="switchTab('mandatory')">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<button
|
||||
class="tab"
|
||||
data-tab="mandatory"
|
||||
onclick="switchTab('mandatory')"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
<span data-i18n="learn-pending">Pendentes</span>
|
||||
<span class="badge warning" id="mandatoryCount">0</span>
|
||||
</button>
|
||||
<button class="tab" data-tab="certificates" onclick="switchTab('certificates')">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="tab"
|
||||
data-tab="certificates"
|
||||
onclick="switchTab('certificates')"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="8" r="7"></circle>
|
||||
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline>
|
||||
<polyline
|
||||
points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"
|
||||
></polyline>
|
||||
</svg>
|
||||
<span data-i18n="learn-certificates">Certificados</span>
|
||||
</button>
|
||||
|
|
@ -150,27 +225,57 @@
|
|||
</div>
|
||||
<div class="header-right">
|
||||
<div class="search-box">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input type="text" id="searchCourses" placeholder="Buscar cursos..." data-i18n-placeholder="learn-search-placeholder">
|
||||
<input
|
||||
type="text"
|
||||
id="searchCourses"
|
||||
placeholder="Buscar cursos..."
|
||||
data-i18n-placeholder="learn-search-placeholder"
|
||||
/>
|
||||
</div>
|
||||
<select id="sortCourses" class="sort-select">
|
||||
<option value="recent" data-i18n="learn-sort-recent">Mais Recentes</option>
|
||||
<option value="popular" data-i18n="learn-sort-popular">Mais Populares</option>
|
||||
<option value="duration-asc" data-i18n="learn-sort-duration-asc">Menor Duração</option>
|
||||
<option value="duration-desc" data-i18n="learn-sort-duration-desc">Maior Duração</option>
|
||||
<option value="recent" data-i18n="learn-sort-recent">
|
||||
Mais Recentes
|
||||
</option>
|
||||
<option value="popular" data-i18n="learn-sort-popular">
|
||||
Mais Populares
|
||||
</option>
|
||||
<option
|
||||
value="duration-asc"
|
||||
data-i18n="learn-sort-duration-asc"
|
||||
>
|
||||
Menor Duração
|
||||
</option>
|
||||
<option
|
||||
value="duration-desc"
|
||||
data-i18n="learn-sort-duration-desc"
|
||||
>
|
||||
Maior Duração
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mandatory Training Alert -->
|
||||
<div class="mandatory-alert" id="mandatoryAlert" style="display: none;">
|
||||
<div class="mandatory-alert" id="mandatoryAlert" style="display: none">
|
||||
<div class="alert-icon">⚠️</div>
|
||||
<div class="alert-content">
|
||||
<strong data-i18n="learn-mandatory-alert-title">Treinamentos Obrigatórios Pendentes</strong>
|
||||
<p id="mandatoryAlertText">Você possui treinamentos obrigatórios com prazo próximo.</p>
|
||||
<strong data-i18n="learn-mandatory-alert-title"
|
||||
>Treinamentos Obrigatórios Pendentes</strong
|
||||
>
|
||||
<p id="mandatoryAlertText">
|
||||
Você possui treinamentos obrigatórios com prazo próximo.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn-primary-sm" onclick="switchTab('mandatory')">
|
||||
<span data-i18n="learn-view-pending">Ver Pendentes</span>
|
||||
|
|
@ -184,7 +289,9 @@
|
|||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">✨</span>
|
||||
<span data-i18n="learn-recommended">Recomendados para Você</span>
|
||||
<span data-i18n="learn-recommended"
|
||||
>Recomendados para Você</span
|
||||
>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="courses-carousel" id="recommendedCourses">
|
||||
|
|
@ -197,14 +304,18 @@
|
|||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">📚</span>
|
||||
<span data-i18n="learn-all-courses">Todos os Cursos</span>
|
||||
<span data-i18n="learn-all-courses"
|
||||
>Todos os Cursos</span
|
||||
>
|
||||
</h3>
|
||||
<span class="courses-count" id="coursesCountLabel">0 cursos</span>
|
||||
<span class="courses-count" id="coursesCountLabel"
|
||||
>0 cursos</span
|
||||
>
|
||||
</div>
|
||||
<div class="courses-grid" id="coursesGrid">
|
||||
<!-- Courses loaded dynamically -->
|
||||
</div>
|
||||
<div class="load-more" id="loadMore" style="display: none;">
|
||||
<div class="load-more" id="loadMore" style="display: none">
|
||||
<button class="btn-secondary" onclick="loadMoreCourses()">
|
||||
<span data-i18n="learn-load-more">Carregar Mais</span>
|
||||
</button>
|
||||
|
|
@ -219,7 +330,9 @@
|
|||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">▶️</span>
|
||||
<span data-i18n="learn-continue">Continuar Aprendendo</span>
|
||||
<span data-i18n="learn-continue"
|
||||
>Continuar Aprendendo</span
|
||||
>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="courses-list" id="continueLearning">
|
||||
|
|
@ -232,7 +345,9 @@
|
|||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">✅</span>
|
||||
<span data-i18n="learn-completed-courses">Cursos Concluídos</span>
|
||||
<span data-i18n="learn-completed-courses"
|
||||
>Cursos Concluídos</span
|
||||
>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="courses-list" id="completedCourses">
|
||||
|
|
@ -247,7 +362,9 @@
|
|||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">⚠️</span>
|
||||
<span data-i18n="learn-mandatory-training">Treinamentos Obrigatórios</span>
|
||||
<span data-i18n="learn-mandatory-training"
|
||||
>Treinamentos Obrigatórios</span
|
||||
>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="mandatory-list" id="mandatoryList">
|
||||
|
|
@ -262,7 +379,9 @@
|
|||
<div class="section-header">
|
||||
<h3>
|
||||
<span class="section-icon">🏆</span>
|
||||
<span data-i18n="learn-my-certificates">Meus Certificados</span>
|
||||
<span data-i18n="learn-my-certificates"
|
||||
>Meus Certificados</span
|
||||
>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="certificates-grid" id="certificatesGrid">
|
||||
|
|
@ -278,7 +397,14 @@
|
|||
<div class="modal-header">
|
||||
<h3 id="modalCourseTitle">Título do Curso</h3>
|
||||
<button class="btn-icon-sm" onclick="closeCourseModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
|
|
@ -288,22 +414,49 @@
|
|||
<div class="course-detail">
|
||||
<div class="course-detail-header">
|
||||
<div class="course-thumbnail" id="modalThumbnail">
|
||||
<img src="" alt="Course thumbnail">
|
||||
<img src="" alt="Course thumbnail" />
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<div class="course-meta">
|
||||
<span class="difficulty-badge" id="modalDifficulty">Iniciante</span>
|
||||
<span
|
||||
class="difficulty-badge"
|
||||
id="modalDifficulty"
|
||||
>Iniciante</span
|
||||
>
|
||||
<span class="duration" id="modalDuration">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
<polyline
|
||||
points="12 6 12 12 16 14"
|
||||
></polyline>
|
||||
</svg>
|
||||
<span>30 min</span>
|
||||
</span>
|
||||
<span class="lessons-count" id="modalLessonsCount">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<span
|
||||
class="lessons-count"
|
||||
id="modalLessonsCount"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
></path>
|
||||
<polyline
|
||||
points="14 2 14 8 20 8"
|
||||
></polyline>
|
||||
</svg>
|
||||
<span>5 aulas</span>
|
||||
</span>
|
||||
|
|
@ -311,11 +464,23 @@
|
|||
<p class="course-description" id="modalDescription">
|
||||
Descrição do curso...
|
||||
</p>
|
||||
<div class="course-progress" id="modalProgress" style="display: none;">
|
||||
<div
|
||||
class="course-progress"
|
||||
id="modalProgress"
|
||||
style="display: none"
|
||||
>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="modalProgressFill" style="width: 0%"></div>
|
||||
<div
|
||||
class="progress-fill"
|
||||
id="modalProgressFill"
|
||||
style="width: 0%"
|
||||
></div>
|
||||
</div>
|
||||
<span class="progress-text" id="modalProgressText">0% completo</span>
|
||||
<span
|
||||
class="progress-text"
|
||||
id="modalProgressText"
|
||||
>0% completo</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -329,16 +494,28 @@
|
|||
</div>
|
||||
|
||||
<!-- Quiz Section -->
|
||||
<div class="quiz-section" id="modalQuizSection" style="display: none;">
|
||||
<div
|
||||
class="quiz-section"
|
||||
id="modalQuizSection"
|
||||
style="display: none"
|
||||
>
|
||||
<h4 data-i18n="learn-quiz">Avaliação</h4>
|
||||
<div class="quiz-info">
|
||||
<div class="quiz-meta">
|
||||
<span id="modalQuizQuestions">10 questões</span>
|
||||
<span id="modalQuizTime">15 min</span>
|
||||
<span id="modalQuizPassing">70% para aprovação</span>
|
||||
<span id="modalQuizPassing"
|
||||
>70% para aprovação</span
|
||||
>
|
||||
</div>
|
||||
<button class="btn-primary" id="startQuizBtn" onclick="startQuiz()">
|
||||
<span data-i18n="learn-start-quiz">Iniciar Avaliação</span>
|
||||
<button
|
||||
class="btn-primary"
|
||||
id="startQuizBtn"
|
||||
onclick="startQuiz()"
|
||||
>
|
||||
<span data-i18n="learn-start-quiz"
|
||||
>Iniciar Avaliação</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -348,8 +525,19 @@
|
|||
<button class="btn-secondary" onclick="closeCourseModal()">
|
||||
<span data-i18n="learn-close">Fechar</span>
|
||||
</button>
|
||||
<button class="btn-primary" id="startCourseBtn" onclick="startCourse()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="btn-primary"
|
||||
id="startCourseBtn"
|
||||
onclick="startCourse()"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||
</svg>
|
||||
<span data-i18n="learn-start-course">Iniciar Curso</span>
|
||||
|
|
@ -365,7 +553,14 @@
|
|||
<div class="quiz-header-info">
|
||||
<h3 id="quizTitle">Avaliação</h3>
|
||||
<div class="quiz-timer" id="quizTimer">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
|
|
@ -373,7 +568,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<button class="btn-icon-sm" onclick="confirmExitQuiz()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
|
|
@ -382,9 +584,15 @@
|
|||
<div class="modal-body">
|
||||
<div class="quiz-progress">
|
||||
<div class="quiz-progress-bar">
|
||||
<div class="quiz-progress-fill" id="quizProgressFill" style="width: 0%"></div>
|
||||
<div
|
||||
class="quiz-progress-fill"
|
||||
id="quizProgressFill"
|
||||
style="width: 0%"
|
||||
></div>
|
||||
</div>
|
||||
<span class="quiz-progress-text" id="quizProgressText">Questão 1 de 10</span>
|
||||
<span class="quiz-progress-text" id="quizProgressText"
|
||||
>Questão 1 de 10</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="quiz-question" id="quizQuestion">
|
||||
|
|
@ -396,19 +604,47 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" id="prevQuestionBtn" onclick="prevQuestion()" disabled>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="btn-secondary"
|
||||
id="prevQuestionBtn"
|
||||
onclick="prevQuestion()"
|
||||
disabled
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
<span data-i18n="learn-previous">Anterior</span>
|
||||
</button>
|
||||
<button class="btn-primary" id="nextQuestionBtn" onclick="nextQuestion()">
|
||||
<button
|
||||
class="btn-primary"
|
||||
id="nextQuestionBtn"
|
||||
onclick="nextQuestion()"
|
||||
>
|
||||
<span data-i18n="learn-next">Próxima</span>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-primary" id="submitQuizBtn" onclick="submitQuiz()" style="display: none;">
|
||||
<button
|
||||
class="btn-primary"
|
||||
id="submitQuizBtn"
|
||||
onclick="submitQuiz()"
|
||||
style="display: none"
|
||||
>
|
||||
<span data-i18n="learn-submit">Enviar Respostas</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -421,7 +657,14 @@
|
|||
<div class="modal-header">
|
||||
<h3 data-i18n="learn-quiz-result">Resultado da Avaliação</h3>
|
||||
<button class="btn-icon-sm" onclick="closeQuizResult()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
|
|
@ -431,42 +674,84 @@
|
|||
<div class="quiz-result" id="quizResult">
|
||||
<div class="result-icon" id="resultIcon">🎉</div>
|
||||
<h2 class="result-title" id="resultTitle">Parabéns!</h2>
|
||||
<p class="result-message" id="resultMessage">Você passou na avaliação!</p>
|
||||
<p class="result-message" id="resultMessage">
|
||||
Você passou na avaliação!
|
||||
</p>
|
||||
<div class="result-score">
|
||||
<div class="score-circle" id="scoreCircle">
|
||||
<span class="score-value" id="scoreValue">85%</span>
|
||||
</div>
|
||||
<div class="score-details">
|
||||
<div class="score-detail">
|
||||
<span class="detail-label" data-i18n="learn-correct-answers">Acertos</span>
|
||||
<span class="detail-value" id="correctAnswers">8/10</span>
|
||||
<span
|
||||
class="detail-label"
|
||||
data-i18n="learn-correct-answers"
|
||||
>Acertos</span
|
||||
>
|
||||
<span class="detail-value" id="correctAnswers"
|
||||
>8/10</span
|
||||
>
|
||||
</div>
|
||||
<div class="score-detail">
|
||||
<span class="detail-label" data-i18n="learn-time-taken">Tempo</span>
|
||||
<span class="detail-value" id="timeTaken">12:30</span>
|
||||
<span
|
||||
class="detail-label"
|
||||
data-i18n="learn-time-taken"
|
||||
>Tempo</span
|
||||
>
|
||||
<span class="detail-value" id="timeTaken"
|
||||
>12:30</span
|
||||
>
|
||||
</div>
|
||||
<div class="score-detail">
|
||||
<span class="detail-label" data-i18n="learn-attempt">Tentativa</span>
|
||||
<span class="detail-value" id="attemptNumber">1</span>
|
||||
<span
|
||||
class="detail-label"
|
||||
data-i18n="learn-attempt"
|
||||
>Tentativa</span
|
||||
>
|
||||
<span class="detail-value" id="attemptNumber"
|
||||
>1</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-certificate" id="resultCertificate" style="display: none;">
|
||||
<p data-i18n="learn-certificate-earned">🏆 Certificado conquistado!</p>
|
||||
<button class="btn-secondary" onclick="downloadCertificate()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<div
|
||||
class="result-certificate"
|
||||
id="resultCertificate"
|
||||
style="display: none"
|
||||
>
|
||||
<p data-i18n="learn-certificate-earned">
|
||||
🏆 Certificado conquistado!
|
||||
</p>
|
||||
<button
|
||||
class="btn-secondary"
|
||||
onclick="downloadCertificate()"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
|
||||
></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
<span data-i18n="learn-download-certificate">Baixar Certificado</span>
|
||||
<span data-i18n="learn-download-certificate"
|
||||
>Baixar Certificado</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="reviewAnswers()">
|
||||
<span data-i18n="learn-review-answers">Revisar Respostas</span>
|
||||
<span data-i18n="learn-review-answers"
|
||||
>Revisar Respostas</span
|
||||
>
|
||||
</button>
|
||||
<button class="btn-primary" onclick="closeQuizResult()">
|
||||
<span data-i18n="learn-continue">Continuar</span>
|
||||
|
|
@ -480,21 +765,50 @@
|
|||
<div class="modal-content modal-fullscreen">
|
||||
<div class="modal-header">
|
||||
<div class="lesson-nav">
|
||||
<button class="btn-icon-sm" onclick="prevLesson()" id="prevLessonBtn">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="btn-icon-sm"
|
||||
onclick="prevLesson()"
|
||||
id="prevLessonBtn"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<span id="lessonNavTitle">Aula 1 de 5</span>
|
||||
<button class="btn-icon-sm" onclick="nextLesson()" id="nextLessonBtn">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="btn-icon-sm"
|
||||
onclick="nextLesson()"
|
||||
id="nextLessonBtn"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<h3 id="lessonTitle">Título da Aula</h3>
|
||||
<button class="btn-icon-sm" onclick="closeLessonModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
|
|
@ -514,14 +828,31 @@
|
|||
<div class="modal-footer">
|
||||
<div class="lesson-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="lessonProgressFill" style="width: 0%"></div>
|
||||
<div
|
||||
class="progress-fill"
|
||||
id="lessonProgressFill"
|
||||
style="width: 0%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-primary" id="completeLessonBtn" onclick="completeLesson()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="btn-primary"
|
||||
id="completeLessonBtn"
|
||||
onclick="completeLesson()"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
<span data-i18n="learn-mark-complete">Marcar como Concluída</span>
|
||||
<span data-i18n="learn-mark-complete"
|
||||
>Marcar como Concluída</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -533,7 +864,14 @@
|
|||
<div class="modal-header">
|
||||
<h3 data-i18n="learn-certificate">Certificado</h3>
|
||||
<button class="btn-icon-sm" onclick="closeCertificateModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
|
|
@ -552,12 +890,27 @@
|
|||
<h3 id="certUserName">Nome do Usuário</h3>
|
||||
<p>concluiu com sucesso o curso</p>
|
||||
<h4 id="certCourseName">Nome do Curso</h4>
|
||||
<p class="cert-score">com aproveitamento de <strong id="certScore">85%</strong></p>
|
||||
<p class="cert-score">
|
||||
com aproveitamento de
|
||||
<strong id="certScore">85%</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="certificate-footer">
|
||||
<div class="cert-date">
|
||||
<span data-i18n="learn-issued-on">Emitido em</span>
|
||||
<span data-i18n="learn-issued-on"
|
||||
>Emitido em</span
|
||||
>
|
||||
<strong id="certDate">01/01/2025</strong>
|
||||
</div>
|
||||
<div class="cert-code">
|
||||
<span data-i18n="learn-verification-code">Código de Verificação
|
||||
<span data-i18n="learn-verification-code"
|
||||
>Código de Verificação
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="mail/mail.css" />
|
||||
|
||||
<div class="mail-layout">
|
||||
<!-- Sidebar -->
|
||||
<div class="panel mail-sidebar">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="meet/meet.css" />
|
||||
|
||||
<!-- Meet - Video Conferencing -->
|
||||
<div class="meet-container" id="meet-app">
|
||||
<!-- Header -->
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- Alerts partial -->
|
||||
<link rel="stylesheet" href="/static/suite/monitoring/alerts.css">
|
||||
<link rel="stylesheet" href="monitoring/alerts.css">
|
||||
<div class="alerts-container">
|
||||
<!-- Alerts Header -->
|
||||
<div class="alerts-header">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- Health partial -->
|
||||
<link rel="stylesheet" href="/static/suite/monitoring/health.css">
|
||||
<link rel="stylesheet" href="monitoring/health.css">
|
||||
<div class="health-container">
|
||||
<!-- Health Overview -->
|
||||
<div class="health-overview"
|
||||
|
|
|
|||
|
|
@ -491,5 +491,5 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="/static/suite/monitoring/monitoring.css" />
|
||||
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||
<script src="/static/suite/monitoring/monitoring.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- Logs partial -->
|
||||
<link rel="stylesheet" href="/static/suite/monitoring/logs.css" />
|
||||
<link rel="stylesheet" href="monitoring/logs.css" />
|
||||
<div class="logs-container">
|
||||
<!-- Logs Header -->
|
||||
<div class="logs-header">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- Metrics partial -->
|
||||
<link rel="stylesheet" href="/static/suite/monitoring/metrics.css">
|
||||
<link rel="stylesheet" href="monitoring/metrics.css">
|
||||
<div class="metrics-container">
|
||||
<!-- Metrics Header -->
|
||||
<div class="metrics-header">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||
|
||||
<div class="monitoring-container" id="monitoring-app">
|
||||
<header class="monitoring-header">
|
||||
<h2>
|
||||
|
|
@ -1374,7 +1376,6 @@
|
|||
hx-swap="innerHTML"
|
||||
></div>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="monitoring.css" />
|
||||
<script src="monitoring.js"></script>
|
||||
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||
<script src="monitoring/monitoring.js"></script>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- Resources partial -->
|
||||
<link rel="stylesheet" href="/static/suite/monitoring/resources.css">
|
||||
<link rel="stylesheet" href="monitoring/resources.css">
|
||||
<div class="resources-container">
|
||||
<!-- Resource Overview Cards -->
|
||||
<div class="resource-cards">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
<link rel="stylesheet" href="monitoring/services.css" />
|
||||
|
||||
<div class="services-container">
|
||||
<!-- Services Header -->
|
||||
<div class="services-header">
|
||||
<div class="header-stats"
|
||||
hx-get="/api/services/summary"
|
||||
hx-trigger="load, every 10s"
|
||||
hx-swap="innerHTML">
|
||||
<div
|
||||
class="header-stats"
|
||||
hx-get="/api/services/summary"
|
||||
hx-trigger="load, every 10s"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="stat-item running">
|
||||
<span class="stat-number">--</span>
|
||||
<span class="stat-label">Running</span>
|
||||
|
|
@ -24,14 +28,23 @@
|
|||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="search-box">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input type="text"
|
||||
id="service-search"
|
||||
placeholder="Search services..."
|
||||
onkeyup="filterServices(this.value)">
|
||||
<input
|
||||
type="text"
|
||||
id="service-search"
|
||||
placeholder="Search services..."
|
||||
onkeyup="filterServices(this.value)"
|
||||
/>
|
||||
</div>
|
||||
<select id="status-filter" onchange="filterByStatus(this.value)">
|
||||
<option value="all">All Status</option>
|
||||
|
|
@ -39,8 +52,19 @@
|
|||
<option value="warning">Warning</option>
|
||||
<option value="stopped">Stopped</option>
|
||||
</select>
|
||||
<button class="action-btn" onclick="restartAllServices()" title="Restart All">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="action-btn"
|
||||
onclick="restartAllServices()"
|
||||
title="Restart All"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
|
|
@ -50,10 +74,13 @@
|
|||
</div>
|
||||
|
||||
<!-- Services Grid -->
|
||||
<div class="services-grid" id="services-grid"
|
||||
hx-get="/api/services/status"
|
||||
hx-trigger="load, every 10s"
|
||||
hx-swap="innerHTML">
|
||||
<div
|
||||
class="services-grid"
|
||||
id="services-grid"
|
||||
hx-get="/api/services/status"
|
||||
hx-trigger="load, every 10s"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<!-- Loading placeholder -->
|
||||
<div class="service-card skeleton">
|
||||
<div class="skeleton-line"></div>
|
||||
|
|
@ -78,7 +105,14 @@
|
|||
<div class="panel-header">
|
||||
<h3 id="detail-service-name">Service Details</h3>
|
||||
<button class="close-btn" onclick="closeServiceDetail()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
|
|
@ -95,9 +129,30 @@
|
|||
<div class="service-card" data-status="running" data-service="service-name">
|
||||
<div class="card-header">
|
||||
<div class="service-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="20"
|
||||
height="8"
|
||||
rx="2"
|
||||
ry="2"
|
||||
></rect>
|
||||
<rect
|
||||
x="2"
|
||||
y="14"
|
||||
width="20"
|
||||
height="8"
|
||||
rx="2"
|
||||
ry="2"
|
||||
></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
</svg>
|
||||
|
|
@ -112,21 +167,42 @@
|
|||
<p class="service-description">Service description goes here</p>
|
||||
<div class="service-meta">
|
||||
<span class="meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
Uptime: 24d 5h
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
||||
<rect x="9" y="9" width="6" height="6"></rect>
|
||||
</svg>
|
||||
CPU: 12%
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
|
||||
</svg>
|
||||
Mem: 256MB
|
||||
|
|
@ -134,28 +210,74 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="card-btn" onclick="viewServiceDetails('service-id')" title="Details">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="card-btn"
|
||||
onclick="viewServiceDetails('service-id')"
|
||||
title="Details"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="card-btn" onclick="restartService('service-id')" title="Restart">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="card-btn"
|
||||
onclick="restartService('service-id')"
|
||||
title="Restart"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="card-btn" onclick="stopService('service-id')" title="Stop">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
class="card-btn"
|
||||
onclick="stopService('service-id')"
|
||||
title="Stop"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="6" y="4" width="4" height="16"></rect>
|
||||
<rect x="14" y="4" width="4" height="16"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="card-btn" onclick="viewServiceLogs('service-id')" title="Logs">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<button
|
||||
class="card-btn"
|
||||
onclick="viewServiceLogs('service-id')"
|
||||
title="Logs"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
|
@ -167,73 +289,76 @@
|
|||
|
||||
<script>
|
||||
function filterServices(query) {
|
||||
const cards = document.querySelectorAll('.service-card:not(.skeleton)');
|
||||
const cards = document.querySelectorAll(".service-card:not(.skeleton)");
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
cards.forEach(card => {
|
||||
const name = card.dataset.service?.toLowerCase() || '';
|
||||
cards.forEach((card) => {
|
||||
const name = card.dataset.service?.toLowerCase() || "";
|
||||
const text = card.textContent.toLowerCase();
|
||||
const matches = name.includes(lowerQuery) || text.includes(lowerQuery);
|
||||
card.classList.toggle('hidden', !matches);
|
||||
const matches =
|
||||
name.includes(lowerQuery) || text.includes(lowerQuery);
|
||||
card.classList.toggle("hidden", !matches);
|
||||
});
|
||||
}
|
||||
|
||||
function filterByStatus(status) {
|
||||
const cards = document.querySelectorAll('.service-card:not(.skeleton)');
|
||||
const cards = document.querySelectorAll(".service-card:not(.skeleton)");
|
||||
|
||||
cards.forEach(card => {
|
||||
if (status === 'all') {
|
||||
card.classList.remove('hidden');
|
||||
cards.forEach((card) => {
|
||||
if (status === "all") {
|
||||
card.classList.remove("hidden");
|
||||
} else {
|
||||
const cardStatus = card.dataset.status;
|
||||
card.classList.toggle('hidden', cardStatus !== status);
|
||||
card.classList.toggle("hidden", cardStatus !== status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function viewServiceDetails(serviceId) {
|
||||
const panel = document.getElementById('service-detail-panel');
|
||||
const content = document.getElementById('service-detail-content');
|
||||
const panel = document.getElementById("service-detail-panel");
|
||||
const content = document.getElementById("service-detail-content");
|
||||
|
||||
// Load service details via HTMX
|
||||
htmx.ajax('GET', `/api/services/${serviceId}/details`, {
|
||||
htmx.ajax("GET", `/api/services/${serviceId}/details`, {
|
||||
target: content,
|
||||
swap: 'innerHTML'
|
||||
swap: "innerHTML",
|
||||
});
|
||||
|
||||
document.getElementById('detail-service-name').textContent = serviceId;
|
||||
panel.classList.add('open');
|
||||
document.getElementById("detail-service-name").textContent = serviceId;
|
||||
panel.classList.add("open");
|
||||
}
|
||||
|
||||
function closeServiceDetail() {
|
||||
document.getElementById('service-detail-panel').classList.remove('open');
|
||||
document
|
||||
.getElementById("service-detail-panel")
|
||||
.classList.remove("open");
|
||||
}
|
||||
|
||||
function restartService(serviceId) {
|
||||
if (confirm(`Are you sure you want to restart ${serviceId}?`)) {
|
||||
htmx.ajax('POST', `/api/services/${serviceId}/restart`, {
|
||||
swap: 'none'
|
||||
htmx.ajax("POST", `/api/services/${serviceId}/restart`, {
|
||||
swap: "none",
|
||||
}).then(() => {
|
||||
htmx.trigger('#services-grid', 'refresh');
|
||||
htmx.trigger("#services-grid", "refresh");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function stopService(serviceId) {
|
||||
if (confirm(`Are you sure you want to stop ${serviceId}?`)) {
|
||||
htmx.ajax('POST', `/api/services/${serviceId}/stop`, {
|
||||
swap: 'none'
|
||||
htmx.ajax("POST", `/api/services/${serviceId}/stop`, {
|
||||
swap: "none",
|
||||
}).then(() => {
|
||||
htmx.trigger('#services-grid', 'refresh');
|
||||
htmx.trigger("#services-grid", "refresh");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function startService(serviceId) {
|
||||
htmx.ajax('POST', `/api/services/${serviceId}/start`, {
|
||||
swap: 'none'
|
||||
htmx.ajax("POST", `/api/services/${serviceId}/start`, {
|
||||
swap: "none",
|
||||
}).then(() => {
|
||||
htmx.trigger('#services-grid', 'refresh');
|
||||
htmx.trigger("#services-grid", "refresh");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -243,38 +368,44 @@
|
|||
if (logsLink) {
|
||||
logsLink.click();
|
||||
setTimeout(() => {
|
||||
const serviceFilter = document.getElementById('service-filter');
|
||||
const serviceFilter = document.getElementById("service-filter");
|
||||
if (serviceFilter) {
|
||||
serviceFilter.value = serviceId;
|
||||
serviceFilter.dispatchEvent(new Event('change'));
|
||||
serviceFilter.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
function restartAllServices() {
|
||||
if (confirm('Are you sure you want to restart all services? This may cause temporary downtime.')) {
|
||||
htmx.ajax('POST', '/api/services/restart-all', {
|
||||
swap: 'none'
|
||||
if (
|
||||
confirm(
|
||||
"Are you sure you want to restart all services? This may cause temporary downtime.",
|
||||
)
|
||||
) {
|
||||
htmx.ajax("POST", "/api/services/restart-all", {
|
||||
swap: "none",
|
||||
}).then(() => {
|
||||
htmx.trigger('#services-grid', 'refresh');
|
||||
htmx.trigger("#services-grid", "refresh");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Close panel on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") {
|
||||
closeServiceDetail();
|
||||
}
|
||||
});
|
||||
|
||||
// Close panel when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
const panel = document.getElementById('service-detail-panel');
|
||||
if (panel.classList.contains('open') &&
|
||||
document.addEventListener("click", function (e) {
|
||||
const panel = document.getElementById("service-detail-panel");
|
||||
if (
|
||||
panel.classList.contains("open") &&
|
||||
!panel.contains(e.target) &&
|
||||
!e.target.closest('.card-btn')) {
|
||||
!e.target.closest(".card-btn")
|
||||
) {
|
||||
closeServiceDetail();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="paper/paper.css" />
|
||||
|
||||
<!-- Paper - AI Writing & Notes -->
|
||||
<div class="paper-container" id="paper-app">
|
||||
<!-- Sidebar - Notes List -->
|
||||
|
|
|
|||
|
|
@ -11,7 +11,14 @@
|
|||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="search-box">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
|
|
@ -23,11 +30,18 @@
|
|||
/>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openAddContact()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="8.5" cy="7" r="4"/>
|
||||
<line x1="20" y1="8" x2="20" y2="14"/>
|
||||
<line x1="23" y1="11" x2="17" y2="11"/>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="8.5" cy="7" r="4" />
|
||||
<line x1="20" y1="8" x2="20" y2="14" />
|
||||
<line x1="23" y1="11" x2="17" y2="11" />
|
||||
</svg>
|
||||
<span data-i18n="people-add">Add Contact</span>
|
||||
</button>
|
||||
|
|
@ -36,33 +50,83 @@
|
|||
|
||||
<!-- Tab Navigation -->
|
||||
<nav class="tab-nav" role="tablist">
|
||||
<button class="tab-btn active" role="tab" aria-selected="true" onclick="showTab('contacts', this)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<button
|
||||
class="tab-btn active"
|
||||
role="tab"
|
||||
aria-selected="true"
|
||||
onclick="showTab('contacts', this)"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
</svg>
|
||||
<span data-i18n="people-tab-contacts">Contacts</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('groups', this)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
<button
|
||||
class="tab-btn"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
onclick="showTab('groups', this)"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
<span data-i18n="people-tab-groups">Groups</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('directory', this)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
||||
<button
|
||||
class="tab-btn"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
onclick="showTab('directory', this)"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path
|
||||
d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span data-i18n="people-tab-directory">Directory</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('recent', this)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
<button
|
||||
class="tab-btn"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
onclick="showTab('recent', this)"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<span data-i18n="people-tab-recent">Recent</span>
|
||||
</button>
|
||||
|
|
@ -74,33 +138,90 @@
|
|||
<div id="contacts-tab" class="tab-content active">
|
||||
<!-- Alphabet Filter -->
|
||||
<div class="alphabet-filter">
|
||||
<button class="alpha-btn active" onclick="filterByLetter('all', this)">All</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('A', this)">A</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('B', this)">B</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('C', this)">C</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('D', this)">D</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('E', this)">E</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('F', this)">F</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('G', this)">G</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('H', this)">H</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('I', this)">I</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('J', this)">J</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('K', this)">K</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('L', this)">L</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('M', this)">M</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('N', this)">N</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('O', this)">O</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('P', this)">P</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('Q', this)">Q</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('R', this)">R</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('S', this)">S</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('T', this)">T</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('U', this)">U</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('V', this)">V</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('W', this)">W</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('X', this)">X</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('Y', this)">Y</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('Z', this)">Z</button>
|
||||
<button
|
||||
class="alpha-btn active"
|
||||
onclick="filterByLetter('all', this)"
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('A', this)">
|
||||
A
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('B', this)">
|
||||
B
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('C', this)">
|
||||
C
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('D', this)">
|
||||
D
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('E', this)">
|
||||
E
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('F', this)">
|
||||
F
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('G', this)">
|
||||
G
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('H', this)">
|
||||
H
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('I', this)">
|
||||
I
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('J', this)">
|
||||
J
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('K', this)">
|
||||
K
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('L', this)">
|
||||
L
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('M', this)">
|
||||
M
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('N', this)">
|
||||
N
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('O', this)">
|
||||
O
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('P', this)">
|
||||
P
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('Q', this)">
|
||||
Q
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('R', this)">
|
||||
R
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('S', this)">
|
||||
S
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('T', this)">
|
||||
T
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('U', this)">
|
||||
U
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('V', this)">
|
||||
V
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('W', this)">
|
||||
W
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('X', this)">
|
||||
X
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('Y', this)">
|
||||
Y
|
||||
</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('Z', this)">
|
||||
Z
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Contacts List -->
|
||||
|
|
@ -139,22 +260,53 @@
|
|||
<div class="contact-panel" id="contact-panel">
|
||||
<div class="panel-header">
|
||||
<button class="close-btn" onclick="closeContactPanel()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="panel-actions">
|
||||
<button class="icon-btn" title="Edit" onclick="editContact()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn" title="Delete" onclick="deleteContact()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
<button
|
||||
class="icon-btn"
|
||||
title="Delete"
|
||||
onclick="deleteContact()"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path
|
||||
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -171,9 +323,16 @@
|
|||
<div class="modal-header">
|
||||
<h2 id="modal-title" data-i18n="people-add-contact">Add Contact</h2>
|
||||
<button class="close-btn" onclick="closeModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -209,70 +368,80 @@
|
|||
<textarea name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal()" data-i18n="cancel">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" data-i18n="save">Save</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
onclick="closeModal()"
|
||||
data-i18n="cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" data-i18n="save">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
let currentContact = null;
|
||||
let contacts = [];
|
||||
(function () {
|
||||
let currentContact = null;
|
||||
let contacts = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadContacts();
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
loadContacts();
|
||||
});
|
||||
|
||||
async function loadContacts() {
|
||||
try {
|
||||
const response = await fetch('/api/contacts');
|
||||
if (response.ok) {
|
||||
contacts = await response.json();
|
||||
renderContacts(contacts);
|
||||
} else {
|
||||
renderEmptyState();
|
||||
async function loadContacts() {
|
||||
try {
|
||||
const response = await fetch("/api/contacts");
|
||||
if (response.ok) {
|
||||
contacts = await response.json();
|
||||
renderContacts(contacts);
|
||||
} else {
|
||||
renderEmptyState();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load contacts:", error);
|
||||
renderEmptyState();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load contacts:', error);
|
||||
renderEmptyState();
|
||||
}
|
||||
}
|
||||
|
||||
function renderContacts(contactsList) {
|
||||
const container = document.getElementById('contacts-list');
|
||||
if (!contactsList || contactsList.length === 0) {
|
||||
renderEmptyState();
|
||||
return;
|
||||
}
|
||||
function renderContacts(contactsList) {
|
||||
const container = document.getElementById("contacts-list");
|
||||
if (!contactsList || contactsList.length === 0) {
|
||||
renderEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
const grouped = groupByLetter(contactsList);
|
||||
let html = '';
|
||||
const grouped = groupByLetter(contactsList);
|
||||
let html = "";
|
||||
|
||||
for (const [letter, group] of Object.entries(grouped)) {
|
||||
html += `<div class="contact-group" data-letter="${letter}">
|
||||
for (const [letter, group] of Object.entries(grouped)) {
|
||||
html += `<div class="contact-group" data-letter="${letter}">
|
||||
<div class="group-header">${letter}</div>
|
||||
<div class="group-contacts">`;
|
||||
|
||||
for (const contact of group) {
|
||||
html += renderContactCard(contact);
|
||||
for (const contact of group) {
|
||||
html += renderContactCard(contact);
|
||||
}
|
||||
|
||||
html += "</div></div>";
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
html += '</div></div>';
|
||||
}
|
||||
function renderContactCard(contact) {
|
||||
const initials = getInitials(contact.firstName, contact.lastName);
|
||||
const name = `${contact.firstName} ${contact.lastName}`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderContactCard(contact) {
|
||||
const initials = getInitials(contact.firstName, contact.lastName);
|
||||
const name = `${contact.firstName} ${contact.lastName}`;
|
||||
|
||||
return `<div class="contact-card" onclick="showContact('${contact.id}')">
|
||||
return `<div class="contact-card" onclick="showContact('${contact.id}')">
|
||||
<div class="contact-avatar" style="background: ${getAvatarColor(name)}">${initials}</div>
|
||||
<div class="contact-info">
|
||||
<div class="contact-name">${name}</div>
|
||||
<div class="contact-detail">${contact.email || contact.phone || ''}</div>
|
||||
<div class="contact-detail">${contact.email || contact.phone || ""}</div>
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
<button class="icon-btn small" onclick="event.stopPropagation(); startChat('${contact.id}')" title="Chat">
|
||||
|
|
@ -288,10 +457,10 @@ function renderContactCard(contact) {
|
|||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmptyState() {
|
||||
document.getElementById('contacts-list').innerHTML = `
|
||||
function renderEmptyState() {
|
||||
document.getElementById("contacts-list").innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
|
|
@ -310,91 +479,124 @@ function renderEmptyState() {
|
|||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function groupByLetter(contactsList) {
|
||||
const grouped = {};
|
||||
for (const contact of contactsList) {
|
||||
const letter = (contact.lastName || contact.firstName || '#').charAt(0).toUpperCase();
|
||||
if (!grouped[letter]) grouped[letter] = [];
|
||||
grouped[letter].push(contact);
|
||||
}
|
||||
return Object.fromEntries(Object.entries(grouped).sort());
|
||||
}
|
||||
|
||||
function getInitials(firstName, lastName) {
|
||||
return ((firstName?.charAt(0) || '') + (lastName?.charAt(0) || '')).toUpperCase() || '?';
|
||||
}
|
||||
|
||||
function getAvatarColor(name) {
|
||||
const colors = ['#6366f1', '#8b5cf6', '#ec4899', '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6'];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
}
|
||||
|
||||
function showTab(tabId, btn) {
|
||||
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.remove('active');
|
||||
b.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
document.getElementById(tabId + '-tab').classList.add('active');
|
||||
btn.classList.add('active');
|
||||
btn.setAttribute('aria-selected', 'true');
|
||||
}
|
||||
|
||||
function filterByLetter(letter, btn) {
|
||||
document.querySelectorAll('.alpha-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.contact-group').forEach(group => {
|
||||
if (letter === 'all' || group.dataset.letter === letter) {
|
||||
group.style.display = '';
|
||||
} else {
|
||||
group.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showContact(id) {
|
||||
currentContact = contacts.find(c => c.id === id);
|
||||
if (!currentContact) return;
|
||||
function groupByLetter(contactsList) {
|
||||
const grouped = {};
|
||||
for (const contact of contactsList) {
|
||||
const letter = (contact.lastName || contact.firstName || "#")
|
||||
.charAt(0)
|
||||
.toUpperCase();
|
||||
if (!grouped[letter]) grouped[letter] = [];
|
||||
grouped[letter].push(contact);
|
||||
}
|
||||
return Object.fromEntries(Object.entries(grouped).sort());
|
||||
}
|
||||
|
||||
const panel = document.getElementById('contact-panel');
|
||||
const detail = document.getElementById('contact-detail');
|
||||
function getInitials(firstName, lastName) {
|
||||
return (
|
||||
(
|
||||
(firstName?.charAt(0) || "") + (lastName?.charAt(0) || "")
|
||||
).toUpperCase() || "?"
|
||||
);
|
||||
}
|
||||
|
||||
detail.innerHTML = `
|
||||
function getAvatarColor(name) {
|
||||
const colors = [
|
||||
"#6366f1",
|
||||
"#8b5cf6",
|
||||
"#ec4899",
|
||||
"#ef4444",
|
||||
"#f97316",
|
||||
"#eab308",
|
||||
"#22c55e",
|
||||
"#14b8a6",
|
||||
"#06b6d4",
|
||||
"#3b82f6",
|
||||
];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
}
|
||||
|
||||
function showTab(tabId, btn) {
|
||||
document
|
||||
.querySelectorAll(".tab-content")
|
||||
.forEach((tab) => tab.classList.remove("active"));
|
||||
document.querySelectorAll(".tab-btn").forEach((b) => {
|
||||
b.classList.remove("active");
|
||||
b.setAttribute("aria-selected", "false");
|
||||
});
|
||||
|
||||
document.getElementById(tabId + "-tab").classList.add("active");
|
||||
btn.classList.add("active");
|
||||
btn.setAttribute("aria-selected", "true");
|
||||
}
|
||||
|
||||
function filterByLetter(letter, btn) {
|
||||
document
|
||||
.querySelectorAll(".alpha-btn")
|
||||
.forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
|
||||
document.querySelectorAll(".contact-group").forEach((group) => {
|
||||
if (letter === "all" || group.dataset.letter === letter) {
|
||||
group.style.display = "";
|
||||
} else {
|
||||
group.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showContact(id) {
|
||||
currentContact = contacts.find((c) => c.id === id);
|
||||
if (!currentContact) return;
|
||||
|
||||
const panel = document.getElementById("contact-panel");
|
||||
const detail = document.getElementById("contact-detail");
|
||||
|
||||
detail.innerHTML = `
|
||||
<div class="contact-header">
|
||||
<div class="contact-avatar large" style="background: ${getAvatarColor(currentContact.firstName + ' ' + currentContact.lastName)}">
|
||||
<div class="contact-avatar large" style="background: ${getAvatarColor(currentContact.firstName + " " + currentContact.lastName)}">
|
||||
${getInitials(currentContact.firstName, currentContact.lastName)}
|
||||
</div>
|
||||
<h2>${currentContact.firstName} ${currentContact.lastName}</h2>
|
||||
${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ''}
|
||||
${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ''}
|
||||
${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ""}
|
||||
${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ""}
|
||||
</div>
|
||||
<div class="contact-fields">
|
||||
${currentContact.email ? `
|
||||
${
|
||||
currentContact.email
|
||||
? `
|
||||
<div class="field">
|
||||
<label>Email</label>
|
||||
<a href="mailto:${currentContact.email}">${currentContact.email}</a>
|
||||
</div>
|
||||
` : ''}
|
||||
${currentContact.phone ? `
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
currentContact.phone
|
||||
? `
|
||||
<div class="field">
|
||||
<label>Phone</label>
|
||||
<a href="tel:${currentContact.phone}">${currentContact.phone}</a>
|
||||
</div>
|
||||
` : ''}
|
||||
${currentContact.notes ? `
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
currentContact.notes
|
||||
? `
|
||||
<div class="field">
|
||||
<label>Notes</label>
|
||||
<p>${currentContact.notes}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div class="contact-quick-actions">
|
||||
<button class="action-btn" onclick="startChat('${currentContact.id}')">
|
||||
|
|
@ -422,106 +624,132 @@ function showContact(id) {
|
|||
</div>
|
||||
`;
|
||||
|
||||
panel.classList.add('open');
|
||||
}
|
||||
|
||||
function closeContactPanel() {
|
||||
document.getElementById('contact-panel').classList.remove('open');
|
||||
currentContact = null;
|
||||
}
|
||||
|
||||
function openAddContact() {
|
||||
currentContact = null;
|
||||
document.getElementById('modal-title').textContent = 'Add Contact';
|
||||
document.getElementById('contact-form').reset();
|
||||
document.getElementById('contact-modal').showModal();
|
||||
}
|
||||
|
||||
function editContact() {
|
||||
if (!currentContact) return;
|
||||
document.getElementById('modal-title').textContent = 'Edit Contact';
|
||||
const form = document.getElementById('contact-form');
|
||||
form.firstName.value = currentContact.firstName || '';
|
||||
form.lastName.value = currentContact.lastName || '';
|
||||
form.email.value = currentContact.email || '';
|
||||
form.phone.value = currentContact.phone || '';
|
||||
form.company.value = currentContact.company || '';
|
||||
form.title.value = currentContact.title || '';
|
||||
form.notes.value = currentContact.notes || '';
|
||||
document.getElementById('contact-modal').showModal();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('contact-modal').close();
|
||||
}
|
||||
|
||||
async function saveContact(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
const data = {
|
||||
firstName: form.firstName.value,
|
||||
lastName: form.lastName.value,
|
||||
email: form.email.value,
|
||||
phone: form.phone.value,
|
||||
company: form.company.value,
|
||||
title: form.title.value,
|
||||
notes: form.notes.value
|
||||
};
|
||||
|
||||
try {
|
||||
const url = currentContact ? `/api/contacts/${currentContact.id}` : '/api/contacts';
|
||||
const method = currentContact ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
closeModal();
|
||||
loadContacts();
|
||||
if (currentContact) closeContactPanel();
|
||||
panel.classList.add("open");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save contact:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteContact() {
|
||||
if (!currentContact || !confirm('Delete this contact?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/contacts/${currentContact.id}`, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
closeContactPanel();
|
||||
loadContacts();
|
||||
function closeContactPanel() {
|
||||
document.getElementById("contact-panel").classList.remove("open");
|
||||
currentContact = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete contact:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function startChat(contactId) {
|
||||
window.location.href = `/#chat?contact=${contactId}`;
|
||||
}
|
||||
function openAddContact() {
|
||||
currentContact = null;
|
||||
document.getElementById("modal-title").textContent = "Add Contact";
|
||||
document.getElementById("contact-form").reset();
|
||||
document.getElementById("contact-modal").showModal();
|
||||
}
|
||||
|
||||
function sendEmail(email) {
|
||||
if (email) window.location.href = `mailto:${email}`;
|
||||
}
|
||||
function editContact() {
|
||||
if (!currentContact) return;
|
||||
document.getElementById("modal-title").textContent = "Edit Contact";
|
||||
const form = document.getElementById("contact-form");
|
||||
form.firstName.value = currentContact.firstName || "";
|
||||
form.lastName.value = currentContact.lastName || "";
|
||||
form.email.value = currentContact.email || "";
|
||||
form.phone.value = currentContact.phone || "";
|
||||
form.company.value = currentContact.company || "";
|
||||
form.title.value = currentContact.title || "";
|
||||
form.notes.value = currentContact.notes || "";
|
||||
document.getElementById("contact-modal").showModal();
|
||||
}
|
||||
|
||||
function scheduleMeeting(contactId) {
|
||||
window.location.href = `/#calendar?new=meeting&contact=${contactId}`;
|
||||
}
|
||||
function closeModal() {
|
||||
document.getElementById("contact-modal").close();
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('people-search')?.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = contacts.filter(c =>
|
||||
(c.firstName + ' ' + c.lastName).toLowerCase().includes(query) ||
|
||||
(c.email || '').toLowerCase().includes(query) ||
|
||||
(c.company || '').toLowerCase().includes(query)
|
||||
);
|
||||
renderContacts(filtered);
|
||||
});
|
||||
async function saveContact(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
const data = {
|
||||
firstName: form.firstName.value,
|
||||
lastName: form.lastName.value,
|
||||
email: form.email.value,
|
||||
phone: form.phone.value,
|
||||
company: form.company.value,
|
||||
title: form.title.value,
|
||||
notes: form.notes.value,
|
||||
};
|
||||
|
||||
try {
|
||||
const url = currentContact
|
||||
? `/api/contacts/${currentContact.id}`
|
||||
: "/api/contacts";
|
||||
const method = currentContact ? "PUT" : "POST";
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
closeModal();
|
||||
loadContacts();
|
||||
if (currentContact) closeContactPanel();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save contact:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteContact() {
|
||||
if (!currentContact || !confirm("Delete this contact?")) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/contacts/${currentContact.id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
if (response.ok) {
|
||||
closeContactPanel();
|
||||
loadContacts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete contact:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function startChat(contactId) {
|
||||
window.location.href = `/#chat?contact=${contactId}`;
|
||||
}
|
||||
|
||||
function sendEmail(email) {
|
||||
if (email) window.location.href = `mailto:${email}`;
|
||||
}
|
||||
|
||||
function scheduleMeeting(contactId) {
|
||||
window.location.href = `/#calendar?new=meeting&contact=${contactId}`;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
document
|
||||
.getElementById("people-search")
|
||||
?.addEventListener("input", (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = contacts.filter(
|
||||
(c) =>
|
||||
(c.firstName + " " + c.lastName)
|
||||
.toLowerCase()
|
||||
.includes(query) ||
|
||||
(c.email || "").toLowerCase().includes(query) ||
|
||||
(c.company || "").toLowerCase().includes(query),
|
||||
);
|
||||
renderContacts(filtered);
|
||||
});
|
||||
|
||||
window.showTab = showTab;
|
||||
window.filterByLetter = filterByLetter;
|
||||
window.showContact = showContact;
|
||||
window.closeContactPanel = closeContactPanel;
|
||||
window.openAddContact = openAddContact;
|
||||
window.editContact = editContact;
|
||||
window.closeModal = closeModal;
|
||||
window.saveContact = saveContact;
|
||||
window.deleteContact = deleteContact;
|
||||
window.startChat = startChat;
|
||||
window.sendEmail = sendEmail;
|
||||
window.scheduleMeeting = scheduleMeeting;
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- Products - Product & Service Catalog -->
|
||||
<!-- Dynamics nomenclature: Product, Service, PriceList -->
|
||||
|
||||
<link rel="stylesheet" href="/suite/products/products.css">
|
||||
<link rel="stylesheet" href="products/products.css">
|
||||
|
||||
<div class="products-container">
|
||||
<!-- Header -->
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="research/research.css" />
|
||||
|
||||
<!-- Research - AI-Powered Search & Discovery -->
|
||||
<div class="research-container" id="research-app">
|
||||
<!-- Sidebar - Sources & Collections -->
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
<link rel="stylesheet" href="settings/settings.css" />
|
||||
|
||||
<div class="settings-layout">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="settings-sidebar">
|
||||
<div class="settings-header">
|
||||
<svg width="24" height="24" 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>
|
||||
<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>
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
|
|
@ -42,7 +46,9 @@
|
|||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
||||
<path
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z">
|
||||
</path>
|
||||
</svg>
|
||||
<span>Language</span>
|
||||
</a>
|
||||
|
|
@ -119,7 +125,9 @@
|
|||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
||||
<path
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z">
|
||||
</path>
|
||||
</svg>
|
||||
<span data-i18n="admin-dns">DNS</span>
|
||||
</a>
|
||||
|
|
@ -133,6 +141,15 @@
|
|||
</svg>
|
||||
<span data-i18n="admin-audit">Audit Log</span>
|
||||
</a>
|
||||
<a href="#about" class="nav-item" hx-get="/suite/about/about.html" hx-target="#main-content"
|
||||
hx-push-url="/#about">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
<span data-i18n="nav-about">About</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="settings-footer">
|
||||
|
|
@ -155,10 +172,8 @@
|
|||
<p class="subtitle">Manage your personal information and preferences</p>
|
||||
</div>
|
||||
|
||||
<form class="settings-form"
|
||||
hx-put="/api/user/profile"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="showToast('Profile updated successfully')">
|
||||
<form class="settings-form" hx-put="/api/user/profile" hx-swap="none"
|
||||
hx-on::after-request="showToast('Profile updated successfully')">
|
||||
<!-- Avatar -->
|
||||
<div class="setting-card">
|
||||
<div class="card-header">
|
||||
|
|
@ -172,7 +187,8 @@
|
|||
<div class="avatar-actions">
|
||||
<label class="btn-secondary upload-btn">
|
||||
<input type="file" name="avatar" accept="image/*" hidden onchange="previewAvatar(this)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
|
|
@ -192,11 +208,8 @@
|
|||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Display Name</label>
|
||||
<input type="text" name="display_name" placeholder="John Doe"
|
||||
hx-get="/api/user/profile"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="[name='display_name']">
|
||||
<input type="text" name="display_name" placeholder="John Doe" hx-get="/api/user/profile"
|
||||
hx-trigger="load" hx-swap="outerHTML" hx-select="[name='display_name']">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
|
|
@ -266,9 +279,8 @@
|
|||
<h2>Change Password</h2>
|
||||
<p>Update your password regularly for better security</p>
|
||||
</div>
|
||||
<form hx-post="/api/user/password"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="this.reset(); showToast('Password changed successfully')">
|
||||
<form hx-post="/api/user/password" hx-swap="none"
|
||||
hx-on::after-request="this.reset(); showToast('Password changed successfully')">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label>Current Password</label>
|
||||
|
|
@ -276,7 +288,8 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label>New Password</label>
|
||||
<input type="password" name="new_password" required minlength="8" autocomplete="new-password">
|
||||
<input type="password" name="new_password" required minlength="8"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Confirm New Password</label>
|
||||
|
|
@ -295,17 +308,13 @@
|
|||
<h2>Two-Factor Authentication</h2>
|
||||
<p>Add an extra layer of security to your account</p>
|
||||
</div>
|
||||
<div class="setting-row" id="2fa-status"
|
||||
hx-get="/api/user/security/2fa/status"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="setting-row" id="2fa-status" hx-get="/api/user/security/2fa/status" hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Authenticator App</span>
|
||||
<span class="setting-desc">Use an authenticator app for 2FA codes</span>
|
||||
</div>
|
||||
<button class="btn-primary"
|
||||
hx-post="/api/user/security/2fa/enable"
|
||||
hx-swap="none">
|
||||
<button class="btn-primary" hx-post="/api/user/security/2fa/enable" hx-swap="none">
|
||||
Enable 2FA
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -317,19 +326,16 @@
|
|||
<h2>Active Sessions</h2>
|
||||
<p>Manage your active login sessions</p>
|
||||
</div>
|
||||
<div class="sessions-list" id="sessions-list"
|
||||
hx-get="/api/user/security/sessions"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="sessions-list" id="sessions-list" hx-get="/api/user/security/sessions" hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="btn-danger-outline"
|
||||
hx-post="/api/user/security/sessions/revoke-all"
|
||||
hx-confirm="Sign out of all other devices?"
|
||||
hx-on::after-request="htmx.trigger('#sessions-list', 'load'); showToast('All other sessions revoked')">
|
||||
<button class="btn-danger-outline" hx-post="/api/user/security/sessions/revoke-all"
|
||||
hx-confirm="Sign out of all other devices?"
|
||||
hx-on::after-request="htmx.trigger('#sessions-list', 'load'); showToast('All other sessions revoked')">
|
||||
Sign Out All Other Sessions
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -341,10 +347,8 @@
|
|||
<h2>Connected Devices</h2>
|
||||
<p>Devices that have accessed your account</p>
|
||||
</div>
|
||||
<div class="devices-list" id="devices-list"
|
||||
hx-get="/api/user/security/devices"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="devices-list" id="devices-list" hx-get="/api/user/security/devices" hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
|
@ -365,127 +369,144 @@
|
|||
<h2>Display Language</h2>
|
||||
<p>Choose the language for the application interface</p>
|
||||
</div>
|
||||
<div class="language-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-top: 16px;">
|
||||
<button class="language-option" data-locale="en" onclick="selectLanguage('en', this)" style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: pointer; position: relative; transition: all 0.2s; text-align: left;">
|
||||
<div class="language-grid"
|
||||
style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-top: 16px;">
|
||||
<button class="language-option" data-locale="en" onclick="selectLanguage('en', this)"
|
||||
style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: pointer; position: relative; transition: all 0.2s; text-align: left;">
|
||||
<span style="font-size: 40px; line-height: 1;">🇺🇸</span>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #f8fafc;">English</div>
|
||||
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">English</div>
|
||||
</div>
|
||||
<svg class="lang-check" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="3" style="opacity: 0;">
|
||||
<svg class="lang-check" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3b82f6"
|
||||
stroke-width="3" style="opacity: 0;">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="language-option" data-locale="pt-BR" onclick="selectLanguage('pt-BR', this)" style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: pointer; position: relative; transition: all 0.2s; text-align: left;">
|
||||
<button class="language-option" data-locale="pt-BR" onclick="selectLanguage('pt-BR', this)"
|
||||
style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: pointer; position: relative; transition: all 0.2s; text-align: left;">
|
||||
<span style="font-size: 40px; line-height: 1;">🇧🇷</span>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #f8fafc;">Portuguese (Brazil)</div>
|
||||
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">Português (Brasil)</div>
|
||||
</div>
|
||||
<svg class="lang-check" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="3" style="opacity: 0;">
|
||||
<svg class="lang-check" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3b82f6"
|
||||
stroke-width="3" style="opacity: 0;">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="language-option" data-locale="es" disabled style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: not-allowed; position: relative; opacity: 0.5; text-align: left;">
|
||||
<button class="language-option" data-locale="es" disabled
|
||||
style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: not-allowed; position: relative; opacity: 0.5; text-align: left;">
|
||||
<span style="font-size: 40px; line-height: 1;">🇪🇸</span>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #f8fafc;">Spanish</div>
|
||||
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">Español</div>
|
||||
</div>
|
||||
<span style="font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 4px 8px; background: #334155; border-radius: 4px; color: #94a3b8;">Coming Soon</span>
|
||||
<span
|
||||
style="font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 4px 8px; background: #334155; border-radius: 4px; color: #94a3b8;">Coming
|
||||
Soon</span>
|
||||
</button>
|
||||
<button class="language-option" data-locale="zh-CN" disabled style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: not-allowed; position: relative; opacity: 0.5; text-align: left;">
|
||||
<button class="language-option" data-locale="zh-CN" disabled
|
||||
style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: not-allowed; position: relative; opacity: 0.5; text-align: left;">
|
||||
<span style="font-size: 40px; line-height: 1;">🇨🇳</span>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #f8fafc;">Chinese (Simplified)</div>
|
||||
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">简体中文</div>
|
||||
</div>
|
||||
<span style="font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 4px 8px; background: #334155; border-radius: 4px; color: #94a3b8;">Coming Soon</span>
|
||||
<span
|
||||
style="font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 4px 8px; background: #334155; border-radius: 4px; color: #94a3b8;">Coming
|
||||
Soon</span>
|
||||
</button>
|
||||
<button class="language-option" data-locale="fr" disabled style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: not-allowed; position: relative; opacity: 0.5; text-align: left;">
|
||||
<button class="language-option" data-locale="fr" disabled
|
||||
style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: not-allowed; position: relative; opacity: 0.5; text-align: left;">
|
||||
<span style="font-size: 40px; line-height: 1;">🇫🇷</span>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #f8fafc;">French</div>
|
||||
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">Français</div>
|
||||
</div>
|
||||
<span style="font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 4px 8px; background: #334155; border-radius: 4px; color: #94a3b8;">Coming Soon</span>
|
||||
<span
|
||||
style="font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 4px 8px; background: #334155; border-radius: 4px; color: #94a3b8;">Coming
|
||||
Soon</span>
|
||||
</button>
|
||||
<button class="language-option" data-locale="de" disabled style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: not-allowed; position: relative; opacity: 0.5; text-align: left;">
|
||||
<button class="language-option" data-locale="de" disabled
|
||||
style="display: flex; align-items: center; gap: 16px; padding: 20px; background: #0f172a; border: 2px solid #334155; border-radius: 12px; cursor: not-allowed; position: relative; opacity: 0.5; text-align: left;">
|
||||
<span style="font-size: 40px; line-height: 1;">🇩🇪</span>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #f8fafc;">German</div>
|
||||
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">Deutsch</div>
|
||||
</div>
|
||||
<span style="font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 4px 8px; background: #334155; border-radius: 4px; color: #94a3b8;">Coming Soon</span>
|
||||
<span
|
||||
style="font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 4px 8px; background: #334155; border-radius: 4px; color: #94a3b8;">Coming
|
||||
Soon</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
function initLanguageSelector() {
|
||||
var savedLocale = localStorage.getItem('gb-locale') || 'en';
|
||||
var buttons = document.querySelectorAll('.language-option[data-locale]');
|
||||
(function () {
|
||||
function initLanguageSelector() {
|
||||
var savedLocale = localStorage.getItem('gb-locale') || 'en';
|
||||
var buttons = document.querySelectorAll('.language-option[data-locale]');
|
||||
|
||||
buttons.forEach(function(btn) {
|
||||
var locale = btn.dataset.locale;
|
||||
var checkmark = btn.querySelector('.lang-check');
|
||||
buttons.forEach(function (btn) {
|
||||
var locale = btn.dataset.locale;
|
||||
var checkmark = btn.querySelector('.lang-check');
|
||||
|
||||
if (locale === savedLocale && checkmark) {
|
||||
btn.style.borderColor = '#3b82f6';
|
||||
btn.style.background = 'rgba(59, 130, 246, 0.1)';
|
||||
checkmark.style.opacity = '1';
|
||||
if (locale === savedLocale && checkmark) {
|
||||
btn.style.borderColor = '#3b82f6';
|
||||
btn.style.background = 'rgba(59, 130, 246, 0.1)';
|
||||
checkmark.style.opacity = '1';
|
||||
}
|
||||
|
||||
btn.addEventListener('mouseover', function () {
|
||||
if (!btn.disabled) {
|
||||
btn.style.borderColor = '#3b82f6';
|
||||
}
|
||||
});
|
||||
|
||||
btn.addEventListener('mouseout', function () {
|
||||
if (!btn.disabled && locale !== localStorage.getItem('gb-locale')) {
|
||||
btn.style.borderColor = '#334155';
|
||||
btn.style.background = '#0f172a';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.selectLanguage = function (locale, element) {
|
||||
var buttons = document.querySelectorAll('.language-option[data-locale]');
|
||||
buttons.forEach(function (btn) {
|
||||
btn.style.borderColor = '#334155';
|
||||
btn.style.background = '#0f172a';
|
||||
var check = btn.querySelector('.lang-check');
|
||||
if (check) check.style.opacity = '0';
|
||||
});
|
||||
|
||||
if (element) {
|
||||
element.style.borderColor = '#3b82f6';
|
||||
element.style.background = 'rgba(59, 130, 246, 0.1)';
|
||||
var check = element.querySelector('.lang-check');
|
||||
if (check) check.style.opacity = '1';
|
||||
}
|
||||
|
||||
btn.addEventListener('mouseover', function() {
|
||||
if (!btn.disabled) {
|
||||
btn.style.borderColor = '#3b82f6';
|
||||
}
|
||||
});
|
||||
localStorage.setItem('gb-locale', locale);
|
||||
document.documentElement.lang = locale;
|
||||
|
||||
btn.addEventListener('mouseout', function() {
|
||||
if (!btn.disabled && locale !== localStorage.getItem('gb-locale')) {
|
||||
btn.style.borderColor = '#334155';
|
||||
btn.style.background = '#0f172a';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (window.showToast) {
|
||||
showToast('Language changed to ' + (locale === 'pt-BR' ? 'Português' : 'English') + '. Reloading...');
|
||||
}
|
||||
|
||||
window.selectLanguage = function(locale, element) {
|
||||
var buttons = document.querySelectorAll('.language-option[data-locale]');
|
||||
buttons.forEach(function(btn) {
|
||||
btn.style.borderColor = '#334155';
|
||||
btn.style.background = '#0f172a';
|
||||
var check = btn.querySelector('.lang-check');
|
||||
if (check) check.style.opacity = '0';
|
||||
});
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
if (element) {
|
||||
element.style.borderColor = '#3b82f6';
|
||||
element.style.background = 'rgba(59, 130, 246, 0.1)';
|
||||
var check = element.querySelector('.lang-check');
|
||||
if (check) check.style.opacity = '1';
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initLanguageSelector);
|
||||
} else {
|
||||
initLanguageSelector();
|
||||
}
|
||||
|
||||
localStorage.setItem('gb-locale', locale);
|
||||
document.documentElement.lang = locale;
|
||||
|
||||
if (window.showToast) {
|
||||
showToast('Language changed to ' + (locale === 'pt-BR' ? 'Português' : 'English') + '. Reloading...');
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initLanguageSelector);
|
||||
} else {
|
||||
initLanguageSelector();
|
||||
}
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Regional Settings -->
|
||||
|
|
@ -657,7 +678,8 @@
|
|||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title" data-i18n="settings-display-language">Display Language</span>
|
||||
<span class="setting-desc" data-i18n="settings-language-affects">Affects all text in the application</span>
|
||||
<span class="setting-desc" data-i18n="settings-language-affects">Affects all text in the
|
||||
application</span>
|
||||
</div>
|
||||
<select id="language-select" class="form-select" onchange="changeLanguage(this.value)">
|
||||
<option value="en">🇺🇸 English</option>
|
||||
|
|
@ -673,7 +695,8 @@
|
|||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title" data-i18n="settings-date-format">Date Format</span>
|
||||
<span class="setting-desc" data-i18n="settings-date-format-desc">How dates are displayed</span>
|
||||
<span class="setting-desc" data-i18n="settings-date-format-desc">How dates are
|
||||
displayed</span>
|
||||
</div>
|
||||
<select id="date-format-select" class="form-select" onchange="changeDateFormat(this.value)">
|
||||
<option value="MM/DD/YYYY">MM/DD/YYYY (US)</option>
|
||||
|
|
@ -684,7 +707,8 @@
|
|||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title" data-i18n="settings-time-format">Time Format</span>
|
||||
<span class="setting-desc" data-i18n="settings-time-format-desc">12-hour or 24-hour clock</span>
|
||||
<span class="setting-desc" data-i18n="settings-time-format-desc">12-hour or 24-hour
|
||||
clock</span>
|
||||
</div>
|
||||
<select id="time-format-select" class="form-select" onchange="changeTimeFormat(this.value)">
|
||||
<option value="12h">12-hour (1:30 PM)</option>
|
||||
|
|
@ -702,9 +726,8 @@
|
|||
<p class="subtitle">Control how you receive notifications</p>
|
||||
</div>
|
||||
|
||||
<form hx-put="/api/user/notifications/preferences"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="showToast('Notification preferences saved')">
|
||||
<form hx-put="/api/user/notifications/preferences" hx-swap="none"
|
||||
hx-on::after-request="showToast('Notification preferences saved')">
|
||||
|
||||
<!-- Email Notifications -->
|
||||
<div class="setting-card">
|
||||
|
|
@ -825,10 +848,7 @@
|
|||
<div class="card-header">
|
||||
<h2>Storage Usage</h2>
|
||||
</div>
|
||||
<div class="storage-display"
|
||||
hx-get="/api/user/storage"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="storage-display" hx-get="/api/user/storage" hx-trigger="load" hx-swap="innerHTML">
|
||||
<div class="storage-bar-container">
|
||||
<div class="storage-bar">
|
||||
<div class="storage-bar-fill" style="width: 45%"></div>
|
||||
|
|
@ -905,10 +925,8 @@
|
|||
<div class="card-header">
|
||||
<h2>Connected Cloud Storage</h2>
|
||||
</div>
|
||||
<div class="connections-list" id="storage-connections"
|
||||
hx-get="/api/user/storage/connections"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="connections-list" id="storage-connections" hx-get="/api/user/storage/connections"
|
||||
hx-trigger="load" hx-swap="innerHTML">
|
||||
<div class="connection-item">
|
||||
<div class="connection-icon google-drive">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
|
|
@ -924,7 +942,9 @@
|
|||
<div class="connection-item">
|
||||
<div class="connection-icon dropbox">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 2l6 3.75L6 9.5 0 5.75 6 2zm12 0l6 3.75-6 3.75-6-3.75L18 2zM0 13.25L6 9.5l6 3.75-6 3.75-6-3.75zm18-3.75l6 3.75-6 3.75-6-3.75 6-3.75zM6 18.25l6-3.75 6 3.75-6 3.75-6-3.75z"></path>
|
||||
<path
|
||||
d="M6 2l6 3.75L6 9.5 0 5.75 6 2zm12 0l6 3.75-6 3.75-6-3.75L18 2zM0 13.25L6 9.5l6 3.75-6 3.75-6-3.75zm18-3.75l6 3.75-6 3.75-6-3.75 6-3.75zM6 18.25l6-3.75 6 3.75-6 3.75-6-3.75z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="connection-info">
|
||||
|
|
@ -949,17 +969,16 @@
|
|||
<div class="card-header">
|
||||
<h2>API Keys</h2>
|
||||
<button class="btn-primary btn-sm" disabled title="Coming soon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
Create Key
|
||||
</button>
|
||||
</div>
|
||||
<div class="api-keys-list" id="api-keys-list"
|
||||
hx-get="/api/user/api-keys"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="api-keys-list" id="api-keys-list" hx-get="/api/user/api-keys" hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="empty-state">
|
||||
<p>No API keys created yet</p>
|
||||
</div>
|
||||
|
|
@ -971,17 +990,16 @@
|
|||
<div class="card-header">
|
||||
<h2>Webhooks</h2>
|
||||
<button class="btn-primary btn-sm" disabled title="Coming soon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
Add Webhook
|
||||
</button>
|
||||
</div>
|
||||
<div class="webhooks-list" id="webhooks-list"
|
||||
hx-get="/api/user/webhooks"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="webhooks-list" id="webhooks-list" hx-get="/api/user/webhooks" hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="empty-state">
|
||||
<p>No webhooks configured</p>
|
||||
</div>
|
||||
|
|
@ -997,19 +1015,26 @@
|
|||
<div class="oauth-item">
|
||||
<div class="oauth-icon google">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" 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"></path>
|
||||
<path fill="#34A853" 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"></path>
|
||||
<path fill="#FBBC05" 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"></path>
|
||||
<path fill="#EA4335" 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"></path>
|
||||
<path fill="#4285F4"
|
||||
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">
|
||||
</path>
|
||||
<path fill="#34A853"
|
||||
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">
|
||||
</path>
|
||||
<path fill="#FBBC05"
|
||||
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">
|
||||
</path>
|
||||
<path fill="#EA4335"
|
||||
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">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="oauth-info">
|
||||
<span class="oauth-name">Google</span>
|
||||
<span class="oauth-status">Not connected</span>
|
||||
</div>
|
||||
<button class="btn-secondary btn-sm"
|
||||
hx-post="/api/oauth/google/connect"
|
||||
hx-swap="none">Connect</button>
|
||||
<button class="btn-secondary btn-sm" hx-post="/api/oauth/google/connect"
|
||||
hx-swap="none">Connect</button>
|
||||
</div>
|
||||
<div class="oauth-item">
|
||||
<div class="oauth-icon microsoft">
|
||||
|
|
@ -1024,23 +1049,23 @@
|
|||
<span class="oauth-name">Microsoft</span>
|
||||
<span class="oauth-status">Not connected</span>
|
||||
</div>
|
||||
<button class="btn-secondary btn-sm"
|
||||
hx-post="/api/oauth/microsoft/connect"
|
||||
hx-swap="none">Connect</button>
|
||||
<button class="btn-secondary btn-sm" hx-post="/api/oauth/microsoft/connect"
|
||||
hx-swap="none">Connect</button>
|
||||
</div>
|
||||
<div class="oauth-item">
|
||||
<div class="oauth-icon github">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"></path>
|
||||
<path
|
||||
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="oauth-info">
|
||||
<span class="oauth-name">GitHub</span>
|
||||
<span class="oauth-status">Not connected</span>
|
||||
</div>
|
||||
<button class="btn-secondary btn-sm"
|
||||
hx-post="/api/oauth/github/connect"
|
||||
hx-swap="none">Connect</button>
|
||||
<button class="btn-secondary btn-sm" hx-post="/api/oauth/github/connect"
|
||||
hx-swap="none">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1102,10 +1127,8 @@
|
|||
<span class="setting-title">Export Data</span>
|
||||
<span class="setting-desc">Download all your data in a portable format</span>
|
||||
</div>
|
||||
<button class="btn-secondary"
|
||||
hx-post="/api/user/data/export"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="showToast('Export started. You will receive an email when ready.')">
|
||||
<button class="btn-secondary" hx-post="/api/user/data/export" hx-swap="none"
|
||||
hx-on::after-request="showToast('Export started. You will receive an email when ready.')">
|
||||
Request Export
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1138,10 +1161,7 @@
|
|||
<div class="card-header">
|
||||
<h2>Current Plan</h2>
|
||||
</div>
|
||||
<div class="plan-display"
|
||||
hx-get="/api/user/billing/plan"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="plan-display" hx-get="/api/user/billing/plan" hx-trigger="load" hx-swap="innerHTML">
|
||||
<div class="plan-info">
|
||||
<div class="plan-badge">Pro</div>
|
||||
<div class="plan-details">
|
||||
|
|
@ -1164,15 +1184,14 @@
|
|||
Add Method
|
||||
</button>
|
||||
</div>
|
||||
<div class="payment-methods" id="payment-methods"
|
||||
hx-get="/api/user/billing/payment-methods"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="payment-methods" id="payment-methods" hx-get="/api/user/billing/payment-methods"
|
||||
hx-trigger="load" hx-swap="innerHTML">
|
||||
<div class="payment-card">
|
||||
<div class="card-brand visa">
|
||||
<svg width="32" height="20" viewBox="0 0 32 20">
|
||||
<rect width="32" height="20" rx="2" fill="#1A1F71"></rect>
|
||||
<text x="16" y="13" text-anchor="middle" fill="white" font-size="8" font-weight="bold">VISA</text>
|
||||
<text x="16" y="13" text-anchor="middle" fill="white" font-size="8"
|
||||
font-weight="bold">VISA</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-details">
|
||||
|
|
@ -1189,10 +1208,8 @@
|
|||
<div class="card-header">
|
||||
<h2>Billing History</h2>
|
||||
</div>
|
||||
<div class="invoices-list" id="invoices-list"
|
||||
hx-get="/api/user/billing/invoices"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div class="invoices-list" id="invoices-list" hx-get="/api/user/billing/invoices" hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<table class="invoices-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -1234,8 +1251,10 @@
|
|||
<div class="setting-card">
|
||||
<div class="card-header">
|
||||
<h2>User Management</h2>
|
||||
<button class="btn-primary btn-sm" onclick="document.getElementById('create-user-modal').showModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button class="btn-primary btn-sm"
|
||||
onclick="document.getElementById('create-user-modal').showModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
|
|
@ -1261,8 +1280,10 @@
|
|||
<div class="setting-card">
|
||||
<div class="card-header">
|
||||
<h2>Group Management</h2>
|
||||
<button class="btn-primary btn-sm" onclick="document.getElementById('create-group-modal').showModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button class="btn-primary btn-sm"
|
||||
onclick="document.getElementById('create-group-modal').showModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
|
|
@ -1308,8 +1329,10 @@
|
|||
<div class="setting-card">
|
||||
<div class="card-header">
|
||||
<h2>Domain Configuration</h2>
|
||||
<button class="btn-primary btn-sm" onclick="document.getElementById('register-dns-modal').showModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button class="btn-primary btn-sm"
|
||||
onclick="document.getElementById('register-dns-modal').showModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
|
|
@ -1359,7 +1382,8 @@
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form hx-post="/api/users/create" hx-swap="none" hx-on::after-request="if(event.detail.successful) { document.getElementById('create-user-modal').close(); showToast('User created successfully'); }">
|
||||
<form hx-post="/api/users/create" hx-swap="none"
|
||||
hx-on::after-request="if(event.detail.successful) { document.getElementById('create-user-modal').close(); showToast('User created successfully'); }">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" required placeholder="username" autocomplete="username">
|
||||
|
|
@ -1385,7 +1409,8 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" onclick="document.getElementById('create-user-modal').close()">Cancel</button>
|
||||
<button type="button" class="btn-secondary"
|
||||
onclick="document.getElementById('create-user-modal').close()">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Create User</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1404,7 +1429,8 @@
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form hx-post="/api/groups/create" hx-swap="none" hx-on::after-request="if(event.detail.successful) { document.getElementById('create-group-modal').close(); showToast('Group created successfully'); }">
|
||||
<form hx-post="/api/groups/create" hx-swap="none"
|
||||
hx-on::after-request="if(event.detail.successful) { document.getElementById('create-group-modal').close(); showToast('Group created successfully'); }">
|
||||
<div class="form-group">
|
||||
<label>Group Name</label>
|
||||
<input type="text" name="name" required placeholder="Engineering Team">
|
||||
|
|
@ -1414,7 +1440,8 @@
|
|||
<textarea name="description" placeholder="Group description..." rows="3"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" onclick="document.getElementById('create-group-modal').close()">Cancel</button>
|
||||
<button type="button" class="btn-secondary"
|
||||
onclick="document.getElementById('create-group-modal').close()">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Create Group</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1433,7 +1460,8 @@
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form hx-post="/api/dns/register" hx-swap="none" hx-on::after-request="if(event.detail.successful) { document.getElementById('register-dns-modal').close(); showToast('DNS record registered'); }">
|
||||
<form hx-post="/api/dns/register" hx-swap="none"
|
||||
hx-on::after-request="if(event.detail.successful) { document.getElementById('register-dns-modal').close(); showToast('DNS record registered'); }">
|
||||
<div class="form-group">
|
||||
<label>Hostname</label>
|
||||
<input type="text" name="hostname" required placeholder="mybot.example.com">
|
||||
|
|
@ -1451,7 +1479,8 @@
|
|||
<input type="text" name="target" placeholder="192.168.1.1 or target.domain.com">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" onclick="document.getElementById('register-dns-modal').close()">Cancel</button>
|
||||
<button type="button" class="btn-secondary"
|
||||
onclick="document.getElementById('register-dns-modal').close()">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1530,7 +1559,9 @@
|
|||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-layout {
|
||||
|
|
@ -1856,11 +1887,11 @@
|
|||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.toggle input:checked + .toggle-slider {
|
||||
.toggle input:checked+.toggle-slider {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.toggle input:checked + .toggle-slider::before {
|
||||
.toggle input:checked+.toggle-slider::before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
|
|
@ -2312,7 +2343,8 @@
|
|||
<div class="sidebar-overlay" id="sidebar-overlay" onclick="toggleSettingsSidebar()"></div>
|
||||
|
||||
<!-- Mobile menu toggle -->
|
||||
<button class="mobile-menu-toggle" id="mobile-menu-toggle" onclick="toggleSettingsSidebar()" aria-label="Toggle settings menu">
|
||||
<button class="mobile-menu-toggle" id="mobile-menu-toggle" onclick="toggleSettingsSidebar()"
|
||||
aria-label="Toggle settings menu">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
|
|
@ -2365,7 +2397,7 @@
|
|||
}
|
||||
|
||||
// Handle hash navigation on load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash) {
|
||||
const navItem = document.querySelector(`a[href="#${hash}"]`);
|
||||
|
|
@ -2377,7 +2409,7 @@
|
|||
|
||||
// Theme selection - handle both .theme-card and .theme-option elements
|
||||
document.querySelectorAll('.theme-card').forEach(card => {
|
||||
card.addEventListener('click', function() {
|
||||
card.addEventListener('click', function () {
|
||||
const theme = this.dataset.theme;
|
||||
document.body.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('gb-theme', theme);
|
||||
|
|
@ -2414,11 +2446,11 @@
|
|||
(function loadTranslations() {
|
||||
var script = document.createElement('script');
|
||||
script.src = '/suite/js/translations.js';
|
||||
script.onload = function() {
|
||||
script.onload = function () {
|
||||
if (window.gbTranslations) {
|
||||
window.gbTranslations.translatePage();
|
||||
}
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
})();
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -3,82 +3,35 @@
|
|||
<div class="sheet-app" id="sheet-app">
|
||||
<div class="sheet-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<input
|
||||
type="text"
|
||||
class="sheet-name-input"
|
||||
id="sheetName"
|
||||
value="Untitled Spreadsheet"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<input type="text" class="sheet-name-input" id="sheetName" value="Untitled Spreadsheet"
|
||||
spellcheck="false" />
|
||||
</div>
|
||||
<div class="toolbar-center">
|
||||
<div class="toolbar-group">
|
||||
<button class="btn-icon" id="undoBtn" title="Undo (Ctrl+Z)">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 7v6h6"></path>
|
||||
<path
|
||||
d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"
|
||||
></path>
|
||||
<path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="redoBtn" title="Redo (Ctrl+Y)">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 7v6h-6"></path>
|
||||
<path
|
||||
d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"
|
||||
></path>
|
||||
<path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="printPreviewBtn"
|
||||
title="Print Preview"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="printPreviewBtn" title="Print Preview">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 6 2 18 2 18 9"></polyline>
|
||||
<path
|
||||
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
|
||||
></path>
|
||||
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
|
||||
<rect x="6" y="14" width="12" height="8"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="findReplaceBtn"
|
||||
title="Find & Replace (Ctrl+H)"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="findReplaceBtn" title="Find & Replace (Ctrl+H)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
|
|
@ -86,11 +39,7 @@
|
|||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<select
|
||||
class="toolbar-select font-family"
|
||||
id="fontFamily"
|
||||
title="Font"
|
||||
>
|
||||
<select class="toolbar-select font-family" id="fontFamily" title="Font">
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Helvetica">Helvetica</option>
|
||||
<option value="Times New Roman">Times</option>
|
||||
|
|
@ -98,11 +47,7 @@
|
|||
<option value="Georgia">Georgia</option>
|
||||
<option value="Verdana">Verdana</option>
|
||||
</select>
|
||||
<select
|
||||
class="toolbar-select font-size"
|
||||
id="fontSize"
|
||||
title="Size"
|
||||
>
|
||||
<select class="toolbar-select font-size" id="fontSize" title="Size">
|
||||
<option value="8">8</option>
|
||||
<option value="9">9</option>
|
||||
<option value="10">10</option>
|
||||
|
|
@ -123,11 +68,7 @@
|
|||
<button class="btn-icon" id="italicBtn" title="Italic (Ctrl+I)">
|
||||
<em>I</em>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="underlineBtn"
|
||||
title="Underline (Ctrl+U)"
|
||||
>
|
||||
<button class="btn-icon" id="underlineBtn" title="Underline (Ctrl+U)">
|
||||
<u>U</u>
|
||||
</button>
|
||||
<button class="btn-icon" id="strikeBtn" title="Strikethrough">
|
||||
|
|
@ -136,100 +77,38 @@
|
|||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="btn-icon color-btn"
|
||||
id="textColorBtn"
|
||||
title="Text Color"
|
||||
>
|
||||
<button class="btn-icon color-btn" id="textColorBtn" title="Text Color">
|
||||
<span class="color-letter">A</span>
|
||||
<span
|
||||
class="color-indicator"
|
||||
id="textColorIndicator"
|
||||
></span>
|
||||
<input
|
||||
type="color"
|
||||
class="color-input"
|
||||
id="textColorInput"
|
||||
value="#000000"
|
||||
/>
|
||||
<span class="color-indicator" id="textColorIndicator"></span>
|
||||
<input type="color" class="color-input" id="textColorInput" value="#000000" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon color-btn"
|
||||
id="bgColorBtn"
|
||||
title="Fill Color"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<button class="btn-icon color-btn" id="bgColorBtn" title="Fill Color">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" fill="none" stroke="currentColor"
|
||||
stroke-width="2" />
|
||||
</svg>
|
||||
<span
|
||||
class="color-indicator bg-indicator"
|
||||
id="bgColorIndicator"
|
||||
></span>
|
||||
<input
|
||||
type="color"
|
||||
class="color-input"
|
||||
id="bgColorInput"
|
||||
value="#ffffff"
|
||||
/>
|
||||
<span class="color-indicator bg-indicator" id="bgColorIndicator"></span>
|
||||
<input type="color" class="color-input" id="bgColorInput" value="#ffffff" />
|
||||
</button>
|
||||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button class="btn-icon" id="alignLeftBtn" title="Align Left">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="12" x2="15" y2="12"></line>
|
||||
<line x1="3" y1="18" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="alignCenterBtn"
|
||||
title="Align Center"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="alignCenterBtn" title="Align Center">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="6" y1="12" x2="18" y2="12"></line>
|
||||
<line x1="4" y1="18" x2="20" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="alignRightBtn" title="Align Right">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="9" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="6" y1="18" x2="21" y2="18"></line>
|
||||
|
|
@ -238,11 +117,7 @@
|
|||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<select
|
||||
class="toolbar-select number-format"
|
||||
id="numberFormat"
|
||||
title="Number Format"
|
||||
>
|
||||
<select class="toolbar-select number-format" id="numberFormat" title="Number Format">
|
||||
<option value="general">General</option>
|
||||
<option value="number">Number (1,234.56)</option>
|
||||
<option value="currency">Currency ($1,234.56)</option>
|
||||
|
|
@ -257,38 +132,16 @@
|
|||
<option value="text">Text</option>
|
||||
<option value="custom">Custom...</option>
|
||||
</select>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="decreaseDecimalBtn"
|
||||
title="Decrease Decimal"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="decreaseDecimalBtn" title="Decrease Decimal">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<text x="2" y="16" font-size="10" fill="currentColor">
|
||||
.0
|
||||
</text>
|
||||
<path d="M18 8l-4 4 4 4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="increaseDecimalBtn"
|
||||
title="Increase Decimal"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="increaseDecimalBtn" title="Increase Decimal">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<text x="2" y="16" font-size="10" fill="currentColor">
|
||||
.00
|
||||
</text>
|
||||
|
|
@ -299,165 +152,56 @@
|
|||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button class="btn-icon" id="mergeCellsBtn" title="Merge Cells">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<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"></rect>
|
||||
<line x1="12" y1="3" x2="12" y2="21"></line>
|
||||
<path
|
||||
d="M9 12h6M9 12l2-2M9 12l2 2M15 12l-2-2M15 12l-2 2"
|
||||
></path>
|
||||
<path d="M9 12h6M9 12l2-2M9 12l2 2M15 12l-2-2M15 12l-2 2"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="conditionalFormatBtn"
|
||||
title="Conditional Formatting"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="7"
|
||||
height="7"
|
||||
fill="#4CAF50"
|
||||
></rect>
|
||||
<rect
|
||||
x="14"
|
||||
y="3"
|
||||
width="7"
|
||||
height="7"
|
||||
fill="#FFC107"
|
||||
></rect>
|
||||
<rect
|
||||
x="3"
|
||||
y="14"
|
||||
width="7"
|
||||
height="7"
|
||||
fill="#F44336"
|
||||
></rect>
|
||||
<rect
|
||||
x="14"
|
||||
y="14"
|
||||
width="7"
|
||||
height="7"
|
||||
fill="#2196F3"
|
||||
></rect>
|
||||
<button class="btn-icon" id="conditionalFormatBtn" title="Conditional Formatting">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" fill="#4CAF50"></rect>
|
||||
<rect x="14" y="3" width="7" height="7" fill="#FFC107"></rect>
|
||||
<rect x="3" y="14" width="7" height="7" fill="#F44336"></rect>
|
||||
<rect x="14" y="14" width="7" height="7" fill="#2196F3"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="dataValidationBtn"
|
||||
title="Data Validation"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="dataValidationBtn" title="Data Validation">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 11l3 3L22 4"></path>
|
||||
<path
|
||||
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
|
||||
></path>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="toolbar-divider"></span>
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="insertChartBtn"
|
||||
title="Insert Chart"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-icon" id="insertChartBtn" title="Insert Chart">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="12" width="4" height="9"></rect>
|
||||
<rect x="10" y="6" width="4" height="15"></rect>
|
||||
<rect x="17" y="3" width="4" height="18"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="insertImageBtn"
|
||||
title="Insert Image"
|
||||
>
|
||||
<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>
|
||||
<button class="btn-icon" id="insertImageBtn" title="Insert Image">
|
||||
<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>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="filterBtn" title="Filter">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polygon
|
||||
points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"
|
||||
></polygon>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="sortAscBtn" title="Sort A→Z">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 5h10M11 9h7M11 13h4"></path>
|
||||
<path d="M3 17l3 3 3-3M6 18V4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="sortDescBtn" title="Sort Z→A">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 5h4M11 9h7M11 13h10"></path>
|
||||
<path d="M3 7l3-3 3 3M6 6v12"></path>
|
||||
</svg>
|
||||
|
|
@ -467,14 +211,7 @@
|
|||
<div class="toolbar-right">
|
||||
<div class="collaborators" id="collaborators"></div>
|
||||
<button class="btn-primary" id="shareBtn">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="18" cy="5" r="3"></circle>
|
||||
<circle cx="6" cy="12" r="3"></circle>
|
||||
<circle cx="18" cy="19" r="3"></circle>
|
||||
|
|
@ -483,24 +220,7 @@
|
|||
</svg>
|
||||
<span>Share</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon chat-toggle"
|
||||
id="chatToggle"
|
||||
title="Toggle AI Chat"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
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"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -509,12 +229,7 @@
|
|||
<div class="formula-bar">
|
||||
<div class="cell-address" id="cellAddress">A1</div>
|
||||
<span class="formula-icon">fx</span>
|
||||
<input
|
||||
type="text"
|
||||
class="formula-input"
|
||||
id="formulaInput"
|
||||
placeholder="Enter value or formula..."
|
||||
/>
|
||||
<input type="text" class="formula-input" id="formulaInput" placeholder="Enter value or formula..." />
|
||||
</div>
|
||||
|
||||
<div class="sheet-grid-container" id="gridContainer">
|
||||
|
|
@ -527,18 +242,9 @@
|
|||
<div class="selection-box" id="selectionBox"></div>
|
||||
<div class="copy-box hidden" id="copyBox"></div>
|
||||
<div class="autofill-handle" id="autofillHandle"></div>
|
||||
<div
|
||||
class="cursor-indicators"
|
||||
id="cursorIndicators"
|
||||
></div>
|
||||
<div
|
||||
class="charts-container"
|
||||
id="chartsContainer"
|
||||
></div>
|
||||
<div
|
||||
class="images-container"
|
||||
id="imagesContainer"
|
||||
></div>
|
||||
<div class="cursor-indicators" id="cursorIndicators"></div>
|
||||
<div class="charts-container" id="chartsContainer"></div>
|
||||
<div class="images-container" id="imagesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -550,19 +256,8 @@
|
|||
<button class="tab-menu-btn">▼</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn-add-sheet"
|
||||
id="addSheetBtn"
|
||||
title="Add Sheet"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<button class="btn-add-sheet" id="addSheetBtn" title="Add Sheet">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
|
|
@ -587,105 +282,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="chat-panel" id="chatPanel">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">
|
||||
<div class="chat-avatar">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"
|
||||
/>
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
<path
|
||||
d="M12 15c-3 0-5 1.5-5 3v1h10v-1c0-1.5-2-3-5-3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3>AI Assistant</h3>
|
||||
<span class="chat-status">Ready to help</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="chat-close" id="chatClose" title="Close">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
<div class="chat-message assistant">
|
||||
<div class="message-bubble">
|
||||
<p>
|
||||
Hi! I can help you edit your spreadsheet. Try asking
|
||||
me to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Sum a column or range</li>
|
||||
<li>Format cells as currency</li>
|
||||
<li>Create a chart</li>
|
||||
<li>Sort or filter data</li>
|
||||
<li>Apply conditional formatting</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-suggestions" id="chatSuggestions">
|
||||
<button class="suggestion-btn" data-action="sum">
|
||||
Sum column B
|
||||
</button>
|
||||
<button class="suggestion-btn" data-action="format">
|
||||
Format as $
|
||||
</button>
|
||||
<button class="suggestion-btn" data-action="chart">
|
||||
Create chart
|
||||
</button>
|
||||
<button class="suggestion-btn" data-action="sort">
|
||||
Sort A-Z
|
||||
</button>
|
||||
</div>
|
||||
<form class="chat-input-container" id="chatForm">
|
||||
<input
|
||||
type="text"
|
||||
class="chat-input"
|
||||
id="chatInput"
|
||||
placeholder="Ask me to edit your spreadsheet..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="chat-send"
|
||||
id="chatSend"
|
||||
title="Send"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -730,11 +327,7 @@
|
|||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="share-input-group">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter email address"
|
||||
id="shareEmail"
|
||||
/>
|
||||
<input type="email" placeholder="Enter email address" id="shareEmail" />
|
||||
<select id="sharePermission">
|
||||
<option value="view">Can view</option>
|
||||
<option value="edit">Can edit</option>
|
||||
|
|
@ -746,25 +339,10 @@
|
|||
<div class="share-link-group">
|
||||
<input type="text" readonly id="shareLink" />
|
||||
<button class="btn-icon" id="copyLinkBtn" title="Copy link">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="9"
|
||||
y="9"
|
||||
width="13"
|
||||
height="13"
|
||||
rx="2"
|
||||
ry="2"
|
||||
></rect>
|
||||
<path
|
||||
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
|
||||
></path>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -782,12 +360,7 @@
|
|||
<div class="modal-body">
|
||||
<div class="chart-type-selector">
|
||||
<button class="chart-type-btn active" data-type="bar">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="3" y="12" width="4" height="9"></rect>
|
||||
<rect x="10" y="6" width="4" height="15"></rect>
|
||||
<rect x="17" y="3" width="4" height="18"></rect>
|
||||
|
|
@ -795,27 +368,13 @@
|
|||
<span>Bar</span>
|
||||
</button>
|
||||
<button class="chart-type-btn" data-type="line">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 18 9 12 13 16 21 6"></polyline>
|
||||
</svg>
|
||||
<span>Line</span>
|
||||
</button>
|
||||
<button class="chart-type-btn" data-type="pie">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 2v10l8.5 5"></path>
|
||||
</svg>
|
||||
|
|
@ -824,17 +383,9 @@
|
|||
</div>
|
||||
<div class="chart-options">
|
||||
<label>Data Range</label>
|
||||
<input
|
||||
type="text"
|
||||
id="chartDataRange"
|
||||
placeholder="e.g., A1:B10"
|
||||
/>
|
||||
<input type="text" id="chartDataRange" placeholder="e.g., A1:B10" />
|
||||
<label>Chart Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="chartTitle"
|
||||
placeholder="Enter chart title"
|
||||
/>
|
||||
<input type="text" id="chartTitle" placeholder="Enter chart title" />
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-secondary" id="cancelChartBtn">
|
||||
|
|
@ -857,20 +408,11 @@
|
|||
<div class="modal-body">
|
||||
<div class="find-replace-group">
|
||||
<label>Find:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="findInput"
|
||||
placeholder="Search text..."
|
||||
autofocus
|
||||
/>
|
||||
<input type="text" id="findInput" placeholder="Search text..." autofocus />
|
||||
</div>
|
||||
<div class="find-replace-group">
|
||||
<label>Replace:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="replaceInput"
|
||||
placeholder="Replace with..."
|
||||
/>
|
||||
<input type="text" id="replaceInput" placeholder="Replace with..." />
|
||||
</div>
|
||||
<div class="find-replace-options">
|
||||
<label class="checkbox-label">
|
||||
|
|
@ -939,12 +481,7 @@
|
|||
</div>
|
||||
<div class="cf-section cf-values" id="cfValuesSection">
|
||||
<input type="text" id="cfValue1" placeholder="Value" />
|
||||
<input
|
||||
type="text"
|
||||
id="cfValue2"
|
||||
placeholder="and"
|
||||
class="hidden"
|
||||
/>
|
||||
<input type="text" id="cfValue2" placeholder="and" class="hidden" />
|
||||
</div>
|
||||
<div class="cf-section">
|
||||
<label>Formatting style:</label>
|
||||
|
|
@ -996,11 +533,7 @@
|
|||
<div class="dv-tab-content active" id="dvSettingsTab">
|
||||
<div class="dv-section">
|
||||
<label>Apply to range:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="dvRange"
|
||||
placeholder="e.g., A1:A100"
|
||||
/>
|
||||
<input type="text" id="dvRange" placeholder="e.g., A1:A100" />
|
||||
</div>
|
||||
<div class="dv-section">
|
||||
<label>Allow:</label>
|
||||
|
|
@ -1044,10 +577,7 @@
|
|||
</div>
|
||||
<div class="dv-section dv-list hidden" id="dvListSection">
|
||||
<label>Source (comma-separated):</label>
|
||||
<textarea
|
||||
id="dvListSource"
|
||||
placeholder="Option 1, Option 2, Option 3"
|
||||
></textarea>
|
||||
<textarea id="dvListSource" placeholder="Option 1, Option 2, Option 3"></textarea>
|
||||
</div>
|
||||
<div class="dv-section">
|
||||
<label class="checkbox-label">
|
||||
|
|
@ -1069,18 +599,11 @@
|
|||
</div>
|
||||
<div class="dv-section">
|
||||
<label>Title:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="dvInputTitle"
|
||||
placeholder="Input title"
|
||||
/>
|
||||
<input type="text" id="dvInputTitle" placeholder="Input title" />
|
||||
</div>
|
||||
<div class="dv-section">
|
||||
<label>Input message:</label>
|
||||
<textarea
|
||||
id="dvInputMessage"
|
||||
placeholder="Enter instructions for the user"
|
||||
></textarea>
|
||||
<textarea id="dvInputMessage" placeholder="Enter instructions for the user"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dv-tab-content" id="dvErrorTab">
|
||||
|
|
@ -1100,18 +623,11 @@
|
|||
</div>
|
||||
<div class="dv-section">
|
||||
<label>Title:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="dvErrorTitle"
|
||||
placeholder="Error title"
|
||||
/>
|
||||
<input type="text" id="dvErrorTitle" placeholder="Error title" />
|
||||
</div>
|
||||
<div class="dv-section">
|
||||
<label>Error message:</label>
|
||||
<textarea
|
||||
id="dvErrorMessage"
|
||||
placeholder="Enter error message"
|
||||
></textarea>
|
||||
<textarea id="dvErrorMessage" placeholder="Enter error message"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
|
|
@ -1166,18 +682,9 @@
|
|||
<div class="modal-actions">
|
||||
<button class="btn-secondary" id="cancelPrintBtn">Cancel</button>
|
||||
<button class="btn-primary" id="printBtn">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 6 2 18 2 18 9"></polyline>
|
||||
<path
|
||||
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
|
||||
></path>
|
||||
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
|
||||
<rect x="6" y="14" width="12" height="8"></rect>
|
||||
</svg>
|
||||
Print
|
||||
|
|
@ -1274,22 +781,8 @@
|
|||
<input type="file" id="imgFile" accept="image/*" />
|
||||
</div>
|
||||
<div class="img-drop-zone" id="imgDropZone">
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
ry="2"
|
||||
></rect>
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
|
|
@ -1308,4 +801,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="sheet/sheet.js"></script>
|
||||
<script src="sheet/sheet.js"></script>
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
isSelecting: false,
|
||||
isDirty: false,
|
||||
autoSaveTimer: null,
|
||||
chatPanelOpen: true,
|
||||
|
||||
findMatches: [],
|
||||
findMatchIndex: -1,
|
||||
decimalPlaces: 2,
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
bindEvents();
|
||||
loadFromUrlParams();
|
||||
connectWebSocket();
|
||||
connectChatWebSocket();
|
||||
|
||||
selectCell(0, 0);
|
||||
updateCellAddress();
|
||||
renderCharts();
|
||||
|
|
@ -77,10 +77,7 @@
|
|||
elements.calculationResult = document.getElementById("calculationResult");
|
||||
elements.saveStatus = document.getElementById("saveStatus");
|
||||
elements.zoomLevel = document.getElementById("zoomLevel");
|
||||
elements.chatPanel = document.getElementById("chatPanel");
|
||||
elements.chatMessages = document.getElementById("chatMessages");
|
||||
elements.chatInput = document.getElementById("chatInput");
|
||||
elements.chatForm = document.getElementById("chatForm");
|
||||
|
||||
elements.findReplaceModal = document.getElementById("findReplaceModal");
|
||||
elements.conditionalFormatModal = document.getElementById(
|
||||
"conditionalFormatModal",
|
||||
|
|
@ -453,19 +450,7 @@
|
|||
.getElementById("cnfFormatCode")
|
||||
?.addEventListener("input", updateCnfPreview);
|
||||
|
||||
document
|
||||
.getElementById("chatToggle")
|
||||
?.addEventListener("click", toggleChatPanel);
|
||||
document
|
||||
.getElementById("chatClose")
|
||||
?.addEventListener("click", toggleChatPanel);
|
||||
elements.chatForm?.addEventListener("submit", handleChatSubmit);
|
||||
|
||||
document.querySelectorAll(".suggestion-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () =>
|
||||
handleSuggestionClick(btn.dataset.action),
|
||||
);
|
||||
});
|
||||
|
||||
document.querySelectorAll(".context-item").forEach((item) => {
|
||||
item.addEventListener("click", () =>
|
||||
|
|
@ -1970,18 +1955,18 @@
|
|||
return isNaN(d2.getTime())
|
||||
? value
|
||||
: d2.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
case "time":
|
||||
const d3 = new Date(num);
|
||||
return isNaN(d3.getTime())
|
||||
? value
|
||||
: d3.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
case "datetime":
|
||||
const d4 = new Date(num);
|
||||
return isNaN(d4.getTime()) ? value : d4.toLocaleString("en-US");
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@
|
|||
<div class="modal-actions">
|
||||
<button
|
||||
class="btn-secondary"
|
||||
onclick="gbSlides.hideModal('imageModal')"
|
||||
onclick="window.slidesApp.hideModal('imageModal')"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
|
@ -781,7 +781,7 @@
|
|||
<div class="modal-actions">
|
||||
<button
|
||||
class="btn-secondary"
|
||||
onclick="gbSlides.hideModal('notesModal')"
|
||||
onclick="window.slidesApp.hideModal('notesModal')"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
|
@ -815,7 +815,7 @@
|
|||
<div class="modal-actions">
|
||||
<button
|
||||
class="btn-secondary"
|
||||
onclick="gbSlides.hideModal('backgroundModal')"
|
||||
onclick="window.slidesApp.hideModal('backgroundModal')"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -452,40 +452,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function cacheElements() {
|
||||
elements.container = document.querySelector(".slides-container");
|
||||
elements.sidebar = document.getElementById("slides-sidebar");
|
||||
elements.thumbnails = document.getElementById("slide-thumbnails");
|
||||
elements.canvas = document.getElementById("slide-canvas");
|
||||
elements.canvasContainer = document.getElementById("canvas-container");
|
||||
elements.selectionHandles = document.getElementById("selection-handles");
|
||||
elements.propertiesPanel = document.getElementById("properties-panel");
|
||||
elements.presentationName = document.getElementById("presentation-name");
|
||||
elements.collaborators = document.getElementById("collaborators");
|
||||
elements.contextMenu = document.getElementById("context-menu");
|
||||
elements.slideContextMenu = document.getElementById("slide-context-menu");
|
||||
elements.cursorIndicators = document.getElementById("cursor-indicators");
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
elements.canvas.addEventListener("mousedown", handleCanvasMouseDown);
|
||||
elements.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
document.addEventListener("click", hideContextMenus);
|
||||
elements.canvas.addEventListener("contextmenu", handleContextMenu);
|
||||
|
||||
const handles = elements.selectionHandles.querySelectorAll(
|
||||
".handle, .rotate-handle",
|
||||
);
|
||||
handles.forEach((handle) => {
|
||||
handle.addEventListener("mousedown", handleResizeStart);
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
}
|
||||
|
||||
function createNewPresentation() {
|
||||
const titleSlide = createSlide("title");
|
||||
state.slides = [titleSlide];
|
||||
|
|
@ -1094,7 +1060,7 @@
|
|||
}
|
||||
} else if (e.key === "Escape") {
|
||||
clearSelection();
|
||||
hideContextMenus();
|
||||
hideAllContextMenus();
|
||||
if (state.isPresenting) {
|
||||
exitPresentation();
|
||||
}
|
||||
|
|
@ -1740,6 +1706,14 @@
|
|||
elements.slideContextMenu?.classList.add("hidden");
|
||||
}
|
||||
|
||||
function showSlideContextMenu(e, slideIndex) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
state.currentSlideIndex = slideIndex;
|
||||
hideAllContextMenus();
|
||||
showContextMenu(elements.slideContextMenu, e.clientX, e.clientY);
|
||||
}
|
||||
|
||||
function showContextMenu(menu, x, y) {
|
||||
if (!menu) return;
|
||||
menu.style.left = `${x}px`;
|
||||
|
|
@ -2899,7 +2873,7 @@
|
|||
updateMasterPreview();
|
||||
}
|
||||
|
||||
window.gbSlides = {
|
||||
window.slidesApp = {
|
||||
init,
|
||||
addSlide,
|
||||
addTextBox,
|
||||
|
|
@ -2919,6 +2893,7 @@
|
|||
showSlideSorter,
|
||||
exportToPdf,
|
||||
showMasterSlideModal,
|
||||
showSlideContextMenu,
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="social/social.css" />
|
||||
|
||||
<div class="social-app">
|
||||
<div class="social-header">
|
||||
<div class="social-tabs">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="sources/sources.css" />
|
||||
|
||||
<div class="sources-container">
|
||||
<!-- Header -->
|
||||
<header class="sources-header">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="tasks/autotask.css" />
|
||||
|
||||
<div class="autotask-container" data-theme="sentient">
|
||||
<!-- Top Navigation Bar -->
|
||||
<div class="autotask-topbar">
|
||||
|
|
@ -476,6 +478,6 @@ Examples:
|
|||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<link rel="stylesheet" href="progress-panel.css" />
|
||||
<script src="progress-panel.js"></script>
|
||||
<script src="autotask.js"></script>
|
||||
<link rel="stylesheet" href="tasks/progress-panel.css" />
|
||||
<script src="tasks/progress-panel.js"></script>
|
||||
<script src="tasks/autotask.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="tasks/tasks.css" />
|
||||
|
||||
<!-- =============================================================================
|
||||
TASKS APP - Autonomous Task Management
|
||||
Respects Theme Manager - No hardcoded theme
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- Tickets - AI-Assisted Support Cases -->
|
||||
<!-- Dynamics nomenclature: Case, Resolution, Activity -->
|
||||
|
||||
<link rel="stylesheet" href="/suite/tickets/tickets.css" />
|
||||
<link rel="stylesheet" href="tickets/tickets.css" />
|
||||
|
||||
<div class="tickets-container">
|
||||
<!-- Header -->
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="tools/tools.css" />
|
||||
|
||||
<div class="compliance-container" id="compliance-app">
|
||||
<style>
|
||||
.compliance-container {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<link rel="stylesheet" href="tools/tools.css" />
|
||||
|
||||
<div class="security-container" id="security-app">
|
||||
<style>
|
||||
.security-container {
|
||||
|
|
|
|||
|
|
@ -2,274 +2,650 @@
|
|||
* Tools Module JavaScript
|
||||
* Compliance, Analytics, and Developer Tools
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Initialize the Tools module
|
||||
*/
|
||||
function init() {
|
||||
setupBotSelector();
|
||||
setupFilters();
|
||||
setupKeyboardShortcuts();
|
||||
setupHTMXEvents();
|
||||
/**
|
||||
* Initialize the Tools module
|
||||
*/
|
||||
function init() {
|
||||
setupBotSelector();
|
||||
setupFilters();
|
||||
setupKeyboardShortcuts();
|
||||
setupHTMXEvents();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup bot chip selection
|
||||
*/
|
||||
function setupBotSelector() {
|
||||
document.addEventListener("click", function (e) {
|
||||
const chip = e.target.closest(".bot-chip");
|
||||
if (chip) {
|
||||
// Toggle selection
|
||||
chip.classList.toggle("selected");
|
||||
|
||||
// Update hidden checkbox
|
||||
const checkbox = chip.querySelector('input[type="checkbox"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = chip.classList.contains("selected");
|
||||
}
|
||||
|
||||
// Handle "All Bots" logic
|
||||
if (chip.querySelector('input[value="all"]')) {
|
||||
if (chip.classList.contains("selected")) {
|
||||
// Deselect all other chips
|
||||
document
|
||||
.querySelectorAll(".bot-chip:not([data-all])")
|
||||
.forEach((c) => {
|
||||
c.classList.remove("selected");
|
||||
const cb = c.querySelector('input[type="checkbox"]');
|
||||
if (cb) cb.checked = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Deselect "All Bots" when selecting individual bots
|
||||
const allChip = document
|
||||
.querySelector('.bot-chip input[value="all"]')
|
||||
?.closest(".bot-chip");
|
||||
if (allChip) {
|
||||
allChip.classList.remove("selected");
|
||||
const cb = allChip.querySelector('input[type="checkbox"]');
|
||||
if (cb) cb.checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup filter controls
|
||||
*/
|
||||
function setupFilters() {
|
||||
// Filter select changes
|
||||
document.querySelectorAll(".filter-select").forEach((select) => {
|
||||
select.addEventListener("change", function () {
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
// Search input
|
||||
const searchInput = document.querySelector(
|
||||
'.filter-input[name="filter-search"]',
|
||||
);
|
||||
if (searchInput) {
|
||||
let debounceTimer;
|
||||
searchInput.addEventListener("input", function () {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => applyFilters(), 300);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to results
|
||||
*/
|
||||
function applyFilters() {
|
||||
const severity = document.getElementById("filter-severity")?.value || "all";
|
||||
const type = document.getElementById("filter-type")?.value || "all";
|
||||
const search =
|
||||
document
|
||||
.querySelector('.filter-input[name="filter-search"]')
|
||||
?.value.toLowerCase() || "";
|
||||
|
||||
const rows = document.querySelectorAll("#results-body tr");
|
||||
let visibleCount = 0;
|
||||
|
||||
rows.forEach((row) => {
|
||||
let visible = true;
|
||||
|
||||
// Filter by severity
|
||||
if (severity !== "all") {
|
||||
const badge = row.querySelector(".severity-badge");
|
||||
if (badge && !badge.classList.contains(severity)) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
if (type !== "all" && visible) {
|
||||
const issueIcon = row.querySelector(".issue-icon");
|
||||
if (issueIcon && !issueIcon.classList.contains(type)) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by search
|
||||
if (search && visible) {
|
||||
const text = row.textContent.toLowerCase();
|
||||
if (!text.includes(search)) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
row.style.display = visible ? "" : "none";
|
||||
if (visible) visibleCount++;
|
||||
});
|
||||
|
||||
// Update results count
|
||||
const countEl = document.getElementById("results-count");
|
||||
if (countEl) {
|
||||
countEl.textContent = `${visibleCount} issues found`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup keyboard shortcuts
|
||||
*/
|
||||
function setupKeyboardShortcuts() {
|
||||
document.addEventListener("keydown", function (e) {
|
||||
// Ctrl+Enter to run scan
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
document.getElementById("scan-btn")?.click();
|
||||
}
|
||||
|
||||
// Escape to close any open modals
|
||||
if (e.key === "Escape") {
|
||||
closeModals();
|
||||
}
|
||||
|
||||
// Ctrl+E to export report
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "e") {
|
||||
e.preventDefault();
|
||||
exportReport();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup HTMX events
|
||||
*/
|
||||
function setupHTMXEvents() {
|
||||
if (typeof htmx === "undefined") return;
|
||||
|
||||
document.body.addEventListener("htmx:afterSwap", function (e) {
|
||||
if (e.detail.target.id === "scan-results") {
|
||||
updateStats();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup bot chip selection
|
||||
*/
|
||||
function setupBotSelector() {
|
||||
document.addEventListener('click', function(e) {
|
||||
const chip = e.target.closest('.bot-chip');
|
||||
if (chip) {
|
||||
// Toggle selection
|
||||
chip.classList.toggle('selected');
|
||||
/**
|
||||
* Update statistics from results
|
||||
*/
|
||||
function updateStats() {
|
||||
const rows = document.querySelectorAll("#results-body tr");
|
||||
let stats = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
||||
|
||||
// Update hidden checkbox
|
||||
const checkbox = chip.querySelector('input[type="checkbox"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = chip.classList.contains('selected');
|
||||
}
|
||||
rows.forEach((row) => {
|
||||
if (row.style.display === "none") return;
|
||||
|
||||
// Handle "All Bots" logic
|
||||
if (chip.querySelector('input[value="all"]')) {
|
||||
if (chip.classList.contains('selected')) {
|
||||
// Deselect all other chips
|
||||
document.querySelectorAll('.bot-chip:not([data-all])').forEach(c => {
|
||||
c.classList.remove('selected');
|
||||
const cb = c.querySelector('input[type="checkbox"]');
|
||||
if (cb) cb.checked = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Deselect "All Bots" when selecting individual bots
|
||||
const allChip = document.querySelector('.bot-chip input[value="all"]')?.closest('.bot-chip');
|
||||
if (allChip) {
|
||||
allChip.classList.remove('selected');
|
||||
const cb = allChip.querySelector('input[type="checkbox"]');
|
||||
if (cb) cb.checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const badge = row.querySelector(".severity-badge");
|
||||
if (badge) {
|
||||
if (badge.classList.contains("critical")) stats.critical++;
|
||||
else if (badge.classList.contains("high")) stats.high++;
|
||||
else if (badge.classList.contains("medium")) stats.medium++;
|
||||
else if (badge.classList.contains("low")) stats.low++;
|
||||
else if (badge.classList.contains("info")) stats.info++;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Setup filter controls
|
||||
*/
|
||||
function setupFilters() {
|
||||
// Filter select changes
|
||||
document.querySelectorAll('.filter-select').forEach(select => {
|
||||
select.addEventListener('change', function() {
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
// Search input
|
||||
const searchInput = document.querySelector('.filter-input[name="filter-search"]');
|
||||
if (searchInput) {
|
||||
let debounceTimer;
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => applyFilters(), 300);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to results
|
||||
*/
|
||||
function applyFilters() {
|
||||
const severity = document.getElementById('filter-severity')?.value || 'all';
|
||||
const type = document.getElementById('filter-type')?.value || 'all';
|
||||
const search = document.querySelector('.filter-input[name="filter-search"]')?.value.toLowerCase() || '';
|
||||
|
||||
const rows = document.querySelectorAll('#results-body tr');
|
||||
let visibleCount = 0;
|
||||
|
||||
rows.forEach(row => {
|
||||
let visible = true;
|
||||
|
||||
// Filter by severity
|
||||
if (severity !== 'all') {
|
||||
const badge = row.querySelector('.severity-badge');
|
||||
if (badge && !badge.classList.contains(severity)) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
if (type !== 'all' && visible) {
|
||||
const issueIcon = row.querySelector('.issue-icon');
|
||||
if (issueIcon && !issueIcon.classList.contains(type)) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by search
|
||||
if (search && visible) {
|
||||
const text = row.textContent.toLowerCase();
|
||||
if (!text.includes(search)) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
row.style.display = visible ? '' : 'none';
|
||||
if (visible) visibleCount++;
|
||||
});
|
||||
|
||||
// Update results count
|
||||
const countEl = document.getElementById('results-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = `${visibleCount} issues found`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup keyboard shortcuts
|
||||
*/
|
||||
function setupKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Ctrl+Enter to run scan
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
document.getElementById('scan-btn')?.click();
|
||||
}
|
||||
|
||||
// Escape to close any open modals
|
||||
if (e.key === 'Escape') {
|
||||
closeModals();
|
||||
}
|
||||
|
||||
// Ctrl+E to export report
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'e') {
|
||||
e.preventDefault();
|
||||
exportReport();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup HTMX events
|
||||
*/
|
||||
function setupHTMXEvents() {
|
||||
if (typeof htmx === 'undefined') return;
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', function(e) {
|
||||
if (e.detail.target.id === 'scan-results') {
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update statistics from results
|
||||
*/
|
||||
function updateStats() {
|
||||
const rows = document.querySelectorAll('#results-body tr');
|
||||
let stats = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
||||
|
||||
rows.forEach(row => {
|
||||
if (row.style.display === 'none') return;
|
||||
|
||||
const badge = row.querySelector('.severity-badge');
|
||||
if (badge) {
|
||||
if (badge.classList.contains('critical')) stats.critical++;
|
||||
else if (badge.classList.contains('high')) stats.high++;
|
||||
else if (badge.classList.contains('medium')) stats.medium++;
|
||||
else if (badge.classList.contains('low')) stats.low++;
|
||||
else if (badge.classList.contains('info')) stats.info++;
|
||||
}
|
||||
});
|
||||
|
||||
// Update stat cards
|
||||
const updateStat = (id, value) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = value;
|
||||
};
|
||||
|
||||
updateStat('stat-critical', stats.critical);
|
||||
updateStat('stat-high', stats.high);
|
||||
updateStat('stat-medium', stats.medium);
|
||||
updateStat('stat-low', stats.low);
|
||||
updateStat('stat-info', stats.info);
|
||||
|
||||
// Update total count
|
||||
const total = stats.critical + stats.high + stats.medium + stats.low + stats.info;
|
||||
const countEl = document.getElementById('results-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = `${total} issues found`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export compliance report
|
||||
*/
|
||||
function exportReport() {
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.ajax('GET', '/api/compliance/export', {
|
||||
swap: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix an issue
|
||||
*/
|
||||
function fixIssue(issueId) {
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.ajax('POST', `/api/compliance/fix/${issueId}`, {
|
||||
swap: 'none'
|
||||
}).then(() => {
|
||||
// Refresh results
|
||||
const scanBtn = document.getElementById('scan-btn');
|
||||
if (scanBtn) scanBtn.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all modals
|
||||
*/
|
||||
function closeModals() {
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('show');
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Expose for external use
|
||||
window.Tools = {
|
||||
updateStats,
|
||||
applyFilters,
|
||||
fixIssue,
|
||||
exportReport,
|
||||
showToast
|
||||
// Update stat cards
|
||||
const updateStat = (id, value) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = value;
|
||||
};
|
||||
|
||||
updateStat("stat-critical", stats.critical);
|
||||
updateStat("stat-high", stats.high);
|
||||
updateStat("stat-medium", stats.medium);
|
||||
updateStat("stat-low", stats.low);
|
||||
updateStat("stat-info", stats.info);
|
||||
|
||||
// Update total count
|
||||
const total =
|
||||
stats.critical + stats.high + stats.medium + stats.low + stats.info;
|
||||
const countEl = document.getElementById("results-count");
|
||||
if (countEl) {
|
||||
countEl.textContent = `${total} issues found`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export compliance report
|
||||
*/
|
||||
function exportReport() {
|
||||
if (typeof htmx !== "undefined") {
|
||||
htmx.ajax("GET", "/api/compliance/export", {
|
||||
swap: "none",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix an issue
|
||||
*/
|
||||
function fixIssue(issueId) {
|
||||
if (typeof htmx !== "undefined") {
|
||||
htmx
|
||||
.ajax("POST", `/api/compliance/fix/${issueId}`, {
|
||||
swap: "none",
|
||||
})
|
||||
.then(() => {
|
||||
// Refresh results
|
||||
const scanBtn = document.getElementById("scan-btn");
|
||||
if (scanBtn) scanBtn.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all modals
|
||||
*/
|
||||
function closeModals() {
|
||||
document.querySelectorAll(".modal").forEach((modal) => {
|
||||
modal.classList.add("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
function showToast(message, type = "success") {
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add("show");
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove("show");
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a protection tool
|
||||
*/
|
||||
function configureProtectionTool(toolName) {
|
||||
const modal =
|
||||
document.getElementById("configure-modal") ||
|
||||
document.getElementById("tool-config-modal");
|
||||
if (modal) {
|
||||
const titleEl = modal.querySelector(".modal-title, h2, h3");
|
||||
if (titleEl) {
|
||||
titleEl.textContent = `Configure ${toolName}`;
|
||||
}
|
||||
modal.dataset.tool = toolName;
|
||||
if (modal.showModal) {
|
||||
modal.showModal();
|
||||
} else {
|
||||
modal.classList.remove("hidden");
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
} else {
|
||||
showToast(`Opening configuration for ${toolName}...`, "info");
|
||||
fetch(`/api/tools/security/${toolName}/config`)
|
||||
.then((r) => r.json())
|
||||
.then((config) => {
|
||||
console.log(`${toolName} config:`, config);
|
||||
showToast(`${toolName} configuration loaded`, "success");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error loading ${toolName} config:`, err);
|
||||
showToast(`Failed to load ${toolName} configuration`, "error");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a protection tool scan
|
||||
*/
|
||||
function runProtectionTool(toolName) {
|
||||
showToast(`Running ${toolName} scan...`, "info");
|
||||
|
||||
const statusEl = document.querySelector(
|
||||
`[data-tool="${toolName}"] .tool-status, #${toolName}-status`,
|
||||
);
|
||||
if (statusEl) {
|
||||
statusEl.textContent = "Running...";
|
||||
statusEl.classList.add("running");
|
||||
}
|
||||
|
||||
fetch(`/api/tools/security/${toolName}/run`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((result) => {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = "Completed";
|
||||
statusEl.classList.remove("running");
|
||||
statusEl.classList.add("completed");
|
||||
}
|
||||
showToast(`${toolName} scan completed`, "success");
|
||||
|
||||
if (result.report_url) {
|
||||
viewReport(toolName);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error running ${toolName}:`, err);
|
||||
if (statusEl) {
|
||||
statusEl.textContent = "Error";
|
||||
statusEl.classList.remove("running");
|
||||
statusEl.classList.add("error");
|
||||
}
|
||||
showToast(`Failed to run ${toolName}`, "error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a protection tool
|
||||
*/
|
||||
function updateProtectionTool(toolName) {
|
||||
showToast(`Updating ${toolName}...`, "info");
|
||||
|
||||
fetch(`/api/tools/security/${toolName}/update`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((result) => {
|
||||
showToast(
|
||||
`${toolName} updated to version ${result.version || "latest"}`,
|
||||
"success",
|
||||
);
|
||||
|
||||
const versionEl = document.querySelector(
|
||||
`[data-tool="${toolName}"] .tool-version, #${toolName}-version`,
|
||||
);
|
||||
if (versionEl && result.version) {
|
||||
versionEl.textContent = result.version;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error updating ${toolName}:`, err);
|
||||
showToast(`Failed to update ${toolName}`, "error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* View report for a protection tool
|
||||
*/
|
||||
function viewReport(toolName) {
|
||||
const reportModal =
|
||||
document.getElementById("report-modal") ||
|
||||
document.getElementById("view-report-modal");
|
||||
|
||||
if (reportModal) {
|
||||
const titleEl = reportModal.querySelector(".modal-title, h2, h3");
|
||||
if (titleEl) {
|
||||
titleEl.textContent = `${toolName} Report`;
|
||||
}
|
||||
|
||||
const contentEl = reportModal.querySelector(
|
||||
".report-content, .modal-body",
|
||||
);
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML = '<div class="loading">Loading report...</div>';
|
||||
}
|
||||
|
||||
if (reportModal.showModal) {
|
||||
reportModal.showModal();
|
||||
} else {
|
||||
reportModal.classList.remove("hidden");
|
||||
reportModal.style.display = "flex";
|
||||
}
|
||||
|
||||
fetch(`/api/tools/security/${toolName}/report`)
|
||||
.then((r) => r.json())
|
||||
.then((report) => {
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML = renderReport(toolName, report);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error loading ${toolName} report:`, err);
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML =
|
||||
'<div class="error">Failed to load report</div>';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
window.open(
|
||||
`/api/tools/security/${toolName}/report?format=html`,
|
||||
"_blank",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a security tool report
|
||||
*/
|
||||
function renderReport(toolName, report) {
|
||||
const findings = report.findings || [];
|
||||
const summary = report.summary || {};
|
||||
|
||||
return `
|
||||
<div class="report-summary">
|
||||
<h4>Summary</h4>
|
||||
<div class="summary-stats">
|
||||
<span class="stat critical">${summary.critical || 0} Critical</span>
|
||||
<span class="stat high">${summary.high || 0} High</span>
|
||||
<span class="stat medium">${summary.medium || 0} Medium</span>
|
||||
<span class="stat low">${summary.low || 0} Low</span>
|
||||
</div>
|
||||
<p>Scan completed: ${report.completed_at || new Date().toISOString()}</p>
|
||||
</div>
|
||||
<div class="report-findings">
|
||||
<h4>Findings (${findings.length})</h4>
|
||||
${findings.length === 0 ? '<p class="no-findings">No issues found</p>' : ""}
|
||||
${findings
|
||||
.map(
|
||||
(f) => `
|
||||
<div class="finding ${f.severity || "info"}">
|
||||
<span class="severity-badge ${f.severity || "info"}">${f.severity || "info"}</span>
|
||||
<span class="finding-title">${f.title || f.message || "Finding"}</span>
|
||||
<p class="finding-description">${f.description || ""}</p>
|
||||
${f.remediation ? `<p class="finding-remediation"><strong>Fix:</strong> ${f.remediation}</p>` : ""}
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle auto action for a protection tool
|
||||
*/
|
||||
function toggleAutoAction(toolName, btn) {
|
||||
const isEnabled =
|
||||
btn.classList.contains("active") ||
|
||||
btn.getAttribute("aria-pressed") === "true";
|
||||
const newState = !isEnabled;
|
||||
|
||||
fetch(`/api/tools/security/${toolName}/auto`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled: newState }),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((result) => {
|
||||
if (newState) {
|
||||
btn.classList.add("active");
|
||||
btn.setAttribute("aria-pressed", "true");
|
||||
showToast(`Auto-scan enabled for ${toolName}`, "success");
|
||||
} else {
|
||||
btn.classList.remove("active");
|
||||
btn.setAttribute("aria-pressed", "false");
|
||||
showToast(`Auto-scan disabled for ${toolName}`, "info");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error toggling auto action for ${toolName}:`, err);
|
||||
showToast(`Failed to update ${toolName} settings`, "error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reindex a data source for search
|
||||
*/
|
||||
function reindexSource(sourceName) {
|
||||
showToast(`Reindexing ${sourceName}...`, "info");
|
||||
|
||||
fetch(`/api/search/reindex/${sourceName}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((result) => {
|
||||
showToast(
|
||||
`${sourceName} reindexing started. ${result.documents || 0} documents queued.`,
|
||||
"success",
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error reindexing ${sourceName}:`, err);
|
||||
showToast(`Failed to reindex ${sourceName}`, "error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show TSC (Trust Service Criteria) details
|
||||
*/
|
||||
function showTscDetails(category) {
|
||||
const detailPanel =
|
||||
document.getElementById("tsc-detail-panel") ||
|
||||
document.querySelector(".tsc-details");
|
||||
|
||||
if (detailPanel) {
|
||||
fetch(`/api/compliance/tsc/${category}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
detailPanel.innerHTML = renderTscDetails(category, data);
|
||||
detailPanel.classList.add("open");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error loading TSC details for ${category}:`, err);
|
||||
showToast(`Failed to load ${category} details`, "error");
|
||||
});
|
||||
} else {
|
||||
showToast(`Viewing ${category} criteria...`, "info");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render TSC details
|
||||
*/
|
||||
function renderTscDetails(category, data) {
|
||||
const controls = data.controls || [];
|
||||
return `
|
||||
<div class="tsc-detail-header">
|
||||
<h3>${category.charAt(0).toUpperCase() + category.slice(1)} Criteria</h3>
|
||||
<button class="close-btn" onclick="document.querySelector('.tsc-details').classList.remove('open')">×</button>
|
||||
</div>
|
||||
<div class="tsc-controls">
|
||||
${controls
|
||||
.map(
|
||||
(c) => `
|
||||
<div class="control-item ${c.status || "pending"}">
|
||||
<span class="control-id">${c.id}</span>
|
||||
<span class="control-name">${c.name}</span>
|
||||
<span class="control-status">${c.status || "Pending"}</span>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show control remediation steps
|
||||
*/
|
||||
function showControlRemediation(controlId) {
|
||||
const modal =
|
||||
document.getElementById("remediation-modal") ||
|
||||
document.getElementById("control-modal");
|
||||
|
||||
if (modal) {
|
||||
const titleEl = modal.querySelector(".modal-title, h2, h3");
|
||||
if (titleEl) {
|
||||
titleEl.textContent = `Remediate ${controlId}`;
|
||||
}
|
||||
|
||||
const contentEl = modal.querySelector(
|
||||
".modal-body, .remediation-content",
|
||||
);
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML =
|
||||
'<div class="loading">Loading remediation steps...</div>';
|
||||
}
|
||||
|
||||
if (modal.showModal) {
|
||||
modal.showModal();
|
||||
} else {
|
||||
modal.classList.remove("hidden");
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
|
||||
fetch(`/api/compliance/controls/${controlId}/remediation`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML = `
|
||||
<div class="remediation-steps">
|
||||
<h4>Steps to Remediate</h4>
|
||||
<ol>
|
||||
${(data.steps || []).map((s) => `<li>${s}</li>`).join("")}
|
||||
</ol>
|
||||
${data.documentation_url ? `<a href="${data.documentation_url}" target="_blank" class="btn btn-secondary">View Documentation</a>` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Error loading remediation for ${controlId}:`, err);
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML =
|
||||
'<div class="error">Failed to load remediation steps</div>';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showToast(`Loading remediation for ${controlId}...`, "info");
|
||||
}
|
||||
}
|
||||
|
||||
// Expose for external use
|
||||
window.Tools = {
|
||||
updateStats,
|
||||
applyFilters,
|
||||
fixIssue,
|
||||
exportReport,
|
||||
showToast,
|
||||
};
|
||||
|
||||
// Expose security tool functions globally
|
||||
window.configureProtectionTool = configureProtectionTool;
|
||||
window.runProtectionTool = runProtectionTool;
|
||||
window.updateProtectionTool = updateProtectionTool;
|
||||
window.viewReport = viewReport;
|
||||
window.toggleAutoAction = toggleAutoAction;
|
||||
window.reindexSource = reindexSource;
|
||||
window.showTscDetails = showTscDetails;
|
||||
window.showControlRemediation = showControlRemediation;
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -127,7 +127,9 @@
|
|||
>
|
||||
<!-- Empty state -->
|
||||
<div class="empty-page">
|
||||
<p>Press <kbd>/</kbd> for commands or start typing...</p>
|
||||
<p>
|
||||
Press <kbd>/</kbd> for commands or start typing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -196,340 +198,346 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.workspace-app {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.workspace-sidebar {
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-search {
|
||||
padding: 0.75rem 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-search input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-section h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sidebar-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.workspace-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.page-title::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.page-meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.blocks-container {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.empty-page {
|
||||
color: var(--text-muted);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.empty-page kbd {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.slash-menu {
|
||||
position: absolute;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
min-width: 280px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.slash-menu.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slash-menu-header {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.members-panel {
|
||||
width: 280px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border-color);
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.members-panel.collapsed {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.members-panel.collapsed .panel-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel-toggle {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.panel-content h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.modal-container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-results:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workspace-sidebar {
|
||||
position: absolute;
|
||||
left: -260px;
|
||||
.workspace-app {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
z-index: 50;
|
||||
transition: left 0.2s;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.workspace-sidebar.open {
|
||||
left: 0;
|
||||
.workspace-sidebar {
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-search {
|
||||
padding: 0.75rem 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-search input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-section h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sidebar-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.workspace-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.page-title::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.page-meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.blocks-container {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.empty-page {
|
||||
color: var(--text-muted);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.empty-page kbd {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.slash-menu {
|
||||
position: absolute;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
min-width: 280px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.slash-menu.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slash-menu-header {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.members-panel {
|
||||
width: 280px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border-color);
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.members-panel.collapsed {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.members-panel.collapsed .panel-content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-toggle {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.panel-content h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.modal-container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-results:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workspace-sidebar {
|
||||
position: absolute;
|
||||
left: -260px;
|
||||
height: 100%;
|
||||
z-index: 50;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
.workspace-sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.members-panel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function toggleMembersPanel() {
|
||||
const panel = document.getElementById('members-panel');
|
||||
panel.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const blocksContainer = document.getElementById('blocks-container');
|
||||
if (blocksContainer) {
|
||||
blocksContainer.addEventListener('keydown', function(e) {
|
||||
if (e.key === '/') {
|
||||
const slashMenu = document.getElementById('slash-menu');
|
||||
slashMenu.classList.remove('hidden');
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
const slashMenu = document.getElementById('slash-menu');
|
||||
slashMenu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
function toggleMembersPanel() {
|
||||
const panel = document.getElementById("members-panel");
|
||||
if (panel) {
|
||||
panel.classList.toggle("collapsed");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const blocksContainer = document.getElementById("blocks-container");
|
||||
if (blocksContainer) {
|
||||
blocksContainer.addEventListener("keydown", function (e) {
|
||||
if (e.key === "/") {
|
||||
const slashMenu = document.getElementById("slash-menu");
|
||||
if (slashMenu) {
|
||||
slashMenu.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
const slashMenu = document.getElementById("slash-menu");
|
||||
if (slashMenu) {
|
||||
slashMenu.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue