gitsite: serve usable styled layout

ober

7dc0be16d577e87d5f6082a0a3089f5725e4fe0f

diff --git a/src/gitsite/views.ss b/src/gitsite/views.ss
index e9a1577..12e1c41 100644
--- a/src/gitsite/views.ss
+++ b/src/gitsite/views.ss
@@ -1,27 +1,39 @@
 (import (jerboa prelude)
-        (std markup html)
-        (only (std text html) html-escape))
+        (std markup html))
 
 (export page-html css-string)
 
-(def css-string "body{font-family:system-ui,sans-serif;margin:0;background:#fafafa;color:#222}a{color:#06c;text-decoration:none}a:hover{text-decoration:underline}header.top{display:flex;align-items:center;padding:.6em 1em;border-bottom:1px solid #ddd;background:#f5f5f5}header.top nav a{margin-left:1em}main{max-width:900px;margin:auto;padding:1em}ul.repolist{list-style:none;padding:0}ul.repolist li{padding:.4em 0;border-bottom:1px solid #eee}.desc{color:#666;margin-left:.6em}pre{background:#f5f5f5;padding:1em;overflow:auto}footer{color:#999;padding:1em;border-top:1px solid #eee;margin-top:2em}")
+(def css-string
+  (string-append
+    ":root{--ink:#18212b;--muted:#607080;--line:#d9e2ea;--page:#f5f7f8;--panel:#fff;--blue:#2457c5;--green:#217857;--red:#b54c2f;--shadow:0 18px 50px rgba(24,33,43,.10)}"
+    "*{box-sizing:border-box}html{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;color:var(--ink);background:var(--page)}body{margin:0;font-size:16px;line-height:1.6;background:radial-gradient(circle at top left,#e7f5f5 0,#f5f7f8 26rem),linear-gradient(180deg,#fff 0,#f5f7f8 38rem);min-width:320px}a{color:var(--blue);text-decoration:none}a:hover{text-decoration:underline}"
+    ".navbar{position:sticky;top:0;z-index:10;min-height:4.25rem;background:rgba(255,255,255,.92);border-bottom:1px solid rgba(217,226,234,.9);backdrop-filter:saturate(160%) blur(18px);display:flex;align-items:center;justify-content:space-between;padding:.75rem clamp(1rem,3vw,2.4rem);gap:1rem}.brand{display:flex;align-items:center;color:var(--ink);font-size:1.08rem;font-weight:850}.brand-mark{display:grid;place-items:center;width:2.4rem;height:2.4rem;margin-right:.7rem;border:1px solid #bfd0da;border-radius:8px;background:linear-gradient(135deg,#fff7dc,#e9fbff 60%,#fff2f7);box-shadow:0 8px 24px rgba(24,33,43,.10);font-family:Georgia,serif;font-weight:900}.nav{display:flex;gap:.35rem;align-items:center;font-size:.94rem;white-space:nowrap}.nav-link{color:#33465a;border-radius:6px;padding:.45rem .64rem}.nav-link.active{color:var(--ink);background:#eaf2f4;font-weight:800}.user-pill{color:#516579;border-left:1px solid var(--line);padding-left:.85rem;margin-left:.35rem}"
+    "main{max-width:1080px;margin:0 auto;padding:clamp(1.1rem,3vw,2.4rem)}main h1{font-size:clamp(2rem,4vw,3.4rem);line-height:1;margin:.2rem 0 1rem;color:#101923;letter-spacing:-.02em}main h2{margin-top:2rem;color:#172230}.card,form{border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.86);padding:1rem;box-shadow:0 10px 30px rgba(24,33,43,.05)}form p{display:grid;gap:.35rem;margin:.75rem 0}label{font-weight:800;color:#33465a}input,textarea,select{width:100%;max-width:44rem;border:1px solid #c9d5de;border-radius:6px;background:#fff;color:#172230;font:inherit;padding:.58rem .68rem}button{border:1px solid #172230;border-radius:6px;background:#172230;color:#fff;font:inherit;font-weight:850;min-height:2.55rem;padding:.58rem .9rem;cursor:pointer}button:hover{background:#283847}.inline-form{display:inline;padding:0;border:0;background:transparent;box-shadow:none}.inline-form button{min-height:1.9rem;padding:.25rem .55rem;font-size:.9rem}"
+    "ul.repolist{list-style:none;padding:0;margin:1rem 0;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:#fff}ul.repolist li{display:flex;gap:.75rem;align-items:baseline;padding:.75rem .9rem;border-bottom:1px solid #edf1f4}ul.repolist li:last-child{border-bottom:0}.desc{color:var(--muted);margin-left:.4rem}.tabs{display:flex;gap:.35rem;flex-wrap:wrap;margin:0 0 1rem;border-bottom:1px solid var(--line);padding-bottom:.65rem}.tabs a{color:#33465a;border-radius:6px;padding:.42rem .62rem}.tabs a.active{background:#eaf2f4;color:var(--ink);font-weight:800}pre{background:#fff;border:1px solid var(--line);border-radius:8px;padding:1rem;overflow:auto}table{width:100%;border-collapse:collapse;background:#fff;border:1px solid var(--line)}td,th{padding:.55rem .7rem;border-bottom:1px solid #edf1f4;text-align:left}footer{color:#728396;text-align:center;padding:2rem;border-top:1px solid var(--line);margin-top:2rem}@media(max-width:720px){.navbar{position:static;align-items:flex-start;flex-direction:column}.nav{width:100%;overflow:auto;padding-bottom:.15rem}ul.repolist li{display:block}.user-pill{border-left:0;padding-left:0;margin-left:0}}"))
+
+(def (nav-link href label)
+  `(a (@ (href ,href) (class "nav-link")) ,label))
 
 (def (page-html title user body)
-  `(html
+  `(html (@ (lang "en"))
      (head
       (meta (@ (charset "utf-8")))
+      (meta (@ (name "viewport") (content "width=device-width,initial-scale=1")))
       (title ,(str title " - gitsite"))
-      (style ,css-string))
+      (link (@ (rel "stylesheet") (href "/assets/site.css"))))
      (body
-      (header (@ (class "top"))
-        (a (@ (href "/")) "gitsite")
-        (nav
-         (a (@ (href "/")) "home")
-         ,(if user
-            '(a (@ (href "/new")) "new repo")
-            '(a (@ (href "/login")) "login"))
-         ,(if user
-            '(a (@ (href "/logout")) "logout")
-            '(a (@ (href "/register")) "register"))))
+      (header (@ (class "navbar"))
+        (a (@ (class "brand") (href "/"))
+          (span (@ (class "brand-mark")) "λ")
+          (span "gitsite"))
+        (nav (@ (class "nav") (aria-label "Main"))
+          ,(nav-link "/" "Home")
+          ,(if user
+             (nav-link "/new" "New repo")
+             (nav-link "/login" "Login"))
+          ,(if user
+             (nav-link "/logout" "Logout")
+             (nav-link "/register" "Register"))
+          ,@(if user `((span (@ (class "user-pill")) ,(str "@" user))) '())))
       (main ,@body)
       (footer "gitsite - a tiny forge"))))
