§10 - one menu everywhere (buildPluginChrome) + shell on every page; instructional starter dashboard; Kratos-native email docs
Collapse the three nav builders into buildPluginChrome: chrome.bestHref does longest-prefix matching so deep admin routes mark their leaf. Delete adminNav; buildConfirmModel and the admin model builders take the resolved nav. The same role-filtered sidebar now renders signed in or out, collapsing to a burger on narrow screens. shell.ejs gains menu (default true; menu:false -> single-column .app-bare), docTitle (separate <title> from the topbar, so the body keeps the single <h1>), and hideSignIn (suppress the footer Sign-in on auth pages to avoid a login loop). auth/home/landing now render inside the shell. Dashboard is a replaceable instructional starter (definePlugin snippet, no mock data). Email stays delegated to Kratos: documented its built-in courier.template_override_path instead of adding web-side SMTP.
This commit is contained in:
+31
-38
@@ -1,40 +1,33 @@
|
||||
<%#
|
||||
Themed Kratos self-service page (todo §4): sign-in / register / reset / verify / settings.
|
||||
Renders a FlowView (src/flow-view.ts) into the html-css-foundation auth layout, reusing the
|
||||
auth-card + field partials. The form posts straight to flow.ui.action — Kratos owns its CSRF.
|
||||
Auto theme follows the OS (styles.css), so no theme switch is shown here.
|
||||
Themed Kratos self-service page (todo §4) inside the unified app shell (§10): sign-in / register /
|
||||
reset / verify / settings. Renders a FlowView (src/flow-view.ts) into the shell content, reusing the
|
||||
auth-card + field partials. The form posts straight to flow.ui.action — Kratos owns its CSRF. The
|
||||
shell's menu is role-filtered (anonymous ⇒ public items + Sign in); the topbar carries no heading,
|
||||
so the card's own <h1> is the page's single heading. Data: chrome (PageChrome), flow (FlowView).
|
||||
%><%
|
||||
const brand = locals.brand || "Plainpages";
|
||||
const body = include("partials/flow-body", { flow });
|
||||
%><!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title><%= flow.title %></title>
|
||||
<link rel="stylesheet" href="/public/css/styles.css" />
|
||||
<link rel="stylesheet" href="/public/css/auth.css" />
|
||||
<link rel="icon" href="/public/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<%- include("partials/icons") %>
|
||||
<main class="auth-stage">
|
||||
<div class="auth">
|
||||
<div class="auth-brand">
|
||||
<span class="brand-mark"><svg class="ico ico-sm"><use href="#i-box" /></svg></span>
|
||||
<span class="brand-name"><%= brand %></span>
|
||||
</div>
|
||||
<%- include("partials/auth-card", {
|
||||
action: flow.action,
|
||||
alt: flow.alt,
|
||||
back: flow.back,
|
||||
body,
|
||||
method: flow.method,
|
||||
sso: { providers: flow.sso },
|
||||
sub: flow.sub,
|
||||
title: flow.title,
|
||||
}) %>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
const nav = include("partials/nav-tree", { nodes: chrome.nav });
|
||||
const card = include("partials/auth-card", {
|
||||
action: flow.action,
|
||||
alt: flow.alt,
|
||||
back: flow.back,
|
||||
body: include("partials/flow-body", { flow }),
|
||||
method: flow.method,
|
||||
sso: { providers: flow.sso },
|
||||
sub: flow.sub,
|
||||
title: flow.title,
|
||||
});
|
||||
const body = `<div class="shell-auth"><div class="auth">${card}</div></div>`;
|
||||
-%>
|
||||
<%- include("partials/shell", {
|
||||
body,
|
||||
brand: chrome.brand,
|
||||
csrfToken: chrome.csrfToken,
|
||||
docTitle: flow.title,
|
||||
hideSignIn: true,
|
||||
nav,
|
||||
signInHref: chrome.signInHref,
|
||||
styles: ["/public/css/auth.css"],
|
||||
theme: chrome.theme,
|
||||
title: "",
|
||||
user: chrome.user,
|
||||
}) %>
|
||||
|
||||
+19
-41
@@ -1,43 +1,21 @@
|
||||
<%#
|
||||
Public landing page (todo §10): the ungated "/", what an anonymous visitor sees. A brief intro to
|
||||
the product and prominent paths to sign in / register — or to the app dashboard when already signed
|
||||
in. Standalone (no app shell — the sidebar/menu belong to the signed-in app). A plugin may replace
|
||||
this via its `home` handler. Auto theme follows the OS (styles.css). Data: brand, user (or null).
|
||||
Public landing page (todo §10): the ungated "/", rendered inside the unified app shell so the menu
|
||||
shows (role-filtered — anonymous ⇒ public items + Sign in). A brief intro + a prominent way in, or a
|
||||
dashboard link when already signed in. A plugin may replace this via its `home` handler.
|
||||
Data: chrome (PageChrome), user (or null).
|
||||
%><%
|
||||
const brand = locals.brand || "Plainpages";
|
||||
%><!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title><%= brand %></title>
|
||||
<link rel="stylesheet" href="/public/css/styles.css" />
|
||||
<link rel="stylesheet" href="/public/css/auth.css" />
|
||||
<link rel="icon" href="/public/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<%- include("partials/icons") %>
|
||||
<main class="auth-stage">
|
||||
<div class="landing">
|
||||
<div class="auth-brand">
|
||||
<span class="brand-mark"><svg class="ico ico-sm"><use href="#i-box" /></svg></span>
|
||||
<span class="brand-name"><%= brand %></span>
|
||||
</div>
|
||||
<h1 class="landing-title">Operational web apps, without the boilerplate.</h1>
|
||||
<p class="landing-lead">
|
||||
<%= brand %> is a self-hostable foundation for admin and operational UIs — sign-in,
|
||||
a config-driven menu, and a server-rendered, zero-JS design system. You add the
|
||||
domain-specific screens by dropping in plugin folders.
|
||||
</p>
|
||||
<div class="landing-actions">
|
||||
<% if (locals.user) { %>
|
||||
<a class="btn btn-primary" href="/dashboard">Go to your dashboard</a>
|
||||
<% } else { %>
|
||||
<a class="btn btn-primary" href="/login">Log in</a>
|
||||
<a class="btn" href="/registration">Create account</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
const nav = include("partials/nav-tree", { nodes: chrome.nav });
|
||||
const body = include("partials/landing-body", { brand: chrome.brand.name, signedIn: !!locals.user });
|
||||
-%>
|
||||
<%- include("partials/shell", {
|
||||
body,
|
||||
brand: chrome.brand,
|
||||
csrfToken: chrome.csrfToken,
|
||||
docTitle: chrome.brand.name,
|
||||
nav,
|
||||
signInHref: chrome.signInHref,
|
||||
styles: ["/public/css/auth.css"],
|
||||
theme: chrome.theme,
|
||||
title: "",
|
||||
user: chrome.user,
|
||||
}) %>
|
||||
|
||||
+27
-15
@@ -1,27 +1,39 @@
|
||||
<%#
|
||||
Dashboard (todo §1): the app-shell "People" list. Composes the building-block partials around
|
||||
the shell using the view model from src/dashboard.ts. EJS `include()` returns the rendered
|
||||
string, so each partial is captured and handed to the shell as a slot. The filter form,
|
||||
sortable headers and pager are real GET links — q/sort/page round-trip the URL, zero-JS.
|
||||
Dashboard (todo §10): the gated post-login app home. The built-in default is a short instructional
|
||||
starter — what /dashboard is and how to replace it from a plugin. Composes the app shell (the one
|
||||
global menu via model.nav) around the prose. A plugin owns the real dashboard via a `dashboard`
|
||||
handler; this renders until then. Data: model { nav, shell }.
|
||||
%><%
|
||||
const nav = include("partials/nav-tree", { nodes: model.nav });
|
||||
// The default home is a demo over mock data (its rows/actions/links are illustrative, not wired) —
|
||||
// say so up front so the inert affordances read as a starter to replace, not a half-built product.
|
||||
const note = include("partials/alert", { text: "The built-in demo home over mock data. Replace it by exporting a dashboard handler from a plugin.", title: "Starter dashboard" });
|
||||
const filters = include("partials/filter-bar", model.filterBar);
|
||||
const table = include("partials/data-table", model.table);
|
||||
const pager = include("partials/pagination", model.pagination);
|
||||
const actions =
|
||||
'<button class="btn"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-download"/></svg>Export</button>' +
|
||||
'<button class="btn btn-primary"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-plus"/></svg>Add person</button>';
|
||||
const body = `
|
||||
<div class="form-page">
|
||||
<section class="form-card">
|
||||
<h2 class="card-title">Starter dashboard</h2>
|
||||
<p>This is the built-in <code>/dashboard</code> — the gated home shown to a signed-in user.
|
||||
It's a placeholder so a fresh clone has something here; it holds no real data.</p>
|
||||
<p>Replace it from a plugin: export a <code>dashboard</code> handler from your plugin's
|
||||
manifest and it owns this page, rendered against your own views with the native app shell
|
||||
(the same menu you see now) via <code>ctx.chrome</code>.</p>
|
||||
<pre class="code-block"><code>export default definePlugin({
|
||||
apiVersion: "1.0.0",
|
||||
// view names plugins/<id>/views/<view>.ejs, rendered in this same shell
|
||||
dashboard: (ctx) => ({ view: "dashboard", data: { /* … */ } }),
|
||||
});</code></pre>
|
||||
<p>See the plugin contract in <code>docs/plugin-contract.md</code> (the landing-pages section)
|
||||
and the bundled <code>plugins/scheduling/</code> reference.</p>
|
||||
<div class="form-actions">
|
||||
<a class="btn btn-primary" href="/scheduling"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-grid"/></svg>Browse the example plugin</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>`;
|
||||
-%>
|
||||
<%- include("partials/shell", {
|
||||
actions,
|
||||
body: note + filters + table + pager,
|
||||
body,
|
||||
brand: model.shell.brand,
|
||||
breadcrumbs: model.shell.breadcrumbs,
|
||||
csrfToken: model.shell.csrfToken,
|
||||
nav,
|
||||
signInHref: model.shell.signInHref,
|
||||
theme: model.shell.theme,
|
||||
title: model.shell.title,
|
||||
user: model.shell.user,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<%#
|
||||
Public landing body (§10): hero + intro + a prominent way in (or a dashboard link when signed in).
|
||||
Rendered into the app-shell content. Locals: brand (name), signedIn (bool).
|
||||
%><div class="shell-auth">
|
||||
<div class="landing">
|
||||
<h1 class="landing-title">Operational web apps, without the boilerplate.</h1>
|
||||
<p class="landing-lead"><%= brand %> is a self-hostable foundation for admin and operational UIs — sign-in, a config-driven menu, and a server-rendered, zero-JS design system. You add the domain-specific screens by dropping in plugin folders.</p>
|
||||
<div class="landing-actions">
|
||||
<% if (signedIn) { %><a class="btn btn-primary" href="/dashboard">Go to your dashboard</a>
|
||||
<% } else { %><a class="btn btn-primary" href="/login">Log in</a><a class="btn" href="/registration">Create account</a><% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+24
-13
@@ -1,15 +1,21 @@
|
||||
<%#
|
||||
App shell: sidebar (brand + nav slot + footer) · topbar · content slot.
|
||||
App shell: sidebar (brand + nav slot + footer) · topbar · content slot. The one chrome every page
|
||||
renders (§10) — dashboard, admin, plugin, login/registration/front — so the menu is identical
|
||||
everywhere, role-filtered to the visitor (anonymous ⇒ public items + Sign in).
|
||||
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.
|
||||
extra stylesheet hrefs (e.g. a plugin's own /public/<id>/x.css). Text locals: `title`
|
||||
(topbar heading; empty ⇒ the page body owns the single <h1>, e.g. login/landing), `docTitle`
|
||||
(the <title> tag; defaults to title or the brand), `brand` ({ name, logo?, sub? }), `theme`
|
||||
(theme-switch default), `user`, `breadcrumbs`, `csrfToken` (the Sign-out form's hidden field),
|
||||
`signInHref` (anonymous "Sign in" target; default /login). `menu` (default true) — set false to
|
||||
drop the sidebar and render a focused single-column page (§10).
|
||||
%><%
|
||||
const title = locals.title || "Plainpages";
|
||||
const brand = locals.brand || { name: "Plainpages" };
|
||||
const title = locals.title || ""; // topbar heading; empty ⇒ no topbar <h1> (the body owns it)
|
||||
const docTitle = locals.docTitle || title || brand.name;
|
||||
const menu = locals.menu !== false; // sidebar shown by default; a page may opt out
|
||||
const hideSignIn = locals.hideSignIn === true; // the auth pages are already a way in — no footer Sign-in there
|
||||
const user = locals.user || { name: "Guest", initials: "G", email: "" };
|
||||
const breadcrumbs = locals.breadcrumbs || [];
|
||||
const nav = locals.nav || "";
|
||||
@@ -21,7 +27,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title><%= title %></title>
|
||||
<title><%= docTitle %></title>
|
||||
<link rel="stylesheet" href="/public/css/styles.css" />
|
||||
<% styles.forEach((href) => { %><link rel="stylesheet" href="<%= href %>" />
|
||||
<% }) %><link rel="icon" href="/public/favicon.svg" />
|
||||
@@ -29,10 +35,13 @@
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to content</a>
|
||||
<%- include("icons") %>
|
||||
<% if (menu) { %>
|
||||
<!-- nav-toggle drives the mobile overlay (pure CSS) -->
|
||||
<input type="checkbox" id="nav-toggle" aria-hidden="true" tabindex="-1" />
|
||||
<% } %>
|
||||
|
||||
<div class="app">
|
||||
<div class="app<%= menu ? "" : " app-bare" %>">
|
||||
<% if (menu) { %>
|
||||
<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><% } %>
|
||||
@@ -66,9 +75,10 @@
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<% } else { %>
|
||||
<% } else if (!hideSignIn) { %>
|
||||
<%# 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. %>
|
||||
signInHref carries this page as return_to (chrome.signInHref); falls back to bare /login.
|
||||
Suppressed on the auth pages themselves (hideSignIn) — a Sign-in there only loops back. %>
|
||||
<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>
|
||||
<% } %>
|
||||
|
||||
@@ -83,11 +93,12 @@
|
||||
|
||||
<!-- 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 (menu) { %><label class="btn icon-btn hamburger" for="nav-toggle" aria-label="Open menu"><svg class="ico"><use href="#i-menu" /></svg></label><% } %>
|
||||
<% if (title) { %><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><% } %><% }) %>
|
||||
|
||||
Reference in New Issue
Block a user