147 lines
5.5 KiB
Plaintext
147 lines
5.5 KiB
Plaintext
|
|
<%–
|
|||
|
|
LOTUSGUILD TERMINAL DESIGN SYSTEM — Node.js / Express EJS Base Layout
|
|||
|
|
Extend this in every page template via res.render('page', { ... }).
|
|||
|
|
|
|||
|
|
Required Express setup (server.js / app.js):
|
|||
|
|
const { requireAuth, cspNonce, injectLocals } = require('./middleware');
|
|||
|
|
app.use(cspNonce);
|
|||
|
|
app.use(requireAuth);
|
|||
|
|
app.use(injectLocals);
|
|||
|
|
app.set('view engine', 'ejs');
|
|||
|
|
|
|||
|
|
Locals injected automatically by middleware.js:
|
|||
|
|
user { username, name, email, groups, isAdmin }
|
|||
|
|
nonce CSP nonce string
|
|||
|
|
appName process.env.APP_NAME
|
|||
|
|
appSubtitle process.env.APP_SUBTITLE
|
|||
|
|
|
|||
|
|
Locals to set per-route (or via a second res.locals middleware):
|
|||
|
|
pageTitle string — page <title> suffix
|
|||
|
|
activeNav string — must match a navLinks[].key
|
|||
|
|
navLinks array — navigation items:
|
|||
|
|
[{ href: '/path', key: 'mykey', label: 'My Page' }, ...]
|
|||
|
|
Dropdown:
|
|||
|
|
{ label: 'Admin', key: 'admin', adminOnly: true, children: [
|
|||
|
|
{ href: '/admin/users', label: 'Users' }
|
|||
|
|
]}
|
|||
|
|
pageStyles array — optional extra CSS hrefs
|
|||
|
|
pageScripts array — optional extra <script src> paths
|
|||
|
|
–%>
|
|||
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="en" data-theme="dark">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|||
|
|
<meta name="theme-color" content="#030508">
|
|||
|
|
<meta name="robots" content="noindex, nofollow">
|
|||
|
|
<title><%= pageTitle ? pageTitle + ' — ' : '' %><%= appName || 'LotusGuild' %></title>
|
|||
|
|
|
|||
|
|
<!-- Fonts -->
|
|||
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|||
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|||
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
|
|||
|
|
|
|||
|
|
<!-- Design system -->
|
|||
|
|
<link rel="stylesheet" href="/web_template/base.css">
|
|||
|
|
<!-- App-specific CSS (extends base, never overrides variables without good reason) -->
|
|||
|
|
<link rel="stylesheet" href="/assets/app.css">
|
|||
|
|
|
|||
|
|
<% if (typeof pageStyles !== 'undefined') { pageStyles.forEach(href => { %>
|
|||
|
|
<link rel="stylesheet" href="<%- href %>">
|
|||
|
|
<% }); } %>
|
|||
|
|
|
|||
|
|
<link rel="icon" href="/assets/favicon.png" type="image/png">
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
|
|||
|
|
<!-- Boot overlay -->
|
|||
|
|
<div id="lt-boot" class="lt-boot-overlay"
|
|||
|
|
data-app-name="<%= (appName || 'APP').toUpperCase() %>"
|
|||
|
|
style="display:none">
|
|||
|
|
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- =========================================================
|
|||
|
|
HEADER
|
|||
|
|
========================================================= -->
|
|||
|
|
<header class="lt-header">
|
|||
|
|
<div class="lt-header-left">
|
|||
|
|
|
|||
|
|
<div class="lt-brand">
|
|||
|
|
<a href="/" class="lt-brand-title" style="text-decoration:none">
|
|||
|
|
<%= (appName || 'APP').toUpperCase() %>
|
|||
|
|
</a>
|
|||
|
|
<span class="lt-brand-subtitle"><%= appSubtitle || 'LotusGuild Infrastructure' %></span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<nav class="lt-nav" aria-label="Main navigation">
|
|||
|
|
<% (navLinks || []).forEach(link => {
|
|||
|
|
if (link.adminOnly && !user.isAdmin) return;
|
|||
|
|
if (link.children) { %>
|
|||
|
|
<div class="lt-nav-dropdown">
|
|||
|
|
<a href="#" class="lt-nav-link <%= (activeNav || '').startsWith(link.key) ? 'active' : '' %>">
|
|||
|
|
<%= link.label %> ▾
|
|||
|
|
</a>
|
|||
|
|
<ul class="lt-nav-dropdown-menu">
|
|||
|
|
<% link.children.forEach(child => { %>
|
|||
|
|
<li><a href="<%= child.href %>"><%= child.label %></a></li>
|
|||
|
|
<% }); %>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
<% } else { %>
|
|||
|
|
<a href="<%= link.href %>"
|
|||
|
|
class="lt-nav-link <%= activeNav === link.key ? 'active' : '' %>">
|
|||
|
|
<%= link.label %>
|
|||
|
|
</a>
|
|||
|
|
<% } }); %>
|
|||
|
|
</nav>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="lt-header-right">
|
|||
|
|
<% if (user && (user.name || user.username)) { %>
|
|||
|
|
<span class="lt-header-user"><%= user.name || user.username %></span>
|
|||
|
|
<% } %>
|
|||
|
|
<% if (user && user.isAdmin) { %>
|
|||
|
|
<span class="lt-badge-admin">admin</span>
|
|||
|
|
<% } %>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<!-- =========================================================
|
|||
|
|
MAIN CONTENT — provided by the including view via <%- body %>
|
|||
|
|
or by structuring routes to render with this as a wrapper.
|
|||
|
|
========================================================= -->
|
|||
|
|
<main class="lt-main lt-container">
|
|||
|
|
<%- body %>
|
|||
|
|
</main>
|
|||
|
|
|
|||
|
|
<!-- =========================================================
|
|||
|
|
SCRIPTS — all tags carry the CSP nonce
|
|||
|
|
========================================================= -->
|
|||
|
|
|
|||
|
|
<!-- Runtime globals -->
|
|||
|
|
<script nonce="<%= nonce %>">
|
|||
|
|
window.CSRF_TOKEN = <%= JSON.stringify(csrfToken || '') %>;
|
|||
|
|
window.CURRENT_USER = {
|
|||
|
|
username: <%= JSON.stringify(user.username || '') %>,
|
|||
|
|
name: <%= JSON.stringify(user.name || '') %>,
|
|||
|
|
groups: <%= JSON.stringify(user.groups || []) %>,
|
|||
|
|
isAdmin: <%= user.isAdmin ? 'true' : 'false' %>,
|
|||
|
|
};
|
|||
|
|
// App-specific config: set window.APP_CONFIG in your route's inline script,
|
|||
|
|
// not here. This file is shared across all apps.
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<!-- Design system -->
|
|||
|
|
<script nonce="<%= nonce %>" src="/web_template/base.js"></script>
|
|||
|
|
|
|||
|
|
<!-- App JS -->
|
|||
|
|
<script nonce="<%= nonce %>" src="/assets/app.js"></script>
|
|||
|
|
|
|||
|
|
<% if (typeof pageScripts !== 'undefined') { pageScripts.forEach(src => { %>
|
|||
|
|
<script nonce="<%= nonce %>" src="<%- src %>"></script>
|
|||
|
|
<% }); } %>
|
|||
|
|
|
|||
|
|
</body>
|
|||
|
|
</html>
|