/**
* Page Layout — shared HTML wrapper for all server-rendered pages
*
* Provides DOCTYPE, head, body, nav, identity script, and shared CSS.
* Each page passes title, content, and optional page-specific CSS.
*
* Usage:
* import { renderPage } from './page-layout.js'
* return renderPage('Deploy', deployContent, { pathPrefix, activePage: 'deploy', css: pageSpecificCss })
*/
import { renderIdentityScript } from './identity-ui.js'
import { renderNav } from './nav-ui.js'
/**
* Render a full HTML page
*
* @param {string} title — page title (appended with " - Pear Git")
* @param {string} content — page body HTML
* @param {Object} opts
* @param {string} opts.pathPrefix — URL prefix (e.g., '/git')
* @param {string} opts.activePage — active nav tab (e.g., 'repos', 'admin', 'deploy')
* @param {string} opts.css — page-specific CSS (inserted after shared styles)
* @param {string} opts.scripts — page-specific inline JS (inserted before </body>)
* @param {number} opts.maxWidth — container max-width in px (default: 1200)
* @returns {string} full HTML document
*/
export function renderPage (title, content, opts = {}) {
const pathPrefix = opts.pathPrefix || ''
const activePage = opts.activePage || ''
const pageCss = opts.css || ''
const pageScripts = opts.scripts || ''
const headExtra = opts.head || ''
const maxWidth = opts.maxWidth || 1200
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeForAttr(title)} - Pear Git</title>
${headExtra}
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117;
color: #c9d1d9;
padding: 20px;
}
a { color: #58a6ff; text-decoration: none; }
a:hover { text-decoration: underline; }
.container { max-width: ${maxWidth}px; margin: 0 auto; }
/* Shared component styles */
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
button {
background: #238636;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
button:hover { background: #2ea043; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
button.secondary {
background: #21262d;
border: 1px solid #30363d;
}
button.secondary:hover { background: #30363d; }
button.small { padding: 4px 10px; font-size: 12px; }
button.danger { background: #da3633; }
button.danger:hover { background: #f85149; }
.form-group { margin-bottom: 15px; }
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
font-size: 14px;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
max-width: 400px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
padding: 8px 12px;
color: #c9d1d9;
font-size: 14px;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: #58a6ff; }
.help-text { font-size: 12px; color: #8b949e; margin-top: 4px; }
.status-msg {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 15px;
font-size: 14px;
}
.status-msg.success { background: #0d2818; border: 1px solid #238636; color: #3fb950; }
.status-msg.error { background: #2d1117; border: 1px solid #da3633; color: #f85149; }
.status-msg.info { background: #0d1d32; border: 1px solid #1f6feb; color: #58a6ff; }
.spinner {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid #30363d;
border-top: 2px solid #58a6ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state { text-align: center; padding: 40px 20px; color: #8b949e; }
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 20px;
margin-bottom: 15px;
}
.data-table { width: 100%; border-collapse: collapse; }
.data-table th {
text-align: left; padding: 10px 15px;
background: #0d1117; border-bottom: 1px solid #30363d;
font-size: 12px; text-transform: uppercase; color: #8b949e;
}
.data-table td {
padding: 10px 15px; border-bottom: 1px solid #21262d; font-size: 13px;
}
.data-table tr:hover td { background: #0d1117; }
${pageCss}
</style>
</head>
<body>
${renderIdentityScript(pathPrefix)}
<div class="container">
${renderNav(pathPrefix, activePage)}
${content}
</div>
${pageScripts ? `<script>\n${pageScripts}\n</script>` : ''}
</body>
</html>`
}
function escapeForAttr (s) {
if (!s) return ''
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<')
}