Match README card presentation
- SHA
0063bfe3b645a1b9f70be9ca9175660c0f7e3bf3- Parents
-
adf15d6 - Tree
0176264
0063bfe
0063bfe3b645a1b9f70be9ca9175660c0f7e3bf3adf15d6
0176264internal/web/handlers/repo/code.gomodified@@ -166,6 +166,7 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co | |||
| 166 | h.d.Logger.WarnContext(r.Context(), "code: about root LsTree", "error", rerr) | 166 | h.d.Logger.WarnContext(r.Context(), "code: about root LsTree", "error", rerr) |
| 167 | } | 167 | } |
| 168 | } | 168 | } |
| 169 | + about := h.repoAbout(r.Context(), cc.gitDir, cc.ref, cc.owner, cc.row, aboutEntries) | ||
| 169 | 170 | ||
| 170 | h.d.Render.RenderPage(w, r, "repo/tree", map[string]any{ | 171 | h.d.Render.RenderPage(w, r, "repo/tree", map[string]any{ |
| 171 | "Title": cc.row.Name + " · " + cc.owner, | 172 | "Title": cc.row.Name + " · " + cc.owner, |
@@ -188,7 +189,8 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co | |||
| 188 | "SSHEnabled": h.d.CloneURLs.SSHEnabled, | 189 | "SSHEnabled": h.d.CloneURLs.SSHEnabled, |
| 189 | "SSHCloneURL": h.cloneSSH(cc.owner, cc.row.Name), | 190 | "SSHCloneURL": h.cloneSSH(cc.owner, cc.row.Name), |
| 190 | "RepoTopics": topics, | 191 | "RepoTopics": topics, |
| 191 | - "RepoAbout": h.repoAbout(r.Context(), cc.gitDir, cc.ref, cc.owner, cc.row, aboutEntries), | 192 | + "RepoAbout": about, |
| 193 | + "ReadmeTabs": repoReadmeTabs(about.Resources), | ||
| 192 | "RepoActions": h.repoActions(r, cc.row.ID), | 194 | "RepoActions": h.repoActions(r, cc.row.ID), |
| 193 | "RepoCounts": h.subnavCounts(r.Context(), cc.row.ID, cc.row.ForkCount), | 195 | "RepoCounts": h.subnavCounts(r.Context(), cc.row.ID, cc.row.ForkCount), |
| 194 | "CanSettings": h.canViewSettings(middleware.CurrentUserFromContext(r.Context())), | 196 | "CanSettings": h.canViewSettings(middleware.CurrentUserFromContext(r.Context())), |
@@ -450,8 +452,10 @@ func isHex(s string) bool { | |||
| 450 | func rawContentType(p string) (string, bool) { | 452 | func rawContentType(p string) (string, bool) { |
| 451 | ext := strings.ToLower(path.Ext(p)) | 453 | ext := strings.ToLower(path.Ext(p)) |
| 452 | switch ext { | 454 | switch ext { |
| 453 | - case ".html", ".htm", ".xhtml", ".svg", ".js", ".mjs", ".wasm": | 455 | + case ".html", ".htm", ".xhtml", ".js", ".mjs", ".wasm": |
| 454 | return "text/plain; charset=utf-8", true | 456 | return "text/plain; charset=utf-8", true |
| 457 | + case ".svg": | ||
| 458 | + return "image/svg+xml", false | ||
| 455 | case ".png": | 459 | case ".png": |
| 456 | return "image/png", false | 460 | return "image/png", false |
| 457 | case ".jpg", ".jpeg": | 461 | case ".jpg", ".jpeg": |
@@ -477,7 +481,7 @@ func rawContentType(p string) (string, bool) { | |||
| 477 | 481 | ||
| 478 | func isImageExt(p string) bool { | 482 | func isImageExt(p string) bool { |
| 479 | switch strings.ToLower(path.Ext(p)) { | 483 | switch strings.ToLower(path.Ext(p)) { |
| 480 | - case ".png", ".jpg", ".jpeg", ".gif", ".webp": | 484 | + case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg": |
| 481 | return true | 485 | return true |
| 482 | } | 486 | } |
| 483 | return false | 487 | return false |
internal/web/handlers/repo/markdown_links_test.gomodified@@ -75,3 +75,14 @@ func TestRewriteRelativeMarkdownURL(t *testing.T) { | |||
| 75 | t.Fatalf("rewrite = %q, want %q", got, want) | 75 | t.Fatalf("rewrite = %q, want %q", got, want) |
| 76 | } | 76 | } |
| 77 | } | 77 | } |
| 78 | + | ||
| 79 | +func TestRawContentType_AllowsSVGImages(t *testing.T) { | ||
| 80 | + t.Parallel() | ||
| 81 | + contentType, attachment := rawContentType("internal/web/static/logo/shithub-mark.svg") | ||
| 82 | + if contentType != "image/svg+xml" || attachment { | ||
| 83 | + t.Fatalf("rawContentType(svg) = (%q, %v), want image/svg+xml without attachment", contentType, attachment) | ||
| 84 | + } | ||
| 85 | + if !isImageExt("logo.svg") { | ||
| 86 | + t.Fatalf("svg should be treated as an image extension") | ||
| 87 | + } | ||
| 88 | +} | ||
internal/web/static/css/shithub.cssmodified@@ -1614,13 +1614,91 @@ button.shithub-repo-action { | |||
| 1614 | .shithub-readme-head { | 1614 | .shithub-readme-head { |
| 1615 | display: flex; | 1615 | display: flex; |
| 1616 | align-items: center; | 1616 | align-items: center; |
| 1617 | + min-height: 48px; | ||
| 1618 | + padding: 0 1rem; | ||
| 1619 | + border-bottom: 1px solid var(--border-default); | ||
| 1620 | + overflow-x: auto; | ||
| 1621 | +} | ||
| 1622 | +.shithub-readme-tabs { | ||
| 1623 | + display: flex; | ||
| 1624 | + align-items: stretch; | ||
| 1625 | + gap: 1.25rem; | ||
| 1626 | + min-width: 0; | ||
| 1627 | +} | ||
| 1628 | +.shithub-readme-tab { | ||
| 1629 | + display: inline-flex; | ||
| 1630 | + align-items: center; | ||
| 1617 | gap: 0.45rem; | 1631 | gap: 0.45rem; |
| 1618 | - padding: 0.75rem 1rem; | 1632 | + margin-bottom: -1px; |
| 1633 | + padding: 0.85rem 0 0.78rem; | ||
| 1634 | + border-bottom: 2px solid transparent; | ||
| 1635 | + color: var(--fg-muted); | ||
| 1636 | + font-size: 0.85rem; | ||
| 1637 | + font-weight: 600; | ||
| 1638 | + text-decoration: none; | ||
| 1639 | + white-space: nowrap; | ||
| 1640 | +} | ||
| 1641 | +.shithub-readme-tab:hover { | ||
| 1642 | + color: var(--fg-default); | ||
| 1643 | + text-decoration: none; | ||
| 1644 | +} | ||
| 1645 | +.shithub-readme-tab.is-active { | ||
| 1646 | + border-bottom-color: var(--accent-emphasis, #fd8c73); | ||
| 1647 | + color: var(--fg-default); | ||
| 1648 | +} | ||
| 1649 | +.shithub-readme-body { padding: 2rem; } | ||
| 1650 | +.markdown-body { | ||
| 1651 | + color: var(--fg-default); | ||
| 1652 | + font-size: 0.875rem; | ||
| 1653 | + line-height: 1.5; | ||
| 1654 | + overflow-wrap: break-word; | ||
| 1655 | +} | ||
| 1656 | +.markdown-body > :first-child { margin-top: 0; } | ||
| 1657 | +.markdown-body > :last-child { margin-bottom: 0; } | ||
| 1658 | +.markdown-body p, | ||
| 1659 | +.markdown-body blockquote, | ||
| 1660 | +.markdown-body ul, | ||
| 1661 | +.markdown-body ol, | ||
| 1662 | +.markdown-body dl, | ||
| 1663 | +.markdown-body table, | ||
| 1664 | +.markdown-body pre, | ||
| 1665 | +.markdown-body details { | ||
| 1666 | + margin-top: 0; | ||
| 1667 | + margin-bottom: 1rem; | ||
| 1668 | +} | ||
| 1669 | +.markdown-body h1, | ||
| 1670 | +.markdown-body h2 { | ||
| 1671 | + margin-top: 1.5rem; | ||
| 1672 | + margin-bottom: 1rem; | ||
| 1673 | + padding-bottom: 0.3em; | ||
| 1619 | border-bottom: 1px solid var(--border-default); | 1674 | border-bottom: 1px solid var(--border-default); |
| 1675 | +} | ||
| 1676 | +.markdown-body h1 { font-size: 2em; } | ||
| 1677 | +.markdown-body h2 { font-size: 1.5em; } | ||
| 1678 | +.markdown-body h3 { font-size: 1.25em; } | ||
| 1679 | +.markdown-body h1, | ||
| 1680 | +.markdown-body h2, | ||
| 1681 | +.markdown-body h3, | ||
| 1682 | +.markdown-body h4, | ||
| 1683 | +.markdown-body h5, | ||
| 1684 | +.markdown-body h6 { | ||
| 1620 | font-weight: 600; | 1685 | font-weight: 600; |
| 1686 | + line-height: 1.25; | ||
| 1687 | +} | ||
| 1688 | +.markdown-body hr { | ||
| 1689 | + height: 0.25em; | ||
| 1690 | + padding: 0; | ||
| 1691 | + margin: 1.5rem 0; | ||
| 1692 | + background: var(--border-default); | ||
| 1693 | + border: 0; | ||
| 1694 | +} | ||
| 1695 | +.markdown-body img { | ||
| 1696 | + max-width: 100%; | ||
| 1697 | + box-sizing: content-box; | ||
| 1621 | } | 1698 | } |
| 1622 | -.shithub-readme-body { padding: 1rem; } | 1699 | +.markdown-body [align="center"] { text-align: center; } |
| 1623 | -.shithub-readme h1, .shithub-readme h2 { border-bottom: 1px solid var(--border-default); padding-bottom: 0.3rem; } | 1700 | +.markdown-body [align="right"] { text-align: right; } |
| 1701 | +.markdown-body [align="left"] { text-align: left; } | ||
| 1624 | .shithub-readme code { font-family: monospace; padding: 0.1em 0.3em; background: var(--canvas-default); border-radius: 3px; } | 1702 | .shithub-readme code { font-family: monospace; padding: 0.1em 0.3em; background: var(--canvas-default); border-radius: 3px; } |
| 1625 | .shithub-readme pre code { padding: 0; background: none; } | 1703 | .shithub-readme pre code { padding: 0; background: none; } |
| 1626 | .shithub-readme-plain { white-space: pre-wrap; } | 1704 | .shithub-readme-plain { white-space: pre-wrap; } |
internal/web/templates/repo/tree.htmlmodified@@ -122,7 +122,20 @@ | |||
| 122 | </section> | 122 | </section> |
| 123 | {{ if .README }} | 123 | {{ if .README }} |
| 124 | <section class="shithub-readme markdown-body" id="readme" aria-label="README"> | 124 | <section class="shithub-readme markdown-body" id="readme" aria-label="README"> |
| 125 | - <div class="shithub-readme-head">{{ octicon "book" }} README</div> | 125 | + <div class="shithub-readme-head"> |
| 126 | + {{ if .ReadmeTabs }} | ||
| 127 | + <nav class="shithub-readme-tabs" aria-label="Repository documents"> | ||
| 128 | + {{ range .ReadmeTabs }} | ||
| 129 | + <a href="{{ .Href }}" class="shithub-readme-tab{{ if .Active }} is-active{{ end }}"> | ||
| 130 | + {{ octicon .Icon }} | ||
| 131 | + <span>{{ .Label }}</span> | ||
| 132 | + </a> | ||
| 133 | + {{ end }} | ||
| 134 | + </nav> | ||
| 135 | + {{ else }} | ||
| 136 | + <span class="shithub-readme-tab is-active">{{ octicon "book" }} <span>README</span></span> | ||
| 137 | + {{ end }} | ||
| 138 | + </div> | ||
| 126 | <div class="shithub-readme-body">{{ .README }}</div> | 139 | <div class="shithub-readme-body">{{ .README }}</div> |
| 127 | </section> | 140 | </section> |
| 128 | {{ end }} | 141 | {{ end }} |