diff --git a/src/gitsite/web.ss b/src/gitsite/web.ss
index d24f8a7..22f2760 100644
--- a/src/gitsite/web.ss
+++ b/src/gitsite/web.ss
@@ -14,7 +14,7 @@
         (only (gitsite git_http) git-bridge)
         (only (gitsite builds) submit-job! list-repo-jobs get-job)
         (only (gitsite worker) run-one-job)
-        (only (gitsite views) page-html)
+        (only (gitsite views) page-html css-string)
         (only (sinatra request) sinatra-request-raw sinatra-request-method sinatra-request-body-params)
         (only (sinatra security) secure-headers!))
 
@@ -222,6 +222,13 @@
     (body! "User-agent: *\nAllow: /\n")
     (halt))
 
+  (GET "/assets/site.css"
+    (status! 200)
+    (header! "Content-Type" "text/css; charset=utf-8")
+    (header! "Cache-Control" "public, max-age=300")
+    (body! css-string)
+    (halt))
+
   (GET  "/git/~:owner/:name*" (git-route-handler))
   (POST "/git/~:owner/:name*" (git-route-handler))
 
@@ -368,7 +375,7 @@
                  `(ul ,@(map (lambda (k)
                                `(li (strong ,(cdr (assq 'title k)))
                                     (span (@ (class "desc")) ,(str " - " (cdr (assq 'fingerprint k))))
-                                    (form (@ (method "post") (action ,(str "/settings/keys/" (cdr (assq 'id k)) "/delete")) (style "display:inline"))
+                                    (form (@ (method "post") (action ,(str "/settings/keys/" (cdr (assq 'id k)) "/delete")) (class "inline-form"))
                                       ,(csrf-input)
                                       (button (@ (type "submit")) "delete"))))
                              keys)))))))))
@@ -428,7 +435,7 @@
                                          (if (and (string? lu) (not (string=? lu "")))
                                            (str " - created " (cdr (assq 'created_at t)) ", last used " lu)
                                            (str " - created " (cdr (assq 'created_at t))))))
-                                    (form (@ (method "post") (action ,(str "/settings/tokens/" (cdr (assq 'id t)) "/delete")) (style "display:inline"))
+                                    (form (@ (method "post") (action ,(str "/settings/tokens/" (cdr (assq 'id t)) "/delete")) (class "inline-form"))
                                       ,(csrf-input)
                                       (button (@ (type "submit")) "delete"))))
                              tokens)))))))))