191 lines
7.8 KiB
Plaintext
191 lines
7.8 KiB
Plaintext
<%#
|
|||
|
|
PULSE — LotusGuild Terminal Design System base layout.
|
||
|
|
|
||
|
|
Vendored from web_template/node/layout.ejs (bbec859) and extended for Pulse:
|
||
|
|
fixed comment delimiters, skip link, mobile drawer, theme button, command
|
||
|
|
palette, WS status, footer key hints and the keyboard-shortcuts modal.
|
||
|
|
Modelled on /root/code/gandalf/templates/base.html.
|
||
|
|
|
||
|
|
Rendered by lib/render.js renderPage(); the page view is pre-rendered into `body`.
|
||
|
|
|
||
|
|
Locals (all always provided by renderPage):
|
||
|
|
user { username, name, email, groups, isAdmin }
|
||
|
|
nonce CSP nonce string
|
||
|
|
appName APP_NAME || 'PULSE'
|
||
|
|
appSubtitle APP_SUBTITLE || 'Worker Orchestration // LotusGuild'
|
||
|
|
csrfToken string (currently '')
|
||
|
|
navLinks [{ href, key, label }]
|
||
|
|
pageTitle string
|
||
|
|
activeNav string, matches navLinks[].key
|
||
|
|
pageStyles [href]
|
||
|
|
pageScripts [src]
|
||
|
|
pageConfig object serialised to window.PULSE_CONFIG
|
||
|
|
assetVersion cache-busting string
|
||
|
|
body pre-rendered page HTML
|
||
|
|
%>
|
||
|
|
<!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 %></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?v=<%= assetVersion %>">
|
||
|
|
<!-- App-specific CSS (extends base, never overrides variables without good reason) -->
|
||
|
|
<link rel="stylesheet" href="/assets/app.css?v=<%= assetVersion %>">
|
||
|
|
|
||
|
|
<% (pageStyles || []).forEach(function (href) { %>
|
||
|
|
<link rel="stylesheet" href="<%= href %>?v=<%= assetVersion %>">
|
||
|
|
<% }); %>
|
||
|
|
|
||
|
|
<link rel="icon" href="/assets/favicon.png" type="image/png">
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
|
||
|
|
<a class="lt-skip-link" href="#main-content">Skip to main content</a>
|
||
|
|
|
||
|
|
<!-- BOOT OVERLAY -->
|
||
|
|
<div id="lt-boot" class="lt-boot-overlay"
|
||
|
|
data-app-name="<%= appName.toUpperCase() %>"
|
||
|
|
style="display:none" aria-hidden="true">
|
||
|
|
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- MOBILE NAV DRAWER -->
|
||
|
|
<div id="lt-nav-drawer" class="lt-nav-drawer" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Navigation menu">
|
||
|
|
<div class="lt-nav-drawer-header">
|
||
|
|
<span class="lt-brand-title"><%= appName.toUpperCase() %></span>
|
||
|
|
<button type="button" class="lt-nav-drawer-close" id="lt-nav-drawer-close" aria-label="Close navigation">✕</button>
|
||
|
|
</div>
|
||
|
|
<nav class="lt-nav-drawer-links" aria-label="Mobile navigation">
|
||
|
|
<% (navLinks || []).forEach(function (link) { %>
|
||
|
|
<a href="<%= link.href %>"
|
||
|
|
class="lt-nav-drawer-link<%= activeNav === link.key ? ' active' : '' %>"
|
||
|
|
<%- activeNav === link.key ? 'aria-current="page"' : '' %>><%= link.label %></a>
|
||
|
|
<% }); %>
|
||
|
|
</nav>
|
||
|
|
</div>
|
||
|
|
<div id="lt-nav-overlay" class="lt-nav-drawer-overlay"></div>
|
||
|
|
|
||
|
|
<!-- PRIMARY HEADER -->
|
||
|
|
<header class="lt-header" role="banner">
|
||
|
|
<div class="lt-header-left">
|
||
|
|
|
||
|
|
<!-- Hamburger (mobile) -->
|
||
|
|
<button type="button"
|
||
|
|
class="lt-menu-btn"
|
||
|
|
id="lt-menu-btn"
|
||
|
|
data-action="open-nav-drawer"
|
||
|
|
aria-label="Open navigation menu"
|
||
|
|
aria-expanded="false"
|
||
|
|
aria-controls="lt-nav-drawer">
|
||
|
|
<span class="lt-menu-btn-bar"></span>
|
||
|
|
<span class="lt-menu-btn-bar"></span>
|
||
|
|
<span class="lt-menu-btn-bar"></span>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<!-- Brand -->
|
||
|
|
<div class="lt-brand">
|
||
|
|
<a href="/" class="lt-brand-title lt-glitch"
|
||
|
|
data-text="<%= appName.toUpperCase() %>"
|
||
|
|
aria-label="<%= appName.toUpperCase() %> home"><%= appName.toUpperCase() %></a>
|
||
|
|
<span class="lt-brand-subtitle"><%= appSubtitle %></span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Desktop nav -->
|
||
|
|
<nav class="lt-nav" aria-label="Main navigation">
|
||
|
|
<% (navLinks || []).forEach(function (link) { %>
|
||
|
|
<a href="<%= link.href %>"
|
||
|
|
class="lt-nav-link<%= activeNav === link.key ? ' active' : '' %>"
|
||
|
|
<%- activeNav === link.key ? 'aria-current="page"' : '' %>><%= link.label %></a>
|
||
|
|
<% }); %>
|
||
|
|
</nav>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="lt-header-right">
|
||
|
|
|
||
|
|
<!-- WebSocket connection status -->
|
||
|
|
<div class="lt-ws-status" id="pulse-ws-status" data-state="connecting" aria-live="polite"><span class="lt-dot"></span><span>Connecting…</span></div>
|
||
|
|
|
||
|
|
<span id="pulse-last-refreshed" class="lt-text-xs lt-text-muted"></span>
|
||
|
|
|
||
|
|
<!-- ⌘K affordance -->
|
||
|
|
<button type="button"
|
||
|
|
class="lt-btn lt-btn-ghost lt-btn-sm lt-cmd-hint-btn"
|
||
|
|
data-action="open-cmdpalette"
|
||
|
|
title="Command palette (Ctrl+K)"
|
||
|
|
aria-label="Open command palette">⌕ K</button>
|
||
|
|
|
||
|
|
<button type="button" class="lt-theme-btn" id="lt-theme-btn"
|
||
|
|
aria-label="Toggle theme" title="Toggle light/dark mode">☀</button>
|
||
|
|
|
||
|
|
<% if (user && (user.name || user.username)) { %>
|
||
|
|
<span class="lt-header-user"><%= user.name || user.username %></span>
|
||
|
|
<% } %>
|
||
|
|
<% if (user && user.isAdmin) { %>
|
||
|
|
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
|
||
|
|
<% } %>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<%- include('partials/cmd-palette') %>
|
||
|
|
|
||
|
|
<!-- MAIN CONTENT -->
|
||
|
|
<main class="lt-main lt-container" id="main-content">
|
||
|
|
<%- body %>
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<!-- FOOTER -->
|
||
|
|
<footer class="lt-footer" role="contentinfo">
|
||
|
|
<nav class="lt-footer-hints" aria-label="Keyboard shortcuts">
|
||
|
|
<button type="button" class="lt-footer-hint" data-action="app:refresh"><span class="lt-footer-key">[ R ]</span> REFRESH</button>
|
||
|
|
<span class="lt-footer-sep">|</span>
|
||
|
|
<button type="button" class="lt-footer-hint" data-action="app:theme"><span class="lt-footer-key">[ T ]</span> THEME</button>
|
||
|
|
<span class="lt-footer-sep">|</span>
|
||
|
|
<button type="button" class="lt-footer-hint" data-action="open-cmdpalette"><span class="lt-footer-key">[ ^K ]</span> CMD</button>
|
||
|
|
<span class="lt-footer-sep">|</span>
|
||
|
|
<button type="button" class="lt-footer-hint" data-action="app:keys-help"><span class="lt-footer-key">[ ? ]</span> HELP</button>
|
||
|
|
</nav>
|
||
|
|
<span><%= appName.toUpperCase() %> — TDS v1.2</span>
|
||
|
|
</footer>
|
||
|
|
|
||
|
|
<%- include('partials/keys-help') %>
|
||
|
|
|
||
|
|
<!-- =========================================================
|
||
|
|
SCRIPTS — all tags carry the CSP nonce
|
||
|
|
========================================================= -->
|
||
|
|
|
||
|
|
<!-- Runtime globals (the only inline script on the page) -->
|
||
|
|
<script nonce="<%= nonce %>">
|
||
|
|
window.CSRF_TOKEN = <%- JSON.stringify(csrfToken || '').replace(/</g, '\\u003c') %>;
|
||
|
|
window.CURRENT_USER = {
|
||
|
|
username: <%- JSON.stringify((user && user.username) || '').replace(/</g, '\\u003c') %>,
|
||
|
|
name: <%- JSON.stringify((user && user.name) || '').replace(/</g, '\\u003c') %>,
|
||
|
|
email: <%- JSON.stringify((user && user.email) || '').replace(/</g, '\\u003c') %>,
|
||
|
|
groups: <%- JSON.stringify((user && user.groups) || []).replace(/</g, '\\u003c') %>,
|
||
|
|
isAdmin: <%= user && user.isAdmin ? 'true' : 'false' %>
|
||
|
|
};
|
||
|
|
window.PULSE_CONFIG = <%- JSON.stringify(pageConfig || {}).replace(/</g, '\\u003c') %>;
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<!-- Design system -->
|
||
|
|
<script nonce="<%= nonce %>" src="/web_template/base.js?v=<%= assetVersion %>"></script>
|
||
|
|
|
||
|
|
<!-- App JS -->
|
||
|
|
<script nonce="<%= nonce %>" src="/assets/app.js?v=<%= assetVersion %>"></script>
|
||
|
|
|
||
|
|
<% (pageScripts || []).forEach(function (src) { %>
|
||
|
|
<script nonce="<%= nonce %>" src="<%= src %>?v=<%= assetVersion %>"></script>
|
||
|
|
<% }); %>
|
||
|
|
|
||
|
|
</body>
|
||
|
|
</html>
|