256 lines
8 KiB
Text
256 lines
8 KiB
Text
package ui
|
|
|
|
import appver "git.lohmar.co.uk/lexton-it/NextWks/core/version"
|
|
|
|
templ LauncherPage(userName string, role string, apps []AppTile) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
|
|
<title>Next Workspace</title>
|
|
<link rel="manifest" href="/static/manifest.json"/>
|
|
<meta name="theme-color" content="#1a1a2e"/>
|
|
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
|
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
<style>{ workspaceCSS() }</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
@topBar(userName, role)
|
|
<main class="content">
|
|
@greetingSection(userName)
|
|
@AppGrid(apps)
|
|
</main>
|
|
<footer class="footer">
|
|
<span>NextWks v{ appver.Version }</span>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- PWA Modal -->
|
|
<div id="pwa-overlay" class="overlay" style="display:none" onclick="closePWA()">
|
|
<div class="modal" onclick="event.stopPropagation()">
|
|
<div class="modal-head">
|
|
<h2>Install Next Workspace</h2>
|
|
<button class="close-btn" onclick="closePWA()">×</button>
|
|
</div>
|
|
<div class="modal-body" id="pwa-content">
|
|
@pwaInstructions()
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>{ launcherJS() }</script>
|
|
</body>
|
|
</html>
|
|
}
|
|
|
|
templ greetingSection(userName string) {
|
|
<div class="greeting">
|
|
<div class="avatar">{ initials(userName) }</div>
|
|
<div class="greeting-text">
|
|
<h1>Good { timeOfDay() }, { userName }</h1>
|
|
<p>Your workspace is ready</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ topBar(userName string, role string) {
|
|
<header class="topbar">
|
|
<div class="topbar-left">
|
|
<div class="logo-icon">N</div>
|
|
<span class="logo-text">NextWks</span>
|
|
</div>
|
|
<div class="topbar-right">
|
|
<button class="icon-btn" onclick="showPWA()" title="Install app">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
</button>
|
|
<div class="user-menu">
|
|
<div class="avatar sm">{ initials(userName) }</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
}
|
|
|
|
templ workspaceCSS() {
|
|
<style type="text/css">
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
:root{
|
|
--bg:#0d1117; --surface:#161b22; --surface2:#21262d; --surface3:#30363d;
|
|
--border:#30363d; --text:#e6edf3; --text2:#8b949e; --text3:#6e7681;
|
|
--blue:#58a6ff; --green:#3fb950; --orange:#d2991d; --red:#f85149;
|
|
--purple:#a371f7; --pink:#db61a2; --teal:#39d353;
|
|
--radius:16px; --radius-sm:10px
|
|
}
|
|
html{font-size:15px}
|
|
body{
|
|
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif;
|
|
background:var(--bg); color:var(--text); min-height:100vh; min-height:100dvh;
|
|
-webkit-tap-highlight-color:transparent; -webkit-user-select:none; user-select:none
|
|
}
|
|
.shell{display:flex;flex-direction:column;min-height:100vh;min-height:100dvh}
|
|
.topbar{
|
|
display:flex; justify-content:space-between; align-items:center;
|
|
padding:12px 16px; background:var(--surface); border-bottom:1px solid var(--border);
|
|
position:sticky; top:0; z-index:100
|
|
}
|
|
.topbar-left{display:flex;align-items:center;gap:10px}
|
|
.logo-icon{
|
|
width:32px;height:32px;border-radius:8px;background:var(--blue);
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-weight:700;font-size:16px;color:#fff
|
|
}
|
|
.logo-text{font-weight:600;font-size:18px;color:var(--text)}
|
|
.topbar-right{display:flex;align-items:center;gap:8px}
|
|
.icon-btn{
|
|
width:36px;height:36px;border-radius:50%;border:none;background:transparent;
|
|
color:var(--text2);cursor:pointer;display:flex;align-items:center;justify-content:center;
|
|
transition:all 0.15s
|
|
}
|
|
.icon-btn:hover{background:var(--surface2);color:var(--text)}
|
|
.avatar{
|
|
width:40px;height:40px;border-radius:50%;background:var(--purple);
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-weight:600;font-size:15px;color:#fff;flex-shrink:0
|
|
}
|
|
.avatar.sm{width:32px;height:32px;font-size:13px}
|
|
.content{flex:1;padding:24px 16px;max-width:900px;margin:0 auto;width:100%}
|
|
.greeting{display:flex;align-items:center;gap:12px;margin-bottom:24px}
|
|
@media(min-width:600px){.greeting{margin-bottom:32px}}
|
|
.greeting-text h1{font-size:1.25rem;font-weight:600;color:var(--text);line-height:1.3}
|
|
.greeting-text p{color:var(--text2);font-size:0.85rem;margin-top:2px}
|
|
|
|
/* App Grid */
|
|
.app-grid{
|
|
display:grid;
|
|
grid-template-columns:repeat(3,1fr);
|
|
gap:20px 10px; padding:8px 0
|
|
}
|
|
@media(min-width:600px){.app-grid{grid-template-columns:repeat(4,1fr);gap:24px 14px}}
|
|
@media(min-width:900px){.app-grid{grid-template-columns:repeat(6,1fr);gap:28px 16px}}
|
|
|
|
/* App Card */
|
|
.app-card{
|
|
display:flex; flex-direction:column; align-items:center; gap:6px;
|
|
cursor:pointer; text-decoration:none; color:inherit; padding:8px 4px;
|
|
border-radius:12px; transition:all 0.15s; -webkit-tap-highlight-color:transparent
|
|
}
|
|
.app-card:active{transform:scale(0.92)}
|
|
.app-card:hover{background:rgba(255,255,255,0.03)}
|
|
.app-card .app-icon{
|
|
width:56px;height:56px;border-radius:14px;
|
|
display:flex;align-items:center;justify-content:center;flex-shrink:0
|
|
}
|
|
@media(min-width:600px){.app-card .app-icon{width:64px;height:64px;border-radius:16px}}
|
|
.app-card .app-icon svg{width:28px;height:28px}
|
|
@media(min-width:600px){.app-card .app-icon svg{width:32px;height:32px}}
|
|
.app-card .app-name{
|
|
font-size:0.72rem;font-weight:400;color:var(--text);text-align:center;
|
|
line-height:1.2;max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap
|
|
}
|
|
@media(min-width:600px){.app-card .app-name{font-size:0.78rem}}
|
|
.app-card .app-badge{display:none}
|
|
.app-card.disabled{opacity:0.35;cursor:default}
|
|
.app-card.disabled:active{transform:none}
|
|
|
|
/* Footer */
|
|
.footer{
|
|
text-align:center;padding:16px;color:var(--text3);font-size:0.7rem;
|
|
border-top:1px solid var(--border);margin-top:auto
|
|
}
|
|
</style>
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
func version() string {
|
|
return appver.Version
|
|
}
|
|
|
|
func initials(name string) string {
|
|
if name == "" { return "?" }
|
|
if len(name) == 1 { return name }
|
|
return string(name[0])
|
|
}
|
|
|
|
func timeOfDay() string {
|
|
// Simple: always show "morning" for now
|
|
return "morning"
|
|
}
|
|
|
|
// --- PWA Instructions ---
|
|
|
|
templ pwaInstructions() {
|
|
<h3>Desktop Chrome/Edge</h3>
|
|
<ol>
|
|
<li>Click the <code>⊕</code> icon in the address bar</li>
|
|
<li>Click <strong>Install</strong></li>
|
|
</ol>
|
|
<h3>iOS Safari</h3>
|
|
<ol>
|
|
<li>Tap <strong>Share</strong> <code>⎋</code></li>
|
|
<li>Tap <strong>Add to Home Screen</strong></li>
|
|
</ol>
|
|
<h3>Android Chrome</h3>
|
|
<ol>
|
|
<li>Tap <strong>⋮</strong> menu</li>
|
|
<li>Tap <strong>Install app</strong></li>
|
|
</ol>
|
|
<button class="btn" onclick="closePWA()">Got it</button>
|
|
}
|
|
|
|
// --- JavaScript ---
|
|
|
|
templ launcherJS() {
|
|
<script type="text/javascript">
|
|
let longPressTimer = null;
|
|
let currentUrl = null;
|
|
|
|
function openApp(url) {
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
function startLongPress(e, url) {
|
|
currentUrl = url || null;
|
|
longPressTimer = setTimeout(function() {
|
|
showPWA();
|
|
}, 600);
|
|
}
|
|
|
|
function cancelLongPress() {
|
|
if (longPressTimer) {
|
|
clearTimeout(longPressTimer);
|
|
longPressTimer = null;
|
|
}
|
|
}
|
|
|
|
function showPWA() {
|
|
document.getElementById('pwa-overlay').style.display = 'flex';
|
|
}
|
|
|
|
function closePWA() {
|
|
document.getElementById('pwa-overlay').style.display = 'none';
|
|
}
|
|
|
|
// PWA install handler
|
|
let deferredPrompt = null;
|
|
window.addEventListener('beforeinstallprompt', function(e) {
|
|
e.preventDefault();
|
|
deferredPrompt = e;
|
|
});
|
|
|
|
// Override showPWA to try native prompt first
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var origShowPWA = showPWA;
|
|
showPWA = function() {
|
|
if (deferredPrompt) {
|
|
deferredPrompt.prompt();
|
|
deferredPrompt.userChoice.then(function() { deferredPrompt = null; });
|
|
} else {
|
|
origShowPWA();
|
|
}
|
|
};
|
|
});
|
|
</script>
|
|
}
|