Files
plainpages/views/partials/shell.ejs

105 lines
5.3 KiB
Plaintext

<%#
App shell: sidebar (brand + nav slot + footer) · topbar · content slot.
Slots are pre-rendered HTML locals — `nav` (sidebar tree, see nav-tree partial),
`actions` (topbar buttons), `body` (page content); `styles` is an optional array of
extra stylesheet hrefs (e.g. a plugin's own /public/<id>/x.css). Text locals: `title`, `brand`
({ name, logo?, sub? } — logo image else the default mark), `theme` (default for the
theme-switch), `user`, `breadcrumbs`, `csrfToken` (the Sign-out POST form's hidden field),
`signInHref` (anonymous "Sign in" target, carries return_to; default /login).
Branding comes from config/menu.ts; `user`/`csrfToken` from §4 auth.
%><%
const title = locals.title || "Plainpages";
const brand = locals.brand || { name: "Plainpages" };
const user = locals.user || { name: "Guest", initials: "G", email: "" };
const breadcrumbs = locals.breadcrumbs || [];
const nav = locals.nav || "";
const actions = locals.actions || "";
const body = locals.body || "";
const styles = locals.styles || []; // extra per-page stylesheet hrefs (e.g. a plugin's own CSS)
%><!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><%= title %></title>
<link rel="stylesheet" href="/public/css/styles.css" />
<% styles.forEach((href) => { %><link rel="stylesheet" href="<%= href %>" />
<% }) %><link rel="icon" href="/public/favicon.svg" />
</head>
<body>
<a class="skip-link" href="#main-content">Skip to content</a>
<%- include("icons") %>
<!-- nav-toggle drives the mobile overlay (pure CSS) -->
<input type="checkbox" id="nav-toggle" aria-hidden="true" tabindex="-1" />
<div class="app">
<aside class="sidebar" aria-label="Primary">
<div class="brand">
<% if (brand.logo) { %><img class="brand-logo" src="<%= brand.logo %>" alt="" /><% } else { %><span class="brand-mark"><svg class="ico ico-sm"><use href="#i-box" /></svg></span><% } %>
<span class="brand-name"><%= brand.name %></span>
<% if (brand.sub) { %><span class="brand-sub"><%= brand.sub %></span><% } %>
</div>
<nav class="nav" aria-label="Main navigation"><%- nav %></nav>
<div class="side-footer">
<%- include("theme-switch", { value: locals.theme }) %>
<div class="footer-actions">
<% if (user.email) { %>
<%# signed in: profile menu inline (the summary composes escaped user values) %>
<details class="menu" style="flex:1 1 auto">
<summary class="profile">
<span class="avatar" aria-hidden="true"><%= user.initials %></span>
<span class="profile-meta">
<span class="profile-name"><%= user.name %></span>
<span class="profile-mail"><%= user.email %></span>
</span>
</summary>
<div class="menu-pop left up" style="min-width:220px">
<div class="menu-head">Signed in as <%= user.name %></div>
<button class="menu-item" type="button"><svg class="ico"><use href="#i-user" /></svg>Profile</button>
<%# Sign out is a state change → a POST form (not a GET link), CSRF-guarded by app.ts %>
<form class="menu-item-form" method="post" action="/logout">
<input type="hidden" name="_csrf" value="<%= locals.csrfToken || '' %>" />
<button class="menu-item danger" type="submit"><svg class="ico"><use href="#i-logout" /></svg>Sign out</button>
</form>
</div>
</details>
<% } else { %>
<%# anonymous (a public page in the shell, §10): no session to end — offer a way in instead.
signInHref carries this page as return_to (chrome.signInHref); falls back to bare /login. %>
<a class="btn btn-primary" href="<%= locals.signInHref || '/login' %>" style="flex:1 1 auto"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-user" /></svg>Sign in</a>
<% } %>
<%- include("menu", {
up: true,
trigger: { class: "btn icon-btn", label: "Settings", html: '<svg class="ico"><use href="#i-gear"/></svg>' },
items: [{ head: "Settings" }, { label: "Preferences", icon: "i-gear" }],
}) %>
</div>
</div>
</aside>
<!-- scrim closes the mobile menu (label toggles the checkbox) -->
<label class="scrim" for="nav-toggle" aria-label="Close menu"></label>
<main class="content" id="main-content">
<header class="topbar">
<label class="btn icon-btn hamburger" for="nav-toggle" aria-label="Open menu"><svg class="ico"><use href="#i-menu" /></svg></label>
<h1 class="page-title"><%= title %></h1>
<% if (breadcrumbs.length) { %>
<nav class="crumbs" aria-label="Breadcrumb">
<% breadcrumbs.forEach((c, i) => { %><% if (i) { %><span class="sep">/</span><% } %><% if (c.href) { %><a href="<%= c.href %>"><%= c.label %></a><% } else { %><span><%= c.label %></span><% } %><% }) %>
</nav>
<% } %>
<div class="topbar-spacer"></div>
<%- actions %>
</header>
<%- body %>
</main>
</div>
</body>
</html>