Commit dd796a6

Nick Faro committed on
Add file browser and Gabriel mark
commit dd796a602e07bd4944593ae5af96fb147fa2949b parent 9c8222e
7 changed files +281−32
Addedinternal/site/assets/gabriel-mark.svg +70−0
@@ -0,0 +1,70 @@
1 <?xml version="1.0" standalone="no"?>
2 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3 "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4 <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5 width="1254.000000pt" height="1254.000000pt" viewBox="0 0 1254.000000 1254.000000"
6 preserveAspectRatio="xMidYMid meet">
7 <metadata>
8 Created by potrace 1.16, written by Peter Selinger 2001-2019
9 </metadata>
10 <g transform="translate(0.000000,1254.000000) scale(0.100000,-0.100000)"
11 fill="#000000" stroke="none">
12 <path d="M5850 12110 c-587 -50 -1151 -184 -1695 -402 -190 -76 -568 -262
13 -750 -369 -1125 -660 -1990 -1674 -2469 -2892 -206 -526 -322 -1013 -383
14 -1617 -24 -244 -24 -832 0 -1080 99 -994 409 -1871 949 -2685 82 -122 100
15 -149 225 -315 285 -379 709 -797 1124 -1107 668 -498 1450 -852 2258 -1023
16 1732 -367 3544 106 4906 1279 631 543 1133 1221 1479 1994 268 598 415 1166
17 488 1882 20 199 17 834 -5 1050 -96 931 -368 1763 -825 2524 -568 946 -1401
18 1709 -2397 2196 -617 301 -1215 472 -1950 556 -120 13 -822 20 -955 9z m555
19 -1361 c4 -6 -4 -22 -16 -36 l-24 -26 25 6 c135 32 226 31 315 -5 39 -15 87
20 -22 215 -28 199 -9 255 -27 391 -122 46 -33 79 -45 195 -74 192 -47 284 -93
21 364 -183 56 -62 70 -72 154 -105 52 -20 112 -53 135 -72 37 -32 41 -40 41 -80
22 0 -41 3 -46 31 -56 63 -22 154 -73 157 -89 1 -8 -9 -21 -23 -29 -14 -7 -25
23 -17 -25 -21 0 -4 37 -29 81 -56 103 -61 200 -142 262 -218 50 -62 127 -190
24 127 -212 0 -8 -14 -13 -35 -13 -19 0 -35 -2 -35 -5 0 -3 31 -46 70 -96 147
25 -191 249 -426 275 -635 l7 -55 -25 6 -25 7 19 -64 c44 -138 61 -249 62 -391 1
26 -88 -3 -141 -9 -145 -7 -4 -14 -77 -18 -182 -3 -96 -13 -209 -21 -250 -9 -41
27 -19 -136 -24 -210 -9 -129 -23 -206 -55 -298 l-15 -42 30 -16 c43 -22 69 -83
28 69 -165 0 -64 -29 -258 -50 -328 -5 -19 -7 -37 -5 -40 13 -12 453 217 680 354
29 290 175 483 318 668 495 183 174 274 302 317 449 58 190 -18 312 -265 431
30 -177 86 -342 133 -630 181 -78 13 -79 13 -40 21 123 22 408 3 639 -43 387 -77
31 608 -208 704 -419 19 -43 26 -77 30 -156 4 -89 1 -113 -22 -194 -103 -358
32 -471 -757 -1056 -1145 -975 -646 -2317 -1166 -3920 -1519 -510 -113 -914 -182
33 -1405 -241 -803 -95 -1609 -80 -2174 41 -525 113 -866 305 -991 557 -39 78
34 -43 94 -48 180 -5 112 7 173 54 273 163 345 787 825 1408 1083 145 60 356 123
35 356 106 0 -2 -89 -56 -197 -121 -710 -423 -1115 -776 -1195 -1038 -19 -62 -20
36 -77 -10 -125 7 -34 27 -78 49 -110 61 -88 204 -181 302 -197 40 -6 44 -10 67
37 -62 52 -118 152 -213 207 -196 12 3 37 20 55 38 l34 31 16 -24 c25 -36 131
38 -111 158 -111 15 0 33 13 51 35 39 49 71 47 116 -8 19 -24 56 -54 81 -67 l45
39 -24 26 24 c15 14 33 37 41 53 20 38 51 34 99 -13 80 -78 143 -82 173 -10 27
40 62 51 63 92 3 41 -60 72 -73 177 -73 105 0 368 17 462 30 73 10 107 35 117 85
41 11 62 -36 115 -130 145 -83 27 -176 153 -197 268 -13 68 -30 102 -80 163 -58
42 70 -113 90 -255 92 -108 2 -115 4 -140 29 -23 22 -27 38 -35 114 -12 126 -17
43 149 -39 177 -42 53 -138 38 -165 -27 -17 -40 -19 -128 -5 -236 9 -67 8 -76 -9
44 -93 -21 -21 -123 -46 -192 -47 -61 0 -80 16 -86 73 -3 26 -10 50 -14 53 -5 3
45 -91 -4 -191 -15 -302 -34 -338 -35 -378 -14 -53 27 -54 90 -2 117 33 17 590
46 116 652 116 4 0 11 17 15 38 25 136 127 212 283 212 148 0 243 -85 282 -250 7
47 -30 14 -60 15 -66 2 -6 32 -17 67 -23 34 -7 90 -24 123 -37 151 -62 203 -136
48 250 -359 16 -73 21 -84 64 -125 65 -63 122 -80 272 -80 135 0 130 -4 105 86
49 -8 27 -21 74 -30 105 l-16 56 -70 7 c-149 15 -237 90 -281 240 -12 39 -39 98
50 -60 131 -70 110 -130 262 -227 575 -72 232 -103 370 -105 460 -1 72 1 81 32
51 131 l33 53 -36 116 c-39 126 -100 385 -116 495 -12 85 -12 313 0 415 9 79 7
52 91 -12 72 -7 -7 -16 -12 -21 -12 -15 0 -2 171 21 285 11 55 28 168 37 250 27
53 254 64 370 168 533 26 40 47 75 47 78 0 4 -11 4 -25 2 -21 -4 -25 -1 -25 18 0
54 36 45 149 81 202 18 26 69 83 113 126 80 79 81 80 81 131 0 39 6 58 25 81 39
55 49 113 99 227 156 58 29 107 57 108 63 2 5 6 22 9 37 15 69 80 109 308 188 81
56 28 149 57 156 67 7 10 23 34 35 53 12 19 35 41 49 50 25 13 34 13 95 -3 l68
57 -18 19 28 c58 82 145 140 253 168 74 19 229 19 331 0 l79 -15 109 43 c93 37
58 160 57 269 80 43 10 164 7 170 -4z m2339 -5286 c-9 -35 -50 -118 -77 -156 -27
59 -36 -102 -82 -162 -100 l-47 -13 -43 -170 c-67 -265 -142 -479 -226 -646 -29
60 -57 -29 -60 -29 -224 l0 -167 39 -51 c107 -141 123 -218 141 -696 21 -545 55
61 -737 160 -899 78 -120 204 -229 310 -267 28 -10 50 -21 50 -24 0 -17 -153
62 -144 -285 -237 -495 -349 -1162 -580 -1795 -623 -528 -35 -998 23 -1485 185
63 -325 108 -598 241 -855 416 -111 76 -292 220 -298 237 -1 5 17 17 40 28 80 35
64 218 135 296 213 169 170 209 294 232 716 32 605 41 691 80 802 21 60 68 139
65 100 168 18 17 20 31 20 185 l0 166 -40 83 c-22 46 -39 86 -37 88 3 2 111 19
66 241 38 738 106 1450 259 2166 465 479 137 1029 321 1395 466 61 24 111 43 113
67 44 1 0 -1 -12 -4 -27z m-4977 -35 c51 -47 12 -138 -58 -138 -74 0 -109 95 -53
68 139 35 27 81 27 111 -1z"/>
69 </g>
70 </svg>
Addedinternal/site/brand.go +26−0
@@ -0,0 +1,26 @@
1 package site
2
3 import _ "embed"
4
5 //go:embed assets/gabriel-mark.svg
6 var gabrielMarkSVG []byte
7
8 //go:embed assets/gabriel-mark.png
9 var gabrielMarkPNG []byte
10
11 func brandPages() []Page {
12 return []Page{
13 {
14 Route: ".dumbforge/assets/gabriel-mark.svg",
15 Body: gabrielMarkSVG,
16 ContentType: "image/svg+xml",
17 Cache: siteCache,
18 },
19 {
20 Route: ".dumbforge/assets/gabriel-mark.png",
21 Body: gabrielMarkPNG,
22 ContentType: "image/png",
23 Cache: siteCache,
24 },
25 }
26 }
Modifiedinternal/site/site.go +2−1
@@ -154,6 +154,7 @@ func (g *Generator) BuildBranch(ctx context.Context, branch, oldOID, newOID stri
154 ContentType: "text/css; charset=utf-8", 154 ContentType: "text/css; charset=utf-8",
155 Cache: siteCache, 155 Cache: siteCache,
156 }) 156 })
157 result.Pages = append(result.Pages, brandPages()...)
157 commitsPage, err := g.commitsPage(branch, summary) 158 commitsPage, err := g.commitsPage(branch, summary)
158 if err != nil { 159 if err != nil {
159 return Build{}, err 160 return Build{}, err
@@ -466,7 +467,7 @@ func (g *Generator) pageData(kind, title, branch string, summary branchSummary)
466 Title: title, 467 Title: title,
467 BaseURL: g.BaseURL, 468 BaseURL: g.BaseURL,
468 RepoName: g.RepoName, 469 RepoName: g.RepoName,
469 Owner: summary.Repository.Owner, 470 Owner: g.Owner,
470 CloneURL: g.CloneURL, 471 CloneURL: g.CloneURL,
471 Branch: branch, 472 Branch: branch,
472 Commit: summary.Latest, 473 Commit: summary.Latest,
Modifiedinternal/site/site_test.go +9−2
@@ -36,11 +36,18 @@ func TestFullAndIncrementalBuild(t *testing.T) {
36 assertPage(t, full, "", "Go to file") 36 assertPage(t, full, "", "Go to file")
37 assertPage(t, full, "", "data-branch-menu") 37 assertPage(t, full, "", "data-branch-menu")
38 assertPage(t, full, "", "data-copied-icon") 38 assertPage(t, full, "", "data-copied-icon")
39 assertPage(t, full, "", ".dumbforge/assets/gabriel-mark.svg")
40 assertPage(t, full, "", ">owner</a><span>/</span>")
39 assertPage(t, full, "", "Languages") 41 assertPage(t, full, "", "Languages")
40 assertPage(t, full, "", "Contributors") 42 assertPage(t, full, "", "Contributors")
41 assertPage(t, full, "", "octicon-folder") 43 assertPage(t, full, "", "octicon-folder")
42 assertPage(t, full, "blob/main/src/main.go", "chroma") 44 assertPage(t, full, "blob/main/src/main.go", "chroma")
45 assertPage(t, full, "blob/main/src/main.go", "data-file-tree")
46 assertPage(t, full, ".dumbforge/assets/gabriel-mark.svg", "<svg")
47 assertPage(t, full, ".dumbforge/assets/gabriel-mark.png", "PNG")
43 assertPage(t, full, "tree/main/src", "main.go") 48 assertPage(t, full, "tree/main/src", "main.go")
49 assertPage(t, full, "tree/main/src", `<div class="tree-row">`)
50 assertPageDoesNotContain(t, full, "tree/main/src", `<a class="tree-row"`)
44 assertPage(t, full, "commits/main", "Commit history") 51 assertPage(t, full, "commits/main", "Commit history")
45 assertPage(t, full, "commits/main", "gravatar.com/avatar") 52 assertPage(t, full, "commits/main", "gravatar.com/avatar")
46 assertPage(t, full, ".dumbforge/files/main.json", `"path":"src/main.go"`) 53 assertPage(t, full, ".dumbforge/files/main.json", `"path":"src/main.go"`)
@@ -56,8 +63,8 @@ func TestFullAndIncrementalBuild(t *testing.T) {
56 if err != nil { 63 if err != nil {
57 t.Fatal(err) 64 t.Fatal(err)
58 } 65 }
59 if len(incremental.Pages) != 7 { 66 if len(incremental.Pages) != 9 {
60 t.Fatalf("incremental build generated %d pages, want 7", len(incremental.Pages)) 67 t.Fatalf("incremental build generated %d pages, want 9", len(incremental.Pages))
61 } 68 }
62 assertPage(t, incremental, "blob/main/src/main.go", "changed") 69 assertPage(t, incremental, "blob/main/src/main.go", "changed")
63 assertPage(t, incremental, "tree/main/src", "change one file") 70 assertPage(t, incremental, "tree/main/src", "change one file")
Modifiedinternal/site/style.go +33−2
@@ -30,7 +30,7 @@ button, input { font: inherit; }
30 .topbar { min-height: 64px; padding: 16px 32px; display: flex; align-items: center; gap: 12px; background: var(--canvas); } 30 .topbar { min-height: 64px; padding: 16px 32px; display: flex; align-items: center; gap: 12px; background: var(--canvas); }
31 .brand { display: inline-grid; place-items: center; color: var(--text); } 31 .brand { display: inline-grid; place-items: center; color: var(--text); }
32 .brand:hover { text-decoration: none; } 32 .brand:hover { text-decoration: none; }
33 .brand-mark { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 50%; background: #24292f; color: #fff; font: 800 17px/1 ui-monospace, SFMono-Regular, Consolas, monospace; } 33 .brand-mark { display: block; width: 32px; height: 32px; object-fit: contain; }
34 .header-crumbs { min-width: 0; display: flex; align-items: center; gap: 8px; font-size: 14px; } 34 .header-crumbs { min-width: 0; display: flex; align-items: center; gap: 8px; font-size: 14px; }
35 .header-crumbs a { color: var(--text); white-space: nowrap; } 35 .header-crumbs a { color: var(--text); white-space: nowrap; }
36 .header-repo { overflow: hidden; font-weight: 650; text-overflow: ellipsis; } 36 .header-repo { overflow: hidden; font-weight: 650; text-overflow: ellipsis; }
@@ -44,6 +44,7 @@ button, input { font: inherit; }
44 .counter { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px; background: rgba(175,184,193,.25); color: var(--text); font-size: 12px; font-weight: 600; } 44 .counter { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px; background: rgba(175,184,193,.25); color: var(--text); font-size: 12px; font-weight: 600; }
45 45
46 .content-shell { max-width: 1280px; margin: 0 auto 64px; padding: 24px; } 46 .content-shell { max-width: 1280px; margin: 0 auto 64px; padding: 24px; }
47 .blob-content-shell { max-width: 1600px; }
47 .repo-overview-head { min-height: 34px; margin-bottom: 18px; display: flex; align-items: center; gap: 16px; } 48 .repo-overview-head { min-height: 34px; margin-bottom: 18px; display: flex; align-items: center; gap: 16px; }
48 .repo-title { min-width: 0; display: flex; align-items: center; gap: 7px; font-size: 20px; } 49 .repo-title { min-width: 0; display: flex; align-items: center; gap: 7px; font-size: 20px; }
49 .repo-title > a { overflow: hidden; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; } 50 .repo-title > a { overflow: hidden; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
@@ -102,11 +103,12 @@ button, input { font: inherit; }
102 .tree-rows { width: 100%; } 103 .tree-rows { width: 100%; }
103 .tree-row { min-height: 42px; display: grid; grid-template-columns: 24px minmax(180px, 2fr) minmax(120px, 3fr) 75px; align-items: center; gap: 8px; padding: 6px 16px; color: var(--text); border-top: 1px solid var(--border-muted); } 104 .tree-row { min-height: 42px; display: grid; grid-template-columns: 24px minmax(180px, 2fr) minmax(120px, 3fr) 75px; align-items: center; gap: 8px; padding: 6px 16px; color: var(--text); border-top: 1px solid var(--border-muted); }
104 .tree-row:first-child { border-top: 0; } 105 .tree-row:first-child { border-top: 0; }
105 .tree-row:hover { background: var(--canvas); text-decoration: none; } 106 .tree-row:hover { background: var(--canvas); }
106 .file-icon { width: 18px; height: 18px; display: inline-grid; place-items: center; color: var(--muted); } 107 .file-icon { width: 18px; height: 18px; display: inline-grid; place-items: center; color: var(--muted); }
107 .file-icon.folder { color: #54aeff; } 108 .file-icon.folder { color: #54aeff; }
108 .file-icon .icon { width: 16px; height: 16px; } 109 .file-icon .icon { width: 16px; height: 16px; }
109 .file-name { color: var(--blue); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 110 .file-name { color: var(--blue); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
111 .file-name:hover { text-decoration: underline; }
110 .row-message { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 112 .row-message { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
111 .file-size { color: var(--muted); text-align: right; font-size: 12px; white-space: nowrap; } 113 .file-size { color: var(--muted); text-align: right; font-size: 12px; white-space: nowrap; }
112 .parent-row { grid-template-columns: 24px 1fr 1fr 75px; } 114 .parent-row { grid-template-columns: 24px 1fr 1fr 75px; }
@@ -151,6 +153,33 @@ button, input { font: inherit; }
151 .language-list li > span:last-child { color: var(--muted); } 153 .language-list li > span:last-child { color: var(--muted); }
152 .language-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--language-color); } 154 .language-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--language-color); }
153 155
156 .blob-layout { display: grid; grid-template-columns: 286px minmax(0, 1fr); gap: 20px; align-items: start; }
157 .blob-main { min-width: 0; }
158 .file-browser { position: sticky; top: 16px; height: calc(100vh - 32px); min-height: 420px; display: flex; flex-direction: column; overflow: visible; border: 1px solid var(--border); border-radius: 7px; background: var(--bg); }
159 .file-browser-heading { min-height: 46px; padding: 0 12px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--border-muted); font-size: 15px; }
160 .file-browser-heading .icon { color: var(--muted); }
161 .file-browser-branch { padding: 10px 10px 0; }
162 .file-browser-branch .branch-menu, .file-browser-branch .branch-button { width: 100%; }
163 .file-browser-branch .branch-button { justify-content: flex-start; max-width: none; }
164 .file-browser-branch .branch-button .icon-chevron { margin-left: auto; }
165 .file-tree-search { min-height: 34px; margin: 8px 10px; padding: 0 8px; display: flex; align-items: center; gap: 7px; border: 1px solid var(--border); border-radius: 6px; }
166 .file-tree-search:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(9,105,218,.15); }
167 .file-tree-search .icon { color: var(--muted); }
168 .file-tree-search input { width: 100%; min-width: 0; height: 30px; border: 0; outline: 0; background: transparent; font-size: 13px; }
169 .file-tree { min-height: 0; flex: 1; padding: 2px 8px 12px; overflow: auto; }
170 .file-tree-node { min-width: max-content; }
171 .file-tree-children { padding-left: 16px; }
172 .file-tree-folder, .file-tree-file { width: 100%; min-height: 30px; padding: 4px 6px; display: flex; align-items: center; gap: 7px; border: 0; border-radius: 5px; color: var(--text); background: transparent; text-align: left; white-space: nowrap; }
173 .file-tree-folder { cursor: pointer; }
174 .file-tree-folder:hover, .file-tree-file:hover { background: var(--canvas); text-decoration: none; }
175 .file-tree-folder > .icon { color: #54aeff; }
176 .file-tree-file > .icon { color: var(--muted); }
177 .file-tree-folder > span:last-child, .file-tree-file > span:last-child { overflow: hidden; text-overflow: ellipsis; }
178 .file-tree-caret { width: 10px; flex: none; color: var(--muted); font-size: 17px; line-height: 1; transform-origin: center; transition: transform .1s ease; }
179 .file-tree-caret.open { transform: rotate(90deg); }
180 .file-tree-file { padding-left: 23px; }
181 .file-tree-file.selected { color: var(--text); background: #ddf4ff; font-weight: 600; }
182 .blob-toolbar { min-height: 42px; }
154 .blob-card { border-radius: 6px; } 183 .blob-card { border-radius: 6px; }
155 .blob-heading { min-height: 54px; padding: 10px 14px; display: flex; align-items: center; gap: 12px; background: var(--canvas); border-bottom: 1px solid var(--border); } 184 .blob-heading { min-height: 54px; padding: 10px 14px; display: flex; align-items: center; gap: 12px; background: var(--canvas); border-bottom: 1px solid var(--border); }
156 .blob-heading > div:first-child { min-width: 0; display: flex; flex-direction: column; } 185 .blob-heading > div:first-child { min-width: 0; display: flex; flex-direction: column; }
@@ -209,6 +238,8 @@ button, input { font: inherit; }
209 238
210 @media (max-width: 900px) { 239 @media (max-width: 900px) {
211 .repo-grid { grid-template-columns: 1fr; } 240 .repo-grid { grid-template-columns: 1fr; }
241 .blob-layout { grid-template-columns: 1fr; }
242 .file-browser { display: none; }
212 .sidebar { padding-top: 0; display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 0 24px; } 243 .sidebar { padding-top: 0; display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 0 24px; }
213 .about-section { grid-column: 1 / -1; } 244 .about-section { grid-column: 1 / -1; }
214 .commit-row { grid-template-columns: 32px minmax(0,1fr) auto; } 245 .commit-row { grid-template-columns: 32px minmax(0,1fr) auto; }
Modifiedinternal/site/template.go +141−27
@@ -7,11 +7,13 @@ const pageTemplate = `<!doctype html>
7 <meta name="viewport" content="width=device-width, initial-scale=1"> 7 <meta name="viewport" content="width=device-width, initial-scale=1">
8 <meta name="color-scheme" content="light"> 8 <meta name="color-scheme" content="light">
9 <title>{{.Title}}</title> 9 <title>{{.Title}}</title>
10 <link rel="icon" type="image/png" sizes="256x256" href="{{.BaseURL}}/.dumbforge/assets/gabriel-mark.png">
11 <link rel="icon" type="image/svg+xml" href="{{.BaseURL}}/.dumbforge/assets/gabriel-mark.svg">
10 <link rel="stylesheet" href="{{.BaseURL}}/.dumbforge/assets/site.css"> 12 <link rel="stylesheet" href="{{.BaseURL}}/.dumbforge/assets/site.css">
11 </head> 13 </head>
12 <body data-base-url="{{.BaseURL}}" data-route-kind="{{.Kind}}" data-route-path="{{.Path}}" data-current-branch="{{.Branch}}" data-default-branch="{{.DefaultBranch}}" data-branches-url="{{.BranchesURL}}" data-files-url="{{.FilesURL}}"> 14 <body data-base-url="{{.BaseURL}}" data-route-kind="{{.Kind}}" data-route-path="{{.Path}}" data-current-branch="{{.Branch}}" data-default-branch="{{.DefaultBranch}}" data-branches-url="{{.BranchesURL}}" data-files-url="{{.FilesURL}}">
13 <header class="topbar"> 15 <header class="topbar">
14 <a class="brand" href="{{.BaseURL}}" aria-label="Dumbforge repository home"><span class="brand-mark">D</span></a> 16 <a class="brand" href="{{.BaseURL}}" aria-label="Dumbforge repository home"><img class="brand-mark" src="{{.BaseURL}}/.dumbforge/assets/gabriel-mark.svg" alt=""></a>
15 <nav class="header-crumbs" aria-label="Repository breadcrumb"> 17 <nav class="header-crumbs" aria-label="Repository breadcrumb">
16 <a href="{{.BaseURL}}">{{.Owner}}</a><span>/</span><a class="header-repo" href="{{.BaseURL}}">{{.RepoName}}</a> 18 <a href="{{.BaseURL}}">{{.Owner}}</a><span>/</span><a class="header-repo" href="{{.BaseURL}}">{{.RepoName}}</a>
17 </nav> 19 </nav>
@@ -24,7 +26,7 @@ const pageTemplate = `<!doctype html>
24 </section> 26 </section>
25 27
26 <main> 28 <main>
27 <div class="content-shell"> 29 <div class="content-shell {{if eq .Kind "blob"}}blob-content-shell{{end}}">
28 {{if eq .Kind "repo"}} 30 {{if eq .Kind "repo"}}
29 <div class="repo-overview-head"> 31 <div class="repo-overview-head">
30 <div class="repo-title"><span class="repo-icon">{{icon "repo"}}</span><a href="{{.BaseURL}}">{{.RepoName}}</a><span class="visibility">Public</span></div> 32 <div class="repo-title"><span class="repo-icon">{{icon "repo"}}</span><a href="{{.BaseURL}}">{{.RepoName}}</a><span class="visibility">Public</span></div>
@@ -113,24 +115,31 @@ const pageTemplate = `<!doctype html>
113 </section>{{end}} 115 </section>{{end}}
114 </section> 116 </section>
115 {{else}} 117 {{else}}
116 <section class="primary full-width"> 118 <div class="blob-layout">
117 <div class="toolbar toolbar-path"> 119 <aside class="file-browser" aria-label="Repository files">
118 {{template "branch-menu" .}} 120 <div class="file-browser-heading">{{icon "folder"}}<strong>Files</strong></div>
119 <div class="breadcrumbs">{{range $i, $crumb := .Breadcrumbs}}{{if $i}}<span>/</span>{{end}}<a href="{{$crumb.URL}}">{{$crumb.Name}}</a>{{end}}</div> 121 <div class="file-browser-branch">{{template "branch-menu" .}}</div>
120 <span class="toolbar-spacer"></span> 122 <label class="file-tree-search">{{icon "search"}}<input type="search" placeholder="Search files" autocomplete="off" data-file-tree-query></label>
121 {{template "go-file-button" .}} 123 <nav class="file-tree" data-file-tree><p class="menu-status">Loading files</p></nav>
122 {{template "clone-menu" .}} 124 </aside>
123 </div> 125 <section class="primary blob-main">
124 <div class="blob-card"> 126 <div class="toolbar toolbar-path blob-toolbar">
125 <div class="blob-heading"> 127 <div class="breadcrumbs">{{range $i, $crumb := .Breadcrumbs}}{{if $i}}<span>/</span>{{end}}<a href="{{$crumb.URL}}">{{$crumb.Name}}</a>{{end}}</div>
126 <div><strong>{{.Path}}</strong><span>{{humanSize .FileSize}} · {{.Language}}</span></div> 128 <span class="toolbar-spacer"></span>
127 <div class="blob-actions"><a href="{{.RawURL}}">Raw</a><button type="button" data-copy="{{.RawURL}}"><span data-copy-icon>{{icon "copy"}}</span><span data-copied-icon hidden>{{icon "check"}}</span><span data-copy-label>Copy link</span></button></div> 129 {{template "go-file-button" .}}
130 {{template "clone-menu" .}}
128 </div> 131 </div>
129 {{if .IsImage}}<div class="image-preview"><img src="{{.RawURL}}" alt="{{.Path}}"></div> 132 <div class="blob-card">
130 {{else if .Code}}<div class="code-view">{{.Code}}</div> 133 <div class="blob-heading">
131 {{else}}<div class="binary-notice"><span>{{icon "file"}}</span><strong>Preview unavailable</strong><p>This file is binary or too large to render safely.</p><a href="{{.RawURL}}">Download raw file</a></div>{{end}} 134 <div><strong>{{.Path}}</strong><span>{{humanSize .FileSize}} · {{.Language}}</span></div>
132 </div> 135 <div class="blob-actions"><a href="{{.RawURL}}">Raw</a><button type="button" data-copy="{{.RawURL}}"><span data-copy-icon>{{icon "copy"}}</span><span data-copied-icon hidden>{{icon "check"}}</span><span data-copy-label>Copy link</span></button></div>
133 </section> 136 </div>
137 {{if .IsImage}}<div class="image-preview"><img src="{{.RawURL}}" alt="{{.Path}}"></div>
138 {{else if .Code}}<div class="code-view">{{.Code}}</div>
139 {{else}}<div class="binary-notice"><span>{{icon "file"}}</span><strong>Preview unavailable</strong><p>This file is binary or too large to render safely.</p><a href="{{.RawURL}}">Download raw file</a></div>{{end}}
140 </div>
141 </section>
142 </div>
134 {{end}} 143 {{end}}
135 </div> 144 </div>
136 </main> 145 </main>
@@ -142,6 +151,8 @@ const pageTemplate = `<!doctype html>
142 <div class="file-results" data-file-results><p class="menu-status">Loading files</p></div> 151 <div class="file-results" data-file-results><p class="menu-status">Loading files</p></div>
143 </section> 152 </section>
144 </div> 153 </div>
154 <template data-tree-folder-icon>{{icon "folder"}}</template>
155 <template data-tree-file-icon>{{icon "file"}}</template>
145 156
146 <script> 157 <script>
147 (function () { 158 (function () {
@@ -262,6 +273,23 @@ const pageTemplate = `<!doctype html>
262 var fileQuery = one('[data-file-query]'); 273 var fileQuery = one('[data-file-query]');
263 var fileResults = one('[data-file-results]'); 274 var fileResults = one('[data-file-results]');
264 var files = null; 275 var files = null;
276 var filesPromise = null;
277 function loadFiles() {
278 if (files) return Promise.resolve(files);
279 if (!filesPromise) {
280 filesPromise = fetch(body.dataset.filesUrl, {cache: 'no-cache'}).then(function (response) {
281 if (!response.ok) throw new Error('HTTP ' + response.status);
282 return response.json();
283 }).then(function (manifest) {
284 files = manifest.files || [];
285 return files;
286 }).catch(function (error) {
287 filesPromise = null;
288 throw error;
289 });
290 }
291 return filesPromise;
292 }
265 function renderFiles() { 293 function renderFiles() {
266 if (!files) return; 294 if (!files) return;
267 var query = fileQuery.value.trim().toLowerCase(); 295 var query = fileQuery.value.trim().toLowerCase();
@@ -293,10 +321,7 @@ const pageTemplate = `<!doctype html>
293 fileQuery.value = ''; 321 fileQuery.value = '';
294 fileQuery.focus(); 322 fileQuery.focus();
295 if (files) { renderFiles(); return; } 323 if (files) { renderFiles(); return; }
296 fetch(body.dataset.filesUrl, {cache: 'no-cache'}).then(function (response) { 324 loadFiles().then(renderFiles)
297 if (!response.ok) throw new Error('HTTP ' + response.status);
298 return response.json();
299 }).then(function (manifest) { files = manifest.files || []; renderFiles(); })
300 .catch(function () { fileResults.innerHTML = '<p class="menu-status">Could not load the file list.</p>'; }); 325 .catch(function () { fileResults.innerHTML = '<p class="menu-status">Could not load the file list.</p>'; });
301 } 326 }
302 function closeFiles() { 327 function closeFiles() {
@@ -310,6 +335,95 @@ const pageTemplate = `<!doctype html>
310 fileQuery.addEventListener('keydown', function (event) { 335 fileQuery.addEventListener('keydown', function (event) {
311 if (event.key === 'Enter') { var first = one('.file-result', fileResults); if (first) window.location.href = first.href; } 336 if (event.key === 'Enter') { var first = one('.file-result', fileResults); if (first) window.location.href = first.href; }
312 }); 337 });
338
339 var fileTree = one('[data-file-tree]');
340 var fileTreeQuery = one('[data-file-tree-query]');
341 function addTreeIcon(target, selector) {
342 var source = one(selector);
343 if (source) target.appendChild(source.content.cloneNode(true));
344 }
345 function fileTreeLink(file, label, compact) {
346 var link = document.createElement('a');
347 link.className = 'file-tree-file';
348 link.href = file.url;
349 addTreeIcon(link, '[data-tree-file-icon]');
350 var text = document.createElement('span');
351 text.textContent = label;
352 if (compact) text.title = file.path;
353 link.appendChild(text);
354 if (file.path === body.dataset.routePath) {
355 link.classList.add('selected');
356 link.setAttribute('aria-current', 'page');
357 }
358 return link;
359 }
360 function renderTreeNode(node, container, prefix) {
361 Object.keys(node.folders).sort().forEach(function (name) {
362 var child = node.folders[name];
363 var childPath = prefix ? prefix + '/' + name : name;
364 var wrapper = document.createElement('div');
365 wrapper.className = 'file-tree-node';
366 var toggle = document.createElement('button');
367 toggle.className = 'file-tree-folder';
368 toggle.type = 'button';
369 var caret = document.createElement('span');
370 caret.className = 'file-tree-caret';
371 caret.textContent = '›';
372 toggle.appendChild(caret);
373 addTreeIcon(toggle, '[data-tree-folder-icon]');
374 var label = document.createElement('span');
375 label.textContent = name;
376 toggle.appendChild(label);
377 var children = document.createElement('div');
378 children.className = 'file-tree-children';
379 var open = body.dataset.routePath === childPath || body.dataset.routePath.indexOf(childPath + '/') === 0;
380 children.hidden = !open;
381 caret.classList.toggle('open', open);
382 toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
383 toggle.addEventListener('click', function () {
384 var opening = children.hidden;
385 children.hidden = !opening;
386 caret.classList.toggle('open', opening);
387 toggle.setAttribute('aria-expanded', opening ? 'true' : 'false');
388 });
389 wrapper.appendChild(toggle);
390 wrapper.appendChild(children);
391 container.appendChild(wrapper);
392 renderTreeNode(child, children, childPath);
393 });
394 node.files.sort(function (a, b) { return a.name.localeCompare(b.name); }).forEach(function (entry) {
395 container.appendChild(fileTreeLink(entry.file, entry.name, false));
396 });
397 }
398 function renderFileTree() {
399 if (!fileTree || !files) return;
400 var query = fileTreeQuery.value.trim().toLowerCase();
401 fileTree.textContent = '';
402 if (query) {
403 var matches = files.filter(function (file) { return file.path.toLowerCase().indexOf(query) !== -1; });
404 matches.slice(0, 200).forEach(function (file) { fileTree.appendChild(fileTreeLink(file, file.path, true)); });
405 if (!matches.length) fileTree.innerHTML = '<p class="menu-status">No matching files.</p>';
406 return;
407 }
408 var root = {folders: {}, files: []};
409 files.forEach(function (file) {
410 var parts = file.path.split('/');
411 var node = root;
412 parts.slice(0, -1).forEach(function (part) {
413 if (!node.folders[part]) node.folders[part] = {folders: {}, files: []};
414 node = node.folders[part];
415 });
416 node.files.push({name: parts[parts.length - 1], file: file});
417 });
418 renderTreeNode(root, fileTree, '');
419 var selected = one('.file-tree-file.selected', fileTree);
420 if (selected) selected.scrollIntoView({block: 'center'});
421 }
422 if (fileTree) {
423 loadFiles().then(renderFileTree).catch(function () { fileTree.innerHTML = '<p class="menu-status">Could not load files.</p>'; });
424 fileTreeQuery.addEventListener('input', renderFileTree);
425 }
426
313 document.addEventListener('keydown', function (event) { 427 document.addEventListener('keydown', function (event) {
314 if (event.key !== 'Escape') return; 428 if (event.key !== 'Escape') return;
315 if (!fileDialog.hidden) { closeFiles(); return; } 429 if (!fileDialog.hidden) { closeFiles(); return; }
@@ -357,13 +471,13 @@ const pageTemplate = `<!doctype html>
357 <a class="commit-count-link" href="{{.CommitsURL}}">{{icon "history"}}<strong>{{.CommitCount}}</strong> commits</a> 471 <a class="commit-count-link" href="{{.CommitsURL}}">{{icon "history"}}<strong>{{.CommitCount}}</strong> commits</a>
358 </div> 472 </div>
359 <div class="tree-rows"> 473 <div class="tree-rows">
360 {{if .ParentURL}}<a class="tree-row parent-row" href="{{.ParentURL}}"><span class="file-icon">{{icon "back"}}</span><span class="file-name">..</span><span></span><span></span></a>{{end}} 474 {{if .ParentURL}}<div class="tree-row parent-row"><span class="file-icon">{{icon "back"}}</span><a class="file-name" href="{{.ParentURL}}">..</a><span></span><span></span></div>{{end}}
361 {{range .Entries}}<a class="tree-row" href="{{.URL}}"> 475 {{range .Entries}}<div class="tree-row">
362 {{if eq .Type "tree"}}<span class="file-icon folder">{{icon "folder"}}</span>{{else}}<span class="file-icon">{{icon "file"}}</span>{{end}} 476 {{if eq .Type "tree"}}<span class="file-icon folder">{{icon "folder"}}</span>{{else}}<span class="file-icon">{{icon "file"}}</span>{{end}}
363 <span class="file-name">{{.Name}}</span> 477 <a class="file-name" href="{{.URL}}">{{.Name}}</a>
364 <span class="row-message">{{$.Commit.Message}}</span> 478 <span class="row-message">{{$.Commit.Message}}</span>
365 <span class="file-size">{{humanSize .Size}}</span> 479 <span class="file-size">{{humanSize .Size}}</span>
366 </a>{{end}} 480 </div>{{end}}
367 </div> 481 </div>
368 </div> 482 </div>
369 {{end}}` 483 {{end}}`