- Replace all CDN script tags (marked, plotly) with self-hosted static files - Add DOMPurify for sanitizing markdown-rendered HTML - Add escapeHtml() helper to base.html for all innerHTML operations - Sanitize dynamic data in innerHTML across 13 templates - Add security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy) - Add SSRF protection to proposal intake URL fetcher (block private/loopback IPs) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
255 lines
19 KiB
HTML
255 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" class="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}IETF Draft Analyzer{% endblock %}</title>
|
|
<script src="/static/js/tailwind.js"></script>
|
|
<script src="/static/js/purify.min.js"></script>
|
|
<link rel="stylesheet" href="/static/css/fonts.css">
|
|
<script>
|
|
// Global HTML escape helper for safe innerHTML usage
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
// Safe markdown rendering: marked + DOMPurify
|
|
function safeMarkdown(md) {
|
|
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
|
|
return DOMPurify.sanitize(marked.parse(md || ''));
|
|
}
|
|
return escapeHtml(md || '');
|
|
}
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ['Inter', 'system-ui', 'sans-serif'],
|
|
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'],
|
|
},
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
body { font-family: 'Inter', system-ui, sans-serif; }
|
|
.sidebar-link {
|
|
transition: all 0.15s ease;
|
|
}
|
|
.sidebar-link:hover, .sidebar-link.active {
|
|
background: rgba(59, 130, 246, 0.15);
|
|
color: #60a5fa;
|
|
}
|
|
.sidebar-link.active {
|
|
border-right: 3px solid #3b82f6;
|
|
}
|
|
.stat-card {
|
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.8), rgba(30, 41, 59, 0.4));
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
.score-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 9999px;
|
|
font-weight: 600;
|
|
font-size: 0.8rem;
|
|
}
|
|
.score-high { background: rgba(34, 197, 94, 0.2); color: #4ade80; }
|
|
.score-mid { background: rgba(234, 179, 8, 0.2); color: #facc15; }
|
|
.score-low { background: rgba(239, 68, 68, 0.2); color: #f87171; }
|
|
.dim-bar {
|
|
display: inline-block;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
background: #3b82f6;
|
|
vertical-align: middle;
|
|
}
|
|
/* Plotly dark overrides */
|
|
.js-plotly-plot .plotly .modebar { right: 8px !important; }
|
|
/* Scrollbar */
|
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
::-webkit-scrollbar-track { background: #1e293b; }
|
|
::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; }
|
|
::-webkit-scrollbar-thumb:hover { background: #64748b; }
|
|
/* Mobile sidebar */
|
|
@media (max-width: 768px) {
|
|
.sidebar { transform: translateX(-100%); }
|
|
.sidebar.open { transform: translateX(0); }
|
|
}
|
|
</style>
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body class="bg-slate-950 text-slate-100 min-h-screen">
|
|
<!-- Mobile menu button -->
|
|
<button id="menuBtn" class="md:hidden fixed top-4 left-4 z-50 p-2 bg-slate-800 rounded-lg border border-slate-700">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Sidebar -->
|
|
<aside id="sidebar" class="sidebar fixed top-0 left-0 h-full w-60 bg-slate-900 border-r border-slate-800 z-40 flex flex-col transition-transform md:translate-x-0">
|
|
<div class="p-5 border-b border-slate-800">
|
|
<h1 class="text-lg font-bold text-white tracking-tight">IETF Draft Analyzer</h1>
|
|
<p class="text-xs text-slate-500 mt-1">AI/Agent Standards Tracker</p>
|
|
</div>
|
|
<!-- Global Search -->
|
|
<div class="px-4 pt-4 pb-2">
|
|
<form action="/search" method="get" class="relative">
|
|
<input type="text" name="q" placeholder="Search everything..."
|
|
value="{{ request.args.get('q', '') if request else '' }}"
|
|
class="w-full bg-slate-800/60 border border-slate-700 rounded-lg pl-9 pr-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition">
|
|
<svg class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
</svg>
|
|
</form>
|
|
</div>
|
|
<nav class="flex-1 py-4 overflow-y-auto">
|
|
<a href="/" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'overview' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
|
|
Overview
|
|
</a>
|
|
<a href="/ask" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'ask' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
Ask
|
|
</a>
|
|
<a href="/drafts" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'drafts' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
Draft Explorer
|
|
</a>
|
|
<a href="/ratings" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'ratings' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/></svg>
|
|
Ratings
|
|
</a>
|
|
<a href="/ideas" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'ideas' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
|
Ideas
|
|
</a>
|
|
<a href="/idea-clusters" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'idea_clusters' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2z"/><circle cx="19" cy="19" r="3" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
|
Idea Clusters
|
|
</a>
|
|
{% if is_admin %}
|
|
<a href="/idea-analysis" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'idea_analysis' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
|
|
Idea Analysis
|
|
</a>
|
|
{% endif %}
|
|
<a href="/architecture" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'architecture' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
|
Architecture
|
|
</a>
|
|
{% if is_admin %}
|
|
<a href="/gaps" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'gaps' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
|
|
Gap Explorer
|
|
</a>
|
|
<a href="/proposals" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'proposals' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
|
Proposals
|
|
</a>
|
|
<a href="/blog" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'blog' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/></svg>
|
|
Blog Drafts
|
|
</a>
|
|
{% endif %}
|
|
<a href="/timeline" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'timeline' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
Timeline
|
|
</a>
|
|
{% if is_admin %}
|
|
<a href="/trends" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'trends' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
|
|
Trends
|
|
</a>
|
|
<a href="/complexity" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'complexity' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/></svg>
|
|
Complexity
|
|
</a>
|
|
<a href="/landscape" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'landscape' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
|
|
Landscape
|
|
</a>
|
|
<a href="/similarity" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'similarity' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/><circle cx="5" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="19" cy="18" r="1.5" fill="currentColor" stroke="none"/><circle cx="18" cy="6" r="1.5" fill="currentColor" stroke="none"/></svg>
|
|
Similarity
|
|
</a>
|
|
{% endif %}
|
|
<a href="/citations" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'citations' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
|
Citations
|
|
</a>
|
|
{% if is_admin %}
|
|
<a href="/sources" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'sources' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
|
|
Sources
|
|
</a>
|
|
<a href="/false-positives" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'false_positives' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg>
|
|
False Positives
|
|
</a>
|
|
{% endif %}
|
|
<a href="/authors" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'authors' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
|
Authors
|
|
</a>
|
|
{% if is_admin %}
|
|
<a href="/monitor" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'monitor' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z"/></svg>
|
|
Monitor
|
|
</a>
|
|
<a href="/admin/analytics" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'analytics' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
|
|
Analytics
|
|
</a>
|
|
{% endif %}
|
|
<div class="border-t border-slate-800 mt-4 pt-4">
|
|
<a href="/about" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'about' }}">
|
|
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
About
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
<div class="p-4 border-t border-slate-800 text-xs text-slate-600 space-y-1">
|
|
<div>IETF Draft Analyzer v0.3</div>
|
|
<div class="flex gap-3">
|
|
<a href="/impressum" class="hover:text-slate-400 transition">Impressum</a>
|
|
<a href="/datenschutz" class="hover:text-slate-400 transition">Datenschutz</a>
|
|
</div>
|
|
{% if is_admin %}
|
|
<div class="flex items-center gap-2 mt-1">
|
|
<span class="inline-block w-1.5 h-1.5 rounded-full bg-green-500"></span>
|
|
<span class="text-green-500">Admin</span>
|
|
<a href="/admin/logout" class="text-slate-600 hover:text-red-400 transition ml-auto">Logout</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main content -->
|
|
<main class="md:ml-60 min-h-screen">
|
|
<div class="p-6 md:p-8 max-w-7xl mx-auto">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Mobile sidebar toggle
|
|
const menuBtn = document.getElementById('menuBtn');
|
|
const sidebar = document.getElementById('sidebar');
|
|
menuBtn?.addEventListener('click', () => sidebar.classList.toggle('open'));
|
|
// Close on click outside
|
|
document.addEventListener('click', (e) => {
|
|
if (window.innerWidth < 768 && sidebar.classList.contains('open') &&
|
|
!sidebar.contains(e.target) && !menuBtn.contains(e.target)) {
|
|
sidebar.classList.remove('open');
|
|
}
|
|
});
|
|
</script>
|
|
{% block extra_scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|