README.md: - Add 'Starting a New App' section with step-by-step instructions: nginx alias setup, which skeleton to copy, how to define nav for each framework, app.css pattern, lt.init() call - Update File Structure section: remove app-specific labels from framework skeletons (was 'PHP / Tinker Tickets' etc.), add layout.ejs to the node/ listing node/layout.ejs: - New EJS base layout skeleton matching the PHP/Python equivalents: generic nav via navLinks locals, lt-* class names throughout, CSP nonce on all script tags, pageStyles/pageScripts arrays, CURRENT_USER + CSRF_TOKEN globals injected at runtime Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -895,18 +895,110 @@ Set `data-theme="light"` on `<html>` directly. All component styles react throug
|
||||
|
||||
---
|
||||
|
||||
## Starting a New App
|
||||
|
||||
### 1. Serve the design system files
|
||||
|
||||
Add an nginx alias so every app on the same host can reference the same files:
|
||||
|
||||
```nginx
|
||||
# In each app's server block (or a shared include):
|
||||
location /web_template/ {
|
||||
alias /path/to/web_template/;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
```
|
||||
|
||||
Then in your HTML:
|
||||
```html
|
||||
<link rel="stylesheet" href="/web_template/base.css">
|
||||
<script src="/web_template/base.js"></script>
|
||||
```
|
||||
|
||||
### 2. Copy the right skeleton
|
||||
|
||||
| Stack | Copy this file | Into your app as |
|
||||
|-------|---------------|-----------------|
|
||||
| PHP | `php/layout.php` | `views/layout.php` (or `layout_header.php`) |
|
||||
| Python/Flask | `python/base.html` | `templates/base.html` |
|
||||
| Node/Express | `node/middleware.js` + `node/layout.ejs` | `middleware.js` + `views/layout.ejs` |
|
||||
|
||||
### 3. Define your nav
|
||||
|
||||
**PHP** — pass `$navLinks` before including the layout:
|
||||
```php
|
||||
$navLinks = [
|
||||
['href' => '/', 'key' => 'dashboard', 'label' => 'Dashboard'],
|
||||
['href' => '/reports', 'key' => 'reports', 'label' => 'Reports'],
|
||||
['label' => 'Admin', 'key' => 'admin', 'adminOnly' => true, 'children' => [
|
||||
['href' => '/admin/users', 'label' => 'Users'],
|
||||
]],
|
||||
];
|
||||
$activeNav = 'dashboard';
|
||||
include __DIR__ . '/views/layout.php';
|
||||
```
|
||||
|
||||
**Python/Flask** — inject via context processor or pass directly to `render_template`:
|
||||
```python
|
||||
nav_links = [
|
||||
{'href': url_for('index'), 'key': 'dashboard', 'label': 'Dashboard'},
|
||||
{'href': url_for('settings'), 'key': 'settings', 'label': 'Settings'},
|
||||
]
|
||||
return render_template('page.html', nav_links=nav_links)
|
||||
```
|
||||
|
||||
**Node/Express** — set on `res.locals` via `injectLocals` middleware:
|
||||
```js
|
||||
app.use((req, res, next) => {
|
||||
res.locals.navLinks = [
|
||||
{ href: '/', key: 'dashboard', label: 'Dashboard' },
|
||||
{ href: '/workers', key: 'workers', label: 'Workers' },
|
||||
];
|
||||
next();
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Add app-specific CSS
|
||||
|
||||
Create `app.css` in your app. Import nothing from `base.css` — just override tokens or add components:
|
||||
```css
|
||||
/* app.css — app-specific extensions only */
|
||||
:root {
|
||||
--app-accent: #FF6B00; /* override if needed */
|
||||
}
|
||||
/* Only put styles here that aren't already in base.css */
|
||||
```
|
||||
|
||||
### 5. Initialise
|
||||
|
||||
In your base template, after `base.js`:
|
||||
```html
|
||||
<script nonce="{{ nonce }}">
|
||||
lt.init({ bootName: 'MY APP' });
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
web_template/
|
||||
├── base.css Design system styles (79 sections, ~5,200 lines)
|
||||
├── base.js Design system JS (55+ modules, ~2,800 lines)
|
||||
├── base.html Living reference template
|
||||
├── base.html Living component reference — open in browser to browse everything
|
||||
├── README.md This file
|
||||
└── (framework skeletons)
|
||||
├── php/ PHP / Tinker Tickets
|
||||
├── python/ Flask / Jinja2 / GANDALF
|
||||
└── node/ Express / EJS / PULSE
|
||||
├── AUTHELIA_INTEGRATION.md Theming Authelia portal with this design system
|
||||
└── framework skeletons/
|
||||
├── php/
|
||||
│ └── layout.php Generic PHP base layout (pass $navLinks)
|
||||
├── python/
|
||||
│ ├── base.html Jinja2 base template (pass nav_links list)
|
||||
│ └── auth.py Authelia SSO helper for Flask
|
||||
└── node/
|
||||
├── middleware.js Express middleware (auth, CSRF, nonce, rate limit)
|
||||
└── layout.ejs EJS base template (uses res.locals.navLinks)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
<%–
|
||||
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>
|
||||
Reference in New Issue
Block a user