feat(basin): fluid code view with column rule and versioned stylesheets
3 files changed, +32 −10
- static/style.css modified +13 −6
- templates/layout.html modified +3 −3
- web.go modified +16 −1
static/style.css modified
| @@ -41,7 +41,11 @@ nav.tabs a{padding:.25rem .9rem} | ||
| 41 | 41 | nav.tabs a.active{background:#fff;color:var(--fg)} |
| 42 | 42 | .repohead .desc{margin:0 0 .4rem;color:var(--muted)} |
| 43 | 43 | .repohead+main{margin-top:1rem} |
| 44 | -.repohead+main.file{margin-top:0;padding-top:.6rem;max-width:none;margin-left:0;margin-right:0} | |
| 44 | +.repohead+main{margin-top:0;padding-top:.6rem} | |
| 45 | +body.fluid .inner,body.fluid nav.site,body.fluid main{max-width:none;margin:0} | |
| 46 | +body.fluid .filebar{margin-bottom:0} | |
| 47 | +body.fluid main.file{padding-top:.2rem} | |
| 48 | +.summary{padding-top:.4rem} | |
| 45 | 49 | .summary{margin-top:.2rem;display:grid;grid-template-columns:4fr 1.5fr 2.6fr;gap:2rem;margin-bottom:1.5rem;align-items:start} |
| 46 | 50 | .summary .event{background:var(--soft);padding:.4rem .5rem .5rem;margin-bottom:.6rem} |
| 47 | 51 | .summary .event .row{display:flex;justify-content:space-between;gap:1rem} |
| @@ -125,7 +129,9 @@ table.tree .size{color:var(--muted);text-align:right} | ||
| 125 | 129 | table.tree .last{max-width:30rem;overflow:hidden;text-overflow:ellipsis;width:100%} |
| 126 | 130 | table.tree .when{text-align:right} |
| 127 | 131 | table.refs td,table.keys td{padding:.3rem 1rem .3rem 0;border-bottom:1px solid var(--line)} |
| 128 | -.code{margin:0 -1rem;line-height:1.45} | |
| 132 | +.code{margin:0 -1rem;line-height:1.45;min-height:70vh} | |
| 133 | +.code .lntd:last-child{position:relative} | |
| 134 | +.code .lntd:last-child::after{content:"";position:absolute;top:0;bottom:0;left:calc(80ch + 1rem);border-left:1px solid var(--bar)} | |
| 129 | 135 | table.blob,.code table{width:100%} |
| 130 | 136 | table.blob td,table.hunk td,.code td{padding:0 .5rem;white-space:pre;vertical-align:top} |
| 131 | 137 | td.n{text-align:right;color:var(--muted);user-select:none;width:1%} |
| @@ -135,11 +141,12 @@ td.l{white-space:pre-wrap;overflow-wrap:anywhere;width:100%} | ||
| 135 | 141 | tr:target td{background:#fff8c5} |
| 136 | 142 | .code pre,.code .chroma{background:#fff;border:0;padding:0;margin:0} |
| 137 | 143 | .code .lntable{margin-top:0} |
| 138 | -.code .lntd:first-child{width:1%;user-select:none;border-right:1px solid var(--bar);padding-top:.4rem} | |
| 144 | +.code .lntd:first-child{width:1%;user-select:none;border-right:1px solid var(--bar);padding-top:.4rem;vertical-align:top} | |
| 139 | 145 | .code .lntd:last-child{padding-top:.4rem} |
| 140 | -.code .lnlinks{text-decoration:none} | |
| 141 | -.code .lnlinks:hover{text-decoration:underline} | |
| 142 | -.code .lnt,.code .ln{color:var(--link);margin-right:0;padding:0 .5rem 0 .6rem} | |
| 146 | +.code .lntd:first-child{min-height:70vh} | |
| 147 | +.code .lnlinks{text-decoration:underline} | |
| 148 | +.code .lnt{padding-right:.5rem} | |
| 149 | +.code .lnt,.code .ln{color:var(--link);margin-right:0;padding:0 .5rem 0 .4rem} | |
| 143 | 150 | .code .lntd a{color:var(--link);text-decoration:none} |
| 144 | 151 | .code .lntd:last-child{padding-left:.6rem} |
| 145 | 152 | .code .line:target,.code .line:target .cl{background:#fff8c5} |
templates/layout.html modified
| @@ -4,14 +4,14 @@ | ||
| 4 | 4 | <meta charset="utf-8"> |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | 6 | <title>{{.Title}}</title> |
| 7 | -<link rel="stylesheet" href="/static/style.css"> | |
| 8 | -<link rel="stylesheet" href="/static/highlight.css"> | |
| 7 | +<link rel="stylesheet" href="/static/style.css?v={{.Version}}"> | |
| 8 | +<link rel="stylesheet" href="/static/highlight.css?v={{.Version}}"> | |
| 9 | 9 | {{if .Math}}<link rel="stylesheet" href="/static/katex/katex.min.css"> |
| 10 | 10 | <script defer src="/static/katex/katex.min.js"></script> |
| 11 | 11 | <script defer src="/static/katex/auto-render.min.js"></script> |
| 12 | 12 | <script>document.addEventListener("DOMContentLoaded",function(){renderMathInElement(document.body,{delimiters:[{left:"\\(",right:"\\)",display:false},{left:"\\[",right:"\\]",display:true}],throwOnError:false})})</script>{{end}} |
| 13 | 13 | </head> |
| 14 | -<body> | |
| 14 | +<body{{if .Fluid}} class="fluid"{{end}}> | |
| 15 | 15 | <header> |
| 16 | 16 | <nav class="site"> |
| 17 | 17 | <a class="brand" href="/">{{.Host}}</a> |
web.go modified
| @@ -2,7 +2,9 @@ package main | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "bytes" |
| 5 | + "crypto/sha256" | |
| 5 | 6 | "embed" |
| 7 | + "encoding/hex" | |
| 6 | 8 | "fmt" |
| 7 | 9 | "html/template" |
| 8 | 10 | "io" |
| @@ -33,6 +35,7 @@ var templateFS embed.FS | ||
| 33 | 35 | var staticFS embed.FS |
| 34 | 36 | |
| 35 | 37 | type Server struct { |
| 38 | + version string | |
| 36 | 39 | limiter *limiter |
| 37 | 40 | store *Store |
| 38 | 41 | root string |
| @@ -45,6 +48,8 @@ type Server struct { | ||
| 45 | 48 | } |
| 46 | 49 | |
| 47 | 50 | type page struct { |
| 51 | + Version string | |
| 52 | + Fluid bool | |
| 48 | 53 | Title string |
| 49 | 54 | User *User |
| 50 | 55 | Host string |
| @@ -89,12 +94,21 @@ func newServer(store *Store, root, host string, open bool, sshUser string) *Serv | ||
| 89 | 94 | s.policy = bluemonday.UGCPolicy() |
| 90 | 95 | s.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^[a-z]{1,8}( [a-z]{1,8})*$`)).OnElements("span", "pre", "code") |
| 91 | 96 | s.writeSigners() |
| 97 | + sum := sha256.New() | |
| 98 | + css, _ := staticFS.ReadFile("static/style.css") | |
| 99 | + sum.Write(css) | |
| 100 | + sum.Write(highlightCSS()) | |
| 101 | + s.version = hex.EncodeToString(sum.Sum(nil))[:12] | |
| 92 | 102 | return s |
| 93 | 103 | } |
| 94 | 104 | |
| 95 | 105 | func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, p page) { |
| 96 | 106 | p.Host = s.host |
| 97 | 107 | p.SSHUser = s.sshUser |
| 108 | + p.Version = s.version | |
| 109 | + if d, ok := p.Data.(map[string]any); ok && d["File"] == true { | |
| 110 | + p.Fluid = true | |
| 111 | + } | |
| 98 | 112 | if p.User == nil { |
| 99 | 113 | p.User = s.current(r) |
| 100 | 114 | } |
| @@ -770,7 +784,7 @@ func (s *Server) tree(w http.ResponseWriter, r *http.Request, repo Repo, spec st | ||
| 770 | 784 | if filePath == "" { |
| 771 | 785 | crumbs = nil |
| 772 | 786 | } |
| 773 | - data := map[string]any{"Page": rp, "Path": filePath, "Crumbs": crumbs, "File": true} | |
| 787 | + data := map[string]any{"Page": rp, "Path": filePath, "Crumbs": crumbs} | |
| 774 | 788 | if last, _ := g.Log(rp.Commit, 1, filePath); len(last) > 0 { |
| 775 | 789 | data["LastCommit"] = last[0] |
| 776 | 790 | } |
| @@ -801,6 +815,7 @@ func (s *Server) tree(w http.ResponseWriter, r *http.Request, repo Repo, spec st | ||
| 801 | 815 | return |
| 802 | 816 | } |
| 803 | 817 | } |
| 818 | + data["File"] = true | |
| 804 | 819 | if size > maxRender { |
| 805 | 820 | data["Large"] = true |
| 806 | 821 | } else if isBinary(content) { |