This commit is contained in:
2025-08-08 21:36:46 +02:00
parent fd195f0824
commit a34b8b70ba
13 changed files with 1928 additions and 57 deletions

View File

@@ -2,6 +2,7 @@ module ui
import veb
import os
import net.http
// Public Context type for veb
pub struct Context {
@@ -91,6 +92,64 @@ pub fn (app &App) admin_index(mut ctx Context) veb.Result {
return ctx.html(app.render_admin('/', 'Welcome'))
}
// HeroScript editor page
@[get; '/admin/heroscript']
pub fn (app &App) admin_heroscript(mut ctx Context) veb.Result {
return ctx.html(app.render_heroscript())
}
// Static CSS files
@[get; '/static/css/colors.css']
pub fn (app &App) serve_colors_css(mut ctx Context) veb.Result {
css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'colors.css')
css_content := os.read_file(css_path) or {
return ctx.text('/* CSS file not found */')
}
ctx.set_content_type('text/css')
return ctx.text(css_content)
}
@[get; '/static/css/main.css']
pub fn (app &App) serve_main_css(mut ctx Context) veb.Result {
css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'main.css')
css_content := os.read_file(css_path) or {
return ctx.text('/* CSS file not found */')
}
ctx.set_content_type('text/css')
return ctx.text(css_content)
}
// Static JS files
@[get; '/static/js/theme.js']
pub fn (app &App) serve_theme_js(mut ctx Context) veb.Result {
js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'theme.js')
js_content := os.read_file(js_path) or {
return ctx.text('/* JS file not found */')
}
ctx.set_content_type('application/javascript')
return ctx.text(js_content)
}
@[get; '/static/js/heroscript.js']
pub fn (app &App) serve_heroscript_js(mut ctx Context) veb.Result {
js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'heroscript.js')
js_content := os.read_file(js_path) or {
return ctx.text('/* JS file not found */')
}
ctx.set_content_type('application/javascript')
return ctx.text(js_content)
}
@[get; '/static/css/heroscript.css']
pub fn (app &App) serve_heroscript_css(mut ctx Context) veb.Result {
css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'heroscript.css')
css_content := os.read_file(css_path) or {
return ctx.text('/* CSS file not found */')
}
ctx.set_content_type('text/css')
return ctx.text(css_content)
}
// Catch-all content under /admin/*
@[get; '/admin/:path...']
pub fn (app &App) admin_section(mut ctx Context, path string) veb.Result {
@@ -119,10 +178,62 @@ fn (app &App) render_admin(path string, heading string) string {
result = result.replace('{{.heading}}', heading)
result = result.replace('{{.path}}', path)
result = result.replace('{{.menu_html}}', menu_content)
result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
result = result.replace('{{.css_main_url}}', '/static/css/main.css')
result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
return result
}
// HeroScript editor rendering using external template
fn (app &App) render_heroscript() string {
// Get the template file path relative to the module
template_path := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.html')
// Read the template file
template_content := os.read_file(template_path) or {
// Fallback to basic template if file not found
return app.render_heroscript_fallback()
}
// Generate menu HTML
menu_content := menu_html(app.menu, 0, 'm')
// Simple template variable replacement
mut result := template_content
result = result.replace('{{.title}}', app.title)
result = result.replace('{{.menu_html}}', menu_content)
result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
result = result.replace('{{.css_main_url}}', '/static/css/main.css')
result = result.replace('{{.css_heroscript_url}}', '/static/css/heroscript.css')
result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
result = result.replace('{{.js_heroscript_url}}', '/static/js/heroscript.js')
return result
}
// Fallback HeroScript rendering method
fn (app &App) render_heroscript_fallback() string {
return '
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${app.title} - HeroScript Editor</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1>HeroScript Editor</h1>
<p>HeroScript editor template not found. Please check the template files.</p>
<a href="/admin" class="btn btn-primary">Back to Admin</a>
</div>
</body>
</html>
'
}
// Fallback rendering method (inline template)
fn (app &App) render_admin_fallback(path string, heading string) string {
return '
@@ -249,6 +360,10 @@ fn default_menu() []MenuItem {
title: 'Dashboard'
href: '/admin'
},
MenuItem{
title: 'HeroScript'
href: '/admin/heroscript'
},
MenuItem{
title: 'Users'
children: [

View File

@@ -5,50 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.title}}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<style>
body { padding-top: 44px; }
.header {
height: 44px;
line-height: 44px;
font-size: 14px;
}
.sidebar {
position: fixed;
top: 44px;
bottom: 0;
left: 0;
width: 260px;
overflow-y: auto;
background: #f8f9fa;
border-right: 1px solid #e0e0e0;
}
.main {
margin-left: 260px;
padding: 16px;
}
.list-group-item {
border: 0;
padding: .35rem .75rem;
background: transparent;
}
.menu-leaf a {
color: #212529;
text-decoration: none;
}
.menu-toggle {
text-decoration: none;
color: #212529;
}
.menu-toggle .chev {
font-size: 10px;
opacity: .6;
}
.menu-section {
font-weight: 600;
color: #6c757d;
padding: .5rem .75rem;
}
</style>
<link rel="stylesheet" href="{{.css_colors_url}}">
<link rel="stylesheet" href="{{.css_main_url}}">
<meta name="color-scheme" content="light dark">
</head>
<body>
<nav class="navbar navbar-dark bg-dark fixed-top header px-2">
@@ -83,5 +42,6 @@
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="{{.js_theme_url}}"></script>
</body>
</html>

View File

@@ -0,0 +1,124 @@
/* CSS Custom Properties for Theme Colors */
/* Light Theme (Default) */
:root {
/* Background Colors */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--bg-dark: #343a40;
/* Text Colors */
--text-primary: #212529;
--text-secondary: #6c757d;
--text-muted: #6c757d;
--text-light: #ffffff;
--text-white-50: rgba(255, 255, 255, 0.5);
/* Border Colors */
--border-primary: #e0e0e0;
--border-secondary: #dee2e6;
/* Header Colors */
--header-bg: #343a40;
--header-text: #ffffff;
--header-text-muted: rgba(255, 255, 255, 0.5);
/* Sidebar Colors */
--sidebar-bg: #f8f9fa;
--sidebar-border: #e0e0e0;
--sidebar-section-text: #6c757d;
/* Menu Colors */
--menu-item-bg: transparent;
--menu-item-text: #212529;
--menu-item-hover-bg: #e9ecef;
--menu-item-hover-text: #212529;
--menu-toggle-text: #212529;
--menu-chevron-opacity: 0.6;
/* Card Colors */
--card-bg: #ffffff;
--card-border: #dee2e6;
--card-shadow: rgba(0, 0, 0, 0.125);
/* Interactive Elements */
--link-color: #0d6efd;
--link-hover-color: #0a58ca;
/* Status Colors */
--success-color: #198754;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #0dcaf0;
}
/* Dark Theme */
[data-theme="dark"] {
/* Background Colors */
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #404040;
--bg-dark: #000000;
/* Text Colors */
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--text-muted: #888888;
--text-light: #ffffff;
--text-white-50: rgba(255, 255, 255, 0.5);
/* Border Colors */
--border-primary: #404040;
--border-secondary: #555555;
/* Header Colors */
--header-bg: #000000;
--header-text: #ffffff;
--header-text-muted: rgba(255, 255, 255, 0.5);
/* Sidebar Colors */
--sidebar-bg: #2d2d2d;
--sidebar-border: #404040;
--sidebar-section-text: #b0b0b0;
/* Menu Colors */
--menu-item-bg: transparent;
--menu-item-text: #ffffff;
--menu-item-hover-bg: #404040;
--menu-item-hover-text: #ffffff;
--menu-toggle-text: #ffffff;
--menu-chevron-opacity: 0.6;
/* Card Colors */
--card-bg: #2d2d2d;
--card-border: #404040;
--card-shadow: rgba(0, 0, 0, 0.3);
/* Interactive Elements */
--link-color: #66b3ff;
--link-hover-color: #4da6ff;
/* Status Colors */
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #17a2b8;
}
/* Theme transition for smooth switching */
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
/* Utility classes for theme-aware styling */
.bg-theme-primary { background-color: var(--bg-primary); }
.bg-theme-secondary { background-color: var(--bg-secondary); }
.bg-theme-tertiary { background-color: var(--bg-tertiary); }
.text-theme-primary { color: var(--text-primary); }
.text-theme-secondary { color: var(--text-secondary); }
.text-theme-muted { color: var(--text-muted); }
.border-theme-primary { border-color: var(--border-primary); }
.border-theme-secondary { border-color: var(--border-secondary); }

View File

@@ -0,0 +1,409 @@
/* HeroScript Editor Specific Styles */
/* Full height layout for editor */
.main .container-fluid {
height: calc(100vh - 44px - 2rem); /* Account for header and padding */
}
.main .row {
height: calc(100% - 60px); /* Account for header section */
}
/* Editor Panel Styles */
#editor-container {
position: relative;
background-color: var(--bg-primary);
height: 100%;
}
#script-editor {
border: none;
resize: none;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 16px;
line-height: 1.6;
padding: 1.5rem;
border-radius: 0;
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
outline: none;
box-shadow: none;
white-space: pre;
word-wrap: break-word;
overflow-wrap: break-word;
min-height: 500px;
height: 100%;
}
/* Highlighted overlay for syntax highlighting */
.highlighted-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 16px;
line-height: 1.6;
padding: 1.5rem;
margin: 0;
border: none;
background: transparent;
white-space: pre;
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
z-index: 1;
}
#script-editor:focus {
outline: none;
box-shadow: 0 0 0 2px var(--link-color);
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
}
#script-editor::placeholder {
color: var(--text-muted);
opacity: 0.7;
}
/* Simple syntax highlighting for basic keywords */
.hljs {
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
padding: 1.5rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 16px;
line-height: 1.6;
}
/* Dark theme syntax highlighting */
[data-theme="dark"] .hljs {
background-color: var(--bg-primary) !important;
}
[data-theme="dark"] .hljs-keyword {
color: #569cd6;
}
[data-theme="dark"] .hljs-string {
color: #ce9178;
}
[data-theme="dark"] .hljs-comment {
color: #6a9955;
}
[data-theme="dark"] .hljs-number {
color: #b5cea8;
}
[data-theme="dark"] .hljs-function {
color: #dcdcaa;
}
/* Light theme syntax highlighting */
[data-theme="light"] .hljs-keyword {
color: #0000ff;
}
[data-theme="light"] .hljs-string {
color: #a31515;
}
[data-theme="light"] .hljs-comment {
color: #008000;
}
[data-theme="light"] .hljs-number {
color: #098658;
}
[data-theme="light"] .hljs-function {
color: #795e26;
}
/* Logs Panel Styles */
#logs-container {
background-color: var(--bg-secondary);
border-radius: 0;
}
#logs-content {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Log Entry Styles */
.log-entry {
display: block;
margin-bottom: 0.25rem;
padding: 0.125rem 0.5rem;
font-size: 13px;
line-height: 1.3;
}
.log-entry:last-child {
border-bottom: none;
}
/* Compact log format: cat: message */
.log-entry.compact {
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.log-entry.compact .log-category {
font-weight: 600;
min-width: 80px;
flex-shrink: 0;
}
.log-entry.compact .log-message {
color: var(--text-primary);
flex: 1;
}
/* Legacy log entry styles (for backward compatibility) */
.log-entry .timestamp {
color: var(--text-muted);
font-size: 11px;
margin-right: 0.5rem;
}
.log-entry .level {
display: inline-block;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
margin-right: 0.5rem;
min-width: 50px;
text-align: center;
}
.log-entry .level.info {
background-color: var(--info-color);
color: white;
}
.log-entry .level.success {
background-color: var(--success-color);
color: white;
}
.log-entry .level.warning {
background-color: var(--warning-color);
color: black;
}
.log-entry .level.error {
background-color: var(--danger-color);
color: white;
}
.log-entry .level.debug {
background-color: var(--text-muted);
color: white;
}
.log-entry .message {
color: var(--text-primary);
}
.log-entry.system {
background-color: var(--bg-tertiary);
padding: 0.5rem;
border-radius: 0.25rem;
border: 1px solid var(--border-secondary);
}
.log-entry.error {
background-color: rgba(220, 53, 69, 0.1);
border-left: 3px solid var(--danger-color);
padding-left: 0.75rem;
}
.log-entry.warning {
background-color: rgba(255, 193, 7, 0.1);
border-left: 3px solid var(--warning-color);
padding-left: 0.75rem;
}
/* Card Header Styles */
.card-header {
background-color: var(--bg-tertiary);
border-bottom: 1px solid var(--border-primary);
padding: 0.75rem 1rem;
}
.card-header h6 {
color: var(--text-primary);
font-weight: 600;
}
/* Control Buttons */
.editor-controls select {
width: 120px;
}
.log-controls .badge {
font-size: 0.7rem;
}
#connection-status.bg-success {
background-color: var(--success-color) !important;
}
#connection-status.bg-danger {
background-color: var(--danger-color) !important;
}
#connection-status.bg-warning {
background-color: var(--warning-color) !important;
color: black !important;
}
/* Auto-scroll button states */
#auto-scroll[data-active="true"] {
background-color: var(--link-color);
border-color: var(--link-color);
color: white;
}
#auto-scroll[data-active="false"] {
background-color: transparent;
border-color: var(--border-primary);
color: var(--text-secondary);
}
/* Action Buttons */
.btn-primary {
background-color: var(--link-color);
border-color: var(--link-color);
}
.btn-primary:hover {
background-color: var(--link-hover-color);
border-color: var(--link-hover-color);
}
.btn-success {
background-color: var(--success-color);
border-color: var(--success-color);
}
.btn-outline-secondary {
border-color: var(--border-primary);
color: var(--text-secondary);
}
.btn-outline-secondary:hover {
background-color: var(--bg-tertiary);
border-color: var(--border-secondary);
color: var(--text-primary);
}
/* Responsive Design */
@media (max-width: 992px) {
.main .row {
flex-direction: column;
height: auto;
}
.main .col-lg-9,
.main .col-md-8 {
height: 65vh;
margin-bottom: 1rem;
}
.main .col-lg-3,
.main .col-md-4 {
height: 35vh;
margin-bottom: 0;
}
#script-editor {
font-size: 14px;
padding: 1rem;
min-height: 300px;
}
.d-flex.align-items-center.mb-3 {
flex-direction: column;
align-items: stretch !important;
}
.ms-auto {
margin-top: 1rem !important;
margin-left: 0 !important;
}
}
/* Loading States */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-spinner {
color: white;
font-size: 2rem;
}
/* Scrollbar Styling */
#logs-container::-webkit-scrollbar {
width: 8px;
}
#logs-container::-webkit-scrollbar-track {
background-color: var(--bg-tertiary);
}
#logs-container::-webkit-scrollbar-thumb {
background-color: var(--border-primary);
border-radius: 4px;
}
#logs-container::-webkit-scrollbar-thumb:hover {
background-color: var(--border-secondary);
}
/* Animation for new log entries */
@keyframes slideInLog {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.log-entry.new {
animation: slideInLog 0.3s ease-out;
}
/* Focus indicators for accessibility */
.btn:focus,
.form-select:focus,
#script-editor:focus {
outline: 2px solid var(--link-color);
outline-offset: 2px;
}

