fix(slides): remove duplicate cacheElements/bindEvents functions causing null error

The duplicate functions at lines 455-486 were redefining cacheElements and
bindEvents with wrong element IDs (kebab-case vs camelCase in HTML).
This caused 'Cannot read properties of null' error on slides app init.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-12 14:05:06 -03:00
parent 08469ecbf6
commit e3b5929b99
39 changed files with 3376 additions and 1697 deletions

View file

@ -569,25 +569,30 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
--- ---
## Final Steps Before Commit ## COMPILATION POLICY - CRITICAL
```bash **NEVER compile during development. NEVER run `cargo build` or `cargo check`. Use static analysis only.**
# Check for warnings
cargo check 2>&1 | grep warning
# Audit dependencies (must be 0 warnings) ### Workflow
cargo audit
# Build both modes 1. Make all code changes
cargo build 2. Use `diagnostics` tool for static analysis (NOT compilation)
cargo build --features desktop 3. Fix any errors found by diagnostics
4. **At the end**, inform user what needs restart
# Verify no dead code with _ prefixes ### After All Changes Complete
grep -r "let _" src/ --include="*.rs"
# Verify no CDN references | Change Type | User Action Required |
grep -r "unpkg.com\|cdnjs\|jsdelivr" ui/ |-------------|----------------------|
``` | Rust code (`.rs` files) | "Recompile and restart **botui**" |
| HTML templates (`.html` in ui/) | "Browser refresh only" |
| CSS/JS files | "Browser refresh only" |
| Askama templates (`.html` in src/) | "Recompile and restart **botui**" |
| Cargo.toml changes | "Recompile and restart **botui**" |
**Format:** At the end of your response, always state:
- ✅ **No restart needed** - browser refresh only
- 🔄 **Restart botui** - recompile required
--- ---

View file

