Add repo product tab shells
- SHA
bc5cbb9f9c7281275bfa794402d18338e37586fc- Parents
-
4463c19 - Tree
44530dd
bc5cbb9
bc5cbb9f9c7281275bfa794402d18338e37586fc4463c19
44530dd| Status | File | + | - |
|---|---|---|---|
| M |
docs/internal/code-tab.md
|
24 | 3 |
| A |
internal/web/handlers/repo/deferred_tabs.go
|
141 | 0 |
| M |
internal/web/handlers/repo/repo.go
|
11 | 3 |
| M |
internal/web/render/octicons.go
|
2 | 0 |
| M |
internal/web/static/css/shithub.css
|
96 | 1 |
| M |
internal/web/templates/_repo_subnav.html
|
14 | 3 |
| A |
internal/web/templates/repo/deferred_tab.html
|
30 | 0 |
docs/internal/code-tab.mdmodified@@ -2,24 +2,45 @@ | ||
| 2 | 2 | |
| 3 | 3 | The code tab is the GitHub-style repo browser: tree listing, blob view |
| 4 | 4 | with syntax highlighting, raw view, "Go to file" finder, and the |
| 5 | -branch/tag switcher. After a successful push, hitting `/{owner}/{repo}` | |
| 6 | -sends the user to `/tree/{default_branch}`. | |
| 5 | +branch/tag switcher. For populated repos, `/{owner}/{repo}` renders the | |
| 6 | +default branch Code tab directly, matching GitHub's canonical repo URL. | |
| 7 | 7 | |
| 8 | 8 | ## Routes |
| 9 | 9 | |
| 10 | 10 | | Route | Handler | |
| 11 | 11 | | ------------------------------------------------ | -------------------------------- | |
| 12 | -| `GET /{owner}/{repo}` | redirects to `/tree/{default}` | | |
| 12 | +| `GET /{owner}/{repo}` | default-branch Code tab | | |
| 13 | 13 | | `GET /{owner}/{repo}/tree/{ref}/{path...}` | `codeTree` | |
| 14 | 14 | | `GET /{owner}/{repo}/blob/{ref}/{path...}` | `codeBlob` | |
| 15 | 15 | | `GET /{owner}/{repo}/raw/{ref}/{path...}` | `codeRaw` | |
| 16 | 16 | | `GET /{owner}/{repo}/find/{ref}?q=...` | `codeFinder` | |
| 17 | +| `GET /{owner}/{repo}/actions` | parked product-tab shell | | |
| 18 | +| `GET /{owner}/{repo}/projects` | parked product-tab shell | | |
| 19 | +| `GET /{owner}/{repo}/wiki` | parked product-tab shell | | |
| 20 | +| `GET /{owner}/{repo}/security` | parked product-tab shell | | |
| 21 | +| `GET /{owner}/{repo}/pulse` | parked product-tab shell | | |
| 22 | +| `GET /{owner}/{repo}/packages` | parked product-tab shell | | |
| 23 | +| `GET /{owner}/{repo}/releases` | parked product-tab shell | | |
| 17 | 24 | | `GET /static/css/chroma.css` | runtime-generated Chroma theme | |
| 18 | 25 | |
| 19 | 26 | Every code-tab handler runs through `policy.Can(... ActionRepoRead)` — |
| 20 | 27 | private repos hide from anonymous viewers and unrelated users via the |
| 21 | 28 | existence-leak 404 guard from S15. |
| 22 | 29 | |
| 30 | +## Repository product tabs | |
| 31 | + | |
| 32 | +The repo header intentionally exposes GitHub's major product-map tabs: | |
| 33 | +Code, Issues, Pull requests, Actions, Projects, Wiki, Security and | |
| 34 | +quality, Insights, and Settings when visible to the viewer. Forks remain | |
| 35 | +available from the repo action button and About sidebar, but are not a | |
| 36 | +top-level tab on GitHub. | |
| 37 | + | |
| 38 | +Actions, Projects, Wiki, Security and quality, Insights, Packages, and | |
| 39 | +Releases currently render honest parked shells via `repo/deferred_tab`. | |
| 40 | +They are public read surfaces gated by `ActionRepoRead`, so private repo | |
| 41 | +existence behavior matches Code/Issues/Pull requests while the deeper | |
| 42 | +systems remain assigned to their later sprints. | |
| 43 | + | |
| 23 | 44 | ## Ref + path disambiguation |
| 24 | 45 | |
| 25 | 46 | `{ref}` is the chi `*` wildcard, so the URL `/tree/feature/x/sub/file.go` |
internal/web/handlers/repo/deferred_tabs.goadded@@ -0,0 +1,141 @@ | ||
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | |
| 3 | +package repo | |
| 4 | + | |
| 5 | +import ( | |
| 6 | + "net/http" | |
| 7 | + | |
| 8 | + "github.com/tenseleyFlow/shithub/internal/auth/policy" | |
| 9 | + reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" | |
| 10 | + "github.com/tenseleyFlow/shithub/internal/web/middleware" | |
| 11 | +) | |
| 12 | + | |
| 13 | +type repoDeferredTab struct { | |
| 14 | + Active string | |
| 15 | + Heading string | |
| 16 | + Description string | |
| 17 | + Icon string | |
| 18 | + Sections []repoDeferredSection | |
| 19 | +} | |
| 20 | + | |
| 21 | +type repoDeferredSection struct { | |
| 22 | + Anchor string | |
| 23 | + Title string | |
| 24 | + Body string | |
| 25 | +} | |
| 26 | + | |
| 27 | +func (h *Handlers) repoTabActions(w http.ResponseWriter, r *http.Request) { | |
| 28 | + h.renderDeferredRepoTab(w, r, repoDeferredTab{ | |
| 29 | + Active: "actions", | |
| 30 | + Heading: "Actions", | |
| 31 | + Description: "Automate your development workflow with repository workflows and check runs.", | |
| 32 | + Icon: "play", | |
| 33 | + Sections: []repoDeferredSection{ | |
| 34 | + {Anchor: "all-workflows", Title: "All workflows", Body: "Workflow execution is parked for the Actions sprint. Check runs posted by external systems still appear on pull requests."}, | |
| 35 | + {Anchor: "workflows", Title: "Workflows", Body: "Add workflow files later under .shithub/workflows to run jobs on push and pull request events."}, | |
| 36 | + }, | |
| 37 | + }) | |
| 38 | +} | |
| 39 | + | |
| 40 | +func (h *Handlers) repoTabProjects(w http.ResponseWriter, r *http.Request) { | |
| 41 | + h.renderDeferredRepoTab(w, r, repoDeferredTab{ | |
| 42 | + Active: "projects", | |
| 43 | + Heading: "Projects", | |
| 44 | + Description: "Organize issues and pull requests with repository project views.", | |
| 45 | + Icon: "table", | |
| 46 | + Sections: []repoDeferredSection{ | |
| 47 | + {Anchor: "open", Title: "Open", Body: "No projects have been created for this repository."}, | |
| 48 | + {Anchor: "closed", Title: "Closed", Body: "Closed repository projects will appear here."}, | |
| 49 | + }, | |
| 50 | + }) | |
| 51 | +} | |
| 52 | + | |
| 53 | +func (h *Handlers) repoTabWiki(w http.ResponseWriter, r *http.Request) { | |
| 54 | + h.renderDeferredRepoTab(w, r, repoDeferredTab{ | |
| 55 | + Active: "wiki", | |
| 56 | + Heading: "Wiki", | |
| 57 | + Description: "Create long-form documentation pages connected to this repository.", | |
| 58 | + Icon: "book", | |
| 59 | + Sections: []repoDeferredSection{ | |
| 60 | + {Anchor: "pages", Title: "Pages", Body: "No wiki pages have been created yet."}, | |
| 61 | + }, | |
| 62 | + }) | |
| 63 | +} | |
| 64 | + | |
| 65 | +func (h *Handlers) repoTabSecurity(w http.ResponseWriter, r *http.Request) { | |
| 66 | + h.renderDeferredRepoTab(w, r, repoDeferredTab{ | |
| 67 | + Active: "security", | |
| 68 | + Heading: "Security and quality", | |
| 69 | + Description: "Review repository security posture, policy files, alerts, and dependency health.", | |
| 70 | + Icon: "shield-check", | |
| 71 | + Sections: []repoDeferredSection{ | |
| 72 | + {Anchor: "overview", Title: "Overview", Body: "Security overview is not wired yet."}, | |
| 73 | + {Anchor: "policy", Title: "Policy", Body: "Security policy detection already appears in the repository About sidebar when SECURITY.md exists."}, | |
| 74 | + {Anchor: "alerts", Title: "Alerts", Body: "Code scanning, secret scanning, and dependency alerts are reserved for later security sprints."}, | |
| 75 | + }, | |
| 76 | + }) | |
| 77 | +} | |
| 78 | + | |
| 79 | +func (h *Handlers) repoTabInsights(w http.ResponseWriter, r *http.Request) { | |
| 80 | + h.renderDeferredRepoTab(w, r, repoDeferredTab{ | |
| 81 | + Active: "insights", | |
| 82 | + Heading: "Insights", | |
| 83 | + Description: "Understand activity, contributors, traffic, community health, and dependency graph data.", | |
| 84 | + Icon: "pulse", | |
| 85 | + Sections: []repoDeferredSection{ | |
| 86 | + {Anchor: "pulse", Title: "Pulse", Body: "Repository pulse data is not available yet."}, | |
| 87 | + {Anchor: "contributors", Title: "Contributors", Body: "Contributor graphs will be derived from git history in a later pass."}, | |
| 88 | + {Anchor: "community", Title: "Community", Body: "README, license, code of conduct, contributing, and security files are already detected on the Code tab."}, | |
| 89 | + }, | |
| 90 | + }) | |
| 91 | +} | |
| 92 | + | |
| 93 | +func (h *Handlers) repoTabPackages(w http.ResponseWriter, r *http.Request) { | |
| 94 | + h.renderDeferredRepoTab(w, r, repoDeferredTab{ | |
| 95 | + Active: "packages", | |
| 96 | + Heading: "Packages", | |
| 97 | + Description: "Publish and install packages associated with this repository.", | |
| 98 | + Icon: "package", | |
| 99 | + Sections: []repoDeferredSection{ | |
| 100 | + {Anchor: "repository-packages", Title: "Repository packages", Body: "No packages have been published for this repository."}, | |
| 101 | + }, | |
| 102 | + }) | |
| 103 | +} | |
| 104 | + | |
| 105 | +func (h *Handlers) repoTabReleases(w http.ResponseWriter, r *http.Request) { | |
| 106 | + h.renderDeferredRepoTab(w, r, repoDeferredTab{ | |
| 107 | + Active: "releases", | |
| 108 | + Heading: "Releases", | |
| 109 | + Description: "Package release notes, binaries, and source archives from repository tags.", | |
| 110 | + Icon: "tag", | |
| 111 | + Sections: []repoDeferredSection{ | |
| 112 | + {Anchor: "latest-release", Title: "Latest release", Body: "No releases have been published yet."}, | |
| 113 | + {Anchor: "tags", Title: "Tags", Body: "Tags are available from the Code tab and the repository tags page."}, | |
| 114 | + }, | |
| 115 | + }) | |
| 116 | +} | |
| 117 | + | |
| 118 | +func (h *Handlers) renderDeferredRepoTab(w http.ResponseWriter, r *http.Request, tab repoDeferredTab) { | |
| 119 | + row, owner, ok := h.loadRepoAndAuthorize(w, r, policy.ActionRepoRead) | |
| 120 | + if !ok { | |
| 121 | + return | |
| 122 | + } | |
| 123 | + data := h.repoHeaderData(r, row, owner.Username, tab.Active) | |
| 124 | + data["Title"] = tab.Heading + " · " + row.Name | |
| 125 | + data["Tab"] = tab | |
| 126 | + if err := h.d.Render.RenderPage(w, r, "repo/deferred_tab", data); err != nil { | |
| 127 | + h.d.Logger.ErrorContext(r.Context(), "repo deferred tab render", "tab", tab.Active, "error", err) | |
| 128 | + } | |
| 129 | +} | |
| 130 | + | |
| 131 | +func (h *Handlers) repoHeaderData(r *http.Request, row reposdb.Repo, owner, active string) map[string]any { | |
| 132 | + return map[string]any{ | |
| 133 | + "CSRFToken": middleware.CSRFTokenForRequest(r), | |
| 134 | + "Owner": owner, | |
| 135 | + "Repo": row, | |
| 136 | + "RepoActions": h.repoActions(r, row.ID), | |
| 137 | + "RepoCounts": h.subnavCounts(r.Context(), row.ID, row.ForkCount), | |
| 138 | + "CanSettings": h.canViewSettings(middleware.CurrentUserFromContext(r.Context())), | |
| 139 | + "ActiveSubnav": active, | |
| 140 | + } | |
| 141 | +} | |
internal/web/handlers/repo/repo.gomodified@@ -113,10 +113,18 @@ func (h *Handlers) MountNew(r chi.Router) { | ||
| 113 | 113 | r.Post("/new", h.newRepoSubmit) |
| 114 | 114 | } |
| 115 | 115 | |
| 116 | -// MountRepoHome registers /{owner}/{repo}. This is a 2-segment route so | |
| 117 | -// it doesn't collide with the /{username} catch-all from S09. Caller is | |
| 118 | -// responsible for ordering: register this BEFORE /{username}. | |
| 116 | +// MountRepoHome registers the root repository route plus product-tab shells | |
| 117 | +// that are intentionally public and read-gated like the Code tab. The | |
| 118 | +// two-segment route doesn't collide with the /{username} catch-all from S09; | |
| 119 | +// caller is responsible for ordering this BEFORE /{username}. | |
| 119 | 120 | func (h *Handlers) MountRepoHome(r chi.Router) { |
| 121 | + r.Get("/{owner}/{repo}/actions", h.repoTabActions) | |
| 122 | + r.Get("/{owner}/{repo}/projects", h.repoTabProjects) | |
| 123 | + r.Get("/{owner}/{repo}/wiki", h.repoTabWiki) | |
| 124 | + r.Get("/{owner}/{repo}/security", h.repoTabSecurity) | |
| 125 | + r.Get("/{owner}/{repo}/pulse", h.repoTabInsights) | |
| 126 | + r.Get("/{owner}/{repo}/packages", h.repoTabPackages) | |
| 127 | + r.Get("/{owner}/{repo}/releases", h.repoTabReleases) | |
| 120 | 128 | r.Get("/{owner}/{repo}", h.repoHome) |
| 121 | 129 | } |
| 122 | 130 | |
internal/web/render/octicons.gomodified@@ -41,6 +41,8 @@ func BuiltinOcticons() OcticonResolver { | ||
| 41 | 41 | `><path d="M6.906.664a1.75 1.75 0 0 1 2.188 0l5.25 4.2c.414.331.656.833.656 1.363v7.023A1.75 1.75 0 0 1 13.25 15h-2.5A1.75 1.75 0 0 1 9 13.25V10H7v3.25A1.75 1.75 0 0 1 5.25 15h-2.5A1.75 1.75 0 0 1 1 13.25V6.227c0-.53.242-1.032.656-1.363Zm1.25 1.171a.25.25 0 0 0-.312 0l-5.25 4.2a.25.25 0 0 0-.094.192v7.023c0 .138.112.25.25.25h2.5a.25.25 0 0 0 .25-.25V9.75A1.25 1.25 0 0 1 6.75 8.5h2.5a1.25 1.25 0 0 1 1.25 1.25v3.5c0 .138.112.25.25.25h2.5a.25.25 0 0 0 .25-.25V6.227a.25.25 0 0 0-.094-.192Z"/></svg>`), |
| 42 | 42 | "table": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls + |
| 43 | 43 | `><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25ZM6.5 6.5v8h7.75a.25.25 0 0 0 .25-.25V6.5Zm8-1.5V1.75a.25.25 0 0 0-.25-.25H6.5V5Zm-13 1.5v7.75c0 .138.112.25.25.25H5v-8ZM5 5V1.5H1.75a.25.25 0 0 0-.25.25V5Z"/></svg>`), |
| 44 | + "play": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls + | |
| 45 | + `><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773 4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z"/></svg>`), | |
| 44 | 46 | "code": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls + |
| 45 | 47 | `><path d="M5.22 4.22a.75.75 0 0 1 1.06 1.06L3.56 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L1.97 8.53a.75.75 0 0 1 0-1.06Zm5.56 0a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 1 1-1.06-1.06L13.5 8l-2.72-2.72a.75.75 0 0 1 0-1.06Z"/></svg>`), |
| 46 | 48 | "git-pull-request": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls + |
internal/web/static/css/shithub.cssmodified@@ -2034,7 +2034,8 @@ button.shithub-repo-action { | ||
| 2034 | 2034 | gap: 0.25rem; |
| 2035 | 2035 | margin: 0; |
| 2036 | 2036 | border-bottom: 1px solid var(--border-default); |
| 2037 | - flex-wrap: wrap; | |
| 2037 | + overflow-x: auto; | |
| 2038 | + overscroll-behavior-x: contain; | |
| 2038 | 2039 | } |
| 2039 | 2040 | .shithub-repo-subnav-tab { |
| 2040 | 2041 | display: inline-flex; |
@@ -2047,6 +2048,8 @@ button.shithub-repo-action { | ||
| 2047 | 2048 | text-decoration: none; |
| 2048 | 2049 | position: relative; |
| 2049 | 2050 | bottom: -1px; |
| 2051 | + flex: 0 0 auto; | |
| 2052 | + white-space: nowrap; | |
| 2050 | 2053 | } |
| 2051 | 2054 | .shithub-repo-subnav-tab:hover { background: var(--canvas-subtle); border-radius: 6px 6px 0 0; } |
| 2052 | 2055 | .shithub-repo-subnav-tab.is-active { border-bottom-color: var(--accent-emphasis, #fd8c73); font-weight: 600; } |
@@ -2063,6 +2066,98 @@ button.shithub-repo-action { | ||
| 2063 | 2066 | align-items: start; |
| 2064 | 2067 | } |
| 2065 | 2068 | .shithub-repo-main { min-width: 0; } |
| 2069 | + | |
| 2070 | +.shithub-repo-product-page { | |
| 2071 | + max-width: 80rem; | |
| 2072 | + margin: 1.5rem auto 2rem; | |
| 2073 | + padding: 0 1rem; | |
| 2074 | + display: grid; | |
| 2075 | + grid-template-columns: 18rem minmax(0, 1fr); | |
| 2076 | + gap: 1.5rem; | |
| 2077 | +} | |
| 2078 | +.shithub-repo-product-sidebar { min-width: 0; } | |
| 2079 | +.shithub-repo-product-sidebar h2 { | |
| 2080 | + margin: 0 0 0.75rem; | |
| 2081 | + font-size: 1rem; | |
| 2082 | +} | |
| 2083 | +.shithub-repo-product-sidebar nav { | |
| 2084 | + display: grid; | |
| 2085 | + gap: 0.15rem; | |
| 2086 | +} | |
| 2087 | +.shithub-repo-product-sidebar a { | |
| 2088 | + display: block; | |
| 2089 | + padding: 0.45rem 0.6rem; | |
| 2090 | + border-radius: 6px; | |
| 2091 | + color: var(--fg-default); | |
| 2092 | + text-decoration: none; | |
| 2093 | + font-size: 0.875rem; | |
| 2094 | +} | |
| 2095 | +.shithub-repo-product-sidebar a:hover, | |
| 2096 | +.shithub-repo-product-sidebar a.is-active { | |
| 2097 | + background: var(--canvas-subtle); | |
| 2098 | +} | |
| 2099 | +.shithub-repo-product-main { min-width: 0; } | |
| 2100 | +.shithub-repo-product-head { | |
| 2101 | + display: flex; | |
| 2102 | + align-items: flex-start; | |
| 2103 | + gap: 0.8rem; | |
| 2104 | + padding-bottom: 1rem; | |
| 2105 | + border-bottom: 1px solid var(--border-default); | |
| 2106 | +} | |
| 2107 | +.shithub-repo-product-icon { | |
| 2108 | + display: inline-flex; | |
| 2109 | + align-items: center; | |
| 2110 | + justify-content: center; | |
| 2111 | + width: 2rem; | |
| 2112 | + height: 2rem; | |
| 2113 | + border: 1px solid var(--border-default); | |
| 2114 | + border-radius: 6px; | |
| 2115 | + color: var(--fg-muted); | |
| 2116 | + background: var(--canvas-subtle); | |
| 2117 | + flex: 0 0 auto; | |
| 2118 | +} | |
| 2119 | +.shithub-repo-product-head h1 { | |
| 2120 | + margin: 0 0 0.2rem; | |
| 2121 | + font-size: 1.5rem; | |
| 2122 | + font-weight: 600; | |
| 2123 | +} | |
| 2124 | +.shithub-repo-product-head p { | |
| 2125 | + margin: 0; | |
| 2126 | + color: var(--fg-muted); | |
| 2127 | + font-size: 0.95rem; | |
| 2128 | +} | |
| 2129 | +.shithub-repo-product-blankslate { | |
| 2130 | + margin-top: 1rem; | |
| 2131 | + border: 1px solid var(--border-default); | |
| 2132 | + border-radius: 6px; | |
| 2133 | + background: var(--canvas-default); | |
| 2134 | +} | |
| 2135 | +.shithub-repo-product-section { | |
| 2136 | + padding: 1rem; | |
| 2137 | + border-bottom: 1px solid var(--border-default); | |
| 2138 | +} | |
| 2139 | +.shithub-repo-product-section:last-child { border-bottom: 0; } | |
| 2140 | +.shithub-repo-product-section h2 { | |
| 2141 | + margin: 0 0 0.35rem; | |
| 2142 | + font-size: 1rem; | |
| 2143 | +} | |
| 2144 | +.shithub-repo-product-section p { | |
| 2145 | + margin: 0; | |
| 2146 | + color: var(--fg-muted); | |
| 2147 | + font-size: 0.9rem; | |
| 2148 | +} | |
| 2149 | +@media (max-width: 800px) { | |
| 2150 | + .shithub-repo-product-page { | |
| 2151 | + grid-template-columns: 1fr; | |
| 2152 | + } | |
| 2153 | + .shithub-repo-product-sidebar nav { | |
| 2154 | + display: flex; | |
| 2155 | + overflow-x: auto; | |
| 2156 | + } | |
| 2157 | + .shithub-repo-product-sidebar a { | |
| 2158 | + white-space: nowrap; | |
| 2159 | + } | |
| 2160 | +} | |
| 2066 | 2161 | .shithub-tree-panel { |
| 2067 | 2162 | border: 1px solid var(--border-default); |
| 2068 | 2163 | border-radius: 6px; |
internal/web/templates/repo/deferred_tab.htmladded@@ -0,0 +1,30 @@ | ||
| 1 | +{{ define "page" -}} | |
| 2 | +{{ template "repo-header" . }} | |
| 3 | +<section class="shithub-repo-product-page"> | |
| 4 | + <aside class="shithub-repo-product-sidebar" aria-label="{{ .Tab.Heading }}"> | |
| 5 | + <h2>{{ .Tab.Heading }}</h2> | |
| 6 | + <nav aria-label="{{ .Tab.Heading }} sections"> | |
| 7 | + {{ range $i, $section := .Tab.Sections }} | |
| 8 | + <a href="#{{ $section.Anchor }}"{{ if eq $i 0 }} class="is-active" aria-current="page"{{ end }}>{{ $section.Title }}</a> | |
| 9 | + {{ end }} | |
| 10 | + </nav> | |
| 11 | + </aside> | |
| 12 | + <div class="shithub-repo-product-main"> | |
| 13 | + <header class="shithub-repo-product-head"> | |
| 14 | + <span class="shithub-repo-product-icon">{{ octicon .Tab.Icon }}</span> | |
| 15 | + <div> | |
| 16 | + <h1>{{ .Tab.Heading }}</h1> | |
| 17 | + <p>{{ .Tab.Description }}</p> | |
| 18 | + </div> | |
| 19 | + </header> | |
| 20 | + <div class="shithub-repo-product-blankslate"> | |
| 21 | + {{ range .Tab.Sections }} | |
| 22 | + <section id="{{ .Anchor }}" class="shithub-repo-product-section"> | |
| 23 | + <h2>{{ .Title }}</h2> | |
| 24 | + <p>{{ .Body }}</p> | |
| 25 | + </section> | |
| 26 | + {{ end }} | |
| 27 | + </div> | |
| 28 | + </div> | |
| 29 | +</section> | |
| 30 | +{{- end }} | |