View File

@@ -0,0 +1,279 @@
/* Main Layout Styles using CSS Custom Properties */
/* Base Layout */
body {
padding-top: 44px;
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* Header Styles */
.header {
height: 44px;
line-height: 44px;
font-size: 14px;
background-color: var(--header-bg) !important;
border-bottom: 1px solid var(--border-primary);
}
.header .navbar-brand,
.header .text-white {
color: var(--header-text) !important;
}
.header .text-white-50 {
color: var(--header-text-muted) !important;
}
/* Sidebar Styles */
.sidebar {
position: fixed;
top: 44px;
bottom: 0;
left: 0;
width: 260px;
overflow-y: auto;
background-color: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
z-index: 1000;
}
/* Main Content Area */
.main {
margin-left: 260px;
padding: 16px;
background-color: var(--bg-primary);
min-height: calc(100vh - 44px);
}
/* Menu Styles */
.menu-section {
font-weight: 600;
color: var(--sidebar-section-text);
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.list-group-item {
border: 0;
padding: 0.35rem 0.75rem;
background-color: var(--menu-item-bg);
color: var(--menu-item-text);
transition: all 0.2s ease;
}
.list-group-item:hover {
background-color: var(--menu-item-hover-bg);
color: var(--menu-item-hover-text);
}
.menu-leaf a {
color: var(--menu-item-text);
text-decoration: none;
display: block;
padding: 0.25rem 0;
transition: color 0.2s ease;
}
.menu-leaf a:hover {
color: var(--link-hover-color);
}
.menu-leaf:hover a {
color: var(--link-hover-color);
}
.menu-toggle {
text-decoration: none;
color: var(--menu-toggle-text);
transition: color 0.2s ease;
}
.menu-toggle:hover {
color: var(--link-hover-color);
}
.menu-toggle .chev {
font-size: 10px;
opacity: var(--menu-chevron-opacity);
transition: transform 0.2s ease, opacity 0.2s ease;
}
.menu-toggle[aria-expanded="true"] .chev {
transform: rotate(90deg);
opacity: 0.8;
}
/* Nested menu indentation */
.sidebar .ms-2 {
margin-left: 1rem !important;
}
.sidebar .ms-2 .list-group-item {
padding-left: 1rem;
font-size: 0.9rem;
}
.sidebar .ms-2 .ms-2 .list-group-item {
padding-left: 1.5rem;
font-size: 0.85rem;
}
/* Card Styles */
.card {
background-color: var(--card-bg);
border: 1px solid var(--card-border);
box-shadow: 0 0.125rem 0.25rem var(--card-shadow);
transition: all 0.2s ease;
}
.card:hover {
box-shadow: 0 0.25rem 0.5rem var(--card-shadow);
}
.card-body {
color: var(--text-primary);
}
/* Content Area Styles */
.container-fluid {
background-color: var(--bg-primary);
}
.mb-3 h5 {
color: var(--text-primary);
font-weight: 600;
}
.text-muted {
color: var(--text-muted) !important;
}
.small {
font-size: 0.875rem;
}
/* Code elements */
code {
background-color: var(--bg-tertiary);
color: var(--text-primary);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875em;
}
/* Theme Toggle Button */
.theme-toggle {
position: fixed;
top: 8px;
right: 16px;
z-index: 1050;
background-color: var(--bg-secondary);
border: 1px solid var(--border-primary);
color: var(--text-primary);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
}
.theme-toggle:hover {
background-color: var(--bg-tertiary);
border-color: var(--border-secondary);
}
/* Responsive Design */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.show {
transform: translateX(0);
}
.main {
margin-left: 0;
}
.mobile-menu-toggle {
display: block;
position: fixed;
top: 8px;
left: 16px;
z-index: 1051;
background-color: var(--header-bg);
border: 1px solid var(--border-primary);
color: var(--header-text);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
cursor: pointer;
}
}
@media (min-width: 769px) {
.mobile-menu-toggle {
display: none;
}
}
/* Scrollbar Styling for Webkit browsers */
.sidebar::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track {
background-color: var(--bg-secondary);
}
.sidebar::-webkit-scrollbar-thumb {
background-color: var(--border-primary);
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover {
background-color: var(--border-secondary);
}
/* Focus styles for accessibility */
.menu-toggle:focus,
.menu-leaf a:focus,
.theme-toggle:focus {
outline: 2px solid var(--link-color);
outline-offset: 2px;
}
/* Animation for collapsible menu items */
.collapse {
transition: height 0.3s ease;
}
/* Loading state */
.loading {
opacity: 0.6;
pointer-events: none;
}
/* Print styles */
@media print {
.sidebar,
.header,
.theme-toggle {
display: none !important;
}
.main {
margin-left: 0 !important;
padding: 0 !important;
}
body {
padding-top: 0 !important;
}
}

View File

@@ -0,0 +1,139 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.title}} - HeroScript Editor</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<link rel="stylesheet" href="{{.css_colors_url}}">
<link rel="stylesheet" href="{{.css_main_url}}">
<link rel="stylesheet" href="{{.css_heroscript_url}}">
<meta name="color-scheme" content="light dark">
</head>
<body>
<nav class="navbar navbar-dark bg-dark fixed-top header px-2">
<div class="d-flex w-100 align-items-center justify-content-between">
<div class="text-white fw-bold">{{.title}}</div>
<div class="text-white-50">HeroScript Editor</div>
</div>
</nav>
<aside class="sidebar">
<div class="p-2">
<div class="menu-section">Navigation</div>
<div class="list-group list-group-flush">
{{.menu_html}}
</div>
</div>
</aside>
<main class="main">
<div class="container-fluid h-100">
<div class="d-flex align-items-center mb-3">
<h5 class="mb-0">HeroScript Editor</h5>
<div class="ms-auto">
<button id="run-script" class="btn btn-primary btn-sm me-2">
<i class="fas fa-play"></i> Run Script
</button>
<button id="clear-logs" class="btn btn-outline-secondary btn-sm me-2">
<i class="fas fa-trash"></i> Clear Logs
</button>
<button id="save-script" class="btn btn-success btn-sm">
<i class="fas fa-save"></i> Save
</button>
</div>
</div>
<div class="row h-100">
<!-- Editor Panel -->
<div class="col-lg-9 col-md-8 h-100">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Script Editor</h6>
<div class="editor-controls">
<select id="syntax-select" class="form-select form-select-sm">
<option value="javascript">JavaScript</option>
<option value="python">Python</option>
<option value="bash">Bash</option>
<option value="yaml">YAML</option>
<option value="json">JSON</option>
</select>
</div>
</div>
<div class="card-body p-0 h-100">
<div id="editor-container" class="h-100">
<textarea id="script-editor" class="form-control h-100"
placeholder="// Enter your HeroScript here...
// Welcome to HeroScript Editor!
console.log('Hello from HeroScript!');
// Example: Simple loop with logging
for (let i = 1; i <= 5; i++) {
console.log(`Step ${i}: Processing...`);
}
// Example: Conditional logic
if (new Date().getHours() < 12) {
console.log('Good morning!');
} else {
console.log('Good afternoon!');
}
console.log('Script execution completed!');
// Try editing this script and click 'Run Script' to see it in action
// Use Ctrl+Enter to run, Ctrl+S to save, Ctrl+L to clear logs"></textarea>
</div>
</div>
</div>
</div>
<!-- Logs Panel -->
<div class="col-lg-3 col-md-4 h-100">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Execution Logs</h6>
<div class="log-controls">
<span class="badge bg-success" id="connection-status">Connected</span>
<button id="auto-scroll" class="btn btn-outline-primary btn-sm ms-2" data-active="true">
<i class="fas fa-arrow-down"></i> Auto-scroll
</button>
</div>
</div>
<div class="card-body p-0 h-100">
<div id="logs-container" class="h-100 overflow-auto">
<div id="logs-content" class="p-3">
<div class="log-entry system">
<span class="timestamp">[${new Date().toISOString()}]</span>
<span class="level info">INFO</span>
<span class="message">HeroScript Editor initialized. Waiting for script execution...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Highlight.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js"></script>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<!-- Theme and HeroScript JS -->
<script src="{{.js_theme_url}}"></script>
<script src="{{.js_heroscript_url}}"></script>
</body>
</html>