@ -56,6 +56,11 @@ const SUITE_DIRS: &[&str] = &[
"goals", "goals",
"player", "player",
"canvas", "canvas",
"people",
"crm",
"billing",
"products",
"tickets",
]; ];
pub async fn index() -> impl IntoResponse { pub async fn index() -> impl IntoResponse {

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="admin/admin.css" />
<div class="admin-layout"> <div class="admin-layout">
<!-- Sidebar Navigation --> <!-- Sidebar Navigation -->
<aside class="admin-sidebar"> <aside class="admin-sidebar">
@ -716,5 +718,5 @@
</div> </div>
</dialog> </dialog>
<link rel="stylesheet" href="admin.css" /> <link rel="stylesheet" href="admin/admin.css" />
<script src="admin.js"></script> <script src="admin/admin.js"></script>

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="analytics/analytics.css" />
<div class="analytics-container" id="analytics-app"> <div class="analytics-container" id="analytics-app">
<header class="analytics-header"> <header class="analytics-header">
<div class="header-title"> <div class="header-title">
@ -534,6 +536,6 @@
</aside> </aside>
</div> </div>
<link rel="stylesheet" href="analytics.css" /> <link rel="stylesheet" href="analytics/analytics.css" />
<script src="analytics.js"></script> <script src="analytics/analytics.js"></script>
</div> </div>

View file

@ -1,5 +1,5 @@
<!-- Attendant Console - General Bots --> <!-- Attendant Console - General Bots -->
<link rel="stylesheet" href="attendant.css" /> <link rel="stylesheet" href="attendant/attendant.css" />
<!-- CRM Disabled State --> <!-- CRM Disabled State -->
<!-- CRM Disabled State - Hidden by default, CRM is now enabled by default --> <!-- CRM Disabled State - Hidden by default, CRM is now enabled by default -->
@ -572,4 +572,4 @@
</div> </div>
<!-- Toast Container --> <!-- Toast Container -->
<script src="attendant.js"></script> <script src="attendant/attendant.js"></script>

View file

@ -1,7 +1,7 @@
<!-- Billing - Invoices, Payments & Quotes --> <!-- Billing - Invoices, Payments & Quotes -->
<!-- Dynamics nomenclature: Quote → Invoice → Payment --> <!-- Dynamics nomenclature: Quote → Invoice → Payment -->
<link rel="stylesheet" href="/suite/billing/billing.css"> <link rel="stylesheet" href="billing/billing.css" />
<div class="billing-container"> <div class="billing-container">
<!-- Header --> <!-- Header -->
@ -9,26 +9,62 @@
<div class="billing-header-left"> <div class="billing-header-left">
<h1 data-i18n="billing-title">Billing</h1> <h1 data-i18n="billing-title">Billing</h1>
<nav class="billing-tabs"> <nav class="billing-tabs">
<button class="billing-tab active" data-view="invoices" data-i18n="billing-invoices">Invoices</button> <button
<button class="billing-tab" data-view="payments" data-i18n="billing-payments">Payments</button> class="billing-tab active"
<button class="billing-tab" data-view="quotes" data-i18n="billing-quotes">Quotes</button> 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> </nav>
</div> </div>
<div class="billing-header-right"> <div class="billing-header-right">
<div class="billing-search"> <div class="billing-search">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/> 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> </svg>
<input type="text" <input
placeholder="Search invoices, quotes..." type="text"
data-i18n-placeholder="billing-search-placeholder" placeholder="Search invoices, quotes..."
hx-get="/api/billing/search" data-i18n-placeholder="billing-search-placeholder"
hx-trigger="keyup changed delay:300ms" hx-get="/api/billing/search"
hx-target="#billing-search-results"> hx-trigger="keyup changed delay:300ms"
hx-target="#billing-search-results"
/>
</div> </div>
<button class="btn-primary" id="billing-new-invoice"> <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"> <svg
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/> 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> </svg>
<span data-i18n="billing-new-invoice">New Invoice</span> <span data-i18n="billing-new-invoice">New Invoice</span>
</button> </button>
@ -42,47 +78,111 @@
<div class="billing-summary"> <div class="billing-summary">
<div class="summary-card"> <div class="summary-card">
<div class="summary-icon pending"> <div class="summary-icon pending">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/> 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> </svg>
</div> </div>
<div class="summary-info"> <div class="summary-info">
<span class="summary-label" data-i18n="billing-pending">Pending</span> <span class="summary-label" data-i18n="billing-pending"
<span class="summary-value" hx-get="/api/billing/stats/pending" hx-trigger="load">$0</span> >Pending</span
>
<span
class="summary-value"
hx-get="/api/billing/stats/pending"
hx-trigger="load"
>$0</span
>
</div> </div>
</div> </div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-icon overdue"> <div class="summary-icon overdue">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
<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"/> width="20"
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/> 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> </svg>
</div> </div>
<div class="summary-info"> <div class="summary-info">
<span class="summary-label" data-i18n="billing-overdue">Overdue</span> <span class="summary-label" data-i18n="billing-overdue"
<span class="summary-value overdue" hx-get="/api/billing/stats/overdue" hx-trigger="load">$0</span> >Overdue</span
>
<span
class="summary-value overdue"
hx-get="/api/billing/stats/overdue"
hx-trigger="load"
>$0</span
>
</div> </div>
</div> </div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-icon paid"> <div class="summary-icon paid">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/> 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> </svg>
</div> </div>
<div class="summary-info"> <div class="summary-info">
<span class="summary-label" data-i18n="billing-paid-month">Paid This Month</span> <span class="summary-label" data-i18n="billing-paid-month"
<span class="summary-value paid" hx-get="/api/billing/stats/paid-month" hx-trigger="load">$0</span> >Paid This Month</span
>
<span
class="summary-value paid"
hx-get="/api/billing/stats/paid-month"
hx-trigger="load"
>$0</span
>
</div> </div>
</div> </div>
<div class="summary-card"> <div class="summary-card">
<div class="summary-icon total"> <div class="summary-icon total">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
<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"/> 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> </svg>
</div> </div>
<div class="summary-info"> <div class="summary-info">
<span class="summary-label" data-i18n="billing-revenue-month">Revenue This Month</span> <span class="summary-label" data-i18n="billing-revenue-month"
<span class="summary-value" hx-get="/api/billing/stats/revenue-month" hx-trigger="load">$0</span> >Revenue This Month</span
>
<span
class="summary-value"
hx-get="/api/billing/stats/revenue-month"
hx-trigger="load"
>$0</span
>
</div> </div>
</div> </div>
</div> </div>
@ -91,23 +191,64 @@
<div id="billing-invoices-view" class="billing-view active"> <div id="billing-invoices-view" class="billing-view active">
<div class="billing-list-header"> <div class="billing-list-header">
<div class="list-filters"> <div class="list-filters">
<select hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" hx-include="this" name="status"> <select
<option value="all" data-i18n="billing-filter-all">All Invoices</option> hx-get="/api/billing/invoices"
<option value="draft" data-i18n="billing-filter-draft">Draft</option> hx-trigger="change"
<option value="sent" data-i18n="billing-filter-sent">Sent</option> hx-target="#invoices-table-body"
<option value="paid" data-i18n="billing-filter-paid">Paid</option> hx-include="this"
<option value="overdue" data-i18n="billing-filter-overdue">Overdue</option> name="status"
<option value="cancelled" data-i18n="billing-filter-cancelled">Cancelled</option> >
<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>
<select hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" hx-include="this" name="period"> <select
<option value="all" data-i18n="billing-period-all">All Time</option> hx-get="/api/billing/invoices"
<option value="month" data-i18n="billing-period-month">This Month</option> hx-trigger="change"
<option value="quarter" data-i18n="billing-period-quarter">This Quarter</option> hx-target="#invoices-table-body"
<option value="year" data-i18n="billing-period-year">This Year</option> 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> </select>
</div> </div>
<div class="list-actions"> <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>
</div> </div>
<table class="billing-table"> <table class="billing-table">
@ -122,7 +263,11 @@
<th data-i18n="billing-col-actions">Actions</th> <th data-i18n="billing-col-actions">Actions</th>
</tr> </tr>
</thead> </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 --> <!-- Invoices loaded via HTMX -->
</tbody> </tbody>
</table> </table>
@ -132,18 +277,49 @@
<div id="billing-payments-view" class="billing-view"> <div id="billing-payments-view" class="billing-view">
<div class="billing-list-header"> <div class="billing-list-header">
<div class="list-filters"> <div class="list-filters">
<select hx-get="/api/billing/payments" hx-trigger="change" hx-target="#payments-table-body" hx-include="this" name="method"> <select
<option value="all" data-i18n="billing-method-all">All Methods</option> hx-get="/api/billing/payments"
<option value="bank" data-i18n="billing-method-bank">Bank Transfer</option> hx-trigger="change"
<option value="card" data-i18n="billing-method-card">Credit Card</option> hx-target="#payments-table-body"
<option value="pix" data-i18n="billing-method-pix">PIX</option> hx-include="this"
<option value="boleto" data-i18n="billing-method-boleto">Boleto</option> name="method"
<option value="cash" data-i18n="billing-method-cash">Cash</option> >
<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> </select>
</div> </div>
<button class="btn-primary" hx-get="/suite/billing/partials/payment-form.html" hx-target="#billing-modal-content" hx-on::after-request="openBillingModal()"> <button
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="btn-primary"
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/> 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> </svg>
<span data-i18n="billing-record-payment">Record Payment</span> <span data-i18n="billing-record-payment">Record Payment</span>
</button> </button>
@ -160,8 +336,11 @@
<th data-i18n="billing-col-actions">Actions</th> <th data-i18n="billing-col-actions">Actions</th>
</tr> </tr>
</thead> </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> </table>
</div> </div>
@ -169,18 +348,49 @@
<div id="billing-quotes-view" class="billing-view"> <div id="billing-quotes-view" class="billing-view">
<div class="billing-list-header"> <div class="billing-list-header">
<div class="list-filters"> <div class="list-filters">
<select hx-get="/api/billing/quotes" hx-trigger="change" hx-target="#quotes-table-body" hx-include="this" name="status"> <select
<option value="all" data-i18n="billing-quote-all">All Quotes</option> hx-get="/api/billing/quotes"
<option value="draft" data-i18n="billing-quote-draft">Draft</option> hx-trigger="change"
<option value="sent" data-i18n="billing-quote-sent">Sent</option> hx-target="#quotes-table-body"
<option value="accepted" data-i18n="billing-quote-accepted">Accepted</option> hx-include="this"
<option value="rejected" data-i18n="billing-quote-rejected">Rejected</option> name="status"
<option value="expired" data-i18n="billing-quote-expired">Expired</option> >
<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> </select>
</div> </div>
<button class="btn-primary" hx-get="/suite/billing/partials/quote-form.html" hx-target="#billing-modal-content" hx-on::after-request="openBillingModal()"> <button
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="btn-primary"
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/> 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> </svg>
<span data-i18n="billing-new-quote">New Quote</span> <span data-i18n="billing-new-quote">New Quote</span>
</button> </button>
@ -198,8 +408,11 @@
<th data-i18n="billing-col-actions">Actions</th> <th data-i18n="billing-col-actions">Actions</th>
</tr> </tr>
</thead> </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> </table>
</div> </div>
</div> </div>
@ -213,44 +426,56 @@
</div> </div>
<script> <script>
(function() { (function () {
// Tab switching // Tab switching
document.querySelectorAll('.billing-tab').forEach(tab => { document.querySelectorAll(".billing-tab").forEach((tab) => {
tab.addEventListener('click', function() { tab.addEventListener("click", function () {
document.querySelectorAll('.billing-tab').forEach(t => t.classList.remove('active')); document
document.querySelectorAll('.billing-view').forEach(v => v.classList.remove('active')); .querySelectorAll(".billing-tab")
this.classList.add('active'); .forEach((t) => t.classList.remove("active"));
const view = this.dataset.view; document
document.getElementById(`billing-${view}-view`).classList.add('active'); .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 // New Invoice button
document.getElementById('billing-new-invoice').addEventListener('click', function() { document
htmx.ajax('GET', '/suite/billing/partials/invoice-form.html', '#billing-modal-content').then(() => { .getElementById("billing-new-invoice")
openBillingModal(); .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 // Initialize i18n if available
window.openBillingModal = function() { if (window.i18n && window.i18n.translatePage) {
document.getElementById('billing-modal').classList.add('open'); window.i18n.translatePage();
};
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();
}
})();
</script> </script>

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="calendar/calendar.css" />
<!-- Calendar - Event Management --> <!-- Calendar - Event Management -->
<div class="calendar-container" id="calendar-app"> <div class="calendar-container" id="calendar-app">
<!-- Sidebar --> <!-- Sidebar -->

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
<!-- CRM - Customer Relationship Management --> <!-- CRM - Customer Relationship Management -->
<!-- Dynamics nomenclature: Lead → Opportunity → Account/Contact --> <!-- 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"> <div class="crm-container">
<!-- Header --> <!-- Header -->

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="dashboards/dashboards.css" />
<div class="dashboards-container" id="dashboards-app"> <div class="dashboards-container" id="dashboards-app">
<header class="dashboards-header"> <header class="dashboards-header">
<div class="header-title"> <div class="header-title">

View file

@ -1747,6 +1747,11 @@
const toolboxItems = document.querySelectorAll('.toolbox-item'); const toolboxItems = document.querySelectorAll('.toolbox-item');
const canvas = document.getElementById('canvas-inner'); const canvas = document.getElementById('canvas-inner');
if (!canvas) {
console.warn('initDragAndDrop: canvas-inner not found');
return;
}
toolboxItems.forEach(item => { toolboxItems.forEach(item => {
item.addEventListener('dragstart', (e) => { item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('nodeType', item.dataset.nodeType); e.dataTransfer.setData('nodeType', item.dataset.nodeType);
@ -1779,6 +1784,11 @@
const canvas = document.getElementById('canvas'); const canvas = document.getElementById('canvas');
const container = document.getElementById('canvas-container'); 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 // Pan with middle mouse or space+drag
let isPanning = false; let isPanning = false;
let panStart = { x: 0, y: 0 }; let panStart = { x: 0, y: 0 };
@ -2295,6 +2305,11 @@
const canvas = document.getElementById('canvas'); const canvas = document.getElementById('canvas');
const contextMenu = document.getElementById('context-menu'); const contextMenu = document.getElementById('context-menu');
if (!canvas || !contextMenu) {
console.warn('initContextMenu: canvas or context-menu not found');
return;
}
canvas.addEventListener('contextmenu', (e) => { canvas.addEventListener('contextmenu', (e) => {
e.preventDefault(); e.preventDefault();
const nodeEl = e.target.closest('.node'); const nodeEl = e.target.closest('.node');
@ -2312,7 +2327,10 @@
} }
function hideContextMenu() { function hideContextMenu() {
document.getElementById('context-menu').classList.remove('visible'); const menu = document.getElementById('context-menu');
if (menu) {
menu.classList.remove('visible');
}
} }
// Context Menu Actions // Context Menu Actions
@ -2353,14 +2371,20 @@
// Modal Management // Modal Management
function showModal(id) { function showModal(id) {
document.getElementById(id).classList.add('visible'); const modal = document.getElementById(id);
if (id === 'open-modal') { if (modal) {
htmx.trigger('#file-list-content', 'load'); modal.classList.add('visible');
if (id === 'open-modal') {
htmx.trigger('#file-list-content', 'load');
}
} }
} }
function hideModal(id) { function hideModal(id) {
document.getElementById(id).classList.remove('visible'); const modal = document.getElementById(id);
if (modal) {
modal.classList.remove('visible');
}
} }
// Save Design // Save Design
@ -2579,7 +2603,10 @@
} }
function hideMagicPanel() { function hideMagicPanel() {
document.getElementById('magic-panel').classList.remove('visible'); const panel = document.getElementById('magic-panel');
if (panel) {
panel.classList.remove('visible');
}
} }
async function analyzeMagicSuggestions() { async function analyzeMagicSuggestions() {

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="docs/docs.css" />
<div class="docs-app" id="docs-app"> <div class="docs-app" id="docs-app">
<div class="docs-toolbar"> <div class="docs-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">

View file

@ -10,35 +10,37 @@
/> />
<meta name="theme-color" content="#d4f505" /> <meta name="theme-color" content="#d4f505" />
<!-- Styles --> <!-- Critical CSS - minimal styles for shell/layout (loaded sync) -->
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/apps-extended.css" />
<link rel="stylesheet" href="css/components.css" />
<link rel="stylesheet" href="css/base.css" /> <link rel="stylesheet" href="css/base.css" />
<link rel="stylesheet" href="css/partials.css" />
<link rel="stylesheet" href="css/theme-sentient.css" /> <link rel="stylesheet" href="css/theme-sentient.css" />
<!-- App-specific CSS --> <!-- Non-critical CSS - loaded async for faster initial paint -->
<link rel="stylesheet" href="chat/chat.css" /> <link
<link rel="stylesheet" href="calendar/calendar.css" /> rel="stylesheet"
<link rel="stylesheet" href="drive/drive.css" /> href="css/components.css"
<link rel="stylesheet" href="mail/mail.css" /> media="print"
<link rel="stylesheet" href="meet/meet.css" /> onload="this.media='all'"
<link rel="stylesheet" href="paper/paper.css" /> />
<link rel="stylesheet" href="sheet/sheet.css" /> <link
<link rel="stylesheet" href="slides/slides.css" /> rel="stylesheet"
<link rel="stylesheet" href="research/research.css" /> href="css/partials.css"
<link rel="stylesheet" href="tasks/tasks.css?v=20251230" /> media="print"
<link rel="stylesheet" href="tasks/taskmd.css?v=20251230" /> onload="this.media='all'"
<link rel="stylesheet" href="analytics/analytics.css" /> />
<link rel="stylesheet" href="dashboards/dashboards.css" /> <link
<link rel="stylesheet" href="monitoring/monitoring.css" /> rel="stylesheet"
<link rel="stylesheet" href="crm/crm.css" /> href="css/apps-extended.css"
<link rel="stylesheet" href="billing/billing.css" /> media="print"
<link rel="stylesheet" href="products/products.css" /> onload="this.media='all'"
<link rel="stylesheet" href="tickets/tickets.css" /> />
<link rel="stylesheet" href="docs/docs.css" /> <noscript>
<link rel="stylesheet" href="social/social.css" /> <link rel="stylesheet" href="css/components.css" />
<link rel="stylesheet" href="css/partials.css" />
<link rel="stylesheet" href="css/apps-extended.css" />
</noscript>
<!-- App-specific CSS loaded lazily by each view via CSSLoader -->
<!-- Local Libraries (no external CDN dependencies) --> <!-- Local Libraries (no external CDN dependencies) -->
<script src="js/vendor/htmx.min.js"></script> <script src="js/vendor/htmx.min.js"></script>
@ -53,6 +55,9 @@
<!-- i18n --> <!-- i18n -->
<script src="js/i18n.js"></script> <script src="js/i18n.js"></script>
<!-- CSS Lazy Loader - enables per-screen CSS loading -->
<script src="js/css-loader.js"></script>
<!-- Enable HTMX to process inline scripts in swapped content --> <!-- Enable HTMX to process inline scripts in swapped content -->
<script> <script>
htmx.config.allowEval = true; htmx.config.allowEval = true;
@ -3286,7 +3291,6 @@
e, e,
); );
currentLoadedSection = null; currentLoadedSection = null;
} finally {
isLoadingSection = false; isLoadingSection = false;
} }
} }
@ -3316,13 +3320,39 @@
} }
}); });
// Also listen for response errors
document.body.addEventListener(
"htmx:responseError",
(event) => {
if (
event.detail.target &&
event.detail.target.id === "main-content"
) {
isLoadingSection = false;
currentLoadedSection = null;
}
},
);
// Load initial content based on hash or default to chat // Load initial content based on hash or default to chat
window.addEventListener("hashchange", handleHashChange); window.addEventListener("hashchange", handleHashChange);
// Initial load // Initial load - wait for HTMX to be ready
setTimeout(() => { function initialLoad() {
handleHashChange(); if (typeof htmx !== "undefined" && htmx.ajax) {
}, 100); handleHashChange();
} else {
setTimeout(initialLoad, 50);
}
}
if (document.readyState === "complete") {
setTimeout(initialLoad, 50);
} else {
window.addEventListener("load", () => {
setTimeout(initialLoad, 50);
});
}
// ========================================================================== // ==========================================================================
// GBAlerts - Global Notification System // GBAlerts - Global Notification System

212
ui/suite/js/css-loader.js Normal file
View 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;
}

View file

@ -1,10 +1,19 @@
<link rel="stylesheet" href="learn/learn.css" />
<div class="learn-container"> <div class="learn-container">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="learn-sidebar"> <aside class="learn-sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<h2 data-i18n="learn-title">📚 Learn</h2> <h2 data-i18n="learn-title">📚 Learn</h2>
<button class="btn-icon-sm" onclick="toggleLearnSidebar()"> <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> <polyline points="15 18 9 12 15 6"></polyline>
</svg> </svg>
</button> </button>
@ -19,19 +28,27 @@
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-item"> <div class="stat-item">
<span class="stat-value" id="statCoursesCompleted">0</span> <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>
<div class="stat-item"> <div class="stat-item">
<span class="stat-value" id="statCoursesInProgress">0</span> <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>
<div class="stat-item"> <div class="stat-item">
<span class="stat-value" id="statCertificates">0</span> <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>
<div class="stat-item"> <div class="stat-item">
<span class="stat-value" id="statTimeSpent">0h</span> <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> </div>
</div> </div>
@ -78,15 +95,21 @@
<h3 data-i18n="learn-difficulty">Dificuldade</h3> <h3 data-i18n="learn-difficulty">Dificuldade</h3>
<div class="difficulty-filter"> <div class="difficulty-filter">
<label class="checkbox-label"> <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> <span class="difficulty-badge beginner">Iniciante</span>
</label> </label>
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" checked data-difficulty="intermediate"> <input
<span class="difficulty-badge intermediate">Intermediário</span> type="checkbox"
checked
data-difficulty="intermediate"
/>
<span class="difficulty-badge intermediate"
>Intermediário</span
>
</label> </label>
<label class="checkbox-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> <span class="difficulty-badge advanced">Avançado</span>
</label> </label>
</div> </div>
@ -103,7 +126,9 @@
<div class="certificates-preview" id="certificatesPreview"> <div class="certificates-preview" id="certificatesPreview">
<div class="empty-state-small"> <div class="empty-state-small">
<span>🏆</span> <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> </div>
</div> </div>
@ -115,34 +140,84 @@
<div class="learn-header"> <div class="learn-header">
<div class="header-left"> <div class="header-left">
<div class="tabs"> <div class="tabs">
<button class="tab active" data-tab="catalog" onclick="switchTab('catalog')"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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="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> </svg>
<span data-i18n="learn-catalog">Catálogo</span> <span data-i18n="learn-catalog">Catálogo</span>
</button> </button>
<button class="tab" data-tab="my-courses" onclick="switchTab('my-courses')"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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="M22 10v6M2 10l10-5 10 5-10 5z"></path>
<path d="M6 12v5c3 3 9 3 12 0v-5"></path> <path d="M6 12v5c3 3 9 3 12 0v-5"></path>
</svg> </svg>
<span data-i18n="learn-my-courses">Meus Cursos</span> <span data-i18n="learn-my-courses">Meus Cursos</span>
<span class="badge" id="myCoursesCount">0</span> <span class="badge" id="myCoursesCount">0</span>
</button> </button>
<button class="tab" data-tab="mandatory" onclick="switchTab('mandatory')"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="tab"
<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> 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="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line> <line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg> </svg>
<span data-i18n="learn-pending">Pendentes</span> <span data-i18n="learn-pending">Pendentes</span>
<span class="badge warning" id="mandatoryCount">0</span> <span class="badge warning" id="mandatoryCount">0</span>
</button> </button>
<button class="tab" data-tab="certificates" onclick="switchTab('certificates')"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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> <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> </svg>
<span data-i18n="learn-certificates">Certificados</span> <span data-i18n="learn-certificates">Certificados</span>
</button> </button>
@ -150,27 +225,57 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="search-box"> <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> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg> </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> </div>
<select id="sortCourses" class="sort-select"> <select id="sortCourses" class="sort-select">
<option value="recent" data-i18n="learn-sort-recent">Mais Recentes</option> <option value="recent" data-i18n="learn-sort-recent">
<option value="popular" data-i18n="learn-sort-popular">Mais Populares</option> Mais Recentes
<option value="duration-asc" data-i18n="learn-sort-duration-asc">Menor Duração</option> </option>
<option value="duration-desc" data-i18n="learn-sort-duration-desc">Maior Duração</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> </select>
</div> </div>
</div> </div>
<!-- Mandatory Training Alert --> <!-- 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-icon">⚠️</div>
<div class="alert-content"> <div class="alert-content">
<strong data-i18n="learn-mandatory-alert-title">Treinamentos Obrigatórios Pendentes</strong> <strong data-i18n="learn-mandatory-alert-title"
<p id="mandatoryAlertText">Você possui treinamentos obrigatórios com prazo próximo.</p> >Treinamentos Obrigatórios Pendentes</strong
>
<p id="mandatoryAlertText">
Você possui treinamentos obrigatórios com prazo próximo.
</p>
</div> </div>
<button class="btn-primary-sm" onclick="switchTab('mandatory')"> <button class="btn-primary-sm" onclick="switchTab('mandatory')">
<span data-i18n="learn-view-pending">Ver Pendentes</span> <span data-i18n="learn-view-pending">Ver Pendentes</span>
@ -184,7 +289,9 @@
<div class="section-header"> <div class="section-header">
<h3> <h3>
<span class="section-icon"></span> <span class="section-icon"></span>
<span data-i18n="learn-recommended">Recomendados para Você</span> <span data-i18n="learn-recommended"
>Recomendados para Você</span
>
</h3> </h3>
</div> </div>
<div class="courses-carousel" id="recommendedCourses"> <div class="courses-carousel" id="recommendedCourses">
@ -197,14 +304,18 @@
<div class="section-header"> <div class="section-header">
<h3> <h3>
<span class="section-icon">📚</span> <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> </h3>
<span class="courses-count" id="coursesCountLabel">0 cursos</span> <span class="courses-count" id="coursesCountLabel"
>0 cursos</span
>
</div> </div>
<div class="courses-grid" id="coursesGrid"> <div class="courses-grid" id="coursesGrid">
<!-- Courses loaded dynamically --> <!-- Courses loaded dynamically -->
</div> </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()"> <button class="btn-secondary" onclick="loadMoreCourses()">
<span data-i18n="learn-load-more">Carregar Mais</span> <span data-i18n="learn-load-more">Carregar Mais</span>
</button> </button>
@ -219,7 +330,9 @@
<div class="section-header"> <div class="section-header">
<h3> <h3>
<span class="section-icon">▶️</span> <span class="section-icon">▶️</span>
<span data-i18n="learn-continue">Continuar Aprendendo</span> <span data-i18n="learn-continue"
>Continuar Aprendendo</span
>
</h3> </h3>
</div> </div>
<div class="courses-list" id="continueLearning"> <div class="courses-list" id="continueLearning">
@ -232,7 +345,9 @@
<div class="section-header"> <div class="section-header">
<h3> <h3>
<span class="section-icon"></span> <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> </h3>
</div> </div>
<div class="courses-list" id="completedCourses"> <div class="courses-list" id="completedCourses">
@ -247,7 +362,9 @@
<div class="section-header"> <div class="section-header">
<h3> <h3>
<span class="section-icon">⚠️</span> <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> </h3>
</div> </div>
<div class="mandatory-list" id="mandatoryList"> <div class="mandatory-list" id="mandatoryList">
@ -262,7 +379,9 @@
<div class="section-header"> <div class="section-header">
<h3> <h3>
<span class="section-icon">🏆</span> <span class="section-icon">🏆</span>
<span data-i18n="learn-my-certificates">Meus Certificados</span> <span data-i18n="learn-my-certificates"
>Meus Certificados</span
>
</h3> </h3>
</div> </div>
<div class="certificates-grid" id="certificatesGrid"> <div class="certificates-grid" id="certificatesGrid">
@ -278,7 +397,14 @@
<div class="modal-header"> <div class="modal-header">
<h3 id="modalCourseTitle">Título do Curso</h3> <h3 id="modalCourseTitle">Título do Curso</h3>
<button class="btn-icon-sm" onclick="closeCourseModal()"> <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="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line>
</svg> </svg>
@ -288,22 +414,49 @@
<div class="course-detail"> <div class="course-detail">
<div class="course-detail-header"> <div class="course-detail-header">
<div class="course-thumbnail" id="modalThumbnail"> <div class="course-thumbnail" id="modalThumbnail">
<img src="" alt="Course thumbnail"> <img src="" alt="Course thumbnail" />
</div> </div>
<div class="course-info"> <div class="course-info">
<div class="course-meta"> <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"> <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> <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> </svg>
<span>30 min</span> <span>30 min</span>
</span> </span>
<span class="lessons-count" id="modalLessonsCount"> <span
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="lessons-count"
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> id="modalLessonsCount"
<polyline points="14 2 14 8 20 8"></polyline> >
<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> </svg>
<span>5 aulas</span> <span>5 aulas</span>
</span> </span>
@ -311,11 +464,23 @@
<p class="course-description" id="modalDescription"> <p class="course-description" id="modalDescription">
Descrição do curso... Descrição do curso...
</p> </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-bar">
<div class="progress-fill" id="modalProgressFill" style="width: 0%"></div> <div
class="progress-fill"
id="modalProgressFill"
style="width: 0%"
></div>
</div> </div>
<span class="progress-text" id="modalProgressText">0% completo</span> <span
class="progress-text"
id="modalProgressText"
>0% completo</span
>
</div> </div>
</div> </div>
</div> </div>
@ -329,16 +494,28 @@
</div> </div>
<!-- Quiz Section --> <!-- 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> <h4 data-i18n="learn-quiz">Avaliação</h4>
<div class="quiz-info"> <div class="quiz-info">
<div class="quiz-meta"> <div class="quiz-meta">
<span id="modalQuizQuestions">10 questões</span> <span id="modalQuizQuestions">10 questões</span>
<span id="modalQuizTime">15 min</span> <span id="modalQuizTime">15 min</span>
<span id="modalQuizPassing">70% para aprovação</span> <span id="modalQuizPassing"
>70% para aprovação</span
>
</div> </div>
<button class="btn-primary" id="startQuizBtn" onclick="startQuiz()"> <button
<span data-i18n="learn-start-quiz">Iniciar Avaliação</span> class="btn-primary"
id="startQuizBtn"
onclick="startQuiz()"
>
<span data-i18n="learn-start-quiz"
>Iniciar Avaliação</span
>
</button> </button>
</div> </div>
</div> </div>
@ -348,8 +525,19 @@
<button class="btn-secondary" onclick="closeCourseModal()"> <button class="btn-secondary" onclick="closeCourseModal()">
<span data-i18n="learn-close">Fechar</span> <span data-i18n="learn-close">Fechar</span>
</button> </button>
<button class="btn-primary" id="startCourseBtn" onclick="startCourse()"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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> <polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg> </svg>
<span data-i18n="learn-start-course">Iniciar Curso</span> <span data-i18n="learn-start-course">Iniciar Curso</span>
@ -365,7 +553,14 @@
<div class="quiz-header-info"> <div class="quiz-header-info">
<h3 id="quizTitle">Avaliação</h3> <h3 id="quizTitle">Avaliação</h3>
<div class="quiz-timer" id="quizTimer"> <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> <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> </svg>
@ -373,7 +568,14 @@
</div> </div>
</div> </div>
<button class="btn-icon-sm" onclick="confirmExitQuiz()"> <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="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line>
</svg> </svg>
@ -382,9 +584,15 @@
<div class="modal-body"> <div class="modal-body">
<div class="quiz-progress"> <div class="quiz-progress">
<div class="quiz-progress-bar"> <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> </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>
<div class="quiz-question" id="quizQuestion"> <div class="quiz-question" id="quizQuestion">
@ -396,19 +604,47 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-secondary" id="prevQuestionBtn" onclick="prevQuestion()" disabled> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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> <polyline points="15 18 9 12 15 6"></polyline>
</svg> </svg>
<span data-i18n="learn-previous">Anterior</span> <span data-i18n="learn-previous">Anterior</span>
</button> </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> <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> <polyline points="9 18 15 12 9 6"></polyline>
</svg> </svg>
</button> </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> <span data-i18n="learn-submit">Enviar Respostas</span>
</button> </button>
</div> </div>
@ -421,7 +657,14 @@
<div class="modal-header"> <div class="modal-header">
<h3 data-i18n="learn-quiz-result">Resultado da Avaliação</h3> <h3 data-i18n="learn-quiz-result">Resultado da Avaliação</h3>
<button class="btn-icon-sm" onclick="closeQuizResult()"> <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="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line>
</svg> </svg>
@ -431,42 +674,84 @@
<div class="quiz-result" id="quizResult"> <div class="quiz-result" id="quizResult">
<div class="result-icon" id="resultIcon">🎉</div> <div class="result-icon" id="resultIcon">🎉</div>
<h2 class="result-title" id="resultTitle">Parabéns!</h2> <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="result-score">
<div class="score-circle" id="scoreCircle"> <div class="score-circle" id="scoreCircle">
<span class="score-value" id="scoreValue">85%</span> <span class="score-value" id="scoreValue">85%</span>
</div> </div>
<div class="score-details"> <div class="score-details">
<div class="score-detail"> <div class="score-detail">
<span class="detail-label" data-i18n="learn-correct-answers">Acertos</span> <span
<span class="detail-value" id="correctAnswers">8/10</span> class="detail-label"
data-i18n="learn-correct-answers"
>Acertos</span
>
<span class="detail-value" id="correctAnswers"
>8/10</span
>
</div> </div>
<div class="score-detail"> <div class="score-detail">
<span class="detail-label" data-i18n="learn-time-taken">Tempo</span> <span
<span class="detail-value" id="timeTaken">12:30</span> class="detail-label"
data-i18n="learn-time-taken"
>Tempo</span
>
<span class="detail-value" id="timeTaken"
>12:30</span
>
</div> </div>
<div class="score-detail"> <div class="score-detail">
<span class="detail-label" data-i18n="learn-attempt">Tentativa</span> <span
<span class="detail-value" id="attemptNumber">1</span> class="detail-label"
data-i18n="learn-attempt"
>Tentativa</span
>
<span class="detail-value" id="attemptNumber"
>1</span
>
</div> </div>
</div> </div>
</div> </div>
<div class="result-certificate" id="resultCertificate" style="display: none;"> <div
<p data-i18n="learn-certificate-earned">🏆 Certificado conquistado!</p> class="result-certificate"
<button class="btn-secondary" onclick="downloadCertificate()"> id="resultCertificate"
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> style="display: none"
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> >
<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> <polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line> <line x1="12" y1="15" x2="12" y2="3"></line>
</svg> </svg>
<span data-i18n="learn-download-certificate">Baixar Certificado</span> <span data-i18n="learn-download-certificate"
>Baixar Certificado</span
>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-secondary" onclick="reviewAnswers()"> <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>
<button class="btn-primary" onclick="closeQuizResult()"> <button class="btn-primary" onclick="closeQuizResult()">
<span data-i18n="learn-continue">Continuar</span> <span data-i18n="learn-continue">Continuar</span>
@ -480,21 +765,50 @@
<div class="modal-content modal-fullscreen"> <div class="modal-content modal-fullscreen">
<div class="modal-header"> <div class="modal-header">
<div class="lesson-nav"> <div class="lesson-nav">
<button class="btn-icon-sm" onclick="prevLesson()" id="prevLessonBtn"> <button
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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> <polyline points="15 18 9 12 15 6"></polyline>
</svg> </svg>
</button> </button>
<span id="lessonNavTitle">Aula 1 de 5</span> <span id="lessonNavTitle">Aula 1 de 5</span>
<button class="btn-icon-sm" onclick="nextLesson()" id="nextLessonBtn"> <button
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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> <polyline points="9 18 15 12 9 6"></polyline>
</svg> </svg>
</button> </button>
</div> </div>
<h3 id="lessonTitle">Título da Aula</h3> <h3 id="lessonTitle">Título da Aula</h3>
<button class="btn-icon-sm" onclick="closeLessonModal()"> <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="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line>
</svg> </svg>
@ -514,14 +828,31 @@
<div class="modal-footer"> <div class="modal-footer">
<div class="lesson-progress"> <div class="lesson-progress">
<div class="progress-bar"> <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>
</div> </div>
<button class="btn-primary" id="completeLessonBtn" onclick="completeLesson()"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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> <polyline points="20 6 9 17 4 12"></polyline>
</svg> </svg>
<span data-i18n="learn-mark-complete">Marcar como Concluída</span> <span data-i18n="learn-mark-complete"
>Marcar como Concluída</span
>
</button> </button>
</div> </div>
</div> </div>
@ -533,7 +864,14 @@
<div class="modal-header"> <div class="modal-header">
<h3 data-i18n="learn-certificate">Certificado</h3> <h3 data-i18n="learn-certificate">Certificado</h3>
<button class="btn-icon-sm" onclick="closeCertificateModal()"> <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="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line>
</svg> </svg>
@ -552,12 +890,27 @@
<h3 id="certUserName">Nome do Usuário</h3> <h3 id="certUserName">Nome do Usuário</h3>
<p>concluiu com sucesso o curso</p> <p>concluiu com sucesso o curso</p>
<h4 id="certCourseName">Nome do Curso</h4> <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>
<div class="certificate-footer"> <div class="certificate-footer">
<div class="cert-date"> <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> <strong id="certDate">01/01/2025</strong>
</div> </div>
<div class="cert-code"> <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>

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="mail/mail.css" />
<div class="mail-layout"> <div class="mail-layout">
<!-- Sidebar --> <!-- Sidebar -->
<div class="panel mail-sidebar"> <div class="panel mail-sidebar">

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="meet/meet.css" />
<!-- Meet - Video Conferencing --> <!-- Meet - Video Conferencing -->
<div class="meet-container" id="meet-app"> <div class="meet-container" id="meet-app">
<!-- Header --> <!-- Header -->

View file

@ -1,5 +1,5 @@
<!-- Alerts partial --> <!-- Alerts partial -->
<link rel="stylesheet" href="/static/suite/monitoring/alerts.css"> <link rel="stylesheet" href="monitoring/alerts.css">
<div class="alerts-container"> <div class="alerts-container">
<!-- Alerts Header --> <!-- Alerts Header -->
<div class="alerts-header"> <div class="alerts-header">

View file

@ -1,5 +1,5 @@
<!-- Health partial --> <!-- Health partial -->
<link rel="stylesheet" href="/static/suite/monitoring/health.css"> <link rel="stylesheet" href="monitoring/health.css">
<div class="health-container"> <div class="health-container">
<!-- Health Overview --> <!-- Health Overview -->
<div class="health-overview" <div class="health-overview"

View file

@ -491,5 +491,5 @@
</div> </div>
</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> <script src="/static/suite/monitoring/monitoring.js"></script>

View file

@ -1,5 +1,5 @@
<!-- Logs partial --> <!-- Logs partial -->
<link rel="stylesheet" href="/static/suite/monitoring/logs.css" /> <link rel="stylesheet" href="monitoring/logs.css" />
<div class="logs-container"> <div class="logs-container">
<!-- Logs Header --> <!-- Logs Header -->
<div class="logs-header"> <div class="logs-header">

View file

@ -1,5 +1,5 @@
<!-- Metrics partial --> <!-- Metrics partial -->
<link rel="stylesheet" href="/static/suite/monitoring/metrics.css"> <link rel="stylesheet" href="monitoring/metrics.css">
<div class="metrics-container"> <div class="metrics-container">
<!-- Metrics Header --> <!-- Metrics Header -->
<div class="metrics-header"> <div class="metrics-header">

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="monitoring/monitoring.css" />
<div class="monitoring-container" id="monitoring-app"> <div class="monitoring-container" id="monitoring-app">
<header class="monitoring-header"> <header class="monitoring-header">
<h2> <h2>
@ -1374,7 +1376,6 @@
hx-swap="innerHTML" hx-swap="innerHTML"
></div> ></div>
<link rel="stylesheet" href="monitoring/monitoring.css" />
<link rel="stylesheet" href="monitoring.css" /> <script src="monitoring/monitoring.js"></script>
<script src="monitoring.js"></script>
</div> </div>

View file

@ -1,5 +1,5 @@
<!-- Resources partial --> <!-- Resources partial -->
<link rel="stylesheet" href="/static/suite/monitoring/resources.css"> <link rel="stylesheet" href="monitoring/resources.css">
<div class="resources-container"> <div class="resources-container">
<!-- Resource Overview Cards --> <!-- Resource Overview Cards -->
<div class="resource-cards"> <div class="resource-cards">

View file

@ -1,10 +1,14 @@
<link rel="stylesheet" href="monitoring/services.css" />
<div class="services-container"> <div class="services-container">
<!-- Services Header --> <!-- Services Header -->
<div class="services-header"> <div class="services-header">
<div class="header-stats" <div
hx-get="/api/services/summary" class="header-stats"
hx-trigger="load, every 10s" hx-get="/api/services/summary"
hx-swap="innerHTML"> hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
<div class="stat-item running"> <div class="stat-item running">
<span class="stat-number">--</span> <span class="stat-number">--</span>
<span class="stat-label">Running</span> <span class="stat-label">Running</span>
@ -24,14 +28,23 @@
</div> </div>
<div class="header-actions"> <div class="header-actions">
<div class="search-box"> <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> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg> </svg>
<input type="text" <input
id="service-search" type="text"
placeholder="Search services..." id="service-search"
onkeyup="filterServices(this.value)"> placeholder="Search services..."
onkeyup="filterServices(this.value)"
/>
</div> </div>
<select id="status-filter" onchange="filterByStatus(this.value)"> <select id="status-filter" onchange="filterByStatus(this.value)">
<option value="all">All Status</option> <option value="all">All Status</option>
@ -39,8 +52,19 @@
<option value="warning">Warning</option> <option value="warning">Warning</option>
<option value="stopped">Stopped</option> <option value="stopped">Stopped</option>
</select> </select>
<button class="action-btn" onclick="restartAllServices()" title="Restart All"> <button
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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> <polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path> <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg> </svg>
@ -50,10 +74,13 @@
</div> </div>
<!-- Services Grid --> <!-- Services Grid -->
<div class="services-grid" id="services-grid" <div
hx-get="/api/services/status" class="services-grid"
hx-trigger="load, every 10s" id="services-grid"
hx-swap="innerHTML"> hx-get="/api/services/status"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
<!-- Loading placeholder --> <!-- Loading placeholder -->
<div class="service-card skeleton"> <div class="service-card skeleton">
<div class="skeleton-line"></div> <div class="skeleton-line"></div>
@ -78,7 +105,14 @@
<div class="panel-header"> <div class="panel-header">
<h3 id="detail-service-name">Service Details</h3> <h3 id="detail-service-name">Service Details</h3>
<button class="close-btn" onclick="closeServiceDetail()"> <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="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line>
</svg> </svg>
@ -95,9 +129,30 @@
<div class="service-card" data-status="running" data-service="service-name"> <div class="service-card" data-status="running" data-service="service-name">
<div class="card-header"> <div class="card-header">
<div class="service-icon"> <div class="service-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect> width="24"
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect> 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="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line> <line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg> </svg>
@ -112,21 +167,42 @@
<p class="service-description">Service description goes here</p> <p class="service-description">Service description goes here</p>
<div class="service-meta"> <div class="service-meta">
<span class="meta-item"> <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> <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> </svg>
Uptime: 24d 5h Uptime: 24d 5h
</span> </span>
<span class="meta-item"> <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="4" y="4" width="16" height="16" rx="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect> <rect x="9" y="9" width="6" height="6"></rect>
</svg> </svg>
CPU: 12% CPU: 12%
</span> </span>
<span class="meta-item"> <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> <rect x="2" y="6" width="20" height="12" rx="2"></rect>
</svg> </svg>
Mem: 256MB Mem: 256MB
@ -134,28 +210,74 @@
</div> </div>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<button class="card-btn" onclick="viewServiceDetails('service-id')" title="Details"> <button
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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> <circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line> <line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line> <line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg> </svg>
</button> </button>
<button class="card-btn" onclick="restartService('service-id')" title="Restart"> <button
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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> <polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path> <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg> </svg>
</button> </button>
<button class="card-btn" onclick="stopService('service-id')" title="Stop"> <button
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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="6" y="4" width="4" height="16"></rect>
<rect x="14" y="4" width="4" height="16"></rect> <rect x="14" y="4" width="4" height="16"></rect>
</svg> </svg>
</button> </button>
<button class="card-btn" onclick="viewServiceLogs('service-id')" title="Logs"> <button
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="card-btn"
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> 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> <polyline points="14 2 14 8 20 8"></polyline>
</svg> </svg>
</button> </button>
@ -167,73 +289,76 @@
<script> <script>
function filterServices(query) { function filterServices(query) {
const cards = document.querySelectorAll('.service-card:not(.skeleton)'); const cards = document.querySelectorAll(".service-card:not(.skeleton)");
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
cards.forEach(card => { cards.forEach((card) => {
const name = card.dataset.service?.toLowerCase() || ''; const name = card.dataset.service?.toLowerCase() || "";
const text = card.textContent.toLowerCase(); const text = card.textContent.toLowerCase();
const matches = name.includes(lowerQuery) || text.includes(lowerQuery); const matches =
card.classList.toggle('hidden', !matches); name.includes(lowerQuery) || text.includes(lowerQuery);
card.classList.toggle("hidden", !matches);
}); });
} }
function filterByStatus(status) { function filterByStatus(status) {
const cards = document.querySelectorAll('.service-card:not(.skeleton)'); const cards = document.querySelectorAll(".service-card:not(.skeleton)");
cards.forEach(card => { cards.forEach((card) => {
if (status === 'all') { if (status === "all") {
card.classList.remove('hidden'); card.classList.remove("hidden");
} else { } else {
const cardStatus = card.dataset.status; const cardStatus = card.dataset.status;
card.classList.toggle('hidden', cardStatus !== status); card.classList.toggle("hidden", cardStatus !== status);
} }
}); });
} }
function viewServiceDetails(serviceId) { function viewServiceDetails(serviceId) {
const panel = document.getElementById('service-detail-panel'); const panel = document.getElementById("service-detail-panel");
const content = document.getElementById('service-detail-content'); const content = document.getElementById("service-detail-content");
// Load service details via HTMX // Load service details via HTMX
htmx.ajax('GET', `/api/services/${serviceId}/details`, { htmx.ajax("GET", `/api/services/${serviceId}/details`, {
target: content, target: content,
swap: 'innerHTML' swap: "innerHTML",
}); });
document.getElementById('detail-service-name').textContent = serviceId; document.getElementById("detail-service-name").textContent = serviceId;
panel.classList.add('open'); panel.classList.add("open");
} }
function closeServiceDetail() { function closeServiceDetail() {
document.getElementById('service-detail-panel').classList.remove('open'); document
.getElementById("service-detail-panel")
.classList.remove("open");
} }
function restartService(serviceId) { function restartService(serviceId) {
if (confirm(`Are you sure you want to restart ${serviceId}?`)) { if (confirm(`Are you sure you want to restart ${serviceId}?`)) {
htmx.ajax('POST', `/api/services/${serviceId}/restart`, { htmx.ajax("POST", `/api/services/${serviceId}/restart`, {
swap: 'none' swap: "none",
}).then(() => { }).then(() => {
htmx.trigger('#services-grid', 'refresh'); htmx.trigger("#services-grid", "refresh");
}); });
} }
} }
function stopService(serviceId) { function stopService(serviceId) {
if (confirm(`Are you sure you want to stop ${serviceId}?`)) { if (confirm(`Are you sure you want to stop ${serviceId}?`)) {
htmx.ajax('POST', `/api/services/${serviceId}/stop`, { htmx.ajax("POST", `/api/services/${serviceId}/stop`, {
swap: 'none' swap: "none",
}).then(() => { }).then(() => {
htmx.trigger('#services-grid', 'refresh'); htmx.trigger("#services-grid", "refresh");
}); });
} }
} }
function startService(serviceId) { function startService(serviceId) {
htmx.ajax('POST', `/api/services/${serviceId}/start`, { htmx.ajax("POST", `/api/services/${serviceId}/start`, {
swap: 'none' swap: "none",
}).then(() => { }).then(() => {
htmx.trigger('#services-grid', 'refresh'); htmx.trigger("#services-grid", "refresh");
}); });
} }
@ -243,38 +368,44 @@
if (logsLink) { if (logsLink) {
logsLink.click(); logsLink.click();
setTimeout(() => { setTimeout(() => {
const serviceFilter = document.getElementById('service-filter'); const serviceFilter = document.getElementById("service-filter");
if (serviceFilter) { if (serviceFilter) {
serviceFilter.value = serviceId; serviceFilter.value = serviceId;
serviceFilter.dispatchEvent(new Event('change')); serviceFilter.dispatchEvent(new Event("change"));
} }
}, 300); }, 300);
} }
} }
function restartAllServices() { function restartAllServices() {
if (confirm('Are you sure you want to restart all services? This may cause temporary downtime.')) { if (
htmx.ajax('POST', '/api/services/restart-all', { confirm(
swap: 'none' "Are you sure you want to restart all services? This may cause temporary downtime.",
)
) {
htmx.ajax("POST", "/api/services/restart-all", {
swap: "none",
}).then(() => { }).then(() => {
htmx.trigger('#services-grid', 'refresh'); htmx.trigger("#services-grid", "refresh");
}); });
} }
} }
// Close panel on escape key // Close panel on escape key
document.addEventListener('keydown', function(e) { document.addEventListener("keydown", function (e) {
if (e.key === 'Escape') { if (e.key === "Escape") {
closeServiceDetail(); closeServiceDetail();
} }
}); });
// Close panel when clicking outside // Close panel when clicking outside
document.addEventListener('click', function(e) { document.addEventListener("click", function (e) {
const panel = document.getElementById('service-detail-panel'); const panel = document.getElementById("service-detail-panel");
if (panel.classList.contains('open') && if (
panel.classList.contains("open") &&
!panel.contains(e.target) && !panel.contains(e.target) &&
!e.target.closest('.card-btn')) { !e.target.closest(".card-btn")
) {
closeServiceDetail(); closeServiceDetail();
} }
}); });

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="paper/paper.css" />
<!-- Paper - AI Writing & Notes --> <!-- Paper - AI Writing & Notes -->
<div class="paper-container" id="paper-app"> <div class="paper-container" id="paper-app">
<!-- Sidebar - Notes List --> <!-- Sidebar - Notes List -->

View file

@ -11,7 +11,14 @@
</div> </div>
<div class="header-actions"> <div class="header-actions">
<div class="search-box"> <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> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg> </svg>
@ -23,11 +30,18 @@
/> />
</div> </div>
<button class="btn btn-primary" onclick="openAddContact()"> <button class="btn btn-primary" onclick="openAddContact()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/> width="18"
<circle cx="8.5" cy="7" r="4"/> height="18"
<line x1="20" y1="8" x2="20" y2="14"/> viewBox="0 0 24 24"
<line x1="23" y1="11" x2="17" y2="11"/> 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> </svg>
<span data-i18n="people-add">Add Contact</span> <span data-i18n="people-add">Add Contact</span>
</button> </button>
@ -36,33 +50,83 @@
<!-- Tab Navigation --> <!-- Tab Navigation -->
<nav class="tab-nav" role="tablist"> <nav class="tab-nav" role="tablist">
<button class="tab-btn active" role="tab" aria-selected="true" onclick="showTab('contacts', this)"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="tab-btn active"
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/> role="tab"
<circle cx="9" cy="7" r="4"/> 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> </svg>
<span data-i18n="people-tab-contacts">Contacts</span> <span data-i18n="people-tab-contacts">Contacts</span>
</button> </button>
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('groups', this)"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="tab-btn"
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/> role="tab"
<circle cx="9" cy="7" r="4"/> aria-selected="false"
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/> onclick="showTab('groups', this)"
<path d="M16 3.13a4 4 0 0 1 0 7.75"/> >
<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> </svg>
<span data-i18n="people-tab-groups">Groups</span> <span data-i18n="people-tab-groups">Groups</span>
</button> </button>
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('directory', this)"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="tab-btn"
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/> role="tab"
<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"/> 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> </svg>
<span data-i18n="people-tab-directory">Directory</span> <span data-i18n="people-tab-directory">Directory</span>
</button> </button>
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('recent', this)"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="tab-btn"
<circle cx="12" cy="12" r="10"/> role="tab"
<polyline points="12 6 12 12 16 14"/> 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> </svg>
<span data-i18n="people-tab-recent">Recent</span> <span data-i18n="people-tab-recent">Recent</span>
</button> </button>
@ -74,33 +138,90 @@
<div id="contacts-tab" class="tab-content active"> <div id="contacts-tab" class="tab-content active">
<!-- Alphabet Filter --> <!-- Alphabet Filter -->
<div class="alphabet-filter"> <div class="alphabet-filter">
<button class="alpha-btn active" onclick="filterByLetter('all', this)">All</button> <button
<button class="alpha-btn" onclick="filterByLetter('A', this)">A</button> class="alpha-btn active"
<button class="alpha-btn" onclick="filterByLetter('B', this)">B</button> onclick="filterByLetter('all', this)"
<button class="alpha-btn" onclick="filterByLetter('C', this)">C</button> >
<button class="alpha-btn" onclick="filterByLetter('D', this)">D</button> All
<button class="alpha-btn" onclick="filterByLetter('E', this)">E</button> </button>
<button class="alpha-btn" onclick="filterByLetter('F', this)">F</button> <button class="alpha-btn" onclick="filterByLetter('A', this)">
<button class="alpha-btn" onclick="filterByLetter('G', this)">G</button> A
<button class="alpha-btn" onclick="filterByLetter('H', this)">H</button> </button>
<button class="alpha-btn" onclick="filterByLetter('I', this)">I</button> <button class="alpha-btn" onclick="filterByLetter('B', this)">
<button class="alpha-btn" onclick="filterByLetter('J', this)">J</button> B
<button class="alpha-btn" onclick="filterByLetter('K', this)">K</button> </button>
<button class="alpha-btn" onclick="filterByLetter('L', this)">L</button> <button class="alpha-btn" onclick="filterByLetter('C', this)">
<button class="alpha-btn" onclick="filterByLetter('M', this)">M</button> C
<button class="alpha-btn" onclick="filterByLetter('N', this)">N</button> </button>
<button class="alpha-btn" onclick="filterByLetter('O', this)">O</button> <button class="alpha-btn" onclick="filterByLetter('D', this)">
<button class="alpha-btn" onclick="filterByLetter('P', this)">P</button> D
<button class="alpha-btn" onclick="filterByLetter('Q', this)">Q</button> </button>
<button class="alpha-btn" onclick="filterByLetter('R', this)">R</button> <button class="alpha-btn" onclick="filterByLetter('E', this)">
<button class="alpha-btn" onclick="filterByLetter('S', this)">S</button> E
<button class="alpha-btn" onclick="filterByLetter('T', this)">T</button> </button>
<button class="alpha-btn" onclick="filterByLetter('U', this)">U</button> <button class="alpha-btn" onclick="filterByLetter('F', this)">
<button class="alpha-btn" onclick="filterByLetter('V', this)">V</button> F
<button class="alpha-btn" onclick="filterByLetter('W', this)">W</button> </button>
<button class="alpha-btn" onclick="filterByLetter('X', this)">X</button> <button class="alpha-btn" onclick="filterByLetter('G', this)">
<button class="alpha-btn" onclick="filterByLetter('Y', this)">Y</button> G
<button class="alpha-btn" onclick="filterByLetter('Z', this)">Z</button> </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> </div>
<!-- Contacts List --> <!-- Contacts List -->
@ -139,22 +260,53 @@
<div class="contact-panel" id="contact-panel"> <div class="contact-panel" id="contact-panel">
<div class="panel-header"> <div class="panel-header">
<button class="close-btn" onclick="closeContactPanel()"> <button class="close-btn" onclick="closeContactPanel()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
<line x1="18" y1="6" x2="6" y2="18"/> width="20"
<line x1="6" y1="6" x2="18" y2="18"/> 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> </svg>
</button> </button>
<div class="panel-actions"> <div class="panel-actions">
<button class="icon-btn" title="Edit" onclick="editContact()"> <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"> <svg
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/> width="18"
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/> 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> </svg>
</button> </button>
<button class="icon-btn" title="Delete" onclick="deleteContact()"> <button
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="icon-btn"
<polyline points="3 6 5 6 21 6"/> title="Delete"
<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"/> 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> </svg>
</button> </button>
</div> </div>
@ -171,9 +323,16 @@
<div class="modal-header"> <div class="modal-header">
<h2 id="modal-title" data-i18n="people-add-contact">Add Contact</h2> <h2 id="modal-title" data-i18n="people-add-contact">Add Contact</h2>
<button class="close-btn" onclick="closeModal()"> <button class="close-btn" onclick="closeModal()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
<line x1="18" y1="6" x2="6" y2="18"/> width="20"
<line x1="6" y1="6" x2="18" y2="18"/> 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> </svg>
</button> </button>
</div> </div>
@ -209,70 +368,80 @@
<textarea name="notes" rows="3"></textarea> <textarea name="notes" rows="3"></textarea>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal()" data-i18n="cancel">Cancel</button> <button
<button type="submit" class="btn btn-primary" data-i18n="save">Save</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> </div>
</form> </form>
</div> </div>
</dialog> </dialog>
<script> <script>
let currentContact = null; (function () {
let contacts = []; let currentContact = null;
let contacts = [];
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
loadContacts(); loadContacts();
}); });
async function loadContacts() { async function loadContacts() {
try { try {
const response = await fetch('/api/contacts'); const response = await fetch("/api/contacts");
if (response.ok) { if (response.ok) {
contacts = await response.json(); contacts = await response.json();
renderContacts(contacts); renderContacts(contacts);
} else { } else {
renderEmptyState(); renderEmptyState();
}
} catch (error) {
console.error("Failed to load contacts:", error);
renderEmptyState();
}
} }
} catch (error) {
console.error('Failed to load contacts:', error);
renderEmptyState();
}
}
function renderContacts(contactsList) { function renderContacts(contactsList) {
const container = document.getElementById('contacts-list'); const container = document.getElementById("contacts-list");
if (!contactsList || contactsList.length === 0) { if (!contactsList || contactsList.length === 0) {
renderEmptyState(); renderEmptyState();
return; return;
} }
const grouped = groupByLetter(contactsList); const grouped = groupByLetter(contactsList);
let html = ''; let html = "";
for (const [letter, group] of Object.entries(grouped)) { for (const [letter, group] of Object.entries(grouped)) {
html += `<div class="contact-group" data-letter="${letter}"> html += `<div class="contact-group" data-letter="${letter}">
<div class="group-header">${letter}</div> <div class="group-header">${letter}</div>
<div class="group-contacts">`; <div class="group-contacts">`;
for (const contact of group) { for (const contact of group) {
html += renderContactCard(contact); 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; return `<div class="contact-card" onclick="showContact('${contact.id}')">
}
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}')">
<div class="contact-avatar" style="background: ${getAvatarColor(name)}">${initials}</div> <div class="contact-avatar" style="background: ${getAvatarColor(name)}">${initials}</div>
<div class="contact-info"> <div class="contact-info">
<div class="contact-name">${name}</div> <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>
<div class="contact-actions"> <div class="contact-actions">
<button class="icon-btn small" onclick="event.stopPropagation(); startChat('${contact.id}')" title="Chat"> <button class="icon-btn small" onclick="event.stopPropagation(); startChat('${contact.id}')" title="Chat">
@ -288,10 +457,10 @@ function renderContactCard(contact) {
</button> </button>
</div> </div>
</div>`; </div>`;
} }
function renderEmptyState() { function renderEmptyState() {
document.getElementById('contacts-list').innerHTML = ` document.getElementById("contacts-list").innerHTML = `
<div class="empty-state"> <div class="empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <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"/> <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
@ -310,91 +479,124 @@ function renderEmptyState() {
</button> </button>
</div> </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) { function groupByLetter(contactsList) {
currentContact = contacts.find(c => c.id === id); const grouped = {};
if (!currentContact) return; 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'); function getInitials(firstName, lastName) {
const detail = document.getElementById('contact-detail'); 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-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)} ${getInitials(currentContact.firstName, currentContact.lastName)}
</div> </div>
<h2>${currentContact.firstName} ${currentContact.lastName}</h2> <h2>${currentContact.firstName} ${currentContact.lastName}</h2>
${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ''} ${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ""}
${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ''} ${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ""}
</div> </div>
<div class="contact-fields"> <div class="contact-fields">
${currentContact.email ? ` ${
currentContact.email
? `
<div class="field"> <div class="field">
<label>Email</label> <label>Email</label>
<a href="mailto:${currentContact.email}">${currentContact.email}</a> <a href="mailto:${currentContact.email}">${currentContact.email}</a>
</div> </div>
` : ''} `
${currentContact.phone ? ` : ""
}
${
currentContact.phone
? `
<div class="field"> <div class="field">
<label>Phone</label> <label>Phone</label>
<a href="tel:${currentContact.phone}">${currentContact.phone}</a> <a href="tel:${currentContact.phone}">${currentContact.phone}</a>
</div> </div>
` : ''} `
${currentContact.notes ? ` : ""
}
${
currentContact.notes
? `
<div class="field"> <div class="field">
<label>Notes</label> <label>Notes</label>
<p>${currentContact.notes}</p> <p>${currentContact.notes}</p>
</div> </div>
` : ''} `
: ""
}
</div> </div>
<div class="contact-quick-actions"> <div class="contact-quick-actions">
<button class="action-btn" onclick="startChat('${currentContact.id}')"> <button class="action-btn" onclick="startChat('${currentContact.id}')">
@ -422,106 +624,132 @@ function showContact(id) {
</div> </div>
`; `;
panel.classList.add('open'); 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();
} }
} catch (error) {
console.error('Failed to save contact:', error);
}
}
async function deleteContact() { function closeContactPanel() {
if (!currentContact || !confirm('Delete this contact?')) return; document.getElementById("contact-panel").classList.remove("open");
currentContact = null;
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) { function openAddContact() {
window.location.href = `/#chat?contact=${contactId}`; currentContact = null;
} document.getElementById("modal-title").textContent = "Add Contact";
document.getElementById("contact-form").reset();
document.getElementById("contact-modal").showModal();
}
function sendEmail(email) { function editContact() {
if (email) window.location.href = `mailto:${email}`; 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) { function closeModal() {
window.location.href = `/#calendar?new=meeting&contact=${contactId}`; document.getElementById("contact-modal").close();
} }
// Search functionality async function saveContact(event) {
document.getElementById('people-search')?.addEventListener('input', (e) => { event.preventDefault();
const query = e.target.value.toLowerCase(); const form = event.target;
const filtered = contacts.filter(c => const data = {
(c.firstName + ' ' + c.lastName).toLowerCase().includes(query) || firstName: form.firstName.value,
(c.email || '').toLowerCase().includes(query) || lastName: form.lastName.value,
(c.company || '').toLowerCase().includes(query) email: form.email.value,
); phone: form.phone.value,
renderContacts(filtered); 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> </script>

View file

@ -1,7 +1,7 @@
<!-- Products - Product & Service Catalog --> <!-- Products - Product & Service Catalog -->
<!-- Dynamics nomenclature: Product, Service, PriceList --> <!-- Dynamics nomenclature: Product, Service, PriceList -->
<link rel="stylesheet" href="/suite/products/products.css"> <link rel="stylesheet" href="products/products.css">
<div class="products-container"> <div class="products-container">
<!-- Header --> <!-- Header -->

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="research/research.css" />
<!-- Research - AI-Powered Search & Discovery --> <!-- Research - AI-Powered Search & Discovery -->
<div class="research-container" id="research-app"> <div class="research-container" id="research-app">
<!-- Sidebar - Sources & Collections --> <!-- Sidebar - Sources & Collections -->

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="settings/settings.css" />
<div class="settings-layout"> <div class="settings-layout">
<!-- Sidebar Navigation --> <!-- Sidebar Navigation -->
<aside class="settings-sidebar"> <aside class="settings-sidebar">

View file

@ -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() { function createNewPresentation() {
const titleSlide = createSlide("title"); const titleSlide = createSlide("title");
state.slides = [titleSlide]; state.slides = [titleSlide];

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="social/social.css" />
<div class="social-app"> <div class="social-app">
<div class="social-header"> <div class="social-header">
<div class="social-tabs"> <div class="social-tabs">

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="sources/sources.css" />
<div class="sources-container"> <div class="sources-container">
<!-- Header --> <!-- Header -->
<header class="sources-header"> <header class="sources-header">

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="tasks/autotask.css" />
<div class="autotask-container" data-theme="sentient"> <div class="autotask-container" data-theme="sentient">
<!-- Top Navigation Bar --> <!-- Top Navigation Bar -->
<div class="autotask-topbar"> <div class="autotask-topbar">
@ -476,6 +478,6 @@ Examples:
<!-- Toast Container --> <!-- Toast Container -->
<div class="toast-container" id="toast-container"></div> <div class="toast-container" id="toast-container"></div>
<link rel="stylesheet" href="progress-panel.css" /> <link rel="stylesheet" href="tasks/progress-panel.css" />
<script src="progress-panel.js"></script> <script src="tasks/progress-panel.js"></script>
<script src="autotask.js"></script> <script src="tasks/autotask.js"></script>

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="tasks/tasks.css" />
<!-- ============================================================================= <!-- =============================================================================
TASKS APP - Autonomous Task Management TASKS APP - Autonomous Task Management
Respects Theme Manager - No hardcoded theme Respects Theme Manager - No hardcoded theme

View file

@ -1,7 +1,7 @@
<!-- Tickets - AI-Assisted Support Cases --> <!-- Tickets - AI-Assisted Support Cases -->
<!-- Dynamics nomenclature: Case, Resolution, Activity --> <!-- Dynamics nomenclature: Case, Resolution, Activity -->
<link rel="stylesheet" href="/suite/tickets/tickets.css" /> <link rel="stylesheet" href="tickets/tickets.css" />
<div class="tickets-container"> <div class="tickets-container">
<!-- Header --> <!-- Header -->

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="tools/tools.css" />
<div class="compliance-container" id="compliance-app"> <div class="compliance-container" id="compliance-app">
<style> <style>
.compliance-container { .compliance-container {

View file

@ -1,3 +1,5 @@
<link rel="stylesheet" href="tools/tools.css" />
<div class="security-container" id="security-app"> <div class="security-container" id="security-app">
<style> <style>
.security-container { .security-container {

View file

@ -127,7 +127,9 @@
> >
<!-- Empty state --> <!-- Empty state -->
<div class="empty-page"> <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>
</div> </div>
@ -196,340 +198,346 @@
</div> </div>
<style> <style>
.workspace-app { .workspace-app {
display: flex; 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;
height: 100%; height: 100%;
z-index: 50; background: var(--bg-primary);
transition: left 0.2s; color: var(--text-primary);
} }
.workspace-sidebar.open { .workspace-sidebar {
left: 0; 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 { .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; 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> </style>
<script> <script>
function toggleMembersPanel() { function toggleMembersPanel() {
const panel = document.getElementById('members-panel'); const panel = document.getElementById("members-panel");
panel.classList.toggle('collapsed'); 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');
slashMenu.classList.remove('hidden');
}
if (e.key === 'Escape') {
const slashMenu = document.getElementById('slash-menu');
slashMenu.classList.add('hidden');
}
});
} }
});
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> </script>