feat(ui): TDS migration stage 1 — EJS layout, CSP, vendored design system, shared runtime
- Vendor web_template base.css/base.js (v1.2 bbec859) under public/web_template with sync script - Add views/layout.ejs (fixed upstream EJS comment delimiters, mobile drawer, cmd palette, keys help, WS status, theme toggle) and lib/render.js two-step renderer - Add page routes for /, /workers, /workflows, /executions, /quick, /scheduler (stub views) - helmet with strict nonce CSP (script-src-attr 'none'), /csp-report, report-only toggle - lib/pageauth.js: shared users upsert + HTML 401/403 for page routes - PULSE_DEV_READONLY guard disabling background writers for local testing - public/assets/app.js shared runtime (Pulse namespace, delegated actions, WS singleton, themed confirm modal) and app.css - eslint globals for browser files; ignore vendored assets Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<%#
|
||||
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>
|
||||
@@ -0,0 +1,18 @@
|
||||
<%#
|
||||
Placeholder page view. Rendered by lib/render.js when views/pages/<view>.ejs
|
||||
does not exist yet, so the shell can be exercised before the page work
|
||||
packages land.
|
||||
%>
|
||||
<div class="lt-page-header">
|
||||
<h1 class="lt-page-title"><%= pageTitle || 'Page' %></h1>
|
||||
</div>
|
||||
<div class="lt-frame">
|
||||
<div class="lt-empty-state">
|
||||
<div class="lt-empty-state-icon">⚙</div>
|
||||
<div class="lt-empty-state-title">Page under construction</div>
|
||||
<div class="lt-empty-state-text">
|
||||
This view has not been implemented yet. The shared shell, navigation and
|
||||
assets are live; the page content lands with its work package.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
<%#
|
||||
Dashboard page (WP-C). Stats + recent manual executions + workers summary.
|
||||
All data loaded client-side by /assets/pages/dashboard.js.
|
||||
%>
|
||||
<%- include('../partials/page-header', { title: 'Dashboard', subtitle: 'System overview', actions: '' }) %>
|
||||
|
||||
<div class="lt-stats-grid">
|
||||
<div class="lt-stat-card" id="dash-stat-total" role="button" tabindex="0" data-action="dash:goto-workers" aria-label="Total workers">
|
||||
<span class="lt-stat-icon lt-text-cyan" aria-hidden="true">◉</span>
|
||||
<div class="lt-stat-info">
|
||||
<span class="lt-stat-value" id="dash-stat-total-val">0</span>
|
||||
<span class="lt-stat-label">Workers</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-stat-card" id="dash-stat-online" role="button" tabindex="0" data-action="dash:goto-workers" aria-label="Online workers">
|
||||
<span class="lt-stat-icon lt-text-green" aria-hidden="true">●</span>
|
||||
<div class="lt-stat-info">
|
||||
<span class="lt-stat-value" id="dash-stat-online-val">0</span>
|
||||
<span class="lt-stat-label">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-stat-card" id="dash-stat-offline" role="button" tabindex="0" data-action="dash:goto-workers" aria-label="Offline workers">
|
||||
<span class="lt-stat-icon lt-text-red" aria-hidden="true">○</span>
|
||||
<div class="lt-stat-info">
|
||||
<span class="lt-stat-value" id="dash-stat-offline-val">0</span>
|
||||
<span class="lt-stat-label">Offline</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-stat-card" id="dash-stat-running" role="button" tabindex="0" data-action="dash:goto-executions" aria-label="Running executions">
|
||||
<span class="lt-stat-icon lt-text-amber" aria-hidden="true">▸</span>
|
||||
<div class="lt-stat-info">
|
||||
<span class="lt-stat-value" id="dash-stat-running-val">0</span>
|
||||
<span class="lt-stat-label">Running</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lt-frame">
|
||||
<div class="lt-section-header">Recent Executions</div>
|
||||
<div id="dash-executions-wrap">
|
||||
<div class="lt-empty-state lt-empty-state--sm" id="dash-executions-loading">
|
||||
<div class="lt-empty-state-title">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lt-frame">
|
||||
<div class="lt-section-header">Workers</div>
|
||||
<div id="dash-workers-wrap">
|
||||
<div class="lt-empty-state lt-empty-state--sm" id="dash-workers-loading">
|
||||
<div class="lt-empty-state-title">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<%#
|
||||
Workers page (WP-C). Grid of worker cards, patched in place on worker_update.
|
||||
%>
|
||||
<%- include('../partials/page-header', {
|
||||
title: 'Workers',
|
||||
subtitle: 'Connected worker agents',
|
||||
actions: '<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="app:refresh">↻ Refresh</button>'
|
||||
}) %>
|
||||
|
||||
<div id="wk-grid" class="wk-grid">
|
||||
<div class="lt-empty-state" id="wk-loading">
|
||||
<div class="lt-empty-state-title">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,125 @@
|
||||
<%#
|
||||
Workflows page (WP-D).
|
||||
Locals: user, nonce, pageTitle, activeNav, assetVersion, pageConfig.
|
||||
All data comes from /api/workflows*; pageConfig only carries the example
|
||||
definition JSON used to pre-fill the Create modal.
|
||||
%>
|
||||
<%- include('../partials/page-header', {
|
||||
title: 'Workflows',
|
||||
subtitle: 'Reusable multi-step jobs run against one or more workers.',
|
||||
actions:
|
||||
'<button type="button" class="lt-btn lt-btn-secondary" data-action="app:refresh">Refresh</button>' +
|
||||
'<button type="button" class="lt-btn lt-btn-primary" data-action="wf:create-open">+ Create Workflow</button>'
|
||||
}) %>
|
||||
|
||||
<div class="lt-frame">
|
||||
<div class="lt-section-header">Workflows</div>
|
||||
<div class="lt-section-body" id="wf-list">
|
||||
<div class="lt-empty-state">
|
||||
<div class="lt-empty-state-icon">⚙</div>
|
||||
<div class="lt-empty-state-title">Loading workflows…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Workflow Modal -->
|
||||
<div class="lt-modal-overlay" id="wf-create-modal" aria-hidden="true" role="dialog"
|
||||
aria-modal="true" aria-labelledby="wf-create-modal-title">
|
||||
<div class="lt-modal lt-modal-lg">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="wf-create-modal-title">Create Workflow</span>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="wf-create-form" data-submit-action="wf:create-submit">
|
||||
<div class="lt-modal-body">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="wf-create-name">Name</label>
|
||||
<input type="text" id="wf-create-name" name="name" class="lt-input" required placeholder="Workflow Name" autocomplete="off">
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="wf-create-description">Description</label>
|
||||
<textarea id="wf-create-description" name="description" class="lt-textarea" placeholder="Description"></textarea>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="wf-create-definition">Definition (JSON)</label>
|
||||
<textarea id="wf-create-definition" name="definition" class="lt-textarea wf-json-textarea" required></textarea>
|
||||
<span class="lt-field-hint">Steps run in order against the selected targets.</span>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="wf-create-webhook">Webhook URL (optional)</label>
|
||||
<input type="url" id="wf-create-webhook" name="webhook_url" class="lt-input" placeholder="https://example.com/webhook">
|
||||
</div>
|
||||
<div class="lt-field-error" id="wf-create-error" hidden></div>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button type="submit" class="lt-btn lt-btn-primary">Create</button>
|
||||
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Workflow Modal (admin) -->
|
||||
<div class="lt-modal-overlay" id="wf-edit-modal" aria-hidden="true" role="dialog"
|
||||
aria-modal="true" aria-labelledby="wf-edit-modal-title">
|
||||
<div class="lt-modal lt-modal-lg">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="wf-edit-modal-title">Edit Workflow</span>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="wf-edit-form" data-submit-action="wf:edit-submit">
|
||||
<input type="hidden" id="wf-edit-id" name="id">
|
||||
<div class="lt-modal-body">
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="wf-edit-name">Name</label>
|
||||
<input type="text" id="wf-edit-name" name="name" class="lt-input" required placeholder="Workflow Name" autocomplete="off">
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="wf-edit-description">Description</label>
|
||||
<textarea id="wf-edit-description" name="description" class="lt-textarea" placeholder="Description"></textarea>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="wf-edit-definition">Definition (JSON)</label>
|
||||
<textarea id="wf-edit-definition" name="definition" class="lt-textarea wf-json-textarea wf-json-textarea-lg" required></textarea>
|
||||
</div>
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="wf-edit-webhook">Webhook URL (optional)</label>
|
||||
<input type="url" id="wf-edit-webhook" name="webhook_url" class="lt-input" placeholder="https://example.com/webhook">
|
||||
</div>
|
||||
<div class="lt-field-error" id="wf-edit-error" hidden></div>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button type="submit" class="lt-btn lt-btn-primary">Save</button>
|
||||
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Run Workflow Modal (execute, with optional params + dry run) -->
|
||||
<div class="lt-modal-overlay" id="wf-run-modal" aria-hidden="true" role="dialog"
|
||||
aria-modal="true" aria-labelledby="wf-run-modal-title">
|
||||
<div class="lt-modal lt-modal-sm">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="wf-run-modal-title">Run Workflow</span>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="wf-run-form" data-submit-action="wf:param-submit">
|
||||
<input type="hidden" id="wf-run-workflow-id" name="workflow_id">
|
||||
<div class="lt-modal-body">
|
||||
<p class="lt-text-sm" id="wf-run-name"></p>
|
||||
<div id="wf-run-params"></div>
|
||||
<div class="lt-form-group">
|
||||
<label class="wf-checkbox-label">
|
||||
<input type="checkbox" id="wf-run-dryrun" class="lt-checkbox">
|
||||
Dry Run (simulate, no commands executed)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button type="submit" class="lt-btn lt-btn-primary">▶ Run</button>
|
||||
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!-- COMMAND PALETTE -->
|
||||
<div id="lt-cmd-overlay" class="lt-cmd-overlay" role="dialog" aria-modal="true" aria-label="Command palette" aria-hidden="true">
|
||||
<div class="lt-cmd-palette" id="lt-cmd-palette">
|
||||
<div class="lt-cmd-input-wrap">
|
||||
<span class="lt-cmd-prompt">></span>
|
||||
<input id="lt-cmd-input" class="lt-cmd-input" type="text"
|
||||
placeholder="Search commands…" autocomplete="off"
|
||||
spellcheck="false" aria-label="Search commands">
|
||||
</div>
|
||||
<div class="lt-cmd-results" id="lt-cmd-results">
|
||||
<div class="lt-cmd-empty">Start typing to search…</div>
|
||||
</div>
|
||||
<div class="lt-cmd-footer">
|
||||
<span><kbd>↑</kbd><kbd>↓</kbd> Navigate</span>
|
||||
<span><kbd>Enter</kbd> Select</span>
|
||||
<span><kbd>Esc</kbd> Close</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!-- KEYBOARD SHORTCUTS MODAL -->
|
||||
<div id="lt-keys-help" class="lt-modal-overlay" aria-hidden="true">
|
||||
<div class="lt-modal" role="dialog" aria-modal="true" aria-labelledby="lt-keys-help-title">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="lt-keys-help-title">Keyboard Shortcuts</span>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<div class="lt-modal-body">
|
||||
<table class="lt-table">
|
||||
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Ctrl / ⌘ + K</td><td>Command palette</td></tr>
|
||||
<tr><td>R</td><td>Refresh data</td></tr>
|
||||
<tr><td>?</td><td>Show this help</td></tr>
|
||||
<tr><td>ESC</td><td>Close modal / drawer / palette</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button type="button" class="lt-btn" data-modal-close>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<%#
|
||||
Page header partial.
|
||||
Locals: title (string), subtitle (optional string), actions (optional raw HTML).
|
||||
Usage from a page view:
|
||||
<%- include('../partials/page-header', { title: 'Workers', subtitle: '', actions: '' }) %>
|
||||
%>
|
||||
<div class="lt-page-header">
|
||||
<div>
|
||||
<h1 class="lt-page-title"><%= title %></h1>
|
||||
<% if (typeof subtitle !== 'undefined' && subtitle) { %>
|
||||
<p class="lt-page-subtitle"><%= subtitle %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (typeof actions !== 'undefined' && actions) { %>
|
||||
<div class="lt-page-actions"><%- actions %></div>
|
||||
<% } %>
|
||||
</div>
|
||||
Reference in New Issue
Block a user