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/components/common/search/search.css b/components/common/search/search.css index daf4316a..0f317dd3 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,145 @@ 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(256px, 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 > span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.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..a8eee458 --- /dev/null +++ b/components/common/search/search.js @@ -0,0 +1,121 @@ +(() => { + // 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) { + 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; + 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..f25156a7 --- /dev/null +++ b/components/common/search/syntax.tmpl @@ -0,0 +1,16 @@ +{{/* 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/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/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); 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/public/assets/js/details-menu.js b/public/assets/js/details-menu.js index 34506909..cc927bbb 100644 --- a/public/assets/js/details-menu.js +++ b/public/assets/js/details-menu.js @@ -25,7 +25,8 @@ document.addEventListener("click", event => { for (const menu of document.querySelectorAll("details.theme-menu[open]")) { if (!menu.contains(event.target)) menu.open = false; - else if (event.target.closest("a, button")) menu.open = false; + // Clearing a search field inside the menu keeps it open. + else if (event.target.closest("a, button:not(.theme-search-clear)")) menu.open = false; } }); document.addEventListener("keydown", event => { diff --git a/public/assets/js/diff-file-tree.js b/public/assets/js/diff-file-tree.js index 8c00eed7..73bd9208 100644 --- a/public/assets/js/diff-file-tree.js +++ b/public/assets/js/diff-file-tree.js @@ -45,6 +45,14 @@ file.Name.toLowerCase().includes(input.value.trim().toLowerCase()) && !excludedExtensions.has(extension(file.Name)); const children = directory => directory.nextElementSibling?.matches(".sub-items") ? directory.nextElementSibling : null; + // Large pull requests have thousands of rows, so only touch attributes that change: every write invalidates styles + // and wakes Forgejo's document-wide observers. + const setAttribute = (element, name, value) => { + if (element.getAttribute(name) !== String(value)) element.setAttribute(name, value); + }; + const toggleAttribute = (element, name, force) => { + if (element.hasAttribute(name) !== force) element.toggleAttribute(name, force); + }; let scheduled = false; let revealMatches = false; let extensionKey = ""; @@ -53,7 +61,12 @@ scheduled = false; const data = files(); const matching = data.filter(matches); + const visibleDirectories = new Set(); + for (const file of matching) + for (let end = file.Name.indexOf("/"); end !== -1; end = file.Name.indexOf("/", end + 1)) + visibleDirectories.add(file.Name.slice(0, end)); 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]); @@ -77,24 +90,24 @@ options.append(label); } } - tree.setAttribute("role", "tree"); - tree.setAttribute("aria-label", "File tree"); + setAttribute(tree, "role", "tree"); + setAttribute(tree, "aria-label", "File tree"); function visit(container, prefix = "", level = 1) { for (const row of container.children) { if (row.matches(".item-directory")) { const path = prefix + row.title; const group = children(row); - const visible = matching.some(file => file.Name.startsWith(`${path}/`)); - row.toggleAttribute("data-tree-filtered", !visible); - row.setAttribute("role", "treeitem"); - row.setAttribute("aria-label", row.title); - row.setAttribute("aria-level", level); - row.setAttribute("aria-expanded", Boolean(group && group.style.display !== "none")); - row.tabIndex = -1; + const visible = visibleDirectories.has(path); + toggleAttribute(row, "data-tree-filtered", !visible); + setAttribute(row, "role", "treeitem"); + setAttribute(row, "aria-label", row.title); + setAttribute(row, "aria-level", level); + setAttribute(row, "aria-expanded", Boolean(group && group.style.display !== "none")); + setAttribute(row, "tabindex", -1); if (group) { - group.setAttribute("role", "group"); - group.setAttribute("aria-label", row.title); - group.toggleAttribute("data-tree-filtered", !visible); + setAttribute(group, "role", "group"); + setAttribute(group, "aria-label", row.title); + toggleAttribute(group, "data-tree-filtered", !visible); visit(group, `${path}/`, level + 1); } if (!row.querySelector(".tree-directory-icon")) { @@ -105,13 +118,15 @@ // 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")); - row.toggleAttribute("data-tree-filtered", file ? !matches(file) : false); - row.setAttribute("role", "treeitem"); - row.setAttribute("aria-level", level); - row.setAttribute("aria-selected", row.classList.contains("selected")); - row.tabIndex = -1; - if (file) row.title = file.Name; + // Forgejo 17 tree items have no href, so remember the path they stand for. Their title is left alone: + // Forgejo turns every title change into an eagerly created tooltip. + row.dataset.diffPath ||= prefix + (row.querySelector("span")?.textContent.trim() || row.title); + const file = byName.get(row.dataset.diffPath); + toggleAttribute(row, "data-tree-filtered", file ? !matches(file) : false); + setAttribute(row, "role", "treeitem"); + setAttribute(row, "aria-level", level); + setAttribute(row, "aria-selected", row.classList.contains("selected")); + setAttribute(row, "tabindex", -1); if (!row.querySelector(".tree-file-icon")) { const kind = row.querySelector(".octicon-diff-added") ? "added" @@ -129,15 +144,16 @@ } visit(tree); revealMatches = false; - const rows = visibleRows(); - const focused = rows.includes(document.activeElement) - ? document.activeElement - : rows.find(row => row.classList.contains("selected")) || rows[0]; + // Find the roving tab stop without laying out every row. + const shown = row => row?.matches('[role="treeitem"]') && tree.contains(row) && row.checkVisibility(); + const focused = + [document.activeElement, tree.querySelector('.selected[role="treeitem"]')].find(shown) || + [...tree.querySelectorAll('[role="treeitem"]')].find(shown); if (focused) focused.tabIndex = 0; empty.hidden = matching.length !== 0; for (const box of document.querySelectorAll("#diff-file-boxes > .diff-file-box[data-new-filename]")) { const file = byHash.get(`#${box.id}`); - box.toggleAttribute("data-tree-filtered", Boolean(file && !matches(file))); + toggleAttribute(box, "data-tree-filtered", Boolean(file && !matches(file))); } } function schedule() { diff --git a/public/assets/js/diff-view.js b/public/assets/js/diff-view.js index ac701d05..5c8d7431 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"; @@ -152,8 +200,37 @@ button.setAttribute("aria-label", expanded ? "Collapse file" : "Expand file"); } } + // Forgejo hides a folded file body at once. Keep it shown while its height slides to or from zero; past the + // viewport the motion is not visible, so long diffs only animate their first screen. + function animateFold(box) { + const body = box.querySelector(":scope > .diff-file-body"); + if (!body || box.hasAttribute("data-deleted-diff-hidden") || matchMedia("(prefers-reduced-motion: reduce)").matches) + return; + // Resizing a very long file each frame walks all its rows and stutters, while only its first screen moves. + // Long files fold at once, as on GitHub. + if (body.getElementsByTagName("tr").length > 500) return; + for (const animation of body.getAnimations()) animation.cancel(); + box.setAttribute("data-folding", ""); + const height = `${Math.min(body.scrollHeight, window.innerHeight)}px`; + const frames = [ + { height, opacity: 1 }, + { height: "0px", opacity: 0 }, + ]; + const animation = body.animate(box.dataset.folded === "true" ? frames : frames.reverse(), { + duration: 150, + easing: "ease-out", + }); + animation.onfinish = () => box.removeAttribute("data-folding"); + } if (boxes) new MutationObserver(mutations => { + for (const mutation of mutations) + if ( + mutation.attributeName === "data-folded" && + mutation.target.matches(".diff-file-box") && + mutation.oldValue !== mutation.target.dataset.folded + ) + animateFold(mutation.target); updateFoldLabels(); if ( search.value && @@ -164,9 +241,23 @@ subtree: true, childList: true, attributes: true, + attributeOldValue: true, attributeFilter: ["data-folded", "data-tree-filtered"], }); updateFoldLabels(); + // Forgejo writes the viewed count into a hidden ; draw it as GitHub's progress ring. + const viewedProgress = document.getElementById("viewed-files-summary"); + const viewedRing = document.querySelector(".diff-viewed-ring"); + if (viewedProgress && viewedRing) { + const updateRing = () => { + const max = Number(viewedProgress.getAttribute("max")) || 0; + const value = Number(viewedProgress.getAttribute("value")) || 0; + viewedRing.style.setProperty("--viewed-ratio", max ? String(Math.min(value / max, 1)) : "0"); + viewedRing.classList.toggle("is-complete", max > 0 && value >= max); + }; + new MutationObserver(updateRing).observe(viewedProgress, { attributes: true, attributeFilter: ["value", "max"] }); + updateRing(); + } // Let Forgejo load the actual referencing branches using its existing handler. window.addEventListener("load", () => document.querySelector(".commit-card .load-branches-and-tags")?.click(), { once: true, diff --git a/public/assets/js/project-board.js b/public/assets/js/project-board.js new file mode 100644 index 00000000..ef05099d --- /dev/null +++ b/public/assets/js/project-board.js @@ -0,0 +1,216 @@ +(() => { + const projectBoard = document.getElementById("project-board"); + const board = projectBoard?.querySelector(".board"); + if (!board) return; + + // Columns fill the viewport below the board's own top, like GitHub's board. + const fitBoard = () => + board.style.setProperty("--project-board-top", `${board.getBoundingClientRect().top + window.scrollY}px`); + fitBoard(); + window.addEventListener("resize", fitBoard); + + // New issue for organization and user projects: list the owner's repositories that take issues. Each opens + // Forgejo's new issue form with this project set, and Forgejo returns to the board once the issue is created. + const newIssue = document.querySelector(".github-project-new-issue"); + if (newIssue) { + const appSubUrl = window.config?.appSubUrl ?? ""; + const menu = newIssue.querySelector(".theme-menu-popover"); + const status = menu.querySelector(".github-project-repo-status"); + const search = menu.querySelector("#project-repo-filter"); + const icon = newIssue.querySelector(".github-project-repo-icon").content.firstElementChild; + let request = 0; + const listRepositories = async () => { + const current = ++request; + const query = new URLSearchParams({ + uid: newIssue.dataset.ownerId, + q: search.value.trim(), + archived: "false", + sort: "alpha", + order: "asc", + limit: "50", + }); + try { + const response = await fetch(`${appSubUrl}/api/v1/repos/search?${query}`, { + headers: { Accept: "application/json" }, + }); + if (!response.ok) throw new Error(response.statusText); + const repositories = (await response.json()).data.filter( + repository => repository.has_issues && !repository.archived + ); + if (current !== request) return; + for (const link of menu.querySelectorAll(":scope > a")) link.remove(); + for (const repository of repositories) { + const link = document.createElement("a"); + link.href = `${appSubUrl}/${repository.full_name}/issues/new/choose?project=${newIssue.dataset.projectId}`; + link.setAttribute("role", "menuitem"); + link.dataset.projectPanelLink = ""; + link.append(icon.cloneNode(true), repository.full_name); + menu.append(link); + } + status.textContent = "No repositories with issues"; + status.hidden = repositories.length > 0; + } catch { + if (current !== request) return; + status.textContent = "Repositories could not be loaded"; + status.hidden = false; + } + }; + let loaded = false; + newIssue.addEventListener("toggle", () => { + if (!newIssue.open) return; + search.focus(); + if (!loaded) { + loaded = true; + listRepositories(); + } + }); + let searchTimer; + search.addEventListener("input", () => { + clearTimeout(searchTimer); + searchTimer = setTimeout(listRepositories, 200); + }); + } + + // Forgejo paints an edited column's colour as its background. Show it as the column's status dot instead. + const adoptColumnColor = column => { + const color = column.style.getPropertyValue("background"); + if (!color) return; + column.style.removeProperty("background"); + column.style.removeProperty("color"); + column.style.setProperty("--project-column-color", color); + }; + const columnObserver = new MutationObserver(mutations => { + for (const mutation of mutations) adoptColumnColor(mutation.target); + }); + for (const column of board.querySelectorAll(".project-column")) { + columnObserver.observe(column, { attributes: true, attributeFilter: ["style"] }); + } + + // Filter by keyword: every word must appear in the card, matching title, number, repository, labels and milestone. + const filter = document.getElementById("project-board-filter"); + const applyFilter = () => { + const words = filter.value.toLowerCase().split(/\s+/).filter(Boolean); + for (const column of board.querySelectorAll(".project-column")) { + const cards = column.querySelectorAll(".issue-card"); + let shown = 0; + for (const card of cards) { + const text = card.textContent.toLowerCase(); + const hidden = !words.every(word => text.includes(word)); + if (card.hasAttribute("data-project-filtered") !== hidden) + card.toggleAttribute("data-project-filtered", hidden); + if (!hidden) shown++; + } + const count = column.querySelector(".project-column-issue-count"); + const label = words.length ? `${shown} / ${cards.length}` : String(cards.length); + if (count && count.textContent !== label) count.textContent = label; + } + }; + if (filter) { + filter.addEventListener("input", applyFilter); + // Forgejo rewrites the counts when a card moves between columns. + const cardsObserver = new MutationObserver(() => filter.value && applyFilter()); + for (const cards of board.querySelectorAll(".project-column > .cards")) + cardsObserver.observe(cards, { childList: true }); + } + + // Side panel: a card opens its issue next to the board, as on GitHub. The issue page loads in a frame and drops + // the site chrome there (see base/head_style). + const panel = document.querySelector(".github-project-panel"); + if (!panel) return; + const frame = panel.querySelector("iframe"); + const openLink = panel.querySelector(".github-project-panel-open"); + const closeButton = panel.querySelector("button[data-project-panel-close]"); + let loads = 0; + let returnFocus = null; + + let closing = false; + function closePanel() { + if (panel.hidden) return; + // Unload the page first, so Forgejo can ask about an unsaved issue or comment. Cancelling keeps the panel open. + closing = true; + frame.src = "about:blank"; + } + function finishClosing() { + // Loads after the first one come from changes made in the panel, which the board does not show yet. + const changed = loads > 1; + closing = false; + panel.hidden = true; + loads = 0; + document.documentElement.style.removeProperty("overflow"); + if (changed) window.location.reload(); + else returnFocus?.focus(); + } + // GitHub opens the panel below the global header, so the repository or owner tabs stay in view. + const fitPanel = () => { + const header = document.querySelectorAll("#navbar, .repository-tab-nav, .secondary-nav, .profile-global-header"); + const top = Math.max(...[...header].map(element => element.getBoundingClientRect().bottom)); + panel.style.setProperty("--project-panel-top", `${Math.max(0, top)}px`); + }; + window.addEventListener("resize", () => panel.hidden || fitPanel()); + function openPanel(url, from) { + fitPanel(); + closing = false; + returnFocus = from; + loads = 0; + openLink.href = url; + // The frame still holds the blank page it was closed on, which paints white over the panel until the issue + // replaces it. Show the panel's own background and a loader until then. + panel.toggleAttribute("data-loading", true); + frame.src = url; + panel.hidden = false; + panel.classList.add("is-opening"); + document.documentElement.style.overflow = "hidden"; + closeButton.focus(); + } + panel.addEventListener("animationend", () => panel.classList.remove("is-opening")); + panel.addEventListener("click", event => { + if (event.target.closest("[data-project-panel-close]")) closePanel(); + }); + const closeOnEscape = event => { + if (event.key !== "Escape" || event.defaultPrevented) return; + const target = event.target; + if (target.closest?.("input, textarea, select, [contenteditable], dialog[open], .tippy-box")) return; + closePanel(); + }; + document.addEventListener("keydown", closeOnEscape); + frame.addEventListener("load", () => { + if (frame.getAttribute("src") === "about:blank") { + if (closing) finishClosing(); + return; + } + panel.removeAttribute("data-loading"); + loads++; + try { + // Forgejo returns to the project once an issue is created from it: show the new card on the board. + const trimmed = path => path.replace(/\/+$/, ""); + if (trimmed(frame.contentWindow.location.pathname) === trimmed(window.location.pathname)) { + window.location.reload(); + return; + } + openLink.href = frame.contentWindow.location.href; + frame.contentDocument.addEventListener("keydown", closeOnEscape); + } catch { + /* A page from another origin keeps the link it was opened with. */ + } + }); + + // New issue opens Forgejo's form in the panel too, with the project set. + document.addEventListener("click", event => { + const link = event.target.closest("a[data-project-panel-link]"); + if (!link || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + event.preventDefault(); + openPanel(link.href, link.closest("details")?.querySelector("summary") ?? link); + }); + + board.addEventListener("click", event => { + const card = event.target.closest(".issue-card"); + if (!card || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + // Labels, assignees, milestones and pull requests keep their own links. + const link = event.target.closest("a"); + if (link && !link.matches(".github-project-card-title")) return; + const title = card.querySelector(".github-project-card-title"); + if (!title) return; + event.preventDefault(); + openPanel(title.href, title); + }); +})(); diff --git a/public/assets/js/resizable-divider.js b/public/assets/js/resizable-divider.js index 98933afe..13ef3df4 100644 --- a/public/assets/js/resizable-divider.js +++ b/public/assets/js/resizable-divider.js @@ -22,7 +22,9 @@ return; this.divider.dataset.initialized = CONFIGURATION.initializedValue; - this.setWidth(this.storedWidth()); + // Reading the container width here would force a style and layout pass while a large page is still loading. + // The ResizeObserver below fires after the first layout and fits the width to the container. + this.setWidth(this.storedWidth(), this.maximumWidth); this.divider.addEventListener("pointerdown", event => this.startResize(event)); this.divider.addEventListener("pointermove", event => this.resize(event)); this.divider.addEventListener("pointerup", event => this.stopResize(event)); @@ -60,11 +62,11 @@ ); } - setWidth(width) { - const limitedWidth = Math.min(Math.max(width, this.minimumWidth), this.widthLimit()); + setWidth(width, limit = this.widthLimit()) { + const limitedWidth = Math.min(Math.max(width, this.minimumWidth), limit); this.container.style.setProperty(this.variable, `${limitedWidth}px`); this.divider.setAttribute("aria-valuenow", limitedWidth.toString()); - this.divider.setAttribute("aria-valuemax", this.widthLimit().toString()); + this.divider.setAttribute("aria-valuemax", limit.toString()); return limitedWidth; } 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/public/repo-latest-release.css b/public/repo-latest-release.css index 38cb37bc..bd699f9d 100644 --- a/public/repo-latest-release.css +++ b/public/repo-latest-release.css @@ -50,9 +50,15 @@ font-size: 12px; padding: 3px 0 10px; } -.page-content.repository.file.list .repo-release-heading { margin-bottom: 0; } -.page-content.repository.file.list .repo-latest-release { padding-top: 0; } -.page-content.repository.file.list .repo-release-heading + .repo-latest-release { margin-top: -8px; } +.page-content.repository.file.list .repo-release-heading { + margin-bottom: 0; +} +.page-content.repository.file.list .repo-latest-release { + padding-top: 0; +} +.page-content.repository.file.list .repo-release-heading + .repo-latest-release { + margin-top: -8px; +} .page-content.repository.file.list .repo-release-count { color: var(--color-primary); text-decoration: none; @@ -83,5 +89,52 @@ text-decoration: underline !important; } /* 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; } +.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/public/repo-mirror.css b/public/repo-mirror.css index 7628f7ea..34ff5ea9 100644 --- a/public/repo-mirror.css +++ b/public/repo-mirror.css @@ -53,9 +53,7 @@ } @media (max-width: 767px) { - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) - .repo-header - > .flex-item:has(.repository-mirror) { + .page-content.repository.file.list.repo-has-sidebar .repo-header > .flex-item:has(.repository-mirror) { display: flex; } diff --git a/scripts/assembleForgejo.ts b/scripts/assembleForgejo.ts index d42f4265..d68ee062 100644 --- a/scripts/assembleForgejo.ts +++ b/scripts/assembleForgejo.ts @@ -47,6 +47,7 @@ for (const file of [ "dashboard-lists.css", "organization.css", "profile-subpages.css", + "projects.css", "pull-request.css", "releases.css", "user-profile.css", diff --git a/src/build-output.test.ts b/src/build-output.test.ts index 5d83d21d..80b87e0d 100644 --- a/src/build-output.test.ts +++ b/src/build-output.test.ts @@ -168,6 +168,57 @@ 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("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. + const output = path.join(DIST_DIR, "forgejo", "public", "assets", "css"); + const pageWrapper = + /(?:^|[\s,{}>~+(])(?:body|html|:root|\.page-content|\.ui\.container|#diff-container)(?:[.#[:][^\s,{}>~+]*)?:has\(/m; + const files = fs + .readdirSync(output, { recursive: true }) + .map(String) + .filter(file => file.endsWith(".css")); + expect(files.length).toBeGreaterThan(0); + for (const file of files) { + const match = fs.readFileSync(path.join(output, file), "utf-8").match(pageWrapper); + expect(match?.[0], `${file} puts :has() on a page wrapper`).toBeUndefined(); + } +}); + it("versions the theme stylesheet link and the auto themes' imports", async () => { const { createHash } = await import("node:crypto"); const version = (...files: string[]) => { @@ -379,9 +430,8 @@ describe("Forgejo 15 native integration", () => { const compactMenu = fs.readFileSync(path.join(ROOT_DIR, "styles", "public", "menu", "compact_menu.ts"), "utf-8"); const notification = fs.readFileSync(path.join(ROOT_DIR, "styles", "components", "notification.ts"), "utf-8"); - expect(compactMenu).toContain( - ".page-content.user.notification > .ui.container:has(> .ui.bottom.active.tab.segment)" - ); + expect(compactMenu).toContain(".page-content.user.notification:not(#notification_div) > .ui.container,"); + expect(compactMenu).not.toContain(".ui.container:has("); expect(compactMenu).not.toContain(".page-content.user.notification > .ui.container,"); expect(notification).toContain("--switch-item-min-height: 32px;"); expect(notification).toContain("background: light-dark("); @@ -413,7 +463,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;"); @@ -437,8 +487,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;"); }); @@ -544,7 +594,7 @@ describe("Forgejo 15 native integration", () => { ); expect(commitPageStyle).toContain("> .repository-content-header"); const diffViewStyle = fs.readFileSync(path.join(ROOT_DIR, "styles", "components", "repo", "diff_view.ts"), "utf8"); - expect(diffViewStyle).toContain("min-height: 46px;"); + expect(diffViewStyle).toContain("min-height: 40px;"); expect(diffBoxTemplate).toContain('template "repo/diff/file_header"'); expect(commitPageTemplate).toContain('template "repo/diff/change_summary"'); }); 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/src/utils.test.ts b/src/utils.test.ts index 315eee9d..de8ecc48 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -130,11 +130,15 @@ describe("getCallerInfo", () => { }); it("skipFrames=1 跳过一个调用帧", () => { + // Not tail calls: Bun's JavaScriptCore drops the frame of a function that ends in `return f()`, which left + // vitest's runner as the frame skipFrames=1 points at. function outer() { - return inner(); + const info = inner(); + return info; } function inner() { - return getCallerInfo(1); + const info = getCallerInfo(1); + return info; } const info = outer(); // inner 被跳过,返回 outer 的调用者(即测试函数本身的位置) 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/issues/pull_files_review.ts b/styles/components/issues/pull_files_review.ts index c607c19a..e9ac7a3b 100644 --- a/styles/components/issues/pull_files_review.ts +++ b/styles/components/issues/pull_files_review.ts @@ -24,12 +24,12 @@ export default css` height: 28px; min-height: 28px; } - /* "Viewed" is a small bordered button on GitHub. */ + /* "Viewed" is a small bordered button on GitHub: the whole box toggles, and it turns blue once checked. */ .diff-file-header .viewed-file-form { display: inline-flex; align-items: center; - gap: 4px; - height: 28px; + gap: 6px; + height: 26px; padding: 0 8px; border: 1px solid ${themeVars.color.light.border}; border-radius: ${otherThemeVars.border.radius}; @@ -37,6 +37,22 @@ export default css` color: ${themeVars.color.text.self}; font-size: 12px; font-weight: 500; + line-height: 1; + cursor: pointer; + & > input[type="checkbox"] { + width: 14px; + height: 14px; + margin: 0; + cursor: pointer; + } + &:hover { + background: ${themeVars.github.control.transparent.bgColor.hover}; + } + &.viewed-file-checked-form { + border-color: ${themeVars.color.info.border}; + background: ${themeVars.color.info.bg}; + color: ${themeVars.color.info.text}; + } } .review-box-panel { display: block; diff --git a/styles/components/issues/pull_review.ts b/styles/components/issues/pull_review.ts index 291815c1..06dddd9a 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,14 +183,12 @@ export default css` margin-left: auto; display: flex; align-items: center; + align-self: center; padding-left: 16px; } &.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; @@ -545,7 +567,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.ts b/styles/components/repo/diff.ts index b438e5ca..ac2820b2 100644 --- a/styles/components/repo/diff.ts +++ b/styles/components/repo/diff.ts @@ -124,6 +124,23 @@ const diffFileBoxHeader = css` const diffFileBoxCodeDiff = css` /* 差异对比文件盒子 */ .repository .diff-file-box { + /* GitHub's line comment button is a small blue square, not the theme's 30px green primary button. */ + .code-diff .ui.primary.button.add-code-comment { + min-height: 0; + width: 20px; + height: 20px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 6px; + color: ${themeVars.github.fgColor.onEmphasis}; + background-color: ${themeVars.github.bgColor.accent.emphasis}; + svg { + width: 16px; + height: 16px; + } + } .code-diff { /* 隐藏多余的空白 */ /* 合并视图的第三列 */ @@ -135,17 +152,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..71661ccc 100644 --- a/styles/components/repo/diff_file_tree.ts +++ b/styles/components/repo/diff_file_tree.ts @@ -1,20 +1,30 @@ 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); + /* Row 1 is the toolbar spanning every column, row 2 the tree, divider and files. */ + grid-template-rows: auto 1fr; position: relative; min-height: 100dvh; 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 { @@ -22,15 +32,15 @@ export default css` position: sticky; height: 100dvh; z-index: 20; - top: var(--sticky-top-offset, 0px); - max-height: calc(100dvh - var(--sticky-top-offset, 0px)); + top: calc(var(--sticky-top-offset, 0px) + var(--diff-toolbar-height)); + max-height: calc(100dvh - var(--sticky-top-offset, 0px) - var(--diff-toolbar-height)); } .repository .diff-file-tree-scroll { overflow: auto; scrollbar-gutter: stable; height: 100dvh; - max-height: calc(100dvh - var(--sticky-top-offset, 0px)); - padding: 24px; + max-height: calc(100dvh - var(--sticky-top-offset, 0px) - var(--diff-toolbar-height)); + padding: 16px 24px 24px; } .repository .diff-file-tree-controls { display: flex; @@ -60,24 +70,30 @@ export default css` } .repository #diff-container > #diff-content-container { min-width: 0; - padding: 24px; + padding: 16px 24px 24px; } - .repository #diff-container .diff-content-controls { + /* The toolbar height is fixed so the sticky tree and file headers can sit right below it. */ + .repository #diff-container { + --diff-toolbar-height: 56px; + } + .repository #diff-container > .diff-content-controls { + grid-column: 1 / -1; + box-sizing: border-box; background: ${themeVars.color.body}; border: 0; + border-bottom: 1px solid ${themeVars.color.light.border}; border-radius: 0; box-shadow: none; display: flex; align-items: center; justify-content: space-between; gap: 8px; - min-height: 34px; - height: auto; - padding: 0; - margin: 0 0 8px; + height: var(--diff-toolbar-height); + padding: 0 24px; + margin: 0; position: sticky; - top: calc(var(--sticky-top-offset, 0px) + 24px); - z-index: 8; + top: var(--sticky-top-offset, 0px); + z-index: 21; } .diff-content-controls-left { display: flex; @@ -85,13 +101,6 @@ export default css` gap: 8px; min-width: 0; } - .repository #diff-container .diff-content-controls::before { - content: ""; - position: absolute; - inset: -24px -1px -8px; - background: ${themeVars.color.body}; - z-index: -1; - } .diff-toggle-file-tree-button > svg.tw-hidden { display: none; } @@ -120,13 +129,22 @@ export default css` } .repository #diff-container.diff-mobile-tree-open .diff-content-controls { order: -1; - margin: 16px; + } + /* On phones the controls wrap, so the toolbar grows and scrolls away; file headers stick to the top. */ + .repository #diff-container { + --diff-toolbar-height: 0px; + } + .repository #diff-container > .diff-content-controls { + position: static; + height: auto; + flex-wrap: wrap; + padding: 8px 16px; } .repository #diff-container.diff-mobile-tree-open .diff-content-body, .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..f97f5760 100644 --- a/styles/components/repo/diff_view.ts +++ b/styles/components/repo/diff_view.ts @@ -57,15 +57,84 @@ 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; } - .repository #diff-container .diff-content-controls { - justify-content: flex-start; - } .diff-content-controls-right { - margin-left: 0; + margin-left: auto; + } + /* GitHub's "n / m viewed": a small progress ring and muted text; Forgejo's stays, hidden. */ + .diff-viewed-summary { + display: flex; + align-items: center; + gap: 6px; + margin-right: 4px; + color: ${themeVars.color.text.light.num1}; + font-size: 12px; + white-space: nowrap; + position: relative; + /* Forgejo sizes the progress by id (width: 100%); the id outranks a plain class rule and the hidden bar, absolutely + placed, stretched the page sideways. */ + & > #viewed-files-summary { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + } + } + .diff-viewed-ring { + flex: 0 0 14px; + width: 14px; + height: 14px; + border-radius: 50%; + background: conic-gradient( + ${themeVars.github.fgColor.done} calc(var(--viewed-ratio, 0) * 360deg), + ${themeVars.color.light.border} 0 + ); + 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-toggle-file-tree-button { + width: 28px; + height: 28px; + } + /* The commit filter is an icon-only Vue button; GitHub labels it ("All commits"). */ + #diff-commit-select :is(#diff-commit-list-expand, .ui.jump.dropdown.button) { + display: inline-flex; + align-items: center; + gap: 6px; + height: 28px; + min-height: 0; + padding: 0 8px; + font-size: 12px; + font-weight: 500; + &::after { + 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; + } + /* 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; @@ -99,21 +168,25 @@ export default css` .theme-menu-popover a > input { pointer-events: none; } + /* Forgejo spaces the boxes with an 8px flex gap; GitHub leaves 16px between files. */ + .repository #diff-file-boxes { + gap: 16px; + } .repository #diff-file-boxes .diff-file-box { - margin: 0 0 16px !important; - scroll-margin-top: 84px; + margin: 0 !important; + scroll-margin-top: calc(var(--diff-toolbar-height, 0px) + 16px); } .repository #diff-file-boxes .diff-file-header { background: ${themeVars.color.box.header}; border: 1px solid ${themeVars.color.light.border}; border-radius: ${otherThemeVars.border.radius} ${otherThemeVars.border.radius} 0 0; box-sizing: border-box; - min-height: 46px; + min-height: 40px; margin: 0; - padding: 6px 8px; + padding: 4px 8px; gap: 8px; flex-wrap: nowrap; - top: calc(var(--sticky-top-offset, 0px) + 66px); + top: calc(var(--sticky-top-offset, 0px) + var(--diff-toolbar-height, 0px)); z-index: 7; } .repository #diff-file-boxes .diff-file-name { @@ -145,6 +218,11 @@ export default css` flex-wrap: nowrap; gap: 8px; } + /* GitHub's file header buttons are 28px, which keeps the header at 40px. */ + .repository #diff-file-boxes .diff-file-menu > .theme-icon-button { + width: 28px; + height: 28px; + } .repository #diff-file-boxes .fold-file { display: inline-flex; justify-content: center; @@ -205,11 +283,66 @@ export default css` .diff-deleted-placeholder > :not(.diff-deleted-skeleton) { position: relative; } + /* Large diffs Forgejo does not render: GitHub's "Load diff" over a skeleton, inside the file body. */ + .repository #diff-file-boxes .diff-suppressed-placeholder { + position: relative; + display: flex; + flex-direction: column; + gap: 4px; + align-items: center; + justify-content: center; + height: 160px; + padding: 0; + color: ${themeVars.color.text.light.num1}; + font-size: 12px; + } + .diff-suppressed-placeholder > .diff-deleted-skeleton { + top: 16px; + } + .diff-suppressed-placeholder > :not(.diff-deleted-skeleton) { + position: relative; + } + .diff-suppressed-placeholder > .diff-load-button { + color: ${themeVars.github.fgColor.accent}; + font-size: 16px; + font-weight: 600; + } + .diff-suppressed-placeholder > .diff-load-button:hover { + text-decoration: underline; + } + /* Forgejo marks the link while it fetches the diff. */ + .diff-suppressed-placeholder > .diff-load-button.disabled { + opacity: 0.6; + pointer-events: none; + } .repository #diff-file-boxes [data-deleted-diff-hidden] > .diff-file-body, .repository #diff-file-boxes [data-folded="true"] > .diff-deleted-placeholder, .repository #diff-file-boxes .diff-deleted-placeholder[hidden] { display: none; } + /* While a file folds, skip painting the files below it that are off screen. Large pull requests otherwise + repaint all of them on each animation frame. Only for that moment: estimated sizes above the viewport + would break jumps to a file. */ + .repository #diff-file-boxes > .diff-file-box[data-folding] ~ .diff-file-box { + content-visibility: auto; + contain-intrinsic-size: auto 400px; + } + /* Forgejo hides a folded file with display: none, which throws away the styles of every row: unfolding a large + file then restyled all of them in one long frame. content-visibility: hidden skips its layout and paint the + same way but keeps that work, so unfolding again is cheap. */ + .repository #diff-file-boxes .diff-file-box[data-folded="true"] > .diff-file-body { + display: block; + } + .repository #diff-file-boxes .diff-file-box[data-folded="true"]:not([data-folding]) > .diff-file-body { + content-visibility: hidden; + height: 0; + padding: 0; + border-width: 0; + } + .repository #diff-file-boxes .diff-file-box[data-folding] > .diff-file-body { + display: block; + overflow: hidden; + } .repository #diff-file-boxes .diff-file-body { border-color: ${themeVars.color.light.border}; } 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/components/repo/source_browser.ts b/styles/components/repo/source_browser.ts index f6d1af2a..4a044449 100644 --- a/styles/components/repo/source_browser.ts +++ b/styles/components/repo/source_browser.ts @@ -1,7 +1,7 @@ import { css, otherThemeVars, themeVars } from "@lutinglt/gitea-github-theme/core"; export default css` - .page-content.repository.file.list:has(#repo-source-browser) > .ui.container { + .page-content.repository.file.list.repo-has-source-browser > .ui.container { width: 100%; max-width: none; margin: 0; 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/projects.css b/styles/pages/projects.css new file mode 100644 index 00000000..bb2e3cd9 --- /dev/null +++ b/styles/pages/projects.css @@ -0,0 +1,446 @@ +/* Project board in GitHub's layout, and the side panel that opens a card's issue. Kept as a page stylesheet, outside + the size-limited theme files. */ + +/* Page: GitHub shows a project without the repository header, at full width. */ +.page-content.repository.github-project-page .repository-content-header { + display: none; +} +.full.height > .page-content.github-project-page { + margin-top: 0; + padding-top: 0; +} +.full.height > .page-content.github-project-page > .github-project-container { + margin-top: 0; +} +.github-project-container { + padding: 0 16px; +} +.github-project-header { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + justify-content: space-between; + padding: 16px 0 8px; +} +.github-project-title { + align-items: center; + display: flex; + font-size: 20px; + font-weight: 600; + gap: 8px; + line-height: 30px; + margin: 0; + min-width: 0; + overflow-wrap: anywhere; +} +.github-project-title > svg { + color: var(--color-text-light-2); + flex-shrink: 0; +} +.github-project-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.github-project-actions .ui.primary.button.theme-control { + border-color: transparent; + color: var(--color-primary-contrast); +} +/* Edit, Close and Delete join into one segmented control, like GitHub's Insights and Workflows. */ +.github-project-button-group { + display: flex; +} +.github-project-button-group > .theme-control:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + margin-left: -1px; +} +.github-project-button-group > .theme-control:not(:last-child) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.github-project-button-group > .theme-control > svg { + color: var(--color-text-light-2); +} +/* New issue in an organization or user project: a repository picker, like GitHub's "Create new issue" dialog. */ +.github-project-new-issue > summary > svg { + margin-right: -4px; +} +.github-project-new-issue .theme-menu-popover { + max-height: min(400px, calc(100vh - 160px)); + overflow-y: auto; + width: 320px; +} +.github-project-new-issue .theme-menu-popover > a > svg { + color: var(--color-text-light-2); + flex-shrink: 0; +} +.github-project-repo-filter { + margin: 4px 0 8px; +} +.github-project-repo-status { + color: var(--color-text-light-2); + font-size: 12px; + margin: 0; + padding: 4px 8px; +} +.github-project-description { + color: var(--color-text-light-2); + font-size: 14px; + margin: 0 0 8px; +} +.github-project-description > :last-child { + margin-bottom: 0; +} +.github-project-filter { + margin: 8px 0 16px; +} +.github-project-filter .theme-search-field { + width: 100%; +} + +/* Board: columns fill the rest of the viewport and scroll on their own, as on GitHub. The board script sets + --project-board-top to where the board starts. */ +#project-board .board { + align-items: stretch; + display: flex; + gap: 8px; + height: max(360px, calc(100vh - var(--project-board-top, 240px) - 16px)); + margin: 0; + overflow-x: auto; + padding-bottom: 4px; +} +#project-board .project-column { + background: var(--color-project-column-bg) !important; + border: 1px solid var(--color-light-border) !important; + border-radius: 6px; + color: var(--color-text) !important; + display: flex; + flex: 0 0 350px; + flex-direction: column; + height: auto; + margin: 0 !important; + max-width: 85vw; + min-height: 0; + padding: 0 !important; + width: 350px; +} +#project-board .project-column:first-child, +#project-board .project-column:last-child { + margin-inline: 0 !important; +} +#project-board .project-column-header { + align-items: center; + display: flex; + flex-shrink: 0; + gap: 8px; + justify-content: flex-start; + min-height: 52px; + padding: 12px 8px 8px 16px; +} +/* GitHub's default statuses and their colours, for columns without a colour of their own. */ +#project-board .project-column[data-column-title="backlog" i] { + --project-column-status-color: var(--github-fgColor-success, var(--color-green)); +} +#project-board + .project-column:is([data-column-title="ready" i], [data-column-title="to do" i], [data-column-title="todo" i]) { + --project-column-status-color: var(--github-fgColor-accent, var(--color-blue)); +} +#project-board .project-column[data-column-title="in progress" i] { + --project-column-status-color: var(--color-yellow); +} +#project-board .project-column[data-column-title="in review" i] { + --project-column-status-color: var(--github-fgColor-done, var(--color-purple)); +} +#project-board .project-column[data-column-title="done" i] { + --project-column-status-color: var(--color-orange); +} +#project-board .project-column-dot { + border: 2px solid var(--project-column-color, var(--project-column-status-color, var(--color-text-light-2))); + border-radius: 50%; + box-sizing: border-box; + flex-shrink: 0; + height: 14px; + width: 14px; +} +#project-board .project-column-title-label { + font-size: 14px; + font-weight: 600; + line-height: 20px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +#project-board .project-column-issue-count { + flex-shrink: 0; +} +#project-board .project-column-default { + color: var(--color-text-light-2); + display: inline-flex; +} +#project-board .project-column-menu { + margin-left: auto; +} +#project-board .project-column-menu > .theme-icon-button { + height: 28px; + width: 28px; +} +#project-board .project-column > .cards { + display: flex; + flex: 1; + flex-direction: column; + flex-wrap: nowrap !important; + gap: 8px; + margin: 0 !important; + min-height: 48px; + overflow-x: hidden; + overflow-y: auto; + padding: 0 8px 8px !important; +} +#project-board .github-project-add-column { + flex-shrink: 0; +} + +/* Cards */ +#project-board .issue-card { + background: var(--color-box-header); + border: 1px solid var(--color-light-border); + border-radius: 6px; + box-shadow: none; + cursor: pointer; + flex-shrink: 0; + margin: 0; + padding: 8px 12px 12px; + width: auto; +} +#project-board .issue-card.tw-cursor-grab { + cursor: pointer; +} +#project-board .issue-card:hover { + border-color: var(--github-borderColor-emphasis, var(--color-secondary-dark-2)); +} +#project-board .issue-card[data-project-filtered] { + display: none; +} +#project-board .issue-card.card-ghost { + background: none !important; +} +.github-project-card { + min-width: 0; +} +.github-project-card-meta { + align-items: center; + color: var(--color-text-light-2); + display: flex; + font-size: 12px; + gap: 4px; + line-height: 20px; + min-height: 20px; +} +.github-project-card-meta .issue-card-icon { + display: inline-flex; +} +.github-project-card-meta .issue-card-icon > svg { + height: 14px; + width: 14px; +} +.github-project-card-ref { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.github-project-card-assignees { + display: flex; + flex-shrink: 0; + margin-left: auto; +} +.github-project-card-assignees > a + a { + margin-left: -4px; +} +.github-project-card-assignees img { + border-radius: 50%; + box-shadow: 0 0 0 1px var(--color-box-header); + display: block; + height: 20px; + width: 20px; +} +#project-board .github-project-card-title { + color: var(--color-text); + display: block; + font-size: 14px; + line-height: 20px; + margin-top: 2px; +} +#project-board .github-project-card-title:hover { + color: var(--color-primary); + text-decoration: underline; +} +.github-project-card .labels-list { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 8px; +} +.github-project-card-details { + color: var(--color-text-light-2); + display: flex; + flex-direction: column; + font-size: 12px; + gap: 4px; + line-height: 18px; + margin-top: 8px; +} +.github-project-card-details > * { + align-items: flex-start; + display: flex; + gap: 4px; +} +.github-project-card-details svg { + flex-shrink: 0; + margin-top: 3px; +} +.github-project-card-details .text { + display: inline-flex; +} + +/* Side panel: the issue page in a frame, sliding in from the right like GitHub's item panel. */ +.github-project-panel { + inset: var(--project-panel-top, 0) 0 0; + position: fixed; + z-index: 1000; +} +.github-project-panel[hidden] { + display: none; +} +.github-project-panel-backdrop { + background: var(--github-overlay-backdrop-bgColor, rgb(1 4 9 / 40%)); + inset: 0; + position: absolute; +} +/* A floating sheet with rounded corners. Its buttons sit on the issue title's row, which the embedded page leaves + free for them (see the end of this file). */ +.github-project-panel-sheet { + background: var(--color-body); + border: 1px solid var(--color-light-border); + border-radius: 12px; + box-shadow: var(--github-shadow-floating-small, 0 8px 24px rgb(1 4 9 / 50%)); + inset: 8px 8px 8px auto; + max-width: calc(100vw - 16px); + overflow: hidden; + position: absolute; + width: min(1280px, 66vw); +} +.github-project-panel-toolbar { + align-items: center; + display: flex; + gap: 4px; + position: absolute; + right: 24px; + top: 24px; + z-index: 1; +} +.github-project-panel-sheet > iframe { + border: 0; + display: block; + height: 100%; + width: 100%; +} +/* While an issue loads, hide the frame and show a loader like Forgejo's .is-loading. That class would block the + panel's buttons, so the loader is drawn here with Forgejo's spin animation. */ +.github-project-panel[data-loading] .github-project-panel-sheet > iframe { + visibility: hidden; +} +.github-project-panel[data-loading] .github-project-panel-sheet::after { + animation: isloadingspin 1000ms infinite linear; + aspect-ratio: 1; + border: 4px solid; + border-color: var(--color-secondary) var(--color-secondary) var(--color-secondary-dark-8) + var(--color-secondary-dark-8); + border-radius: var(--border-radius-full); + content: ""; + height: 32px; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); +} +.github-project-panel.is-opening .github-project-panel-sheet { + animation: github-project-panel-in 200ms cubic-bezier(0.33, 1, 0.68, 1); +} +.github-project-panel.is-opening .github-project-panel-backdrop { + animation: github-project-backdrop-in 200ms ease-out; +} +@keyframes github-project-panel-in { + from { + transform: translateX(100%); + } +} +@keyframes github-project-backdrop-in { + from { + opacity: 0; + } +} +@media (prefers-reduced-motion: reduce) { + .github-project-panel.is-opening :is(.github-project-panel-sheet, .github-project-panel-backdrop), + .github-project-panel[data-loading] .github-project-panel-sheet::after { + animation: none; + } +} +@media (max-width: 767.98px) { + .github-project-panel-sheet { + width: calc(100vw - 16px); + } +} + +/* A page opened inside the side panel drops the site chrome. */ +/* Plain selectors: a :is() list on the right would be tested against every element of every page. */ +.github-embedded #navbar, +.github-embedded #navbar-drawer, +.github-embedded .repository-tab-nav, +.github-embedded .secondary-nav, +.github-embedded .repository-content-header, +.github-embedded .page-footer { + display: none !important; +} +.github-embedded .page-content { + padding-top: 0; +} +/* Room for the panel's buttons on the title row. */ +.github-embedded .issue-title-header .issue-title > .button-row { + margin-right: 72px; +} +/* Organization and user projects mix repositories: name the issue's one next to its state, as GitHub does. */ +.github-issue-repository { + display: none; +} +.github-embedded-owner .github-issue-repository { + align-items: center; + border: 1px solid var(--color-light-border); + border-radius: 2em; + color: var(--color-text-light-2); + display: inline-flex; + flex-shrink: 0; + font-size: 12px; + gap: 6px; + line-height: 18px; + margin-left: 8px; + padding: 6px 12px; +} +.github-embedded-owner .issue-new-heading { + align-items: center; + display: flex; +} +.github-embedded-owner .github-issue-repository:hover { + color: var(--color-text); + text-decoration: none; +} +.github-issue-repository-private { + border: 1px solid var(--color-light-border); + border-radius: 2em; + font-weight: 500; + padding: 0 7px; +} diff --git a/styles/pages/pull-request.css b/styles/pages/pull-request.css index f319c2e0..fb471e58 100644 --- a/styles/pages/pull-request.css +++ b/styles/pages/pull-request.css @@ -55,3 +55,192 @@ .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; +} + +/* "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/styles/pages/releases.css b/styles/pages/releases.css index 11b40799..06a8d17f 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 { @@ -834,3 +834,10 @@ body:has(.page-content.repository.github-tags) .repository-content-header { margin-left: 8px; } } + +/* Forgejo gives the new release tag/target block a 500px minimum, which scrolls a phone screen sideways. */ +@media (max-width: 767.98px) { + .repository.new.release .target { + min-width: 0; + } +} 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: diff --git a/styles/public/dropdown/branch_dropdown.ts b/styles/public/dropdown/branch_dropdown.ts index 781a8327..8bb865c6 100644 --- a/styles/public/dropdown/branch_dropdown.ts +++ b/styles/public/dropdown/branch_dropdown.ts @@ -30,31 +30,142 @@ 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/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/public/menu/compact_menu.ts b/styles/public/menu/compact_menu.ts index a6c9031e..414d4be2 100644 --- a/styles/public/menu/compact_menu.ts +++ b/styles/public/menu/compact_menu.ts @@ -24,7 +24,8 @@ const smallCompactMenu = css` /* 编辑/预览切换菜单(仓库编辑文件时的编辑器操作栏左侧) */ .page-content.repository.file.editor #editor-bar, /* 订阅/关注切换菜单(应只选中订阅/关注页面, 不能选中通知页面) */ - .page-content.user.notification > .ui.container:has(> .ui.bottom.active.tab.segment), + /* No :has() on the container: Chrome would then track every .ui.container and restyle the whole page on editor tab changes. */ + .page-content.user.notification:not(#notification_div) > .ui.container, /* 里程碑/标签切换菜单(里程碑页) */ .page-content.repository.milestones .list-header, /* 里程碑/标签切换菜单(新建里程碑页) */ 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/home.ts b/styles/templates/repo/home.ts index 173a8233..7d42485d 100644 --- a/styles/templates/repo/home.ts +++ b/styles/templates/repo/home.ts @@ -33,14 +33,14 @@ const repositoryHome = css` grid-template-rows: auto auto 1fr; } - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) > .ui.container, - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) > .repository-content-header > .ui.container { + .page-content.repository.file.list.repo-has-sidebar > .ui.container, + .page-content.repository.file.list.repo-has-sidebar > .repository-content-header > .ui.container { width: calc(100% - 64px); max-width: 1216px; margin-left: auto; margin-right: auto; } - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) > .repository-content-header > .ui.container { + .page-content.repository.file.list.repo-has-sidebar > .repository-content-header > .ui.container { padding-bottom: 18px; } .page-content.repository.file.list .repo-home-sidebar-top .repo-home-sidebar-header { @@ -183,7 +183,7 @@ const repositoryHome = css` } @media (max-width: 767.98px) { - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) > .repository-content-header > .ui.container { + .page-content.repository.file.list.repo-has-sidebar > .repository-content-header > .ui.container { border-bottom: 0; padding-bottom: 0; } @@ -193,14 +193,14 @@ const repositoryHome = css` text-transform: lowercase; } - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) .repo-header > .flex-item { + .page-content.repository.file.list.repo-has-sidebar .repo-header > .flex-item { display: none; } - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) .repo-header > .repo-buttons > a[href$=".rss"] { + .page-content.repository.file.list.repo-has-sidebar .repo-header > .repo-buttons > a[href$=".rss"] { display: none; } - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) #watch-button .text.not-mobile, - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) a[href$="/fork"] .text.not-mobile { + .page-content.repository.file.list.repo-has-sidebar #watch-button .text.not-mobile, + .page-content.repository.file.list.repo-has-sidebar a[href$="/fork"] .text.not-mobile { display: inline !important; } .page-content.repository.file.list .repo-about-heading { @@ -280,8 +280,8 @@ const repositoryHome = css` order: 2; } - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) > .ui.container, - .page-content.repository.file.list:has(.repo-grid-filelist-sidebar) > .repository-content-header > .ui.container { + .page-content.repository.file.list.repo-has-sidebar > .ui.container, + .page-content.repository.file.list.repo-has-sidebar > .repository-content-header > .ui.container { width: calc(100% - 32px); } .page-content.repository.file.list .repo-grid-filelist-sidebar { diff --git a/styles/templates/repo/view_content.ts b/styles/templates/repo/view_content.ts index 85a6bcd8..6277b52e 100644 --- a/styles/templates/repo/view_content.ts +++ b/styles/templates/repo/view_content.ts @@ -178,8 +178,8 @@ const repoToolbarDetails = css` align-items: center; column-gap: 0; display: grid; - grid-template-columns: max-content max-content minmax(0, 1fr) 36px; - grid-template-rows: 32px 32px; + grid-template-columns: max-content max-content max-content minmax(0, 1fr) 36px; + grid-template-rows: 32px 32px auto; height: auto !important; min-height: 72px; row-gap: 8px; @@ -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,31 @@ 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; + } + + /* 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; 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 +245,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 +268,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; @@ -412,7 +431,9 @@ 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 +441,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/base/component_assets.tmpl b/templates/base/component_assets.tmpl index 8083c3ac..8361c9e4 100644 --- a/templates/base/component_assets.tmpl +++ b/templates/base/component_assets.tmpl @@ -6,17 +6,21 @@ + + + + diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 22dc864c..62aeb372 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -2,6 +2,11 @@ Forgejo's own partial, kept identical except for the theme link's version. Forgejo lets browsers cache theme files for hours and versions this link with its own release only, so an updated theme kept rendering with the previous stylesheet. The build appends the theme's content version after AssetVersion. + + The script marks pages opened in a project board's side panel, before they paint, so the panel shows the issue + without the site navigation, and names the issue's repository for organization and user projects. The frame name + survives navigation inside the panel. */}} + diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl new file mode 100644 index 00000000..5731cb82 --- /dev/null +++ b/templates/org/projects/view.tmpl @@ -0,0 +1,15 @@ +{{/* + GitHub-style project board for organization and user projects, at full width like the repository one. The owner's + tabs already sit in the global header (base/head_navbar), so the user avatar and tab block Forgejo repeats here + is left out. +*/}} +{{template "base/head" .}} +
+ {{if .ContextUser.IsOrganization}} + {{template "org/header" .}} + {{end}} +
+ {{template "projects/view" .}} +
+
+{{template "base/footer" .}} 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}}
+ {{end}} + {{end}} +
+
+ {{template "shared/issueicon" .}} + {{if not $.Page.Repository}}{{.Repo.Name}} {{end}}#{{.Index}} + {{if .Assignees}} + + {{range .Assignees}} + {{ctx.AvatarUtils.Avatar . 20}} + {{end}} + + {{end}} +
+ {{RenderRefIssueTitle $.Context .Title}} + {{if .Labels}} +
+ {{range .Labels}} + {{RenderLabel ctx .}} + {{end}} +
+ {{end}} + {{$tasks := .GetTasks}} + {{$linkedPRs := and $.Page.LinkedPRs (index $.Page.LinkedPRs .ID)}} + {{if or .MilestoneID $linkedPRs (gt $tasks 0)}} +
+ {{if .MilestoneID}} + {{svg "octicon-milestone" 12}}{{.Milestone.Name}} + {{end}} + {{range $linkedPRs}} + {{svg "octicon-git-pull-request" 12}}{{.Title}} #{{.Index}} + {{end}} + {{if gt $tasks 0}} + {{svg "octicon-checklist" 12}}{{.GetTasksDone}} / {{$tasks}} + {{end}} +
+ {{end}} +
+{{end}} diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl new file mode 100644 index 00000000..36eb1425 --- /dev/null +++ b/templates/projects/view.tmpl @@ -0,0 +1,229 @@ +{{/* + GitHub-style project board, shared by repository, organization and user projects. Forgejo's board script needs + .project-column, .cards, .issue-card[data-issue], the .project-column-issue-count inside each column and the column + modals inside .project-column-header, so those keep Forgejo's classes. Column colours become GitHub's status dot; + columns without one named like GitHub's default statuses get GitHub's colour (see pages/projects.css). +*/}} +{{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} + +
+

+ {{if .Project.IsClosed}}{{svg "octicon-lock" 16}}{{end}} + {{.Project.Title}} +

+
+ {{if and .Repository (not .Repository.IsArchived)}} + {{ctx.Locale.Tr "repo.issues.new"}} + {{else if and (not .Repository) .IsSigned (not .Project.IsClosed)}} + {{/* Organization and user projects have no repository of their own: pick one of the owner's repositories, + then Forgejo's new issue form opens with this project already set. The board script lists them. */}} +
+ + {{ctx.Locale.Tr "repo.issues.new"}} + {{svg "octicon-triangle-down" 16}} + + + +
+ {{end}} + {{if $canWriteProject}} +
+ + {{svg "octicon-pencil"}} + {{ctx.Locale.Tr "repo.issues.label_edit"}} + + {{if .Project.IsClosed}} + + {{else}} + + {{end}} + +
+ {{end}} +
+
+ +{{if .Project.RenderedContent}} +
{{$.Project.RenderedContent}}
+{{end}} + +
+ {{template "components/common/search/search" dict "Kind" "icon" "Local" true "ID" "project-board-filter" "Placeholder" "Filter by keyword" "Label" "Filter cards by keyword"}} +
+ +{{if $canWriteProject}} + +
+
{{ctx.Locale.Tr "repo.projects.column.new"}}
+ +
+
+ + +
+ +
+ +
+ + {{template "repo/issue/label_precolors"}} +
+
+
+
+ + +
+ +
+
+{{end}} + +
+
+ {{range .Columns}} +
+
+ + {{.Title}} + {{len (index $.IssuesMap .ID)}} + {{if .Default}}{{svg "octicon-pin" 12}}{{end}} + {{if $canWriteProject}} + + {{end}} +
+
+ {{range (index $.IssuesMap .ID)}} +
+ {{template "projects/card" (dict "Issue" . "Page" $)}} +
+ {{end}} +
+
+ {{end}} + {{if $canWriteProject}} + + {{end}} +
+
+ +{{/* GitHub opens a card's issue in a side panel. The panel shows the issue page itself, which the theme renders + without the site chrome when it is loaded in this frame (see base/head_style). */}} + + + + +{{if $canWriteProject}} + +{{end}} diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 33229ca3..2507a5f6 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -177,7 +177,7 @@
{{if not .DiffNotAvailable}}
-
{{.Diff.NumFiles}} {{if eq .Diff.NumFiles 1}}file{{else}}files{{end}} changed
+
{{.DiffMetadata.TotalNumberOfFiles}} {{if eq .DiffMetadata.TotalNumberOfFiles 1}}file{{else}}files{{end}} changed
{{template "repo/diff/change_summary" dict "Addition" .Diff.TotalAddition "Deletion" .Diff.TotalDeletion}}
{{end}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index c9d01d54..1c05f077 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -1,4 +1,6 @@ -{{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 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}} @@ -11,14 +13,13 @@
{{end}} {{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. */}} +
+
+ {{if $showFileTree}} + + {{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,58 +117,22 @@
{{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}}
@@ -148,13 +166,16 @@
{{if or $file.IsIncomplete $file.IsBin}} -
+
{{if $file.IsIncomplete}} + {{/* GitHub's large diff placeholder. Forgejo replaces the parent of a.diff-load-button with the loaded + diff and reads data-href from the clicked element, so the link stays a direct child holding text only. */}} + {{if $file.IsIncompleteLineTooLong}} - {{ctx.Locale.Tr "repo.diff.file_suppressed_line_too_long"}} + {{ctx.Locale.Tr "repo.diff.file_suppressed_line_too_long"}} {{else}} - {{ctx.Locale.Tr "repo.diff.file_suppressed"}} - {{ctx.Locale.Tr "repo.diff.load"}} + {{ctx.Locale.Tr "repo.diff.load"}} + Large diffs are not rendered by default. {{end}} {{else}} {{ctx.Locale.Tr "repo.diff.bin_not_shown"}} @@ -186,15 +207,15 @@
{{end}} - {{if .Diff.IsIncomplete}} -
-

- {{ctx.Locale.Tr "repo.diff.too_many_files"}} - {{ctx.Locale.Tr "repo.diff.show_more"}} -

-
- {{end}}
+ {{if .DiffMetadata.HasNext}} +
+

+ {{ctx.Locale.Tr "repo.diff.too_many_files"}} + {{ctx.Locale.Tr "repo.diff.show_more"}} +

+
+ {{end}} {{end}}
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl new file mode 100644 index 00000000..ba692fea --- /dev/null +++ b/templates/repo/diff/compare.tmpl @@ -0,0 +1,254 @@ +{{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}} + +
+ {{if and .Commits (gt .CommitCount 0)}} + {{/* GitHub's summary row and its commits grouped by day. */}} + {{$contributors := 0}}{{$seen := ""}} + {{range .Commits}} + {{$key := printf "<%s>" .Author.Email}} + {{if not (StringUtils.Contains $seen $key)}}{{$seen = print $seen $key}}{{$contributors = Eval $contributors "+" 1}}{{end}} + {{end}} +
+ {{svg "octicon-git-commit" 16}}{{ctx.Locale.TrPluralString .CommitCount "counters.n_commits" (ctx.Locale.PrettyNumber .CommitCount)}} + {{if not .DiffNotAvailable}}{{svg "octicon-file-diff" 16}}{{.DiffMetadata.TotalNumberOfFiles}} {{if eq .DiffMetadata.TotalNumberOfFiles 1}}file{{else}}files{{end}} changed{{end}} + {{svg "octicon-people" 16}}{{$contributors}} {{if eq $contributors 1}}contributor{{else}}contributors{{end}} +
+ {{template "repo/pulls/commits_list" .}} + {{else}} + {{template "repo/commits_table" .}} + {{end}} + {{template "repo/diff/box" .}} +
+ {{end}} +
+{{template "base/footer" .}} diff --git a/templates/repo/diff/file_header.tmpl b/templates/repo/diff/file_header.tmpl index d4c05d20..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}} @@ -41,6 +41,12 @@ {{if and $isReviewFile $file.HasChangedSinceLastReview}} {{ctx.Locale.Tr "repo.pulls.has_changed_since_last_review"}} {{end}} + {{if $isReviewFile}} + + {{end}} + {{/* GitHub keeps the file menu last, after Viewed. */}}
{{svg "octicon-kebab-horizontal" 16}}
@@ -61,10 +67,5 @@ {{end}}
- {{if $isReviewFile}} - - {{end}}
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 diff --git a/templates/repo/global_header.tmpl b/templates/repo/global_header.tmpl index bff6ec2d..15b5e27e 100644 --- a/templates/repo/global_header.tmpl +++ b/templates/repo/global_header.tmpl @@ -25,7 +25,7 @@ {{end}} {{if and .Repository.CanEnablePulls (.Permission.CanRead $.UnitTypePullRequests)}} - + {{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.pulls"}} {{if (.Repository.NumOpenPulls ctx)}} {{CountFmt (.Repository.NumOpenPulls ctx)}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 330d4a87..2c357a6d 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -28,7 +28,7 @@ {{ctx.Locale.Tr "repo.desc.internal"}}
{{svg "octicon-shield-lock" 18}}
{{else}} - {{ctx.Locale.Tr "settings.visibility.public"}} + {{ctx.Locale.Tr "user.visibility.public.title"}} {{end}} {{if .IsTemplate}} {{ctx.Locale.Tr "repo.desc.template"}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index c7a2a1c1..29f7c2e8 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -1,7 +1,8 @@ {{/* Repository home composition adapted to Forgejo 15 partials. */}} {{template "base/head" .}} {{$showSidebar := and (not .TreeNames) (not .HideRepoInfo) (not .IsBlame)}} -
+{{/* Layout classes instead of CSS :has(): a :has() on .page-content restyles the whole page, every line of a large file included, on any DOM change. */}} +
{{if not .TreeNames}}{{template "repo/header" .}}{{end}}
{{template "base/alert" .}} 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. */}}
{{template "components/common/sidebar/toggle" .}} diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl new file mode 100644 index 00000000..ad729771 --- /dev/null +++ b/templates/repo/issue/view_title.tmpl @@ -0,0 +1,168 @@ +{{/* 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}} + {{/* Shown in the side panel of organization and user projects, whose issues come from several repositories. */}} + + {{svg "octicon-repo" 16}} + {{.Repository.FullName}} + {{if .Repository.IsPrivate}}{{ctx.Locale.Tr "repo.desc.private"}}{{end}} + +
+ {{if .Issue.IsPull}} + {{$headHref := .HeadTarget}} + {{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}} + {{$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}} + {{end}} + {{if .Issue.PullRequest.HasMerged}} + {{$mergedStr:= DateUtils.TimeSince .Issue.PullRequest.MergedUnix}} + {{if .Issue.OriginalAuthor}} + {{.Issue.OriginalAuthor}} + {{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}} + {{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 */}} + {{/* 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}} {{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}} + {{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}} + {{/* 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}} +
+
+
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/projects/view.tmpl b/templates/repo/projects/view.tmpl new file mode 100644 index 00000000..93cf6901 --- /dev/null +++ b/templates/repo/projects/view.tmpl @@ -0,0 +1,18 @@ +{{/* + GitHub-style project board page. GitHub shows a project without the repository header and the issue sub-navigation, + so the page only keeps the repository tabs. The New issue button moves to the project toolbar. +*/}} +{{template "base/head" .}} +{{/* Forgejo returns to a user project after creating an issue through the repository's project route, which shows the + user's board inside the repository. Open the project at its own address instead. */}} +{{if not .Project.IsRepositoryProject}} + +{{end}} +
+ {{template "repo/header" .}} +
+ {{template "projects/view" .}} +
+
+ +{{template "base/footer" .}} diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 12ac0045..3ce3e28e 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)}} +
{{if and (not (StringUtils.HasPrefix .BranchNameSubURL "commit")) (or .CodeIndexerDisabled (and (not .TagName) (eq .Repository.DefaultBranch .BranchName)))}} @@ -138,6 +140,43 @@
{{template "repo/clone_script" .}}{{/* Updates .js-clone-url and related elements. */}} +
{{end}}
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index 2d044809..61d03848 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -4,7 +4,7 @@ {{$showFollow := and .IsSigned (ne .SignedUserID .ContextUser.ID)}} -
+
{{if eq .SignedUserID .ContextUser.ID}} @@ -44,7 +44,7 @@
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 80b6b208..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 "explore.go_to")}} + {{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 "explore.go_to")}} + {{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"}}
@@ -78,7 +77,7 @@ {{$sortKeyword := StringUtils.RemoveAllPrefix $.Keyword "sort:"}} {{$o := .}} {{range $opt := StringUtils.Make "recentupdate" "leastupdate" "latest" "oldest" "mostcomment" "leastcomment" "nearduedate" "farduedate"}} - {{$text := ctx.Locale.Tr (printf "repo.issues.filter_sort.%s" $opt)}} + {{$text := ctx.Locale.Tr (printf "issues.search.sort.%s" $opt)}} {{$text}} {{end}}
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index 24e6e8a9..18b026fd 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -64,13 +64,13 @@ {{ctx.Locale.Tr "repo.issues.filter_sort"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 13e85f80..7adc0509 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -79,19 +79,6 @@ {{template "components/repository/results/results" dict "Data" . "View" .TabName "Pagination" "base/paginate"}} {{end}} {{template "user/profile_layout_end" .}} - +{{template "shared/admin/user_delete" .}} {{template "base/footer" .}} diff --git a/templates/user/profile_package_versions.tmpl b/templates/user/profile_package_versions.tmpl index 227ada82..757445ba 100644 --- a/templates/user/profile_package_versions.tmpl +++ b/templates/user/profile_package_versions.tmpl @@ -6,8 +6,8 @@ {{if eq .PackageDescriptor.Package.Type "container"}}