Files

191 lines
7.8 KiB
Plaintext
Raw Permalink Normal View History

<%#
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">&#x2715;</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">&#x2315;&nbsp;K</button>
<button type="button" class="lt-theme-btn" id="lt-theme-btn"
aria-label="Toggle theme" title="Toggle light/dark mode">&#x2600;</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() %> &mdash; 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>