Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview/pages: improve blob/tree topbar UI

as per new designs

Signed-off-by: oppiliappan <me@oppi.li>

authored by

oppiliappan and committed by
Tangled
(Jul 2, 2026, 11:19 AM +0300) a3e12f88 9b807bde

+112 -58
+1
appview/pages/pages.go
··· 1112 1112 LastCommitInfo *types.LastCommitInfo 1113 1113 Ref string 1114 1114 Path string 1115 + Language string 1115 1116 } 1116 1117 1117 1118 func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
+56 -44
appview/pages/templates/repo/blob.html
··· 12 12 13 13 {{ define "repoContent" }} 14 14 {{ $linkstyle := "no-underline hover:underline" }} 15 - <div class="peer pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700"> 16 - <div class="flex flex-col md:flex-row md:justify-between gap-2"> 17 - <div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500"> 18 - {{ range $idx, $value := .BreadCrumbs }} 19 - {{ if ne $idx (sub (len $.BreadCrumbs) 1) }} 20 - <a 21 - href="{{ index . 1 }}" 22 - class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}" 23 - >{{ pathUnescape (index . 0) }}</a 24 - > 25 - / 15 + <div class="peer mb-3 text-sm flex justify-between gap-2 items-center"> 16 + <div class="flex gap-3 items-center"> 17 + <div class="bg-gray-100 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded p-2 flex items-center gap-1 overflow-x-auto whitespace-nowrap w-fit"> 18 + {{ $last := sub (len .BreadCrumbs) 1 }} 19 + {{ range $i, $b := .BreadCrumbs }} 20 + {{ if eq $i $last }} 21 + <span class="text-black dark:text-white">{{ pathUnescape (index $b 0) }}</span> 26 22 {{ else }} 27 - <span class="text-bold text-black dark:text-white" 28 - >{{ pathUnescape (index . 0) }}</span 29 - > 23 + <a href="{{ index $b 1 }}" class="text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index $b 0) }}</a> 24 + <span class="text-gray-500 dark:text-gray-400">/</span> 30 25 {{ end }} 31 26 {{ end }} 32 27 </div> 33 - <div id="file-info" class="text-gray-500 dark:text-gray-400 text-xs md:text-sm flex flex-wrap items-center gap-1 md:gap-0"> 34 - <span>at <a href="/{{ .RepoInfo.FullName }}/tree/{{ pathEscape .Ref }}">{{ .Ref }}</a></span> 35 - 36 - {{ if (and .BlobView.ContentType.HasTextView (not .ShowRendered)) }} 37 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 38 - <span>{{ .BlobView.Lines }} lines</span> 39 - {{ end }} 40 - 28 + <div class="text-gray-500 dark:text-gray-400 flex items-center gap-2"> 41 29 {{ if .BlobView.SizeHint }} 42 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 43 - <span>{{ byteFmt .BlobView.SizeHint }}</span> 30 + <span>{{ byteFmt .BlobView.SizeHint }}</span> 44 31 {{ end }} 45 - 46 - {{ if .BlobView.ContentType.HasRawView }} 47 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 48 - <a href="/{{ .RepoInfo.FullName }}/raw/{{ pathEscape .Ref }}/{{ .Path }}">View raw</a> 32 + {{ if (and .BlobView.ContentType.HasTextView (or (not .BlobView.ContentType.HasRenderedView) (not .ShowRendered))) }} 33 + <span class="select-none [&:before]:content-['·']"></span> 34 + <span>{{ .BlobView.Lines }} lines</span> 49 35 {{ end }} 50 - 51 - {{ if (and .BlobView.ContentType.HasTextView .BlobView.ContentType.HasRenderedView) }} 52 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 53 - <a href="/{{ .RepoInfo.FullName }}/blob/{{ pathEscape .Ref }}/{{ .Path }}?code={{ .ShowRendered }}" hx-boost="true"> 54 - View {{ if .ShowRendered }}code{{ else }}rendered{{ end }} 36 + </div> 37 + </div> 38 + <div class="hidden md:flex items-center gap-2 whitespace-nowrap text-sm"> 39 + {{ if (and .BlobView.ContentType.HasTextView (or (not .BlobView.ContentType.HasRenderedView) (not .ShowRendered))) }} 40 + <div id="toggle-wrap-content" class="px-2"> 41 + <label class="flex items-center font-normal gap-2"> 42 + <input id="toggle-wrap-content-checkbox" type="checkbox" name="Wrap"/> 43 + Wrap 44 + </label> 45 + </div> 46 + {{ end }} 47 + <div class="bg-gray-100 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded p-2 flex items-center gap-2 justify-center"> 48 + <span class="text-gray-500 dark:text-gray-400">at</span> 49 + <a href="/{{ .RepoInfo.FullName }}/tree/{{ pathEscape .Ref }}" class="{{ $linkstyle }}">{{ .Ref }}</a> 50 + </div> 51 + {{ if .Language }} 52 + <div class="bg-gray-100 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded p-2 flex items-center gap-2"> 53 + {{ template "repo/fragments/colorBall" (dict "color" (langColor .Language)) }} 54 + {{ .Language }} 55 + </div> 56 + {{ end }} 57 + <div class="btn-group"> 58 + {{ if (and .BlobView.ContentType.HasTextView .BlobView.ContentType.HasRenderedView) }} 59 + <a href="/{{ .RepoInfo.FullName }}/blob/{{ pathEscape .Ref }}/{{ .Path }}?code={{ .ShowRendered }}" hx-boost="true" class="btn-group-item px-3 no-underline hover:no-underline"> 60 + View {{ if .ShowRendered }}code{{ else }}rendered{{ end }} 55 61 </a> 56 - {{ end }} 57 - 58 - {{ if (and .BlobView.ContentType.HasTextView (not .ShowRendered)) }} 59 - <div id="toggle-wrap-content" class="flex items-center"> 60 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 61 - <label class="flex items-center lowercase font-normal px-1 py-0 gap-1 text-xs md:text-sm"> 62 - <input id="toggle-wrap-content-checkbox" type="checkbox" name="wrap"/> 63 - wrap content 64 - </label> 65 - </div> 66 - {{ end }} 62 + {{ end }} 63 + {{ if .BlobView.ContentType.HasRawView }} 64 + {{ $rawUrl := printf "/%s/raw/%s/%s" .RepoInfo.FullName (pathEscape .Ref) .Path }} 65 + {{ $filename := pathUnescape (index (index .BreadCrumbs (sub (len .BreadCrumbs) 1)) 0) }} 66 + <a href="{{ $rawUrl }}" class="btn-group-item px-3 no-underline hover:no-underline">View raw</a> 67 + {{ if .BlobView.ContentType.HasTextView }} 68 + <button 69 + class="btn-group-item px-3" 70 + title="Copy contents" 71 + onclick="const btn=this,c=document.querySelector('#blob-contents');if(c){const cls=c.querySelectorAll('.cl');const text=cls.length>0?Array.from(cls).map(e=>e.textContent).join(''):c.textContent;navigator.clipboard.writeText(text);const h=btn.innerHTML;btn.innerHTML=`{{ i "copy-check" "size-4" }}`;setTimeout(()=>btn.innerHTML=h,1500)}"> 72 + {{ i "copy" "size-4" }} 73 + </button> 74 + {{ end }} 75 + <a href="{{ $rawUrl }}?download=1" class="btn-group-item px-3 no-underline hover:no-underline" title="Download"> 76 + {{ i "download" "size-4" }} 77 + </a> 78 + {{ end }} 67 79 </div> 68 80 </div> 69 81 </div>
+1 -1
appview/pages/templates/repo/fragments/lastCommitPanel.html
··· 1 1 {{ define "repo/fragments/lastCommitPanel" }} 2 2 {{ $messageParts := splitN .LastCommitInfo.Message "\n\n" 2 }} 3 - <div class="pb-2 mb-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between flex-wrap text-sm gap-2"> 3 + <div class="pb-2 mb-3 border-b border-gray-200 dark:border-gray-700 hidden md:flex items-center justify-between flex-wrap text-sm gap-2"> 4 4 <div class="flex flex-wrap items-center gap-1"> 5 5 {{ if .LastCommitInfo.Author }} 6 6 {{ $authorDid := index .EmailToDid .LastCommitInfo.Author.Email }}
+19 -13
appview/pages/templates/repo/tree.html
··· 21 21 <div class="tree"> 22 22 {{ $linkstyle := "no-underline hover:underline" }} 23 23 24 - <div class="pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700"> 25 - <div class="flex flex-col md:flex-row md:justify-between gap-2"> 26 - <div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500"> 27 - {{ range .BreadCrumbs }} 28 - <a href="{{ index . 1 }}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index . 0) }}</a> / 24 + <div class="mb-3 text-sm flex justify-between gap-2 items-center"> 25 + <div class="flex gap-3 items-center"> 26 + <div class="bg-gray-100 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded p-2 flex items-center gap-1 overflow-x-auto whitespace-nowrap w-fit"> 27 + {{ $last := sub (len .BreadCrumbs) 1 }} 28 + {{ range $i, $b := .BreadCrumbs }} 29 + {{ if (ne $i $last) }} 30 + <a href="{{ index $b 1 }}" class="text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index $b 0) }}</a> 31 + <span class="text-gray-500 dark:text-gray-400">/</span> 32 + {{ else }} 33 + <span class="text-black dark:text-white">{{ pathUnescape (index $b 0) }}</span> 34 + {{ end }} 29 35 {{ end }} 30 36 </div> 31 - <div id="dir-info" class="text-gray-500 dark:text-gray-400 text-xs md:text-sm flex flex-wrap items-center gap-1 md:gap-0"> 37 + <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2"> 32 38 {{ $stats := .TreeStats }} 33 - 34 - <span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ pathEscape $.Ref }}">{{ $.Ref }}</a></span> 35 39 {{ if eq $stats.NumFolders 1 }} 36 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 37 40 <span>{{ $stats.NumFolders }} folder</span> 38 41 {{ else if gt $stats.NumFolders 1 }} 39 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 40 42 <span>{{ $stats.NumFolders }} folders</span> 41 43 {{ end }} 42 44 45 + {{ if and (gt $stats.NumFolders 0) (gt $stats.NumFiles 0) }} 46 + <span class="select-none [&:before]:content-['·']"></span> 47 + {{ end }} 48 + 43 49 {{ if eq $stats.NumFiles 1 }} 44 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 45 50 <span>{{ $stats.NumFiles }} file</span> 46 51 {{ else if gt $stats.NumFiles 1 }} 47 - <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 48 52 <span>{{ $stats.NumFiles }} files</span> 49 53 {{ end }} 50 - 51 54 </div> 55 + </div> 56 + <div class="bg-gray-100 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded p-2 hidden md:flex items-center gap-2 whitespace-nowrap"> 57 + <span class="text-gray-500 dark:text-gray-400">at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ pathEscape $.Ref }}" class="{{ $linkstyle }}">{{ $.Ref }}</a></span> 52 58 </div> 53 59 </div> 54 60
+35
appview/repo/blob.go
··· 24 24 "github.com/bluesky-social/indigo/util" 25 25 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 26 26 "github.com/go-chi/chi/v5" 27 + enry "github.com/go-enry/go-enry/v2" 27 28 "github.com/go-git/go-git/v5/plumbing" 28 29 "github.com/go-git/go-git/v5/plumbing/filemode" 29 30 ) ··· 201 202 } 202 203 } 203 204 205 + baseName := filepath.Base(filePath) 206 + lang, ok := enry.GetLanguageByExtension(baseName) 207 + if !ok { 208 + lang, ok = enry.GetLanguageByFilename(baseName) 209 + } 210 + if !ok && blobView.Contents != "" { 211 + lang = enry.GetLanguage(baseName, []byte(blobView.Contents)) 212 + } 213 + if group := enry.GetLanguageGroup(lang); group != "" { 214 + lang = group 215 + } 216 + 204 217 user := rp.oauth.GetMultiAccountUser(r) 205 218 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 206 219 BaseParams: pages.BaseParamsFromContext(r.Context()), ··· 212 225 ShowRendered: r.URL.Query().Get("code") != "true", 213 226 Ref: ref, 214 227 Path: filePath, 228 + Language: lang, 215 229 }) 216 230 } 217 231 ··· 232 246 filePath, _ = url.PathUnescape(filePath) 233 247 234 248 blobURL := generateBlobURL(rp.config.KnotMirror.Url, f, ref, filePath) 249 + 250 + if r.URL.Query().Get("download") == "1" { 251 + req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, blobURL, nil) 252 + if err != nil { 253 + w.WriteHeader(http.StatusInternalServerError) 254 + return 255 + } 256 + resp, err := util.RobustHTTPClient().Do(req) 257 + if err != nil || resp.StatusCode != http.StatusOK { 258 + w.WriteHeader(http.StatusBadGateway) 259 + return 260 + } 261 + defer resp.Body.Close() 262 + 263 + filename := filepath.Base(filePath) 264 + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, filename)) 265 + w.Header().Set("Content-Type", resp.Header.Get("Content-Type")) 266 + w.Header().Set("Cache-Control", "public, no-cache") 267 + io.Copy(w, resp.Body) 268 + return 269 + } 235 270 236 271 w.Header().Set("Cache-Control", "public, no-cache") 237 272 http.Redirect(w, r, blobURL, http.StatusFound)