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 | 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 | 171 | h.d.Render.RenderPage(w, r, "repo/tree", map[string]any{ |
| 171 | 172 | "Title": cc.row.Name + " · " + cc.owner, |
@@ -188,7 +189,8 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co | ||
| 188 | 189 | "SSHEnabled": h.d.CloneURLs.SSHEnabled, |
| 189 | 190 | "SSHCloneURL": h.cloneSSH(cc.owner, cc.row.Name), |
| 190 | 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 | 194 | "RepoActions": h.repoActions(r, cc.row.ID), |
| 193 | 195 | "RepoCounts": h.subnavCounts(r.Context(), cc.row.ID, cc.row.ForkCount), |
| 194 | 196 | "CanSettings": h.canViewSettings(middleware.CurrentUserFromContext(r.Context())), |
@@ -450,8 +452,10 @@ func isHex(s string) bool { | ||
| 450 | 452 | func rawContentType(p string) (string, bool) { |
| 451 | 453 | ext := strings.ToLower(path.Ext(p)) |
| 452 | 454 | switch ext { |
| 453 | - case ".html", ".htm", ".xhtml", ".svg", ".js", ".mjs", ".wasm": | |
| 455 | + case ".html", ".htm", ".xhtml", ".js", ".mjs", ".wasm": | |
| 454 | 456 | return "text/plain; charset=utf-8", true |
| 457 | + case ".svg": | |
| 458 | + return "image/svg+xml", false | |
| 455 | 459 | case ".png": |
| 456 | 460 | return "image/png", false |
| 457 | 461 | case ".jpg", ".jpeg": |
@@ -477,7 +481,7 @@ func rawContentType(p string) (string, bool) { | ||
| 477 | 481 | |
| 478 | 482 | func isImageExt(p string) bool { |
| 479 | 483 | switch strings.ToLower(path.Ext(p)) { |
| 480 | - case ".png", ".jpg", ".jpeg", ".gif", ".webp": | |
| 484 | + case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg": | |
| 481 | 485 | return true |
| 482 | 486 | } |
| 483 | 487 | return false |
internal/web/handlers/repo/markdown_links_test.gomodified@@ -75,3 +75,14 @@ func TestRewriteRelativeMarkdownURL(t *testing.T) { | ||
| 75 | 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 | 1614 | .shithub-readme-head { |
| 1615 | 1615 | display: flex; |
| 1616 | 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 | 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 | 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 | 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; } | |
| 1623 | -.shithub-readme h1, .shithub-readme h2 { border-bottom: 1px solid var(--border-default); padding-bottom: 0.3rem; } | |
| 1699 | +.markdown-body [align="center"] { text-align: center; } | |
| 1700 | +.markdown-body [align="right"] { text-align: right; } | |
| 1701 | +.markdown-body [align="left"] { text-align: left; } | |
| 1624 | 1702 | .shithub-readme code { font-family: monospace; padding: 0.1em 0.3em; background: var(--canvas-default); border-radius: 3px; } |
| 1625 | 1703 | .shithub-readme pre code { padding: 0; background: none; } |
| 1626 | 1704 | .shithub-readme-plain { white-space: pre-wrap; } |
internal/web/templates/repo/tree.htmlmodified@@ -122,7 +122,20 @@ | ||
| 122 | 122 | </section> |
| 123 | 123 | {{ if .README }} |
| 124 | 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 | 139 | <div class="shithub-readme-body">{{ .README }}</div> |
| 127 | 140 | </section> |
| 128 | 141 | {{ end }} |