...
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -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>
|
||||
124
lib/web/ui/templates/css/colors.css
Normal file
124
lib/web/ui/templates/css/colors.css
Normal 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); }
|
||||
409
lib/web/ui/templates/css/heroscript.css
Normal file
409
lib/web/ui/templates/css/heroscript.css
Normal 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;
|
||||
}
|
||||
279
lib/web/ui/templates/css/main.css
Normal file
279
lib/web/ui/templates/css/main.css
Normal 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;
|
||||
}
|
||||
}
|
||||
139
lib/web/ui/templates/heroscript_editor.html
Normal file
139
lib/web/ui/templates/heroscript_editor.html
Normal 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>
|
||||
621
lib/web/ui/templates/js/heroscript.js
Normal file
621
lib/web/ui/templates/js/heroscript.js
Normal 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 };
|
||||
}
|
||||
236
lib/web/ui/templates/js/theme.js
Normal file
236
lib/web/ui/templates/js/theme.js
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user