From c5a07be0bebaeadfc27ca61e85ba2a7379114334 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 02:25:03 +0200 Subject: [PATCH 01/29] feat: adapt templates to Forgejo 17 --- components/repository/results/list.tmpl | 2 +- components/repository/sort/items.tmpl | 16 ++++----- components/repository/sort/label.tmpl | 2 +- dev/docker-compose.yml | 2 +- public/assets/js/diff-file-tree.js | 5 ++- src/build-output.test.ts | 4 +-- src/repo-mirror.test.ts | 2 +- styles/components/heatmap.ts | 5 +-- styles/components/repo/diff.ts | 11 ------ styles/components/repo/diff_file_tree.ts | 18 +++++++--- styles/components/repo/diff_view.ts | 5 +-- styles/pages/organization.css | 8 ++--- styles/pages/releases.css | 6 ++-- styles/templates/repo/view_content.ts | 23 +----------- templates/org/team/teams.tmpl | 8 +++-- templates/repo/commit_page.tmpl | 2 +- templates/repo/diff/box.tmpl | 36 ++++++++++--------- templates/repo/header.tmpl | 2 +- templates/repo/home_sidebar_top.tmpl | 14 +++----- templates/repo/issue/search.tmpl | 6 ++-- templates/repo/mirror_badge.tmpl | 2 +- templates/repo/view_content.tmpl | 10 +++--- templates/shared/user/profile_big_avatar.tmpl | 4 +-- templates/user/dashboard/issues.tmpl | 6 ++-- templates/user/dashboard/milestones.tmpl | 14 ++++---- templates/user/profile.tmpl | 15 +------- templates/user/profile_package_versions.tmpl | 4 +-- templates/user/profile_people.tmpl | 4 +-- templates/user/profile_projects.tmpl | 6 ++-- 29 files changed, 104 insertions(+), 138 deletions(-) diff --git a/components/repository/results/list.tmpl b/components/repository/results/list.tmpl index b9b88e71..47eb46d8 100644 --- a/components/repository/results/list.tmpl +++ b/components/repository/results/list.tmpl @@ -20,7 +20,7 @@ {{if .Owner.Visibility.IsPrivate}} {{ctx.Locale.Tr "repo.desc.internal"}} {{else}} - {{ctx.Locale.Tr "settings.visibility.public"}} + {{ctx.Locale.Tr "user.visibility.public.title"}} {{end}} {{end}} {{if .IsTemplate}} diff --git a/components/repository/sort/items.tmpl b/components/repository/sort/items.tmpl index 685a79c2..1b9e1087 100644 --- a/components/repository/sort/items.tmpl +++ b/components/repository/sort/items.tmpl @@ -1,14 +1,14 @@ -{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "newest" "Checked" (eq .SortType "newest") "Label" (ctx.Locale.Tr "repo.issues.filter_sort.latest")}} -{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "oldest" "Checked" (eq .SortType "oldest") "Label" (ctx.Locale.Tr "repo.issues.filter_sort.oldest")}} +{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "newest" "Checked" (eq .SortType "newest") "Label" (ctx.Locale.Tr "issues.search.sort.latest")}} +{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "oldest" "Checked" (eq .SortType "oldest") "Label" (ctx.Locale.Tr "issues.search.sort.oldest")}} {{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "alphabetically" "Checked" (eq .SortType "alphabetically") "Label" (ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically")}} {{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "reversealphabetically" "Checked" (eq .SortType "reversealphabetically") "Label" (ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically")}} -{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "recentupdate" "Checked" (eq .SortType "recentupdate") "Label" (ctx.Locale.Tr "repo.issues.filter_sort.recentupdate")}} -{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "leastupdate" "Checked" (eq .SortType "leastupdate") "Label" (ctx.Locale.Tr "repo.issues.filter_sort.leastupdate")}} +{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "recentupdate" "Checked" (eq .SortType "recentupdate") "Label" (ctx.Locale.Tr "issues.search.sort.recentupdate")}} +{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "leastupdate" "Checked" (eq .SortType "leastupdate") "Label" (ctx.Locale.Tr "issues.search.sort.leastupdate")}} {{if not .DisableStars}} -{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "moststars" "Checked" (eq .SortType "moststars") "Label" (ctx.Locale.Tr "repo.issues.filter_sort.moststars")}} -{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "feweststars" "Checked" (eq .SortType "feweststars") "Label" (ctx.Locale.Tr "repo.issues.filter_sort.feweststars")}} +{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "moststars" "Checked" (eq .SortType "moststars") "Label" (ctx.Locale.Tr "search.sort.moststars")}} +{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "feweststars" "Checked" (eq .SortType "feweststars") "Label" (ctx.Locale.Tr "search.sort.feweststars")}} {{end}} -{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "mostforks" "Checked" (eq .SortType "mostforks") "Label" (ctx.Locale.Tr "repo.issues.filter_sort.mostforks")}} -{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "fewestforks" "Checked" (eq .SortType "fewestforks") "Label" (ctx.Locale.Tr "repo.issues.filter_sort.fewestforks")}} +{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "mostforks" "Checked" (eq .SortType "mostforks") "Label" (ctx.Locale.Tr "search.sort.mostforks")}} +{{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "fewestforks" "Checked" (eq .SortType "fewestforks") "Label" (ctx.Locale.Tr "search.sort.fewestforks")}} {{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "size" "Checked" (eq .SortType "size") "Label" (ctx.Locale.Tr "repo.issues.label.filter_sort.by_size")}} {{template "components/common/dropdown/option" dict "Type" "radio" "Name" "sort" "Value" "reversesize" "Checked" (eq .SortType "reversesize") "Label" (ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size")}} diff --git a/components/repository/sort/label.tmpl b/components/repository/sort/label.tmpl index b532e003..e5cd193d 100644 --- a/components/repository/sort/label.tmpl +++ b/components/repository/sort/label.tmpl @@ -1 +1 @@ -{{if eq .SortType "newest"}}{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}{{else if eq .SortType "oldest"}}{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}{{else if eq .SortType "recentupdate"}}{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}{{else if eq .SortType "leastupdate"}}{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}{{else if eq .SortType "moststars"}}{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}{{else if eq .SortType "feweststars"}}{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}{{else if eq .SortType "mostforks"}}{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}{{else if eq .SortType "fewestforks"}}{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}{{else if eq .SortType "alphabetically"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}{{else if eq .SortType "reversealphabetically"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}{{else if eq .SortType "size"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}{{else if eq .SortType "reversesize"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}{{else}}{{ctx.Locale.Tr "repo.issues.filter_sort"}}{{end}} +{{if eq .SortType "newest"}}{{ctx.Locale.Tr "issues.search.sort.latest"}}{{else if eq .SortType "oldest"}}{{ctx.Locale.Tr "issues.search.sort.oldest"}}{{else if eq .SortType "recentupdate"}}{{ctx.Locale.Tr "issues.search.sort.recentupdate"}}{{else if eq .SortType "leastupdate"}}{{ctx.Locale.Tr "issues.search.sort.leastupdate"}}{{else if eq .SortType "moststars"}}{{ctx.Locale.Tr "search.sort.moststars"}}{{else if eq .SortType "feweststars"}}{{ctx.Locale.Tr "search.sort.feweststars"}}{{else if eq .SortType "mostforks"}}{{ctx.Locale.Tr "search.sort.mostforks"}}{{else if eq .SortType "fewestforks"}}{{ctx.Locale.Tr "search.sort.fewestforks"}}{{else if eq .SortType "alphabetically"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}{{else if eq .SortType "reversealphabetically"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}{{else if eq .SortType "size"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}{{else if eq .SortType "reversesize"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}{{else}}{{ctx.Locale.Tr "repo.issues.filter_sort"}}{{end}} diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 34e6fd5c..067137d8 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -15,7 +15,7 @@ services: - forgejo-dev-data:/var/lib/gitea forgejo: - image: code.forgejo.org/forgejo/forgejo:16.0.3-rootless + image: code.forgejo.org/forgejo/forgejo:17.0.0-rootless container_name: forgejo-github-theme-dev environment: USER_GID: "1000" diff --git a/public/assets/js/diff-file-tree.js b/public/assets/js/diff-file-tree.js index 8c00eed7..aa7429e9 100644 --- a/public/assets/js/diff-file-tree.js +++ b/public/assets/js/diff-file-tree.js @@ -54,6 +54,7 @@ const data = files(); const matching = data.filter(matches); const byHash = new Map(data.map(file => [`#diff-${file.NameHash}`, file])); + const byName = new Map(data.map(file => [file.Name, file])); const counts = new Map(); for (const file of data) counts.set(extension(file.Name), (counts.get(extension(file.Name)) || 0) + 1); const key = JSON.stringify([...counts]); @@ -105,7 +106,9 @@ // Reveal matching descendants when searching, including Vue's lazy subtrees. if (revealMatches && visible && row.getAttribute("aria-expanded") === "false") row.click(); } else if (row.matches(".item-file")) { - const file = byHash.get(row.getAttribute("href")); + // Forgejo 17 tree items have no href; remember the path before the title below replaces the name. + row.dataset.diffPath ||= prefix + (row.querySelector("span")?.textContent.trim() || row.title); + const file = byName.get(row.dataset.diffPath); row.toggleAttribute("data-tree-filtered", file ? !matches(file) : false); row.setAttribute("role", "treeitem"); row.setAttribute("aria-level", level); diff --git a/src/build-output.test.ts b/src/build-output.test.ts index 5d83d21d..6006edda 100644 --- a/src/build-output.test.ts +++ b/src/build-output.test.ts @@ -437,8 +437,8 @@ describe("Forgejo 15 native integration", () => { expect(repoContentStyle).toContain(".repo-about-block > .repo-about-heading"); expect(repoContentStyle).toContain("> .repo-description.no-description"); expect(repoContentStyle).toContain("font-style: italic;"); - expect(repoContentStyle).toContain("min-height: 34px;"); - expect(repoContentStyle).toContain("flex: 1 1 120px;"); + expect(sidebarTemplate).toContain('name="topics" type="text" value="{{StringUtils.Join .Topics ","}}"'); + expect(repoContentStyle).toContain('input:is([name="description"], [name="website"], [name="topics"])'); expect(repoContentStyle).toContain("> .ui.dropdown.selection"); expect(repoContentStyle).toContain("border-radius: 0 !important;"); }); diff --git a/src/repo-mirror.test.ts b/src/repo-mirror.test.ts index 69030ad7..e2e060a8 100644 --- a/src/repo-mirror.test.ts +++ b/src/repo-mirror.test.ts @@ -104,7 +104,7 @@ describe("mirror badge disclosure", () => { '{{if .Repository.IsPrivate}}{{ctx.Locale.Tr "repo.desc.private"}}{{else if .Repository.Owner.Visibility.IsPrivate}}' ); expect(template).toContain('{{ctx.Locale.Tr "repo.desc.internal"}}'); - expect(template).toContain('{{ctx.Locale.Tr "settings.visibility.public"}}{{end}} mirror'); + expect(template).toContain('{{ctx.Locale.Tr "user.visibility.public.title"}}{{end}} mirror'); expect(template).toContain("{{if .PullMirror.UpdatedUnix}}"); expect(template).toContain("{{DateUtils.TimeSince .PullMirror.UpdatedUnix}}"); expect(template).toContain("{{AssetUrlPrefix}}/css/repo-mirror.css"); diff --git a/styles/components/heatmap.ts b/styles/components/heatmap.ts index 4526f955..3af5c435 100644 --- a/styles/components/heatmap.ts +++ b/styles/components/heatmap.ts @@ -81,8 +81,9 @@ const heatmap = css` } } } - /* Forgejo's heatmap tooltip is unthemed; scope the GitHub treatment to the heatmap page. */ - body:has(#user-heatmap) .tippy-box:not([data-theme]) { + /* Forgejo's own tooltips always carry data-theme, so an unthemed box is the heatmap's. A body:has() guard here + would restyle the whole document on every DOM change. */ + .tippy-box:not([data-theme]) { background-color: ${themeVars.color.tooltip.bg}; color: ${themeVars.color.tooltip.text}; border: 0; diff --git a/styles/components/repo/diff.ts b/styles/components/repo/diff.ts index b438e5ca..e21381ce 100644 --- a/styles/components/repo/diff.ts +++ b/styles/components/repo/diff.ts @@ -135,17 +135,6 @@ const diffFileBoxCodeDiff = css` width: 0; /* 不要使用 display: none; 否则会影响布局, 无内容时为 0, 有内容时为 20(猜测可能是根据内容宽度自动调整) */ /* visibility: hidden; // 不要使用 visibility: hidden; 当 escape 有内容时会导致背景颜色丢失, escape 目前用于显示检测 unicode 编码错误的内容 */ } - /* 修复当 escape 有内容时, 宽度不够的问题 */ - &:has(td.lines-escape:not(:empty)) { - /* 合并视图的第三列 */ - &.code-diff-unified colgroup col:nth-child(3), - /* 拆分视图的第二列和第六列 */ - &.code-diff-split colgroup col:nth-child(2), - &.code-diff-split colgroup col:nth-child(6), - td.lines-escape { - width: 20; - } - } /* Gitea 分列视图下默认 100% 宽度的目的是如果单文件只增加或只删除的情况下, 保持无内容的列的宽度一致, 始终保持左右两边的列宽度一致 */ /* 保持 Gitea 的默认设置, 不对行号宽度做处理 */ /* &.code-diff-split table { diff --git a/styles/components/repo/diff_file_tree.ts b/styles/components/repo/diff_file_tree.ts index 89f27cc7..5dc3aa6f 100644 --- a/styles/components/repo/diff_file_tree.ts +++ b/styles/components/repo/diff_file_tree.ts @@ -1,8 +1,16 @@ import { css, themeVars } from "@lutinglt/gitea-github-theme/core"; // Shared by commit and pull-request diffs; Forgejo still owns the Vue tree. +// #diff-container wraps every diff line, so a :has() on it restyles the whole diff on each DOM change; +// box.tmpl sets these classes instead. export default css` - .repository #diff-container:has(> .diff-file-tree-panel) { + /* resizable-divider.js writes this on #diff-container. Inherited, every diff line would be restyled. */ + @property --diff-file-tree-width { + syntax: ""; + inherits: false; + initial-value: 296px; + } + .repository #diff-container.diff-has-file-tree { display: grid; grid-template-columns: var(--diff-file-tree-width, 296px) 1px minmax(0, 1fr); position: relative; @@ -10,11 +18,11 @@ export default css` align-items: start; gap: 0; } - .repository #diff-container:has(#diff-file-tree.tw-hidden) { + .repository #diff-container.diff-file-tree-hidden { grid-template-columns: minmax(0, 1fr); } - .repository #diff-container:has(#diff-file-tree.tw-hidden) > .diff-file-tree-panel, - .repository #diff-container:has(#diff-file-tree.tw-hidden) > .resizable-divider { + .repository #diff-container.diff-file-tree-hidden > .diff-file-tree-panel, + .repository #diff-container.diff-file-tree-hidden > .resizable-divider { display: none; } .repository .diff-file-tree-panel { @@ -126,7 +134,7 @@ export default css` .repository #diff-container.diff-mobile-tree-open .diff-code-search { display: none; } - .repository #diff-container:has(> .diff-file-tree-panel) { + .repository #diff-container.diff-has-file-tree { display: block; } .repository #diff-container.diff-mobile-tree-open { diff --git a/styles/components/repo/diff_view.ts b/styles/components/repo/diff_view.ts index a7f2fc16..508081c3 100644 --- a/styles/components/repo/diff_view.ts +++ b/styles/components/repo/diff_view.ts @@ -61,11 +61,8 @@ export default css` flex: 0 0 16px; margin-left: 4px; } - .repository #diff-container .diff-content-controls { - justify-content: flex-start; - } .diff-content-controls-right { - margin-left: 0; + margin-left: auto; } .diff-code-search { display: flex; diff --git a/styles/pages/organization.css b/styles/pages/organization.css index 22b28ae3..0d276b81 100644 --- a/styles/pages/organization.css +++ b/styles/pages/organization.css @@ -1,5 +1,5 @@ /* Organization templates retain Forgejo's routes, permissions, and form handlers. */ -body:has(.organization-global-header) .page-content:is(.organization, .packages) { +.organization-global-header ~ .page-content:is(.organization, .packages) { padding-top: 24px; } @@ -7,7 +7,7 @@ body:has(.organization-global-header) .page-content:is(.organization, .packages) margin-left: 0 !important; } -body:has(.organization-global-header) .page-content:is(.organization, .packages) > .ui.container { +.organization-global-header ~ .page-content:is(.organization, .packages) > .ui.container { width: calc(100% - 64px) !important; max-width: 1248px !important; margin-top: 0; @@ -358,7 +358,7 @@ body:has(.organization-global-header) .page-content:is(.organization, .packages) } @media (max-width: 767px) { - body:has(.organization-global-header) .page-content:is(.organization, .packages) > .ui.container { + .organization-global-header ~ .page-content:is(.organization, .packages) > .ui.container { width: calc(100% - 32px) !important; } @@ -438,7 +438,7 @@ body:has(.organization-global-header) .page-content:is(.organization, .packages) line-height: 32px; } -body:has(.organization-global-header) .page-content.organization.new.team > .ui.container { +.organization-global-header ~ .page-content.organization.new.team > .ui.container { max-width: 768px !important; } diff --git a/styles/pages/releases.css b/styles/pages/releases.css index 11b40799..b958cf34 100644 --- a/styles/pages/releases.css +++ b/styles/pages/releases.css @@ -1,6 +1,6 @@ /* Releases, tags and the release form. Kept as a page stylesheet, outside the size-limited theme files. */ -body:has(.page-content.repository.github-releases) .repository-content-header { +.page-content.repository.github-releases .repository-content-header { display: none; } .page-content.repository.github-releases > .ui.container { @@ -635,7 +635,7 @@ body:has(.page-content.repository.github-releases) .repository-content-header { } } /* New and edit release: GitHub's form page, on Forgejo's own template. */ -body:has(.page-content.repository.new.release) .repository-content-header { +.page-content.repository.new.release .repository-content-header { display: none; } .page-content.repository.new.release { @@ -711,7 +711,7 @@ body:has(.page-content.repository.new.release) .repository-content-header { font-weight: 500; } } -body:has(.page-content.repository.github-tags) .repository-content-header { +.page-content.repository.github-tags .repository-content-header { display: none; } .page-content.repository.github-tags > .ui.container { diff --git a/styles/templates/repo/view_content.ts b/styles/templates/repo/view_content.ts index 85a6bcd8..f15d7ea0 100644 --- a/styles/templates/repo/view_content.ts +++ b/styles/templates/repo/view_content.ts @@ -412,7 +412,7 @@ const repoToolbarDetails = css` line-height: 21px; } - .page-content.repository.file.list .repo-about-modal-body input:is([name="description"], [name="website"]) { + .page-content.repository.file.list .repo-about-modal-body input:is([name="description"], [name="website"], [name="topics"]) { box-sizing: border-box; font-size: 14px; height: 32px; @@ -420,27 +420,6 @@ const repoToolbarDetails = css` padding: 5px 12px; } - .page-content.repository.file.list .repo-about-topics-input { - align-content: flex-start; - align-items: center; - box-sizing: border-box; - font-size: 12px; - line-height: 20px; - min-height: 34px; - padding: 6px 0 6px 12px; - } - - .page-content.repository.file.list .repo-about-topics-input > input.search { - flex: 1 1 120px; - font-size: 12px; - height: 20px; - line-height: 20px; - margin: 0 12px 0 0; - min-width: 120px; - padding: 0; - width: auto !important; - } - .page-content.repository.file.list .repo-about-error { color: ${themeVars.color.error.text}; margin-top: 8px; diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index 13a46431..402c7444 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -16,9 +16,13 @@ {{range .Teams}}
- {{.Name}} + {{if or (index $.DoerIsMember .ID) $.IsOrganizationOwner}} + {{.Name}} + {{else}} + {{.Name}} + {{end}}
- {{if .IsMember ctx $.SignedUser.ID}} + {{if index $.DoerIsMember .ID}}
- - + + {{/* Forgejo 17 passes topics as plain strings; the save handler posts this comma-separated value. */}} +
diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl index cb618a56..2d591800 100644 --- a/templates/repo/issue/search.tmpl +++ b/templates/repo/issue/search.tmpl @@ -10,11 +10,11 @@ {{end}} {{if .PageIsPullList}} - {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} + {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "search.verb")}} {{else if .PageIsMilestones}} - {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.milestone_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} + {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.milestone_kind") "Tooltip" (ctx.Locale.Tr "search.verb")}} {{else}} - {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} + {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "search.verb")}} {{end}} {{if not .PageIsMilestones}} {{template "shared/search/issue/syntax"}} diff --git a/templates/repo/mirror_badge.tmpl b/templates/repo/mirror_badge.tmpl index 62d8e014..9da8a275 100644 --- a/templates/repo/mirror_badge.tmpl +++ b/templates/repo/mirror_badge.tmpl @@ -1,7 +1,7 @@ {{$address := MirrorRemoteAddress .Context .PullMirror}}
- {{if .Repository.IsPrivate}}{{ctx.Locale.Tr "repo.desc.private"}}{{else if .Repository.Owner.Visibility.IsPrivate}}{{ctx.Locale.Tr "repo.desc.internal"}}{{else}}{{ctx.Locale.Tr "settings.visibility.public"}}{{end}} mirror + {{if .Repository.IsPrivate}}{{ctx.Locale.Tr "repo.desc.private"}}{{else if .Repository.Owner.Visibility.IsPrivate}}{{ctx.Locale.Tr "repo.desc.internal"}}{{else}}{{ctx.Locale.Tr "user.visibility.public.title"}}{{end}} mirror
{{ctx.Locale.Tr "repo.mirror_from"}} {{$address.Address}}
{{if .PullMirror.UpdatedUnix}}
{{ctx.Locale.Tr "repo.mirror_sync"}} {{DateUtils.TimeSince .PullMirror.UpdatedUnix}}
{{end}} diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 12ac0045..1eed0e96 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -47,15 +47,15 @@ {{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} + {{end}} + {{if .PageIsPullFiles}} + {{/* Vue replaces the button inside; CSS draws the label from this variable. */}} +
+ {{/* + the following will be replaced by vue component + but this avoids any loading artifacts till the vue component is initialized + */}} + +
+ {{end}} + +
+
+ {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}} +
+ {{/* Forgejo updates the hidden progress; diff-view.js mirrors its value onto the ring. */}} + + + +
+ {{end}} + {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived)}} + {{template "repo/diff/new_review" .}} + {{end}} + {{template "repo/diff/view_settings" .}} +
+
{{if $showFileTree}}
@@ -63,62 +107,21 @@ {{end}}
-
-
- {{if $showFileTree}} - - - {{end}} - -
-
- {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}} -
- - -
- {{end}} - {{template "repo/diff/view_settings" .}} - {{if .PageIsPullFiles}} -
- {{/* - the following will be replaced by vue component - but this avoids any loading artifacts till the vue component is initialized - */}} - -
- {{end}} - {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived)}} - {{template "repo/diff/new_review" .}} - {{end}} -
-
{{if .IsShowingOnlySingleCommit}}
diff --git a/templates/repo/diff/view_settings.tmpl b/templates/repo/diff/view_settings.tmpl index ae792434..8c778529 100644 --- a/templates/repo/diff/view_settings.tmpl +++ b/templates/repo/diff/view_settings.tmpl @@ -1,5 +1,5 @@
- {{svg "octicon-gear" 16}} + {{svg "octicon-gear" 16}}
Layout Unified From fb6ea06c6452ad1ace014ce49247553e308d7111 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 10:33:20 +0200 Subject: [PATCH 05/29] feat(pull): full-width files tab header and title pencil - On the files tab the title, tabs and diff share a 24px side margin instead of a 1216px centered header over a full-width diff. - The +/- summary is vertically centred with the tabs. - Pull request titles are edited from a pencil next to the number, like GitHub. view_title.tmpl is Forgejo 17's with only that change; issues keep their Edit button. - The tree toggle is 28px like the rest of the toolbar. --- styles/components/issues/pull_review.ts | 48 +++++++- styles/components/repo/diff_view.ts | 3 +- templates/repo/issue/view_title.tmpl | 154 ++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 templates/repo/issue/view_title.tmpl diff --git a/styles/components/issues/pull_review.ts b/styles/components/issues/pull_review.ts index 291815c1..45af3e47 100644 --- a/styles/components/issues/pull_review.ts +++ b/styles/components/issues/pull_review.ts @@ -47,12 +47,36 @@ export default css` width: 100% !important; max-width: none !important; } + /* GitHub's files tab is full width: title, tabs and diff share a 24px side margin. */ &.files > .ui.container > .issue-title-header, - &.files > .ui.container > .pull.tabs { - width: calc(100% - 64px); - max-width: 1216px; - margin-left: auto; - margin-right: auto; + &.files > .ui.container > .pull.tabs, + &.files #diff-container { + width: auto; + max-width: none; + margin-left: 24px; + margin-right: 24px; + } + &.files #diff-container > .diff-content-controls { + padding-inline: 0; + } + &.files #diff-container .diff-file-tree-scroll { + padding-left: 0; + padding-right: 16px; + } + &.files #diff-container > #diff-content-container { + padding-right: 0; + } + &.files #diff-container.diff-file-tree-hidden > #diff-content-container { + padding-left: 0; + } + /* GitHub's pencil next to the pull request number. */ + & .issue-title h1 .issue-title-edit-pencil { + display: inline-flex; + vertical-align: middle; + padding: 6px; + margin-left: 4px; + border-radius: ${otherThemeVars.border.radius}; + color: ${themeVars.color.text.light.num1}; } /* The same header box on every tab, so switching tabs does not move the page. */ & .issue-title-header { @@ -159,6 +183,7 @@ export default css` margin-left: auto; display: flex; align-items: center; + align-self: center; padding-left: 16px; } &.files .pull.tabs.container { @@ -545,7 +570,18 @@ export default css` } &.files > .ui.container > .issue-title-header, &.files > .ui.container > .pull.tabs { - width: calc(100% - 32px); + margin-left: 16px; + margin-right: 16px; + } + &.files #diff-container { + margin-left: 0; + margin-right: 0; + } + &.files #diff-container > .diff-content-controls { + padding-inline: 16px; + } + &.files #diff-container > #diff-content-container { + padding: 16px; } & .issue-title h1 { font-size: 24px; diff --git a/styles/components/repo/diff_view.ts b/styles/components/repo/diff_view.ts index 62828cc5..58b70d95 100644 --- a/styles/components/repo/diff_view.ts +++ b/styles/components/repo/diff_view.ts @@ -93,7 +93,8 @@ export default css` mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 1.5px)); } /* GitHub's toolbar buttons are 28px, like the review button next to the gear. */ - .diff-content-controls .diff-view-settings > .theme-icon-button { + .diff-content-controls .diff-view-settings > .theme-icon-button, + .diff-content-controls .diff-toggle-file-tree-button { width: 28px; height: 28px; } diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl new file mode 100644 index 00000000..1fbcbae2 --- /dev/null +++ b/templates/repo/issue/view_title.tmpl @@ -0,0 +1,154 @@ +{{/* Forgejo 17 view_title.tmpl; pull requests get GitHub's pencil next to the number instead of an Edit button. */}} +{{if .Flash}} +
+ {{template "base/alert" .}} +
+{{end}} +
+ {{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} +
+

+ {{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx)}} #{{.Issue.Index}} + {{/* GitHub edits a pull request title from a pencil next to its number. Forgejo binds the button by id. */}} + {{if and $canEditIssueTitle .Issue.IsPull}} + + {{end}} +

+
+ {{if and $canEditIssueTitle (not .Issue.IsPull)}} + + {{end}} + {{if and (not .Issue.IsPull) (not .Repository.IsArchived)}} + {{ctx.Locale.Tr "repo.issues.new"}} + {{end}} +
+
+ {{if $canEditIssueTitle}} +
+
+ +
+
+ + +
+
+ {{end}} +
+ {{if .HasMerged}} +
{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}
+ {{else if .Issue.IsClosed}} +
{{if .Issue.IsPull}}{{svg "octicon-git-pull-request-closed"}}{{else}}{{svg "octicon-issue-closed"}}{{end}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
+ {{else if .Issue.IsPull}} + {{if .IsPullWorkInProgress}} +
{{svg "octicon-git-pull-request-draft"}} {{ctx.Locale.Tr "repo.issues.draft_title"}}
+ {{else}} +
{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.issues.open_title"}}
+ {{end}} + {{else}} +
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.open_title"}}
+ {{end}} +
+ {{if .Issue.IsPull}} + {{$headHref := .HeadTarget}} + {{if .HeadBranchLink}} + {{$headHref = HTMLFormat `%s` .HeadBranchLink $headHref}} + {{end}} + {{if not .MadeUsingAGit}} + {{$headHref = HTMLFormat `%s ` $headHref (ctx.Locale.Tr "copy_branch") .HeadTarget (svg "octicon-copy" 14)}} + {{end}} + {{$baseHref := .BaseTarget}} + {{if .BaseBranchLink}} + {{$baseHref = HTMLFormat `%s` .BaseBranchLink $baseHref}} + {{end}} + {{if .Issue.PullRequest.HasMerged}} + {{$mergedStr:= DateUtils.TimeSince .Issue.PullRequest.MergedUnix}} + {{if .Issue.OriginalAuthor}} + {{.Issue.OriginalAuthor}} + {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}} + {{else}} + {{.Issue.PullRequest.Merger.GetDisplayName}} + {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}} + {{end}} + {{if .MadeUsingAGit}} + {{/* TODO: Move documentation link to the instructions at the bottom of the PR, show instructions when clicking label */}} + {{/* Note: #agit-label is necessary for testing whether the label appears when it should in tests/integration/git_test.go */}} + + + {{ctx.Locale.Tr "repo.pulls.made_using_agit"}} + + + {{end}} + {{else}} + {{if .Issue.OriginalAuthor}} + {{.Issue.OriginalAuthor}} {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}} + {{else}} + + {{.Issue.Poster.GetDisplayName}} + {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}} + + {{end}} + {{if .MadeUsingAGit}} + {{/* TODO: Move documentation link to the instructions at the bottom of the PR, show instructions when clicking label */}} + {{/* Note: #agit-label is necessary for testing whether the label appears when it should in tests/integration/git_test.go */}} + + + {{ctx.Locale.Tr "repo.pulls.made_using_agit"}} + + + {{end}} + {{if and .Issue.PullRequest.AllowMaintainerEdit .CanWriteCode}} + + {{ctx.Locale.Tr "repo.pulls.editable"}} + + {{end}} + + + {{svg "octicon-arrow-right"}} + + + {{end}} + {{else}} + {{$createdStr:= DateUtils.TimeSince .Issue.CreatedUnix}} + + {{if .Issue.OriginalAuthor}} + {{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr .Issue.OriginalAuthor}} + {{else if gt .Issue.Poster.ID 0}} + {{ctx.Locale.Tr "repo.issues.opened_by" $createdStr .Issue.Poster.HomeLink .Issue.Poster.GetDisplayName}} + {{else}} + {{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr .Issue.Poster.GetDisplayName}} + {{end}} + · + {{ctx.Locale.TrN .Issue.NumComments "repo.issues.num_comments_1" "repo.issues.num_comments" .Issue.NumComments}} + + {{end}} +
+
+
From 06f7d3d729511fc44934e154678873699e5400aa Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 10:52:52 +0200 Subject: [PATCH 06/29] feat(diff): collapse non-diff lines, branch order and commit menu - After "Expand all lines" the button turns into GitHub's "Collapse non-diff lines". It reloads the file's original diff through Forgejo's own "Load diff" handler, which re-enables htmx on the restored expander buttons. - English pull request headers read "into from ", with the copy button outside the branch label. Other languages keep Forgejo's translated sentence. - The commit menu opens to the right now that its button is on the left. - Drop the files tab border under the tabs: the active tab showed a line the other tabs do not have. --- public/assets/js/diff-view.js | 50 ++++++++++++++++++++++++- styles/components/issues/pull_review.ts | 3 -- styles/components/repo/diff_view.ts | 13 +++++++ templates/repo/diff/file_header.tmpl | 2 +- templates/repo/issue/view_title.tmpl | 18 ++++++--- 5 files changed, 76 insertions(+), 10 deletions(-) diff --git a/public/assets/js/diff-view.js b/public/assets/js/diff-view.js index db686adf..41ee478a 100644 --- a/public/assets/js/diff-view.js +++ b/public/assets/js/diff-view.js @@ -104,11 +104,50 @@ document .getElementById("diff-minimize-comments") ?.addEventListener("change", event => container.classList.toggle("diff-minimize-comments", event.target.checked)); + function setContextExpanded(box, expanded) { + const button = box.querySelector(".diff-expand-file"); + box.toggleAttribute("data-context-expanded", expanded); + if (!button) return; + const label = button.dataset[expanded ? "collapseLabel" : "expandLabel"]; + // Forgejo moves title into its own tooltip attribute, so keep both in sync. + button.title = label; + if (button.hasAttribute("data-tooltip-content")) button.setAttribute("data-tooltip-content", label); + button.setAttribute("aria-label", `${label}: ${box.dataset.newFilename}`); + if (expanded) button.hidden = false; + } + // GitHub's "Collapse non-diff lines": reload the file's original diff through Forgejo's own "Load diff" + // handler, which also re-enables htmx on the restored expander buttons. + function collapseContext(box, button) { + const source = box.querySelector(".diff-file-body > .file-body"); + if (!source) return; + const params = new URLSearchParams(window.location.search); + params.set("file-only", "true"); + params.delete("files"); + params.append("files", box.dataset.newFilename); + if (box.dataset.oldFilename && box.dataset.oldFilename !== box.dataset.newFilename) + params.append("files", box.dataset.oldFilename); + const holder = document.createElement("div"); + holder.className = "diff-file-body binary"; + holder.textContent = "Loading…"; + const link = document.createElement("a"); + link.className = "diff-load-button tw-hidden"; + link.dataset.href = `?${params}`; + holder.append(link); + source.replaceChildren(holder); + setContextExpanded(box, false); + box.setAttribute("data-context-reloading", ""); + button.disabled = true; + link.click(); + } container.addEventListener("click", async event => { const button = event.target.closest(".diff-expand-file"); if (!button) return; const box = button.closest(".diff-file-box"); if (box.dataset.folded === "true") box.querySelector(".fold-file").click(); + if (box.hasAttribute("data-context-expanded")) { + collapseContext(box, button); + return; + } button.disabled = true; const requested = new Set(); try { @@ -132,6 +171,7 @@ }); if (!success) break; } + if (requested.size) setContextExpanded(box, true); } finally { button.disabled = false; } @@ -144,7 +184,15 @@ } for (const box of boxes?.querySelectorAll(".diff-file-box") || []) { const expand = box.querySelector(".diff-expand-file"); - if (expand) expand.hidden = !box.querySelector(".code-expander-button"); + if (expand) { + const expanders = Boolean(box.querySelector(".code-expander-button")); + expand.hidden = !expanders && !box.hasAttribute("data-context-expanded"); + // Re-enabled once Forgejo has restored the collapsed diff. + if (expanders && box.hasAttribute("data-context-reloading")) { + box.removeAttribute("data-context-reloading"); + expand.disabled = false; + } + } const button = box.querySelector(".fold-file"); if (!button) continue; const expanded = box.dataset.folded !== "true"; diff --git a/styles/components/issues/pull_review.ts b/styles/components/issues/pull_review.ts index 45af3e47..06dddd9a 100644 --- a/styles/components/issues/pull_review.ts +++ b/styles/components/issues/pull_review.ts @@ -189,9 +189,6 @@ export default css` &.files .pull.tabs.container { margin-bottom: 0; } - &.files #diff-container { - border-top: 1px solid ${themeVars.color.light.border}; - } &.github-conversation .issue-content { grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; diff --git a/styles/components/repo/diff_view.ts b/styles/components/repo/diff_view.ts index 58b70d95..7c98a207 100644 --- a/styles/components/repo/diff_view.ts +++ b/styles/components/repo/diff_view.ts @@ -57,6 +57,14 @@ export default css` .diff-expand-file[hidden] { display: none; } + /* Once every line is expanded the button becomes GitHub's "Collapse non-diff lines". */ + .diff-expand-file > .diff-collapse-icon, + .diff-file-box[data-context-expanded] .diff-expand-file > .diff-expand-icon { + display: none; + } + .diff-file-box[data-context-expanded] .diff-expand-file > .diff-collapse-icon { + display: inline-block; + } .diff-file-status { flex: 0 0 16px; margin-left: 4px; @@ -112,6 +120,11 @@ export default css` content: var(--diff-commit-label, ""); } } + /* Forgejo opens the commit menu to the left, as its button sat on the right; it now sits on the left. */ + #diff-commit-select #diff-commit-selector-menu { + left: 0 !important; + right: auto !important; + } .diff-code-search { display: flex; gap: 8px; diff --git a/templates/repo/diff/file_header.tmpl b/templates/repo/diff/file_header.tmpl index 883be3e9..5c927c5e 100644 --- a/templates/repo/diff/file_header.tmpl +++ b/templates/repo/diff/file_header.tmpl @@ -11,7 +11,7 @@

{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}

{{if $file.IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}} - {{if eq $file.Type 1}}{{svg "octicon-file-added" 16 "diff-file-status text green"}}{{else if eq $file.Type 3}}{{svg "octicon-file-removed" 16 "diff-file-status text red"}}{{else if not (or $file.IsBin $file.IsIncomplete $file.IsSubmodule)}}{{end}} + {{if eq $file.Type 1}}{{svg "octicon-file-added" 16 "diff-file-status text green"}}{{else if eq $file.Type 3}}{{svg "octicon-file-removed" 16 "diff-file-status text red"}}{{else if not (or $file.IsBin $file.IsIncomplete $file.IsSubmodule)}}{{end}} {{if $file.IsGenerated}} {{ctx.Locale.Tr "repo.diff.generated"}} {{end}} diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index 1fbcbae2..895cd935 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -57,9 +57,17 @@ {{if .HeadBranchLink}} {{$headHref = HTMLFormat `%s` .HeadBranchLink $headHref}} {{end}} + {{/* GitHub's English sentence reads "into from ", with the copy button outside the branch label. + Other languages keep Forgejo's translated sentence, which needs the button inside the head argument. */}} + {{$githubOrder := eq (slice ctx.Locale.Lang 0 2) "en"}} + {{$copyBranch := ""}} {{if not .MadeUsingAGit}} - {{$headHref = HTMLFormat `%s ` $headHref (ctx.Locale.Tr "copy_branch") .HeadTarget (svg "octicon-copy" 14)}} + {{$copyBranch = HTMLFormat `` (ctx.Locale.Tr "copy_branch") .HeadTarget (svg "octicon-copy" 14)}} + {{if not $githubOrder}} + {{$headHref = HTMLFormat `%s %s` $headHref $copyBranch}} + {{end}} {{end}} + {{$commitsWord := "commits"}}{{if eq .NumCommits 1}}{{$commitsWord = "commit"}}{{end}} {{$baseHref := .BaseTarget}} {{if .BaseBranchLink}} {{$baseHref = HTMLFormat `%s` .BaseBranchLink $baseHref}} @@ -68,10 +76,10 @@ {{$mergedStr:= DateUtils.TimeSince .Issue.PullRequest.MergedUnix}} {{if .Issue.OriginalAuthor}} {{.Issue.OriginalAuthor}} - {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}} + {{if $githubOrder}}merged {{.NumCommits}} {{$commitsWord}} into {{$baseHref}} from {{$headHref}}{{$copyBranch}} {{$mergedStr}}{{else}}{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}{{end}} {{else}} {{.Issue.PullRequest.Merger.GetDisplayName}} - {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}} + {{if $githubOrder}}merged {{.NumCommits}} {{$commitsWord}} into {{$baseHref}} from {{$headHref}}{{$copyBranch}} {{$mergedStr}}{{else}}{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}{{end}} {{end}} {{if .MadeUsingAGit}} {{/* TODO: Move documentation link to the instructions at the bottom of the PR, show instructions when clicking label */}} @@ -84,11 +92,11 @@ {{end}} {{else}} {{if .Issue.OriginalAuthor}} - {{.Issue.OriginalAuthor}} {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}} + {{.Issue.OriginalAuthor}} {{if $githubOrder}}wants to merge {{.NumCommits}} {{$commitsWord}} into {{$baseHref}} from {{$headHref}}{{$copyBranch}}{{else}}{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}}{{end}} {{else}} {{.Issue.Poster.GetDisplayName}} - {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}} + {{if $githubOrder}}wants to merge {{.NumCommits}} {{$commitsWord}} into {{$baseHref}} from {{$headHref}}{{$copyBranch}}{{else}}{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}}{{end}} {{end}} {{if .MadeUsingAGit}} From fd07c7536e1c80e23a664c1f08b9f44ebef6dd69 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 11:10:44 +0200 Subject: [PATCH 07/29] fix(diff): let commit menu entries grow to two lines The shared dropdown style pins menu items to 32px with !important, so commit entries (summary and author line) overlapped the next one. --- styles/components/repo/diff_view.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/styles/components/repo/diff_view.ts b/styles/components/repo/diff_view.ts index 7c98a207..8f5c5848 100644 --- a/styles/components/repo/diff_view.ts +++ b/styles/components/repo/diff_view.ts @@ -125,6 +125,14 @@ export default css` left: 0 !important; right: auto !important; } + /* The shared dropdown style pins items to 32px with !important; commit entries have a summary and an + author line. */ + #diff-commit-selector-menu > .vertical.item { + height: auto !important; + min-height: 32px !important; + padding-top: 8px !important; + padding-bottom: 8px !important; + } .diff-code-search { display: flex; gap: 8px; From 85f85836490dd6210c82c27a702cf7da49c51d42 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 11:15:15 +0200 Subject: [PATCH 08/29] feat(issues): GitHub-style search field and sidebar order - The issue and pull request search shows GitHub's qualifier list on focus instead of Forgejo's "?" button under the field. It lists the qualifiers Forgejo understands with their translated hints, inserts one on click or Enter, and links to the search syntax docs. - Qualifier values (author:) are highlighted in blue through a mirror behind the transparent input text. - Every search field gets a clear button with a pointer; it replaces the browser's own one, which only emptied the field. A submitted search that is applied reloads without it. - The sidebar follows GitHub's order (created, assigned, mentioned, review requested; then milestones and labels). Forgejo's extra entries stay after them. --- components/common/search/search.css | 155 ++++++++++++++++++++++++++- components/common/search/search.js | 120 +++++++++++++++++++++ components/common/search/search.tmpl | 9 +- components/common/search/syntax.tmpl | 15 +++ templates/base/component_assets.tmpl | 1 + templates/repo/issue/search.tmpl | 7 +- templates/repo/issue/sidebar.tmpl | 6 +- templates/user/dashboard/issues.tmpl | 5 +- 8 files changed, 303 insertions(+), 15 deletions(-) create mode 100644 components/common/search/search.js create mode 100644 components/common/search/syntax.tmpl diff --git a/components/common/search/search.css b/components/common/search/search.css index daf4316a..059b19c7 100644 --- a/components/common/search/search.css +++ b/components/common/search/search.css @@ -17,7 +17,7 @@ } /* :is() keeps one specificity for fields inside and outside Forgejo's .ui.form. */ -:is(.ui.form, body) .theme-search-field > input[type="search"] { +:is(.ui.form, body) .theme-search-field input[type="search"] { flex: 1; width: 100%; min-width: 0; @@ -33,7 +33,7 @@ outline: none; } -:is(.ui.form, body) .theme-search-field > input[type="search"]::placeholder { +:is(.ui.form, body) .theme-search-field input[type="search"]::placeholder { color: var(--color-text-light-1); opacity: 1; } @@ -50,7 +50,7 @@ margin-right: 9px; } -:is(.ui.form, body) .theme-search-field-icon > input[type="search"] { +:is(.ui.form, body) .theme-search-field-icon input[type="search"] { padding-left: 0; } @@ -72,6 +72,155 @@ background: var(--color-hover); } +/* The browser's own clear button has no pointer and only empties the field; search.js handles this one. */ +:is(.ui.form, body) .theme-search-field input[type="search"]::-webkit-search-cancel-button { + display: none; +} + +.theme-search-field > .theme-search-clear { + flex: none; + display: grid; + place-items: center; + width: 28px; + padding: 0; + border: 0; + background: transparent; + color: var(--color-text-light-1); + cursor: pointer; +} + +.theme-search-field > .theme-search-clear:hover { + color: var(--color-text); +} + +.theme-search-field:has(> input:placeholder-shown, > .theme-search-input-wrap > input:placeholder-shown) > .theme-search-clear { + display: none; +} + +/* Syntax fields: a mirror behind the transparent input text colours qualifier values (author:). */ +.theme-search-syntax-control { + position: relative; + width: 100%; + min-width: 0; +} + +.theme-search-input-wrap { + position: relative; + display: flex; + flex: 1; + min-width: 0; +} + +.theme-search-highlight { + position: absolute; + inset: 0; + padding: 5px 12px; + overflow: hidden; + color: var(--color-text); + font: 14px/20px var(--fonts-proportional); + white-space: pre; + pointer-events: none; +} + +.theme-search-field-icon .theme-search-highlight { + padding-left: 0; +} + +:is(.ui.form, body) .theme-search-input-wrap > input[type="search"] { + color: transparent; + caret-color: var(--color-text); +} + +.theme-search-qualifier-value { + color: var(--github-fgColor-accent); +} + +/* GitHub's qualifier list under the field. */ +.theme-search-syntax { + position: absolute; + top: calc(100% + 4px); + left: 0; + z-index: 30; + display: flex; + flex-direction: column; + width: min(440px, 100%); + max-height: 340px; + overflow-y: auto; + padding: 8px 0 0; + border: 1px solid var(--color-light-border); + border-radius: 12px; + background: var(--color-menu); + box-shadow: var(--github-shadow-floating-small, 0 8px 24px rgb(0 0 0 / 0.2)); +} + +.theme-search-syntax[hidden] { + display: none; +} + +.theme-search-syntax-item { + display: flex; + align-items: center; + gap: 8px; + min-height: 32px; + margin: 0 8px; + padding: 6px 8px; + border: 0; + border-radius: var(--border-radius); + background: transparent; + color: var(--color-text); + font-size: 14px; + line-height: 20px; + text-align: left; + cursor: pointer; +} + +.theme-search-syntax-item[hidden] { + display: none; +} + +.theme-search-syntax-item > svg { + flex: none; + color: var(--color-text-light-1); +} + +.theme-search-syntax-item > code { + flex: none; + font: 12px/20px var(--fonts-monospace); +} + +.theme-search-syntax-item > span { + min-width: 0; + overflow: hidden; + color: var(--color-text-light-1); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.theme-search-syntax-item > span code { + font-size: 11px; +} + +.theme-search-syntax-item:hover, +.theme-search-syntax-item.active { + background: var(--color-hover); +} + +.theme-search-syntax-docs { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + padding: 8px 16px; + border-top: 1px solid var(--color-light-border); + color: var(--color-text-light-1); + font-size: 12px; +} + +.theme-search-syntax-docs:hover { + color: var(--github-fgColor-accent); +} + /* Forgejo's native query/mode/submit group (shared/search/combo_multi), opted in on its form. */ .ui.form.theme-search-control > .ui.action.input { display: flex; diff --git a/components/common/search/search.js b/components/common/search/search.js new file mode 100644 index 00000000..dce800f2 --- /dev/null +++ b/components/common/search/search.js @@ -0,0 +1,120 @@ +(() => { + // The clear button empties the field; a submitted search that is currently applied reloads without it. + document.addEventListener("click", event => { + const clear = event.target.closest(".theme-search-clear"); + if (!clear) return; + event.preventDefault(); + const input = clear.closest(".theme-search-field")?.querySelector('input[type="search"]'); + if (!input) return; + const applied = Boolean(input.name) && input.defaultValue !== ""; + input.value = ""; + input.dispatchEvent(new Event("input", { bubbles: true })); + input.focus(); + if (applied && input.form) input.form.requestSubmit(); + }); + + // Qualifiers Forgejo's issue search understands; their values are highlighted like GitHub's. + const qualifiers = new Set(["is", "author", "assignee", "review", "mentions", "sort", "modified"]); + + function setup(input) { + const control = input.closest(".theme-search-syntax-control"); + const highlight = control?.querySelector(".theme-search-highlight"); + const popup = control?.querySelector(".theme-search-syntax"); + if (!highlight || !popup) return; + const items = [...popup.querySelectorAll(".theme-search-syntax-item")]; + let active = -1; + + function render() { + highlight.replaceChildren(); + for (const part of input.value.split(/(\s+)/)) { + const match = /^(-?)([a-z]+):(.*)$/i.exec(part); + if (!match || !qualifiers.has(match[2].toLowerCase())) { + highlight.append(part); + continue; + } + const value = document.createElement("span"); + value.className = "theme-search-qualifier-value"; + value.textContent = match[3]; + highlight.append(`${match[1]}${match[2]}:`, value); + } + highlight.scrollLeft = input.scrollLeft; + } + + // The word under the caret decides which qualifiers are offered. + function currentToken() { + const end = input.selectionStart ?? input.value.length; + const start = input.value.lastIndexOf(" ", end - 1) + 1; + return { start, end, text: input.value.slice(start, end) }; + } + const visibleItems = () => items.filter(item => !item.hidden); + function setActive(index) { + active = index; + const target = visibleItems()[active]; + items.forEach(item => item.classList.toggle("active", item === target)); + target?.scrollIntoView({ block: "nearest" }); + } + function update() { + const text = currentToken().text.toLowerCase(); + let any = false; + for (const item of items) { + item.hidden = !item.dataset.insert.toLowerCase().startsWith(text); + any ||= !item.hidden; + } + popup.hidden = document.activeElement !== input || text.includes(":") || !any; + input.setAttribute("aria-expanded", String(!popup.hidden)); + setActive(-1); + } + function close() { + popup.hidden = true; + input.setAttribute("aria-expanded", "false"); + } + function insert(item) { + const { start, end } = currentToken(); + const text = item.dataset.insert; + input.value = input.value.slice(0, start) + text + input.value.slice(end); + const caret = start + text.length; + input.setSelectionRange(caret, caret); + input.focus(); + render(); + update(); + } + + input.addEventListener("input", () => { + render(); + update(); + }); + input.addEventListener("scroll", () => (highlight.scrollLeft = input.scrollLeft)); + input.addEventListener("focus", update); + input.addEventListener("click", update); + input.addEventListener("keyup", event => { + if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) update(); + highlight.scrollLeft = input.scrollLeft; + }); + input.addEventListener("blur", close); + input.addEventListener("keydown", event => { + if (popup.hidden) return; + const visible = visibleItems(); + if (event.key === "ArrowDown" || event.key === "ArrowUp") { + event.preventDefault(); + const step = event.key === "ArrowDown" ? 1 : -1; + setActive((active + step + visible.length) % visible.length); + } else if (event.key === "Enter" && active >= 0) { + event.preventDefault(); + insert(visible[active]); + } else if (event.key === "Escape") { + close(); + } + }); + // Keep the focus in the field while picking a qualifier. + popup.addEventListener("mousedown", event => { + if (!event.target.closest(".theme-search-syntax-docs")) event.preventDefault(); + }); + popup.addEventListener("click", event => { + const item = event.target.closest(".theme-search-syntax-item"); + if (item) insert(item); + }); + render(); + } + + for (const input of document.querySelectorAll("input[data-search-syntax]")) setup(input); +})(); diff --git a/components/common/search/search.tmpl b/components/common/search/search.tmpl index b03a06ae..34f5aa81 100644 --- a/components/common/search/search.tmpl +++ b/components/common/search/search.tmpl @@ -1,6 +1,11 @@ -{{/* Kind: submit (default, trailing search button), icon (leading search icon), or plain. Local marks a client-side filter that is never submitted. Class adds page layout hooks. */}} +{{/* Kind: submit (default, trailing search button), icon (leading search icon), or plain. Local marks a client-side filter that is never submitted. Class adds page layout hooks. Syntax adds GitHub's qualifier popup and value highlighting (issue and pull request searches). */}} {{$icon := eq .Kind "icon"}} +{{if .Syntax}}
{{end}} {{if $icon}}
{{end}} diff --git a/components/common/search/syntax.tmpl b/components/common/search/syntax.tmpl new file mode 100644 index 00000000..e27ab460 --- /dev/null +++ b/components/common/search/syntax.tmpl @@ -0,0 +1,15 @@ +{{/* GitHub lists the search qualifiers when the field gets focus; these are the ones Forgejo's issue search understands + (the entries of Forgejo's shared/search/issue/syntax dialog). search.js inserts data-insert at the caret. */}} + diff --git a/templates/base/component_assets.tmpl b/templates/base/component_assets.tmpl index 8083c3ac..6508e631 100644 --- a/templates/base/component_assets.tmpl +++ b/templates/base/component_assets.tmpl @@ -6,6 +6,7 @@ + diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl index 2d591800..5d83bf11 100644 --- a/templates/repo/issue/search.tmpl +++ b/templates/repo/issue/search.tmpl @@ -10,13 +10,10 @@ {{end}} {{if .PageIsPullList}} - {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "search.verb")}} + {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "search.verb") "Syntax" true}} {{else if .PageIsMilestones}} {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.milestone_kind") "Tooltip" (ctx.Locale.Tr "search.verb")}} {{else}} - {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "search.verb")}} + {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "search.verb") "Syntax" true}} {{end}} - {{if not .PageIsMilestones}} - {{template "shared/search/issue/syntax"}} - {{end}} diff --git a/templates/repo/issue/sidebar.tmpl b/templates/repo/issue/sidebar.tmpl index 85899f13..5f412d42 100644 --- a/templates/repo/issue/sidebar.tmpl +++ b/templates/repo/issue/sidebar.tmpl @@ -5,8 +5,9 @@
{{template "components/common/sidebar/toggle" .}} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 8de2e0a3..7cd9c347 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -35,11 +35,10 @@ {{if .PageIsPulls}} - {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "search.verb")}} + {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "search.verb") "Syntax" true}} {{else}} - {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "search.verb")}} + {{template "components/common/search/search" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "search.verb") "Syntax" true}} {{end}} - {{template "shared/search/issue/syntax"}}
From 1650c410d8366ffcd5379898d24313648cef5248 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 11:33:15 +0200 Subject: [PATCH 09/29] fix: define the colour variables Forgejo 17 expects from a theme Forgejo 16 coloured .text.green/.red/.purple/.orange with var(--color-thin-*, var(--color-*)). Forgejo 17 dropped the fallback and derives --color-thin-* from inputs only its own themes define, so every status icon (pull request state, commit checks, Actions runs) and the markdown note/important alerts lost their colour and turned white. - Define --color-thin-* as the theme's plain colours, as Forgejo 16 did. - Define the other Forgejo-theme-only variables: Actions runner indicators, image diff checkerboard, migration cards and disabled repo button counters. - Add a build test that every theme defines them. - The search qualifier list shows plain names (Open, Closed, Author...) with the qualifier as tooltip, and filters on either. --- components/common/search/search.css | 13 +------------ components/common/search/search.js | 3 ++- components/common/search/syntax.tmpl | 25 +++++++++++++------------ src/build-output.test.ts | 27 +++++++++++++++++++++++++++ styles/public/base.ts | 23 +++++++++++++++++++++++ 5 files changed, 66 insertions(+), 25 deletions(-) diff --git a/components/common/search/search.css b/components/common/search/search.css index 059b19c7..72fef4ad 100644 --- a/components/common/search/search.css +++ b/components/common/search/search.css @@ -143,7 +143,7 @@ z-index: 30; display: flex; flex-direction: column; - width: min(440px, 100%); + width: min(256px, 100%); max-height: 340px; overflow-y: auto; padding: 8px 0 0; @@ -183,24 +183,13 @@ color: var(--color-text-light-1); } -.theme-search-syntax-item > code { - flex: none; - font: 12px/20px var(--fonts-monospace); -} - .theme-search-syntax-item > span { min-width: 0; overflow: hidden; - color: var(--color-text-light-1); - font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } -.theme-search-syntax-item > span code { - font-size: 11px; -} - .theme-search-syntax-item:hover, .theme-search-syntax-item.active { background: var(--color-hover); diff --git a/components/common/search/search.js b/components/common/search/search.js index dce800f2..a8eee458 100644 --- a/components/common/search/search.js +++ b/components/common/search/search.js @@ -57,7 +57,8 @@ const text = currentToken().text.toLowerCase(); let any = false; for (const item of items) { - item.hidden = !item.dataset.insert.toLowerCase().startsWith(text); + const name = item.textContent.trim().toLowerCase(); + item.hidden = !(item.dataset.insert.toLowerCase().startsWith(text) || name.startsWith(text)); any ||= !item.hidden; } popup.hidden = document.activeElement !== input || text.includes(":") || !any; diff --git a/components/common/search/syntax.tmpl b/components/common/search/syntax.tmpl index e27ab460..f25156a7 100644 --- a/components/common/search/syntax.tmpl +++ b/components/common/search/syntax.tmpl @@ -1,15 +1,16 @@ -{{/* GitHub lists the search qualifiers when the field gets focus; these are the ones Forgejo's issue search understands - (the entries of Forgejo's shared/search/issue/syntax dialog). search.js inserts data-insert at the caret. */}} +{{/* GitHub lists the search qualifiers by name when the field gets focus; these are the ones Forgejo's issue search + understands (the entries of Forgejo's shared/search/issue/syntax dialog), with its hint as the tooltip. + search.js inserts data-insert at the caret and matches typing against the name or the qualifier. */}} diff --git a/src/build-output.test.ts b/src/build-output.test.ts index 6b5384dd..6ac3d9b1 100644 --- a/src/build-output.test.ts +++ b/src/build-output.test.ts @@ -168,6 +168,33 @@ it("stamps every template asset link with the hash of the shipped file", async ( } }); +it("defines the colour variables Forgejo 17 only sets in its own themes", () => { + // Forgejo 17 reads these without a fallback (.text.green, Actions runner status, image diffs...). Undefined, the + // coloured status icons all turned white. + const required = [ + "--color-thin-green", + "--color-thin-red", + "--color-thin-purple", + "--color-thin-violet", + "--color-thin-orange", + "--color-thin-blue", + "--color-indicator-offline", + "--color-indicator-idle", + "--color-indicator-active", + "--checkerboard-color-1", + "--checkerboard-color-2", + "--fancy-card-bg", + "--color-light-mimic-enabled", + ]; + // Auto themes only import a light and a dark file. + const themes = fs.readdirSync(DIST_DIR).filter(name => /^theme-.+\.css$/.test(name) && !name.endsWith("-auto.css")); + expect(themes.length).toBeGreaterThan(0); + for (const theme of themes) { + const css = fs.readFileSync(path.join(DIST_DIR, theme), "utf-8"); + for (const variable of required) expect(css, `${theme} defines ${variable}`).toContain(`${variable}:`); + } +}); + it("keeps :has() off elements that wrap a whole page", () => { // Chrome re-evaluates a :has() whenever its subtree changes. On an element that wraps the page, a folded diff file, // an opened comment editor or a selected line in a large file restyles every element on the page. diff --git a/styles/public/base.ts b/styles/public/base.ts index 6875278b..8c605294 100644 --- a/styles/public/base.ts +++ b/styles/public/base.ts @@ -28,6 +28,29 @@ const body = css` :root { --color-selection-bg: ${themeVars.color.primary.light.num1}; --color-selection-fg: ${themeVars.color.white}; + /* + * Forgejo 17 colours .text.green/.red/.purple/.orange, markdown alerts and more with --color-thin-*, which it + * derives from Forgejo-theme-only inputs and no longer falls back from. Undefined, every such icon turned white. + * Forgejo 16 fell back to the plain colours, so use those. + */ + --color-thin-green: var(--color-green); + --color-thin-red: var(--color-red); + --color-thin-purple: var(--color-purple); + --color-thin-violet: var(--color-violet); + --color-thin-orange: var(--color-orange); + --color-thin-blue: var(--color-blue); + /* Other variables only Forgejo's own themes define. */ + --color-indicator-offline: var(--color-text-light-2); + --color-indicator-offline-20: color-mix(in srgb, var(--color-indicator-offline) 10%, transparent); + --color-indicator-idle: var(--color-green); + --color-indicator-idle-20: color-mix(in srgb, var(--color-indicator-idle) 10%, transparent); + --color-indicator-active: var(--color-blue); + --color-indicator-active-20: color-mix(in srgb, var(--color-indicator-active) 20%, transparent); + --checkerboard-color-1: var(--color-box-body); + --checkerboard-color-2: var(--color-secondary); + --fancy-card-bg: var(--color-box-body); + --fancy-card-border: var(--color-light-border); + --color-light-mimic-enabled: var(--color-button); --fonts-proportional: "Mona Sans VF", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; --fonts-monospace: From 5fc42daf54e28859924fffec758d458b38c837be Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 11:50:15 +0200 Subject: [PATCH 10/29] fix(actions): colour job step status icons The job view forced the successful step check to muted grey. Show success in green like every other status icon, and mute skipped steps, whose "grey" is the regular text colour in this theme. --- public/actions-run.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/actions-run.css b/public/actions-run.css index 6811cbac..de9f487f 100644 --- a/public/actions-run.css +++ b/public/actions-run.css @@ -223,8 +223,9 @@ font: 14px/21px var(--fonts-proportional); border-radius: var(--border-radius); } - .job-step-summary .octicon-check-circle-fill { - color: var(--color-text-light-1) !important; + /* Skipped steps are muted on GitHub; the theme's grey text is the regular text colour. */ + .job-step-summary .octicon-skip.text.grey { + color: var(--color-text-light-2) !important; } .job-step-summary:hover { background: var(--github-control-transparent-bgColor-hover); From 62ee28f6bc151a81ef98b7fa232828a6b07f6c81 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 11:54:14 +0200 Subject: [PATCH 11/29] feat(repo): Code label, fj CLI clone and GitHub sidebar sections - Keep the "Code" label on the clone button next to "Go to file", as GitHub does. - Add an "fj CLI" clone option beside HTTPS and SSH showing "fj repo clone //" (forgejo-cli). It wraps Forgejo's updateCloneStates so the choice persists like HTTPS/SSH. - The clone URL field used the undefined --font-monospace; use --fonts-monospace. - Always show Releases (with "No releases published" and a New release link) and add Packages, listing the packages linked to the repository or Forgejo's empty message with a link to the docs. --- public/repo-latest-release.css | 43 +++++++++ styles/components/repo/file_picker.ts | 13 --- styles/templates/repo/view_content.ts | 19 ++-- templates/repo/home_sidebar_bottom.tmpl | 117 +++++++++++++++++------- templates/repo/view_content.tmpl | 36 ++++++++ 5 files changed, 176 insertions(+), 52 deletions(-) diff --git a/public/repo-latest-release.css b/public/repo-latest-release.css index 38cb37bc..e6f04e13 100644 --- a/public/repo-latest-release.css +++ b/public/repo-latest-release.css @@ -85,3 +85,46 @@ /* GitHub leaves a compact gap below the release-count link before the divider. */ .page-content.repository.file.list .repo-release-count { margin-bottom: -8px; } .page-content.repository.file.list .repo-release-count { margin-bottom: 10px; } + +/* GitHub's empty Releases/Packages states: a muted line and a first-step link. */ +.repo-home-sidebar-empty { + display: flex; + flex-direction: column; + gap: 2px; + color: var(--color-text-light-1); + font-size: 12px; + line-height: 18px; +} + +.repo-home-sidebar-empty > a { + color: var(--color-primary); + text-decoration: underline; +} + +.repo-home-packages { + display: flex; + flex-direction: column; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} + +.repo-home-packages a { + display: flex; + align-items: center; + gap: 8px; + color: var(--color-text); + font-size: 14px; + font-weight: 600; +} + +.repo-home-packages a > svg { + flex: none; + color: var(--color-text-light-1); +} + +.repo-home-packages-all { + color: var(--color-primary); + font-size: 12px; +} diff --git a/styles/components/repo/file_picker.ts b/styles/components/repo/file_picker.ts index 102b5103..b5c7732e 100644 --- a/styles/components/repo/file_picker.ts +++ b/styles/components/repo/file_picker.ts @@ -95,19 +95,6 @@ export default css` .repo-button-row-right:has(.repo-file-picker) .repo-add-file { min-width: 98px; } - .gitea-github-theme-templates.repo-button-row - .repo-button-row-right:has(.repo-file-picker) - .repo-code-dropdown - > summary.ui.primary.button { - padding: 5px 8px !important; - gap: 4px; - } - .repo-button-row-right:has(.repo-file-picker) .repo-code-dropdown > summary > span { - display: none; - } - .repo-button-row-right:has(.repo-file-picker) .repo-code-dropdown > summary > svg:last-child { - width: 12px; - } .repo-file-picker-empty { padding: 16px; text-align: center; diff --git a/styles/templates/repo/view_content.ts b/styles/templates/repo/view_content.ts index f15d7ea0..82062434 100644 --- a/styles/templates/repo/view_content.ts +++ b/styles/templates/repo/view_content.ts @@ -178,7 +178,7 @@ const repoToolbarDetails = css` align-items: center; column-gap: 0; display: grid; - grid-template-columns: max-content max-content minmax(0, 1fr) 36px; + grid-template-columns: max-content max-content max-content minmax(0, 1fr) 36px; grid-template-rows: 32px 32px; height: auto !important; min-height: 72px; @@ -187,7 +187,8 @@ const repoToolbarDetails = css` } & .repo-code-dropdown .clone-panel > #repo-clone-https, - & .repo-code-dropdown .clone-panel > #repo-clone-ssh { + & .repo-code-dropdown .clone-panel > #repo-clone-ssh, + & .repo-code-dropdown .clone-panel > #repo-clone-fj { background: transparent !important; border: 1px solid #0000 !important; border-radius: ${otherThemeVars.border.radius} !important; @@ -212,13 +213,17 @@ const repoToolbarDetails = css` grid-column: 2; } - & .repo-code-dropdown .clone-panel > .primary:is(#repo-clone-https, #repo-clone-ssh) { + & .repo-code-dropdown .clone-panel > #repo-clone-fj { + grid-column: 3; + } + + & .repo-code-dropdown .clone-panel > .primary:is(#repo-clone-https, #repo-clone-ssh, #repo-clone-fj) { background: light-dark(${themeVars.github.controlKnob.bgColor.rest}, ${themeVars.color.hover.self}) !important; border-color: ${themeVars.color.secondary.self} !important; font-weight: 600; } - & .repo-code-dropdown .clone-panel > .basic:is(#repo-clone-https, #repo-clone-ssh):hover { + & .repo-code-dropdown .clone-panel > .basic:is(#repo-clone-https, #repo-clone-ssh, #repo-clone-fj):hover { background: ${themeVars.github.control.transparent.bgColor.hover} !important; } @@ -226,9 +231,9 @@ const repoToolbarDetails = css` background: ${themeVars.color.box.header}; border: 1px solid ${themeVars.color.light.border}; border-radius: ${otherThemeVars.border.radius}; - font-family: var(--font-monospace); + font-family: var(--fonts-monospace); font-size: 12px; - grid-column: 1 / 4; + grid-column: 1 / 5; grid-row: 2; height: 32px; min-width: 0; @@ -249,7 +254,7 @@ const repoToolbarDetails = css` display: grid; font-size: 14px; gap: 0; - grid-column: 4; + grid-column: 5; grid-row: 2; height: 32px; justify-content: center; diff --git a/templates/repo/home_sidebar_bottom.tmpl b/templates/repo/home_sidebar_bottom.tmpl index f9a7c7e4..80f20bae 100644 --- a/templates/repo/home_sidebar_bottom.tmpl +++ b/templates/repo/home_sidebar_bottom.tmpl @@ -1,42 +1,95 @@ -{{/* Repository-root metadata sections available in Forgejo 15. */}} +{{/* Repository-root metadata sections: releases, packages and languages, like GitHub's sidebar. */}}
{{if and (not (StringUtils.HasPrefix .BranchNameSubURL "commit")) (or .CodeIndexerDisabled (and (not .TagName) (eq .Repository.DefaultBranch .BranchName)))}} @@ -138,6 +139,41 @@
{{template "repo/clone_script" .}}{{/* Updates .js-clone-url and related elements. */}} +
{{end}}
From 87ee9c2fa7729371626f4865f3c15db7aff8725d Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 11:58:38 +0200 Subject: [PATCH 12/29] feat(repo): name the clone option Forgejo CLI and link its docs Show "Work fast with the Forgejo CLI. Learn more" under the clone field when it is selected, like GitHub does for its CLI. --- src/build-output.test.ts | 2 +- styles/templates/repo/view_content.ts | 16 +++++++++++++++- templates/repo/view_content.tmpl | 5 ++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/build-output.test.ts b/src/build-output.test.ts index 6ac3d9b1..6c5e9032 100644 --- a/src/build-output.test.ts +++ b/src/build-output.test.ts @@ -455,7 +455,7 @@ describe("Forgejo 15 native integration", () => { expect(repoContentTemplate).toContain('svg "octicon-terminal" 16'); expect(repoContentStyle).toContain("border-radius: 12px;"); expect(repoContentStyle).toContain("width: min(400px, calc(100vw - 32px));"); - expect(repoContentStyle).toContain("grid-template-rows: 32px 32px;"); + expect(repoContentStyle).toContain("grid-template-rows: 32px 32px auto;"); expect(repoContentStyle).toContain("border-radius: ${otherThemeVars.border.radius} !important;"); expect(repoContentStyle).toContain("font-size: 14px;"); expect(repoContentStyle).toContain("padding: 0 !important;"); diff --git a/styles/templates/repo/view_content.ts b/styles/templates/repo/view_content.ts index 82062434..fabe450f 100644 --- a/styles/templates/repo/view_content.ts +++ b/styles/templates/repo/view_content.ts @@ -179,7 +179,7 @@ const repoToolbarDetails = css` column-gap: 0; display: grid; grid-template-columns: max-content max-content max-content minmax(0, 1fr) 36px; - grid-template-rows: 32px 32px; + grid-template-rows: 32px 32px auto; height: auto !important; min-height: 72px; row-gap: 8px; @@ -217,6 +217,20 @@ const repoToolbarDetails = css` grid-column: 3; } + /* GitHub explains its CLI under the field: "Work fast with our official CLI. Learn more". */ + & .repo-code-dropdown .clone-panel > .repo-clone-hint { + grid-column: 1 / -1; + grid-row: 3; + margin: 0; + color: ${themeVars.color.text.light.num1}; + font-size: 12px; + line-height: 18px; + } + + & .repo-code-dropdown .clone-panel > .repo-clone-hint[hidden] { + display: none; + } + & .repo-code-dropdown .clone-panel > .primary:is(#repo-clone-https, #repo-clone-ssh, #repo-clone-fj) { background: light-dark(${themeVars.github.controlKnob.bgColor.rest}, ${themeVars.color.hover.self}) !important; border-color: ${themeVars.color.secondary.self} !important; diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 2d58569b..3ce3e28e 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -101,7 +101,8 @@
{{template "repo/clone_buttons" .}} - + +
{{if and (not (StringUtils.HasPrefix .BranchNameSubURL "commit")) (or .CodeIndexerDisabled (and (not .TagName) (eq .Repository.DefaultBranch .BranchName)))}} @@ -159,6 +160,8 @@ const fj = chosen(); fjButton.classList.toggle('primary', fj); fjButton.classList.toggle('basic', !fj); + const hint = document.getElementById('repo-clone-fj-hint'); + if (hint) hint.hidden = !fj; if (!fj) return; for (const button of document.querySelectorAll('#repo-clone-https, #repo-clone-ssh')) { button.classList.replace('primary', 'basic'); From ef5bf8668b7daf267e8690a79b67ceb1ee88f247 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 15:14:17 +0200 Subject: [PATCH 13/29] fix(dropdown): sharp dropdown menus and animated Code menu Fomantic keeps will-change: transform, opacity on every dropdown menu, which leaves it on its own GPU layer; at a fractional position (the right-aligned "Add file" menu) its text rendered blurry. Reset it. The Code menu opened instantly; give it the same appear-down animation as the dropdowns next to it. --- styles/public/dropdown/dropdown.ts | 3 +++ styles/templates/repo/view_content.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/styles/public/dropdown/dropdown.ts b/styles/public/dropdown/dropdown.ts index 2157d7ce..a2de9003 100644 --- a/styles/public/dropdown/dropdown.ts +++ b/styles/public/dropdown/dropdown.ts @@ -25,6 +25,9 @@ const dropdown = css` .ui.menu .ui.dropdown { .menu { ${animationDown}; + /* Fomantic keeps will-change: transform on every menu, which leaves it on its own GPU layer; at a fractional + position (centred or right-aligned menus) the text renders blurry. The animation does not need the hint. */ + will-change: auto; /* 统一所有下拉菜单的样式 */ background-color: ${themeVars.color.menu}; border: none !important; diff --git a/styles/templates/repo/view_content.ts b/styles/templates/repo/view_content.ts index fabe450f..433c5bb6 100644 --- a/styles/templates/repo/view_content.ts +++ b/styles/templates/repo/view_content.ts @@ -18,6 +18,7 @@ */ import { css, cssCombine, otherThemeVars, themeVars } from "@lutinglt/gitea-github-theme/core"; +import { animationDown } from "@lutinglt/gitea-github-theme/styles/common"; const repoButtonRow = css` .gitea-github-theme-templates { @@ -113,6 +114,8 @@ const repoToolbarDetails = css` } & .repo-code-dropdown > .content { + /* Opens like the Forgejo dropdowns next to it ("Add file"). */ + ${animationDown}; background: ${themeVars.color.menu}; border: 0; border-radius: 12px; From 6e433c8fa66be95e0ac181696e812282d38b9f93 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 15:34:04 +0200 Subject: [PATCH 14/29] fix: animate the Code menu on every open, sharp modals - A closed
keeps its content hidden with content-visibility, so the CSS animation only played on the first open. Run it from the toggle event with element.animate() instead. - Fomantic modals kept will-change on their own GPU layer; centred at a fractional position their text was blurry. Reset it like the dropdown menus, and test both resets. --- src/build-output.test.ts | 7 +++++++ styles/public/modal.ts | 3 +++ styles/templates/repo/view_content.ts | 3 --- templates/repo/view_content.tmpl | 11 +++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/build-output.test.ts b/src/build-output.test.ts index 6c5e9032..21a67c74 100644 --- a/src/build-output.test.ts +++ b/src/build-output.test.ts @@ -195,6 +195,13 @@ it("defines the colour variables Forgejo 17 only sets in its own themes", () => } }); +it("resets Fomantic's will-change on dropdown menus and modals", () => { + // will-change keeps the element on its own GPU layer; at a fractional position its text renders blurry. + for (const file of ["styles/public/dropdown/dropdown.ts", "styles/public/modal.ts"]) { + expect(fs.readFileSync(path.join(ROOT_DIR, file), "utf-8"), file).toContain("will-change: auto;"); + } +}); + it("keeps :has() off elements that wrap a whole page", () => { // Chrome re-evaluates a :has() whenever its subtree changes. On an element that wraps the page, a folded diff file, // an opened comment editor or a selected line in a large file restyles every element on the page. diff --git a/styles/public/modal.ts b/styles/public/modal.ts index 94fc793d..939b5cd9 100644 --- a/styles/public/modal.ts +++ b/styles/public/modal.ts @@ -23,6 +23,9 @@ import { animationDown } from "@lutinglt/gitea-github-theme/styles/common"; const modal = css` .ui.ui.modal { ${animationDown}; + /* Fomantic's will-change keeps the modal on its own GPU layer; centred at a fractional position, its text + renders blurry. */ + will-change: auto; border: 1.5px solid ${themeVars.color.light.border}; border-radius: 12px; diff --git a/styles/templates/repo/view_content.ts b/styles/templates/repo/view_content.ts index 433c5bb6..fabe450f 100644 --- a/styles/templates/repo/view_content.ts +++ b/styles/templates/repo/view_content.ts @@ -18,7 +18,6 @@ */ import { css, cssCombine, otherThemeVars, themeVars } from "@lutinglt/gitea-github-theme/core"; -import { animationDown } from "@lutinglt/gitea-github-theme/styles/common"; const repoButtonRow = css` .gitea-github-theme-templates { @@ -114,8 +113,6 @@ const repoToolbarDetails = css` } & .repo-code-dropdown > .content { - /* Opens like the Forgejo dropdowns next to it ("Add file"). */ - ${animationDown}; background: ${themeVars.color.menu}; border: 0; border-radius: 12px; diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 3ce3e28e..82982fe1 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -177,6 +177,17 @@ window.updateCloneStates(); })(); +
{{end}}
From f4898e302dbe73fa31e03cec20323cfa0c326a2d Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 15:46:19 +0200 Subject: [PATCH 15/29] fix(repo): start the Code menu animation without a jump The toggle event fires a frame after the menu opens, so the menu showed in place and then jumped up to start the animation. Start it from the summary click in a requestAnimationFrame, the frame that first paints the open menu. --- templates/repo/view_content.tmpl | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 82982fe1..63ee3c9b 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -179,14 +179,24 @@ {{end}} From 98f91db813f3c244d6496b5926246d8b31009a44 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 15:55:32 +0200 Subject: [PATCH 16/29] feat(dropdown): animate every
menu like Fomantic dropdowns Only the Code menu animated; the user, create, file, diff settings and theme dropdown menus opened instantly while Fomantic dropdowns slide down. One shared script animates details.dropdown, .theme-menu and .theme-dropdown panels from the summary click, in the frame that first paints the open menu (no jump, and on every open). It replaces the Code-only script. Inline sections (release assets, settings groups) keep opening in place. --- components/common/dropdown/open-animation.js | 21 ++++++++++++++++++++ templates/base/component_assets.tmpl | 1 + templates/repo/view_content.tmpl | 21 -------------------- 3 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 components/common/dropdown/open-animation.js diff --git a/components/common/dropdown/open-animation.js b/components/common/dropdown/open-animation.js new file mode 100644 index 00000000..318c031a --- /dev/null +++ b/components/common/dropdown/open-animation.js @@ -0,0 +1,21 @@ +(() => { + // Menus built on
open like Fomantic's dropdowns ("Add file"), sliding down while fading in. + // A closed
keeps its content hidden with content-visibility, so a CSS animation would only play on the + // first open, and the toggle event fires a frame after the menu shows (it would jump). Start from the summary + // click instead, in the frame that first paints the open menu. + const menus = "details.dropdown, details.theme-menu, details.theme-dropdown"; + const keyframes = [ + { opacity: 0, transform: "translateY(-8px)" }, + { opacity: 1, transform: "translateY(0)" }, + ]; + document.addEventListener("click", event => { + const summary = event.target.closest("summary"); + const menu = summary?.parentElement; + if (!menu?.matches(menus) || menu.open || matchMedia("(prefers-reduced-motion: reduce)").matches) return; + const panel = [...menu.children].find(child => child !== summary); + if (!panel) return; + requestAnimationFrame(() => { + if (menu.open) panel.animate(keyframes, { duration: 200, easing: "cubic-bezier(0.33, 1, 0.68, 1)" }); + }); + }); +})(); diff --git a/templates/base/component_assets.tmpl b/templates/base/component_assets.tmpl index 6508e631..df6b4e29 100644 --- a/templates/base/component_assets.tmpl +++ b/templates/base/component_assets.tmpl @@ -13,6 +13,7 @@ + diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 63ee3c9b..3ce3e28e 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -177,27 +177,6 @@ window.updateCloneStates(); })(); -
{{end}}
From d3db97e04c8c0dbf407a3bca1d3467d186b145c8 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 16:05:04 +0200 Subject: [PATCH 17/29] feat(repo): GitHub-style branch and tag menu - branch-selector.js adds the "Switch branches/tags" title (or "Choose a tag to compare" on releases) with a close button, and a "View all branches/tags" footer that follows the active tab, around Forgejo's Vue menu. The data and the selector element are read at click time, as Vue replaces the element when it mounts. - Underlined tabs, a leading magnifier in the search field, and a check mark on the current ref instead of a bold highlighted row. --- public/assets/js/branch-selector.js | 69 +++++++++++ styles/public/dropdown/branch_dropdown.ts | 135 +++++++++++++++++++--- templates/base/component_assets.tmpl | 1 + 3 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 public/assets/js/branch-selector.js diff --git a/public/assets/js/branch-selector.js b/public/assets/js/branch-selector.js new file mode 100644 index 00000000..2bbbfddf --- /dev/null +++ b/public/assets/js/branch-selector.js @@ -0,0 +1,69 @@ +(() => { + // Forgejo's branch/tag selector is a Vue component; add GitHub's "Switch branches/tags" title with a close button + // and the "View all branches/tags" footer around its menu. Vue only patches the nodes it rendered, so these stay. + if (!document.querySelector(".js-branch-tag-selector")) return; + const closeIcon = + "M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"; + + function updateFooter(menu, data) { + const footer = menu.querySelector(":scope > .branch-menu-footer"); + if (!footer) return; + const tags = menu.querySelector(".branch-tag-item.active")?.matches(":last-child:not(:first-child)"); + footer.textContent = tags ? "View all tags" : "View all branches"; + footer.href = `${data.repoLink}/${tags ? "tags" : "branches"}`; + } + + function enhance(container, data) { + const menu = container.querySelector(":scope > .ui.dropdown.custom > .menu"); + if (!menu || menu.querySelector(":scope > .branch-menu-header")) return; + const header = document.createElement("div"); + header.className = "branch-menu-header"; + const title = document.createElement("span"); + title.textContent = data?.release ? "Choose a tag to compare" : "Switch branches/tags"; + const close = document.createElement("button"); + close.type = "button"; + close.className = "branch-menu-close"; + close.setAttribute("aria-label", "Close menu"); + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "0 0 16 16"); + svg.setAttribute("width", "16"); + svg.setAttribute("height", "16"); + svg.setAttribute("aria-hidden", "true"); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", closeIcon); + path.setAttribute("fill", "currentColor"); + svg.append(path); + close.append(svg); + // The component toggles its menu from its own button. + close.addEventListener("click", event => { + event.stopPropagation(); + container.querySelector(".branch-dropdown-button")?.click(); + }); + header.append(title, close); + menu.prepend(header); + if (data?.repoLink && !data.release) { + const footer = document.createElement("a"); + footer.className = "branch-menu-footer"; + menu.append(footer); + updateFooter(menu, data); + menu.addEventListener("click", event => { + if (event.target.closest(".branch-tag-item")) requestAnimationFrame(() => updateFooter(menu, data)); + }); + } + } + + // Vue mounts the component after this script runs, so enhance the menu when it is first opened. + document.addEventListener( + "click", + event => { + const button = event.target.closest(".js-branch-tag-selector .branch-dropdown-button"); + if (!button) return; + const container = button.closest(".js-branch-tag-selector"); + // The template pushes one data entry per selector, in document order; read both now, as Vue replaces the + // selector element when it mounts. + const index = [...document.querySelectorAll(".js-branch-tag-selector")].indexOf(container); + enhance(container, window.config?.pageData?.branchDropdownDataList?.[index]); + }, + true + ); +})(); diff --git a/styles/public/dropdown/branch_dropdown.ts b/styles/public/dropdown/branch_dropdown.ts index 781a8327..5f11ebd7 100644 --- a/styles/public/dropdown/branch_dropdown.ts +++ b/styles/public/dropdown/branch_dropdown.ts @@ -30,31 +30,140 @@ const branchDropdown = css` ${animationDown}; height: 32px; } - /* 分支/标签切换 */ + padding: 0; + /* GitHub's "Switch branches/tags" title and close button, added by branch-selector.js. */ + > .branch-menu-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 8px 8px 16px; + font-size: 14px; + font-weight: 600; + > .branch-menu-close { + display: grid; + place-items: center; + width: 32px; + height: 32px; + padding: 0; + border: 0; + border-radius: ${otherThemeVars.border.radius}; + background: transparent; + color: ${themeVars.color.text.light.num1}; + cursor: pointer; + &:hover { + background: ${themeVars.color.hover.self}; + color: ${themeVars.color.text.self}; + } + } + } + /* Search field with GitHub's leading magnifier instead of Forgejo's trailing filter icon. */ + > .ui.icon.search.input { + margin: 0 16px 8px !important; + width: auto !important; + > i.icon { + left: 0; + right: auto; + padding: 0 0 0 10px; + display: flex; + align-items: center; + color: ${themeVars.color.text.light.num1}; + > svg { + display: none; + } + &::before { + content: ""; + width: 16px; + height: 16px; + background: currentColor; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z'/%3E%3C/svg%3E") center / 16px no-repeat; + } + } + > input { + padding-left: 34px !important; + padding-right: 12px !important; + } + } + /* Underlined tabs, like GitHub's. */ > .branch-tag-tab { + display: flex; + gap: 4px; + padding: 0 8px; + border-bottom: 1px solid ${themeVars.color.light.border}; > .branch-tag-item { - font-weight: 500; - padding: 8px 12px; - height: 32px; - line-height: 1.15; + position: relative; + margin: 0 0 4px; + padding: 8px !important; + height: auto; + border: 0 !important; + border-radius: ${otherThemeVars.border.radius} !important; + background: transparent !important; + color: ${themeVars.color.text.self}; + font-weight: 400; + line-height: 20px; svg { display: none; } &.active { - background: ${themeVars.color.body}; + font-weight: 600; + &::after { + content: ""; + position: absolute; + right: 8px; + bottom: -5px; + left: 8px; + height: 2px; + border-radius: 6px; + background: ${themeVars.github.underlineNav.borderColor.active}; + } } - &:hover { + &:hover:not(.active) { color: inherit; - &:not(.active) { - background: ${themeVars.color.hover.self}; - border: 1px solid ${themeVars.color.secondary.self}; - border-bottom: 0; - border-top-left-radius: ${otherThemeVars.border.radius}; - border-top-right-radius: ${otherThemeVars.border.radius}; + background: ${themeVars.color.hover.self} !important; + } + } + } + > .branch-tag-divider { + display: none; + } + > .scrolling.menu { + padding: 8px 0; + /* The current ref gets a check mark; the others line up after it. The repeated class outranks the shared + dropdown item padding, which is !important too. */ + > .item.item.item.item { + position: relative; + padding-left: 32px !important; + font-weight: 400 !important; + &.selected:not(.active):not(:hover) { + background: transparent !important; + } + &.selected { + &::before { + content: ""; + position: absolute; + left: 12px; + top: 50%; + width: 16px; + height: 16px; + margin-top: -8px; + background: currentColor; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z'/%3E%3C/svg%3E") center / 16px no-repeat; } } } } + /* "View all branches" / "View all tags", added by branch-selector.js. */ + > .branch-menu-footer { + display: block; + padding: 8px 16px; + border-top: 1px solid ${themeVars.color.light.border}; + color: ${themeVars.color.text.self}; + font-size: 14px; + line-height: 20px; + &:hover { + background: ${themeVars.color.hover.self}; + text-decoration: none; + } + } } `; diff --git a/templates/base/component_assets.tmpl b/templates/base/component_assets.tmpl index df6b4e29..91154fc4 100644 --- a/templates/base/component_assets.tmpl +++ b/templates/base/component_assets.tmpl @@ -14,6 +14,7 @@ + From 8d345e2e9109f9e89549cc5a8230c7e16a779264 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 16:13:07 +0200 Subject: [PATCH 18/29] feat(pull): GitHub-style compare page and red close button - compare.tmpl (Forgejo 17's) uses the centred container, a title over a divider and a compact branch bar. Creating a pull request shows the "Discuss and review..." text with the button on the right; equal branches show the notice with a disabled "Create pull request". - "Close pull request" shows a red closed pull request icon instead of Forgejo's closed issue icon in the done colour. Issues keep theirs. --- styles/pages/pull-request.css | 114 +++++++++++++++ templates/repo/diff/compare.tmpl | 238 +++++++++++++++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 templates/repo/diff/compare.tmpl diff --git a/styles/pages/pull-request.css b/styles/pages/pull-request.css index f319c2e0..d3f12c7a 100644 --- a/styles/pages/pull-request.css +++ b/styles/pages/pull-request.css @@ -55,3 +55,117 @@ .github-pull-review .pull-merge-actions .tw-flex > form { margin: 0 !important; } + +/* + * "Compare changes" / new pull request (repo/diff/compare.tmpl), like GitHub's: a title over a divider, a compact branch + * bar, then a notice with its text on the left and the button on the right. + */ +.page-content.github-compare > .ui.container > h2.ui.header { + margin: 24px 0 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--color-light-border); + font-size: 24px; + font-weight: 400; + line-height: 36px; +} + +.page-content.github-compare > .ui.container > h2.ui.header > .sub.header { + margin-top: 0; + color: var(--color-text-light-1); + font-size: 14px; + line-height: 21px; +} + +.page-content.github-compare .ui.segment.choose.branch { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0 0 16px; + padding: 12px 16px; + border-radius: var(--border-radius); + background: var(--color-box-header); +} + +.page-content.github-compare .ui.segment.choose.branch .ui.basic.small.button { + height: 28px; + min-height: 28px; + margin: 0; + padding: 0 8px; + font-size: 12px; + line-height: 18px; +} + +.page-content.github-compare .github-compare-notice { + display: flex !important; + align-items: center; + justify-content: space-between; + gap: 16px; + margin: 0 0 16px; + padding: 20px 16px; + border-radius: var(--border-radius); + color: var(--color-text); + font-size: 14px; + line-height: 21px; +} + +.page-content.github-compare .github-compare-notice.tw-hidden { + display: none !important; +} + +.page-content.github-compare .github-compare-notice.warning { + border: 1px solid var(--color-warning-border); + background: var(--color-warning-bg); +} + +.page-content.github-compare .github-compare-notice.info { + border: 1px solid var(--color-info-border); + background: var(--color-info-bg); +} + +.page-content.github-compare .github-compare-notice a { + color: var(--color-primary); + text-decoration: underline; +} + +.page-content.github-compare .github-compare-notice > .button { + flex: none; + margin: 0; +} + +/* An open pull request for this comparison: a card with its title and "View pull request". */ +.page-content.github-compare .ui.segment.grid.title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin: 0 0 16px; + padding: 16px; + border-radius: var(--border-radius); +} + +.page-content.github-compare .ui.segment.grid.title > .column { + width: auto !important; + padding: 0 !important; +} + +.page-content.github-compare .ui.segment.grid.title h1 { + margin: 4px 0 0; + font-size: 16px; + font-weight: 600; + line-height: 24px; +} + +/* + * "Close pull request" uses Forgejo's closed-issue icon, which the theme colours as done (purple). GitHub shows a red + * closed pull request icon; issues keep theirs. + */ +.page-content.github-pull-review #status-button[value="close"] > svg { + color: var(--color-red) !important; + background: currentColor; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1m9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75m-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.75.75 0 0 1-.018 1.042.75.75 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.75.75 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0M3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5m9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5'/%3E%3C/svg%3E") center / 16px no-repeat; +} + +.page-content.github-pull-review #status-button[value="close"] > svg > * { + visibility: hidden; +} diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl new file mode 100644 index 00000000..f39ed75b --- /dev/null +++ b/templates/repo/diff/compare.tmpl @@ -0,0 +1,238 @@ +{{template "base/head" .}} +{{/* Forgejo 17 compare.tmpl, laid out like GitHub's "Compare changes": centred container, the create box with its text and + the button on the right, and the equal-branches notice with a disabled button. */}} +
+ {{template "repo/header" .}} + {{$showDiffBox := false}} + {{$canReadPullRequests := .Permission.CanRead $.UnitTypePullRequests}} +
+

+ {{if and $.PageIsComparePull $.IsSigned (not .Repository.IsArchived)}} + {{if $canReadPullRequests}} + {{ctx.Locale.Tr "repo.pulls.compare_changes"}} +
{{ctx.Locale.Tr "repo.pulls.compare_changes_desc"}}
+ {{else}} + {{ctx.Locale.Tr "compare.branches.title"}} + {{end}} + {{else}} + {{ctx.Locale.Tr "action.compare_commits_general"}} + {{end}} +

+ {{$BaseCompareName := $.BaseName -}} + {{- $HeadCompareName := $.HeadRepo.OwnerName -}} + {{- if and (eq $.BaseName $.HeadRepo.OwnerName) (ne $.Repository.Name $.HeadRepo.Name) -}} + {{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}} + {{- end -}} + {{- $OwnForkCompareName := "" -}} + {{- if .OwnForkRepo -}} + {{- $OwnForkCompareName = .OwnForkRepo.OwnerName -}} + {{- end -}} + {{- $RootRepoCompareName := "" -}} + {{- if .RootRepo -}} + {{- $RootRepoCompareName = .RootRepo.OwnerName -}} + {{- if eq $.HeadRepo.OwnerName .RootRepo.OwnerName -}} + {{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}} + {{- end -}} + {{- end -}} +
+ {{svg "octicon-git-compare"}} + + {{svg "octicon-arrow-left" 16}}
{{.CompareSeparator}}
+ +
+ + {{if .IsNothingToCompare}} + {{if and $.IsSigned $.AllowEmptyPr (not .Repository.IsArchived) .PageIsComparePull}} +
{{ctx.Locale.Tr "repo.pulls.nothing_to_compare_and_allow_empty_pr"}}
+
+ Discuss and review the changes in this comparison with others. Learn about pull requests + +
+
+ {{template "repo/issue/new_form" .}} +
+ {{else if and .HeadIsBranch .BaseIsBranch}} +
+ {{ctx.Locale.Tr "repo.pulls.nothing_to_compare"}} + {{if and $.IsSigned $canReadPullRequests (not .Repository.IsArchived)}}{{end}} +
+ {{else}} +
{{ctx.Locale.Tr "repo.pulls.nothing_to_compare_have_tag"}}
+ {{end}} + {{else if and .PageIsComparePull (gt .CommitCount 0) $canReadPullRequests}} + {{if .HasPullRequest}} +
+
+ {{ctx.Locale.Tr "repo.pulls.has_pull_request" (print $.RepoLink "/pulls/" .PullRequest.Issue.Index) $.RepoRelPath .PullRequest.Index}} +

+ {{RenderIssueTitle $.Context .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}} + #{{.PullRequest.Issue.Index}} +

+
+ +
+ {{else}} + {{if and $.IsSigned (not .Repository.IsArchived)}} +
+ Discuss and review the changes in this comparison with others. Learn about pull requests + +
+ {{else if .Repository.IsArchived}} +
+ {{if .Repository.ArchivedUnix.IsZero}} + {{ctx.Locale.Tr "repo.archive.title"}} + {{else}} + {{ctx.Locale.Tr "repo.archive.title_date" (DateUtils.AbsoluteLong .Repository.ArchivedUnix)}} + {{end}} +
+ {{else}} +
+ {{ctx.Locale.Tr "repo.pulls.sign_in_require" .SignInLink}} +
+ {{end}} + {{if $.IsSigned}} +
+ {{template "repo/issue/new_form" .}} +
+ {{end}} + {{$showDiffBox = true}} + {{end}} + {{else if not .IsNothingToCompare}} + {{$showDiffBox = true}} + {{end}} +
+ + {{if $showDiffBox}} +
+ {{template "repo/commits_table" .}} + {{template "repo/diff/box" .}} +
+ {{end}} +
+{{template "base/footer" .}} From 0d2110481557ceab443fe529ca54d85687640816 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 18 Oct 2026 16:23:12 +0200 Subject: [PATCH 19/29] feat(pull): GitHub compare labels, summary and diff - Branch buttons read "base: main" / "compare: develop" (Forgejo's translated base/compare); the owner prefix only shows across forks, in the buttons and in the branch lists. - Below the create box: a "N commits / N files changed / N contributors" row and the commits grouped by day (the pull request commits list). - The compare diff has no file tree or review toolbar (kept hidden, as diff-view.js reads it) and shows "Showing N changed files with ..." with Split/Unified instead. --- public/pull-commits.css | 6 +-- styles/pages/pull-request.css | 74 ++++++++++++++++++++++++++++++++ templates/repo/diff/box.tmpl | 13 +++++- templates/repo/diff/compare.tmpl | 38 +++++++++++----- 4 files changed, 116 insertions(+), 15 deletions(-) diff --git a/public/pull-commits.css b/public/pull-commits.css index 960b4f67..7c6bb12f 100644 --- a/public/pull-commits.css +++ b/public/pull-commits.css @@ -1,5 +1,5 @@ -/* PR date groups build on the global commit-row component. */ -.github-pull-review .pull-commit-group { +/* PR and compare-page date groups build on the global commit-row component. */ +:is(.github-pull-review, .github-compare) .pull-commit-group { position: relative; margin-left: 16px; padding: 0 0 16px 24px; @@ -67,7 +67,7 @@ } } @media (max-width: 767.98px) { - .github-pull-review .pull-commit-group { + :is(.github-pull-review, .github-compare) .pull-commit-group { margin-left: 8px; padding-left: 16px; & > ul > li.gitea-github-theme-commit { diff --git a/styles/pages/pull-request.css b/styles/pages/pull-request.css index d3f12c7a..afa54338 100644 --- a/styles/pages/pull-request.css +++ b/styles/pages/pull-request.css @@ -169,3 +169,77 @@ .page-content.github-pull-review #status-button[value="close"] > svg > * { visibility: hidden; } + +/* "base:" and "compare:" are muted in GitHub's branch buttons. */ +.page-content.github-compare .compare-ref-label { + color: var(--color-text-light-1); +} + +/* "N commits · N files changed · N contributors" over the commits. */ +.page-content.github-compare .github-compare-summary { + display: flex; + justify-content: space-around; + gap: 16px; + margin: 0 0 16px; + padding: 8px 16px; + border: 1px solid var(--color-light-border); + border-radius: var(--border-radius); + color: var(--color-text-light-1); + font-size: 14px; + line-height: 21px; +} + +.page-content.github-compare .github-compare-summary > span > svg, +.page-content.github-compare .diff-compare-stats > span > svg { + margin-right: 6px; + vertical-align: text-bottom; +} + +.page-content.github-compare .github-compare-summary strong { + color: var(--color-text); +} + +/* The diff: GitHub's "Showing N changed files..." row with Split/Unified, no review toolbar or file tree. The toolbar + stays in the page, hidden, as diff-view.js reads its search field and settings. */ +.page-content.github-compare #diff-container > .diff-content-controls { + display: none; +} + +.page-content.github-compare #diff-container > #diff-content-container { + padding: 0; +} + +.page-content.github-compare .diff-compare-stats { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin: 8px 0 16px; + font-size: 14px; + line-height: 21px; +} + +.page-content.github-compare .diff-compare-stats > span > strong:first-of-type { + color: var(--color-primary); +} + +.page-content.github-compare .diff-compare-style { + display: inline-flex; + padding: 2px; + border-radius: var(--border-radius); + background: var(--color-box-header); +} + +.page-content.github-compare .diff-compare-style > a { + padding: 2px 8px; + border-radius: calc(var(--border-radius) - 2px); + color: var(--color-text); + font-size: 12px; + line-height: 18px; +} + +.page-content.github-compare .diff-compare-style > a.active { + background: var(--color-body); + box-shadow: 0 0 0 1px var(--color-light-border); + font-weight: 600; +} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 3ca94ffd..8e8cc0ef 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -1,4 +1,6 @@ -{{$showFileTree := (and (not .DiffNotAvailable) (gt .DiffMetadata.TotalNumberOfFiles 0))}} +{{/* Only the compare route sets CompareSeparator; GitHub's compare view has no file tree and a "Showing..." summary. */}} +{{$isCompare := .CompareSeparator}} +{{$showFileTree := (and (not .DiffNotAvailable) (gt .DiffMetadata.TotalNumberOfFiles 0) (not $isCompare))}}
{{if not .DiffNotAvailable}} {{if and .IsShowingOnlySingleCommit .PageIsPullFiles}} @@ -41,6 +43,15 @@ const diffTreeVisible = localStorage?.getItem('diff_file_tree_visible') !== 'false'; {{end}} + {{if and $isCompare (not .DiffNotAvailable)}} +
+ {{svg "octicon-file-diff" 16}}Showing {{ctx.Locale.Tr "repo.diff.stats_desc" .DiffMetadata.TotalNumberOfFiles .Diff.TotalAddition .Diff.TotalDeletion}}. +
+ Split + Unified +
+
+ {{end}}
{{/* GitHub's toolbar spans the tree and the files, in the first row of the #diff-container grid. */}}
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index f39ed75b..ba692fea 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -38,7 +38,7 @@ {{svg "octicon-git-compare"}}