View File

@@ -0,0 +1,621 @@
/**
* HeroScript Editor JavaScript
* Handles code editing, syntax highlighting, script execution, and real-time logging
*/
class HeroScriptEditor {
constructor() {
this.editor = null;
this.logsContainer = null;
this.autoScroll = true;
this.redisConnection = null;
this.currentSyntax = 'javascript';
this.init();
}
/**
* Initialize the HeroScript editor
*/
init() {
this.setupEditor();
this.setupLogging();
this.setupEventListeners();
this.connectToRedis();
this.applySyntaxHighlighting();
}
/**
* Setup the code editor
*/
setupEditor() {
this.editor = document.getElementById('script-editor');
this.logsContainer = document.getElementById('logs-content');
if (!this.editor || !this.logsContainer) {
console.error('HeroScript: Required elements not found');
return;
}
// Set initial content if empty
if (!this.editor.value.trim()) {
this.editor.value = this.getDefaultScript();
}
// Setup basic editor features
this.setupEditorFeatures();
}
/**
* Setup syntax highlighting
*/
setupEditorFeatures() {
// Handle syntax selection change
const syntaxSelect = document.getElementById('syntax-select');
if (syntaxSelect) {
syntaxSelect.addEventListener('change', (e) => {
this.currentSyntax = e.target.value;
this.addLogEntry('user', `Syntax changed to ${this.currentSyntax}`, 'info');
});
}
// Add tab support for better code editing
this.editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = this.editor.selectionStart;
const end = this.editor.selectionEnd;
// Insert tab character
this.editor.value = this.editor.value.substring(0, start) +
' ' +
this.editor.value.substring(end);
// Move cursor
this.editor.selectionStart = this.editor.selectionEnd = start + 4;
}
});
// Auto-resize editor based on content
this.editor.addEventListener('input', () => {
this.autoResizeEditor();
});
// Note: Scroll sync not needed with contenteditable approach
// Initial resize
this.autoResizeEditor();
}
/**
* Auto-resize editor to fit content
*/
autoResizeEditor() {
// Reset height to auto to get the correct scrollHeight
this.editor.style.height = 'auto';
// Set height based on content, with minimum height
const minHeight = 500;
const contentHeight = this.editor.scrollHeight;
this.editor.style.height = Math.max(minHeight, contentHeight) + 'px';
}
/**
* Apply syntax highlighting to the editor content
*/
applySyntaxHighlighting() {
// Replace textarea with a contenteditable div for better highlighting
this.replaceTextareaWithHighlightedEditor();
// Update highlighting when syntax changes
const syntaxSelect = document.getElementById('syntax-select');
if (syntaxSelect) {
syntaxSelect.addEventListener('change', () => {
this.currentSyntax = syntaxSelect.value;
this.updateHighlighting();
});
}
// Initial highlighting
this.updateHighlighting();
}
/**
* Replace textarea with a highlighted contenteditable div
*/
replaceTextareaWithHighlightedEditor() {
const container = document.getElementById('editor-container');
if (!container || !this.editor) return;
// Get current content
const content = this.editor.value || this.getDefaultScript();
// Create new highlighted editor
const highlightedEditor = document.createElement('pre');
highlightedEditor.id = 'highlighted-editor';
highlightedEditor.className = 'hljs';
highlightedEditor.style.cssText = `
margin: 0;
padding: 1.5rem;
border: none;
outline: none;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 16px;
line-height: 1.6;
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
min-height: 500px;
height: 100%;
width: 100%;
box-sizing: border-box;
resize: none;
`;
const codeElement = document.createElement('code');
codeElement.className = `language-${this.currentSyntax}`;
codeElement.contentEditable = 'true';
codeElement.textContent = content;
codeElement.style.cssText = `
display: block;
background: transparent !important;
padding: 0;
margin: 0;
outline: none;
border: none;
`;
highlightedEditor.appendChild(codeElement);
// Replace the textarea
this.editor.style.display = 'none';
container.appendChild(highlightedEditor);
// Update editor reference
this.highlightedEditor = highlightedEditor;
this.codeElement = codeElement;
// Add event listeners for the new editor
this.setupHighlightedEditorEvents();
}
/**
* Setup event listeners for the highlighted editor
*/
setupHighlightedEditorEvents() {
if (!this.codeElement) return;
// Update highlighting on input
this.codeElement.addEventListener('input', () => {
// Update the hidden textarea value
this.editor.value = this.codeElement.textContent;
// Debounce highlighting updates
clearTimeout(this.highlightTimeout);
this.highlightTimeout = setTimeout(() => {
this.updateHighlighting();
}, 300);
});
// Handle tab key for indentation
this.codeElement.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('insertText', false, ' ');
}
});
// Focus the new editor
this.codeElement.focus();
}
/**
* Update syntax highlighting
*/
updateHighlighting() {
if (!this.codeElement || !window.hljs) return;
const content = this.codeElement.textContent;
// Store cursor position
const selection = window.getSelection();
const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
const cursorOffset = range ? range.startOffset : 0;
// Update language class
this.codeElement.className = `language-${this.currentSyntax}`;
// Apply highlighting
window.hljs.highlightElement(this.codeElement);
// Restore cursor position
if (range && this.codeElement.firstChild) {
try {
const newRange = document.createRange();
const textNode = this.codeElement.firstChild;
const maxOffset = textNode.textContent ? textNode.textContent.length : 0;
newRange.setStart(textNode, Math.min(cursorOffset, maxOffset));
newRange.setEnd(textNode, Math.min(cursorOffset, maxOffset));
selection.removeAllRanges();
selection.addRange(newRange);
} catch (e) {
// Cursor restoration failed, that's okay
}
}
}
/**
* Setup logging system
*/
setupLogging() {
this.addLogEntry('system', 'HeroScript Editor initialized', 'info');
this.addLogEntry('system', 'Connecting to Redis queue: hero.gui.logs', 'info');
}
/**
* Setup event listeners
*/
setupEventListeners() {
// Run script button
const runButton = document.getElementById('run-script');
if (runButton) {
runButton.addEventListener('click', () => this.runScript());
}
// Clear logs button
const clearButton = document.getElementById('clear-logs');
if (clearButton) {
clearButton.addEventListener('click', () => this.clearLogs());
}
// Save script button
const saveButton = document.getElementById('save-script');
if (saveButton) {
saveButton.addEventListener('click', () => this.saveScript());
}
// Auto-scroll toggle
const autoScrollButton = document.getElementById('auto-scroll');
if (autoScrollButton) {
autoScrollButton.addEventListener('click', () => this.toggleAutoScroll());
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey)) {
switch (e.key) {
case 'Enter':
e.preventDefault();
this.runScript();
break;
case 's':
e.preventDefault();
this.saveScript();
break;
case 'l':
e.preventDefault();
this.clearLogs();
break;
}
}
});
}
/**
* Connect to Redis queue for real-time logging
*/
connectToRedis() {
// Simulate Redis connection with WebSocket or Server-Sent Events
// In a real implementation, you'd connect to a WebSocket endpoint
// that subscribes to the Redis queue
this.simulateRedisConnection();
}
/**
* Simulate Redis connection for demo purposes
*/
simulateRedisConnection() {
// Update connection status
this.updateConnectionStatus('connected');
// Generate demo logs with different categories
const logCategories = [
{ cat: 'system', messages: ['service started', 'health check ok', 'memory usage normal'], color: 'blue' },
{ cat: 'database', messages: ['connection established', 'query executed', 'backup completed'], color: 'green' },
{ cat: 'api', messages: ['request processed', 'rate limit applied', 'cache hit'], color: 'cyan' },
{ cat: 'security', messages: ['auth successful', 'token refreshed', 'access granted'], color: 'yellow' },
{ cat: 'error', messages: ['connection timeout', 'invalid request', 'service unavailable'], color: 'red' },
{ cat: 'warning', messages: ['high memory usage', 'slow query detected', 'retry attempt'], color: 'orange' }
];
// Generate logs every 2-5 seconds
setInterval(() => {
if (Math.random() < 0.7) { // 70% chance
const category = logCategories[Math.floor(Math.random() * logCategories.length)];
const message = category.messages[Math.floor(Math.random() * category.messages.length)];
this.addCompactLogEntry(category.cat, message, category.color);
}
}, Math.random() * 3000 + 2000); // 2-5 seconds
}
/**
* Update connection status indicator
*/
updateConnectionStatus(status) {
const statusElement = document.getElementById('connection-status');
if (!statusElement) return;
statusElement.className = 'badge';
switch (status) {
case 'connected':
statusElement.classList.add('bg-success');
statusElement.textContent = 'Connected';
break;
case 'connecting':
statusElement.classList.add('bg-warning');
statusElement.textContent = 'Connecting...';
break;
case 'disconnected':
statusElement.classList.add('bg-danger');
statusElement.textContent = 'Disconnected';
break;
}
}
/**
* Run the script in the editor
*/
async runScript() {
const script = this.editor.value.trim();
if (!script) {
this.addLogEntry('user', 'No script to execute', 'warning');
return;
}
this.addLogEntry('user', 'Starting script execution...', 'info');
try {
// Simulate script execution
await this.executeScript(script);
} catch (error) {
this.addLogEntry('user', `Script execution failed: ${error.message}`, 'error');
}
}
/**
* Execute the script (simulation)
*/
async executeScript(script) {
// Simulate script execution with delays
const lines = script.split('\n').filter(line => line.trim());
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line || line.startsWith('//')) continue;
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300));
// Simulate different types of output
if (line.includes('console.log')) {
const match = line.match(/console\.log\(['"`](.+?)['"`]\)/);
if (match) {
this.addLogEntry('script', match[1], 'info');
}
} else if (line.includes('error') || line.includes('Error')) {
this.addLogEntry('script', `Error in line: ${line}`, 'error');
} else if (line.includes('warn')) {
this.addLogEntry('script', `Warning in line: ${line}`, 'warning');
} else {
this.addLogEntry('script', `Executed: ${line}`, 'success');
}
}
this.addLogEntry('user', 'Script execution completed', 'success');
}
/**
* Save the current script
*/
saveScript() {
const script = this.editor.value;
// Simulate saving to server
this.addLogEntry('user', 'Saving script...', 'info');
setTimeout(() => {
// Save to localStorage for demo
localStorage.setItem('heroscript_content', script);
localStorage.setItem('heroscript_syntax', this.currentSyntax);
this.addLogEntry('user', 'Script saved successfully', 'success');
}, 500);
}
/**
* Load saved script
*/
loadScript() {
const savedScript = localStorage.getItem('heroscript_content');
const savedSyntax = localStorage.getItem('heroscript_syntax');
if (savedScript) {
this.editor.value = savedScript;
this.addLogEntry('user', 'Script loaded from storage', 'info');
}
if (savedSyntax) {
this.currentSyntax = savedSyntax;
const syntaxSelect = document.getElementById('syntax-select');
if (syntaxSelect) {
syntaxSelect.value = savedSyntax;
}
}
// Auto-resize after loading
this.autoResizeEditor();
}
/**
* Clear all logs
*/
clearLogs() {
if (this.logsContainer) {
this.logsContainer.innerHTML = '';
this.addLogEntry('user', 'Logs cleared', 'info');
}
}
/**
* Toggle auto-scroll functionality
*/
toggleAutoScroll() {
this.autoScroll = !this.autoScroll;
const button = document.getElementById('auto-scroll');
if (button) {
button.setAttribute('data-active', this.autoScroll.toString());
button.innerHTML = this.autoScroll
? '<i class="fas fa-arrow-down"></i> Auto-scroll'
: '<i class="fas fa-pause"></i> Manual';
}
this.addLogEntry('user', `Auto-scroll ${this.autoScroll ? 'enabled' : 'disabled'}`, 'info');
}
/**
* Add a compact log entry in the format: cat: loginfo
*/
addCompactLogEntry(category, message, color = 'blue') {
if (!this.logsContainer) return;
const logEntry = document.createElement('div');
logEntry.className = `log-entry compact new`;
logEntry.innerHTML = `
<span class="log-category" style="color: ${this.getLogColor(color)}">${this.escapeHtml(category)}:</span>
<span class="log-message">${this.escapeHtml(message)}</span>
`;
this.logsContainer.appendChild(logEntry);
// Remove 'new' class after animation
setTimeout(() => {
logEntry.classList.remove('new');
}, 300);
// Auto-scroll if enabled
if (this.autoScroll) {
this.scrollToBottom();
}
// Limit log entries to prevent memory issues
const maxEntries = 500;
const entries = this.logsContainer.children;
if (entries.length > maxEntries) {
for (let i = 0; i < entries.length - maxEntries; i++) {
entries[i].remove();
}
}
}
/**
* Add a log entry to the logs panel (legacy format for system messages)
*/
addLogEntry(source, message, level = 'info') {
// Use compact format for better display
const colorMap = {
'info': 'blue',
'success': 'green',
'warning': 'orange',
'error': 'red',
'debug': 'gray'
};
this.addCompactLogEntry(source, message, colorMap[level] || 'blue');
}
/**
* Get color value for log categories
*/
getLogColor(colorName) {
const colors = {
'red': '#ff4444',
'blue': '#4488ff',
'green': '#44ff44',
'yellow': '#ffff44',
'orange': '#ff8844',
'cyan': '#44ffff',
'purple': '#ff44ff',
'gray': '#888888'
};
return colors[colorName] || colors['blue'];
}
/**
* Scroll logs to bottom
*/
scrollToBottom() {
const container = document.getElementById('logs-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}
/**
* Escape HTML to prevent XSS
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Get default script content
*/
getDefaultScript() {
return `// Welcome to HeroScript Editor!
// This is a powerful script execution environment
console.log('Hello from HeroScript!');
// Example: Simple loop with logging
for (let i = 1; i <= 5; i++) {
console.log(\`Step \${i}: Processing...\`);
}
// Example: Conditional logic
if (new Date().getHours() < 12) {
console.log('Good morning!');
} else {
console.log('Good afternoon!');
}
console.log('Script execution completed!');
// Try editing this script and click "Run Script" to see it in action
// Use Ctrl+Enter to run, Ctrl+S to save, Ctrl+L to clear logs`;
}
}
/**
* Initialize HeroScript Editor when DOM is ready
*/
document.addEventListener('DOMContentLoaded', () => {
// Only initialize if we're on the HeroScript page
if (document.getElementById('script-editor')) {
window.heroScriptEditor = new HeroScriptEditor();
// Load any saved script
window.heroScriptEditor.loadScript();
console.log('HeroScript Editor initialized');
}
});
/**
* Export for external use
*/
if (typeof module !== 'undefined' && module.exports) {
module.exports = { HeroScriptEditor };
}

View File

@@ -0,0 +1,236 @@
/**
* Theme Management for Admin UI
* Handles light/dark theme switching with localStorage persistence
*/
class ThemeManager {
constructor() {
this.currentTheme = this.getStoredTheme() || this.getPreferredTheme();
this.init();
}
/**
* Initialize theme manager
*/
init() {
this.applyTheme(this.currentTheme);
this.createThemeToggle();
this.bindEvents();
}
/**
* Get theme from localStorage
*/
getStoredTheme() {
return localStorage.getItem('admin-theme');
}
/**
* Get user's preferred theme from system
*/
getPreferredTheme() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
/**
* Store theme preference
*/
setStoredTheme(theme) {
localStorage.setItem('admin-theme', theme);
}
/**
* Apply theme to document
*/
applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
this.currentTheme = theme;
this.setStoredTheme(theme);
this.updateToggleButton();
}
/**
* Toggle between light and dark themes
*/
toggleTheme() {
const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
this.applyTheme(newTheme);
}
/**
* Create theme toggle button
*/
createThemeToggle() {
const toggle = document.createElement('button');
toggle.className = 'theme-toggle';
toggle.id = 'theme-toggle';
toggle.setAttribute('aria-label', 'Toggle theme');
toggle.setAttribute('title', 'Toggle light/dark theme');
document.body.appendChild(toggle);
}
/**
* Update toggle button text and icon
*/
updateToggleButton() {
const toggle = document.getElementById('theme-toggle');
if (toggle) {
const icon = this.currentTheme === 'light' ? '🌙' : '☀️';
const text = this.currentTheme === 'light' ? 'Dark' : 'Light';
toggle.innerHTML = `${icon} ${text}`;
}
}
/**
* Bind event listeners
*/
bindEvents() {
// Theme toggle button click
document.addEventListener('click', (e) => {
if (e.target.id === 'theme-toggle') {
this.toggleTheme();
}
});
// Keyboard shortcut (Ctrl/Cmd + Shift + T)
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'T') {
e.preventDefault();
this.toggleTheme();
}
});
// Listen for system theme changes
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!this.getStoredTheme()) {
this.applyTheme(e.matches ? 'dark' : 'light');
}
});
}
}
/**
* Get current theme
*/
getCurrentTheme() {
return this.currentTheme;
}
/**
* Set specific theme
*/
setTheme(theme) {
if (theme === 'light' || theme === 'dark') {
this.applyTheme(theme);
}
}
}
/**
* Mobile Menu Management
*/
class MobileMenuManager {
constructor() {
this.init();
}
init() {
this.createMobileToggle();
this.bindEvents();
}
createMobileToggle() {
const toggle = document.createElement('button');
toggle.className = 'mobile-menu-toggle';
toggle.id = 'mobile-menu-toggle';
toggle.innerHTML = '☰ Menu';
toggle.setAttribute('aria-label', 'Toggle navigation menu');
document.body.appendChild(toggle);
}
bindEvents() {
document.addEventListener('click', (e) => {
if (e.target.id === 'mobile-menu-toggle') {
this.toggleMobileMenu();
}
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
const sidebar = document.querySelector('.sidebar');
const toggle = document.getElementById('mobile-menu-toggle');
if (sidebar && sidebar.classList.contains('show') &&
!sidebar.contains(e.target) &&
e.target !== toggle) {
this.closeMobileMenu();
}
});
// Close menu on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeMobileMenu();
}
});
}
toggleMobileMenu() {
const sidebar = document.querySelector('.sidebar');
if (sidebar) {
sidebar.classList.toggle('show');
}
}
closeMobileMenu() {
const sidebar = document.querySelector('.sidebar');
if (sidebar) {
sidebar.classList.remove('show');
}
}
}
/**
* Initialize when DOM is ready
*/
document.addEventListener('DOMContentLoaded', () => {
// Initialize theme manager
window.themeManager = new ThemeManager();
// Initialize mobile menu manager
window.mobileMenuManager = new MobileMenuManager();
// Add smooth scrolling to menu links
document.querySelectorAll('.menu-leaf a').forEach(link => {
link.addEventListener('click', (e) => {
// Close mobile menu when link is clicked
window.mobileMenuManager.closeMobileMenu();
});
});
// Enhance menu collapse animations
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(toggle => {
toggle.addEventListener('click', (e) => {
e.preventDefault();
const target = document.querySelector(toggle.getAttribute('href'));
if (target) {
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', !isExpanded);
target.classList.toggle('show');
}
});
});
});
/**
* Export for external use
*/
if (typeof module !== 'undefined' && module.exports) {
module.exports = { ThemeManager, MobileMenuManager };
}