From 7b1068b4ca60000fb4a8a73bb92271278d7e5d16 Mon Sep 17 00:00:00 2001 From: whereareiam Date: Thu, 22 Oct 2026 22:11:20 +0200 Subject: [PATCH] feat(issues): labels and milestones in the issue sidebar layout GitHub lists labels and milestones beside the issue sidebar, as the theme already did for issues, pull requests and projects. Labels, milestones, one milestone and the milestone form now render in that layout, with Milestones or Labels marked in the sidebar and without the Labels/Milestones switch. The project list's box becomes a shared component (common/box-list: Open/Closed tabs or a count, Sort, rows) that the milestone and label lists use too. Milestone rows follow GitHub: title, due date and description on the left; progress (a new common/progress bar), completion, counts and Edit/Close/Delete on the right. The dashboard's milestones use the same list. Label rows hold the label, description, open issues and an Edit/Delete menu; organization label settings share them. One milestone shows its progress above the issue page's filters. Action menus now close before Forgejo's own handlers stop the click, so Edit label no longer leaves the menu open behind its dialog. The old milestone card and switch styles are removed, as is a stray closing tag in the dashboard milestones template. Co-Authored-By: Claude Opus 5.5 --- .../list.css => common/box-list/box-list.css} | 70 +++++----- components/common/box-list/states.tmpl | 6 + components/common/dropdown/dropdown.js | 21 +-- components/common/progress/progress.css | 24 ++++ components/project/list/list.tmpl | 25 ++-- components/project/list/sort.tmpl | 8 +- components/repository/label/actions.tmpl | 3 + components/repository/label/list.css | 86 ++++++++++++ components/repository/label/list.tmpl | 28 ++++ components/repository/label/row.tmpl | 16 +++ components/repository/label/sort.tmpl | 5 + components/repository/milestone/list.tmpl | 61 +++++++++ components/repository/milestone/milestone.css | 101 ++++++++++++++ components/repository/milestone/sort.tmpl | 8 ++ src/build-output.test.ts | 35 ++++- src/types/color/github.ts | 2 + styles/components/issues/issue_list.ts | 30 ----- styles/components/repo/index.ts | 2 - styles/components/repo/milestones.ts | 80 ----------- styles/pages/dashboard-lists.css | 30 +---- styles/public/menu/compact_menu.ts | 8 +- templates/base/component_assets.tmpl | 5 +- templates/repo/issue/labels.tmpl | 23 ++++ templates/repo/issue/labels/label_list.tmpl | 1 + templates/repo/issue/milestone_issues.tmpl | 44 ++++++ templates/repo/issue/milestone_new.tmpl | 58 ++++++++ templates/repo/issue/milestones.tmpl | 37 +++++ templates/repo/issue/sidebar.tmpl | 17 ++- templates/user/dashboard/milestones.tmpl | 126 +++--------------- 29 files changed, 636 insertions(+), 324 deletions(-) rename components/{project/list/list.css => common/box-list/box-list.css} (59%) create mode 100644 components/common/box-list/states.tmpl create mode 100644 components/common/progress/progress.css create mode 100644 components/repository/label/actions.tmpl create mode 100644 components/repository/label/list.css create mode 100644 components/repository/label/list.tmpl create mode 100644 components/repository/label/row.tmpl create mode 100644 components/repository/label/sort.tmpl create mode 100644 components/repository/milestone/list.tmpl create mode 100644 components/repository/milestone/milestone.css create mode 100644 components/repository/milestone/sort.tmpl delete mode 100644 styles/components/repo/milestones.ts create mode 100644 templates/repo/issue/labels.tmpl create mode 100644 templates/repo/issue/labels/label_list.tmpl create mode 100644 templates/repo/issue/milestone_issues.tmpl create mode 100644 templates/repo/issue/milestone_new.tmpl create mode 100644 templates/repo/issue/milestones.tmpl diff --git a/components/project/list/list.css b/components/common/box-list/box-list.css similarity index 59% rename from components/project/list/list.css rename to components/common/box-list/box-list.css index ac1f6a8b..523d4d86 100644 --- a/components/project/list/list.css +++ b/components/common/box-list/box-list.css @@ -1,30 +1,31 @@ -/* GitHub's project list: a toolbar, then a box whose header holds the state tabs and Sort, with a row per project. */ -.theme-project-list-toolbar { +/* GitHub's Box list, as on its projects, milestones and labels pages: a toolbar, then a box whose header holds the + state tabs (or a count) and Sort, with a row per item. */ +.theme-box-list-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; } -.theme-project-list > .theme-list-heading { +.theme-box-list-page > .theme-list-heading { margin-bottom: 16px; } -.theme-project-list-search { +.theme-box-list-search { flex: 1; min-width: 0; } -.theme-project-list-search .theme-search-field { +.theme-box-list-search .theme-search-field { width: 100%; } -.theme-project-list-box { +.theme-box-list { border: 1px solid var(--color-light-border); border-radius: var(--border-radius); } -.theme-project-list-header { +.theme-box-list-header { display: flex; align-items: center; justify-content: space-between; @@ -36,14 +37,14 @@ background: var(--color-box-header); } -.theme-project-list-states { +.theme-box-list-states { display: flex; align-items: center; gap: 8px; min-width: 0; } -.theme-project-list-states > a { +.theme-box-list-states > a { display: inline-flex; align-items: center; gap: 8px; @@ -56,46 +57,55 @@ white-space: nowrap; } -.theme-project-list-states > a:hover { +.theme-box-list-states > a:hover { color: var(--color-text); background: var(--github-control-transparent-bgColor-hover); text-decoration: none; } -.theme-project-list-states > a[aria-current] { +.theme-box-list-states > a[aria-current] { color: var(--color-text); font-weight: 600; } -.theme-project-list-states .theme-counter { +/* A header that counts instead of switching state, as on GitHub's labels page. */ +.theme-box-list-title { + margin: 0; + padding: 0 8px; + font-size: 14px; + font-weight: 600; + line-height: 30px; +} + +.theme-box-list-states .theme-counter { font-weight: 600; } -.theme-project-list-header > .theme-dropdown > summary.theme-control { +.theme-box-list-header > .theme-dropdown > summary.theme-control { font-weight: 400; } -.theme-project-list-header .theme-dropdown-panel { +.theme-box-list-header .theme-dropdown-panel { width: 240px; } /* GitHub's Sort menu marks the current order with a check and keeps the labels aligned. */ -.theme-project-list-check { +.theme-box-list-check { flex: none; visibility: hidden; } -.theme-dropdown-action[aria-current] > .theme-project-list-check { +.theme-dropdown-action[aria-current] > .theme-box-list-check { visibility: visible; } -.theme-project-list-rows { +.theme-box-list-rows { margin: 0; padding: 0; list-style: none; } -.theme-project-list-row { +.theme-box-list-row { display: flex; align-items: center; gap: 16px; @@ -105,16 +115,16 @@ border-top: 1px solid var(--color-light-border); } -.theme-project-list-row:first-child { +.theme-box-list-row:first-child { border-top: 0; } -.theme-project-list-main { +.theme-box-list-main { flex: 1; min-width: 0; } -.theme-project-list-main h3 { +.theme-box-list-main h3 { margin: 0; font-size: 16px; font-weight: 500; @@ -122,47 +132,47 @@ overflow-wrap: anywhere; } -.theme-project-list-main h3 > a { +.theme-box-list-main h3 > a { color: var(--color-text); } -.theme-project-list-main h3 > a:hover { +.theme-box-list-main h3 > a:hover { color: var(--color-primary); } -.theme-project-list-meta, -.theme-project-list-description { +.theme-box-list-meta, +.theme-box-list-description { margin: 0; color: var(--color-text-light-1); font-size: 12px; line-height: 18px; } -.theme-project-list-description.markup { +.theme-box-list-description.markup { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.theme-project-list-description.markup > * { +.theme-box-list-description.markup > * { display: inline; margin: 0; font-size: inherit; } -.theme-project-list-row > .theme-dropdown > summary.theme-control { +.theme-box-list-row > .theme-dropdown > summary.theme-control { width: 32px; padding: 0; justify-content: center; color: var(--color-text-light-1); } -.theme-project-list-row > .theme-dropdown .theme-dropdown-panel { +.theme-box-list-row > .theme-dropdown .theme-dropdown-panel { width: 192px; } @media (max-width: 543.98px) { - .theme-project-list-row { + .theme-box-list-row { margin: 0 8px; } } diff --git a/components/common/box-list/states.tmpl b/components/common/box-list/states.tmpl new file mode 100644 index 00000000..85bb2555 --- /dev/null +++ b/components/common/box-list/states.tmpl @@ -0,0 +1,6 @@ +{{- /* The Open and Closed tabs of a box list header, each with its count. Label names the navigation; OpenLink, +ClosedLink, OpenCount and ClosedCount fill the tabs, and IsClosed marks the closed one as current. */ -}} + diff --git a/components/common/dropdown/dropdown.js b/components/common/dropdown/dropdown.js index 8d8b7b12..8e278383 100644 --- a/components/common/dropdown/dropdown.js +++ b/components/common/dropdown/dropdown.js @@ -11,14 +11,19 @@ dropdown.open = false; if (restoreFocus) dropdown.querySelector("summary")?.focus(); }; - document.addEventListener("click", event => { - const close = event.target.closest("[data-theme-dropdown-close]"); - if (close) closeDropdown(close.closest(".theme-dropdown"), true); - const action = event.target.closest('[data-theme-dropdown="actions"] .theme-dropdown-action'); - if (action && !action.disabled) closeDropdown(action.closest(".theme-dropdown"), false); - for (const dropdown of document.querySelectorAll(".theme-dropdown[open]")) - if (!dropdown.contains(event.target)) closeDropdown(dropdown, false); - }); + // Capture: Forgejo's own handlers on menu actions (Edit label, for one) stop the click from bubbling. + document.addEventListener( + "click", + event => { + const close = event.target.closest("[data-theme-dropdown-close]"); + if (close) closeDropdown(close.closest(".theme-dropdown"), true); + const action = event.target.closest('[data-theme-dropdown="actions"] .theme-dropdown-action'); + if (action && !action.disabled) closeDropdown(action.closest(".theme-dropdown"), false); + for (const dropdown of document.querySelectorAll(".theme-dropdown[open]")) + if (!dropdown.contains(event.target)) closeDropdown(dropdown, false); + }, + true + ); document.addEventListener( "change", event => { diff --git a/components/common/progress/progress.css b/components/common/progress/progress.css new file mode 100644 index 00000000..fea040ed --- /dev/null +++ b/components/common/progress/progress.css @@ -0,0 +1,24 @@ +/* GitHub's Progress bar: 8px on the progress track, filled with the success color. Add theme-progress to a native + , so its value stays readable without the theme. */ +progress.theme-progress { + display: block; + width: 100%; + height: 8px; + border: 0; + border-radius: var(--border-radius); + background: var(--github-progressBar-track-bgColor); + overflow: hidden; + appearance: none; +} + +progress.theme-progress::-webkit-progress-bar { + background: var(--github-progressBar-track-bgColor); +} + +progress.theme-progress::-webkit-progress-value { + background: var(--github-bgColor-success-emphasis); +} + +progress.theme-progress::-moz-progress-bar { + background: var(--github-bgColor-success-emphasis); +} diff --git a/components/project/list/list.tmpl b/components/project/list/list.tmpl index 0a2f34db..b3c2ef13 100644 --- a/components/project/list/list.tmpl +++ b/components/project/list/list.tmpl @@ -5,36 +5,33 @@ the search; without it, New project sits beside the search as on GitHub's profil {{$canWrite := and $data.CanWriteProjects (not $data.Repository.IsArchived)}} {{$newProject := ""}} {{if $canWrite}}{{$newProject = print $data.Link "/new"}}{{end}} -
+
{{if .Heading}}

{{ctx.Locale.Tr "repo.projects"}}

{{if $newProject}}{{ctx.Locale.Tr "repo.projects.new"}}{{end}}
{{end}} -
- -
-
- +
+
+ {{template "components/common/box-list/states" dict "Label" (ctx.Locale.Tr "repo.projects") "OpenLink" (print "?state=open&q=" (QueryEscape $data.Keyword) "&sort=" $data.SortType) "ClosedLink" (print "?state=closed&q=" (QueryEscape $data.Keyword) "&sort=" $data.SortType) "OpenCount" $data.OpenCount "ClosedCount" $data.ClosedCount "IsClosed" $data.IsShowClosed}} {{template "components/common/dropdown/dropdown" dict "Kind" "actions" "Label" (ctx.Locale.Tr "repo.issues.filter_sort") "Quiet" true "Items" "components/project/list/sort" "Data" $data}}
-
    +
      {{range $data.Projects}} -
    • -
      +
    • +

      {{.Title}}

      -

      #{{.ID}} {{ctx.Locale.Tr "issues.updated" (DateUtils.TimeSince .UpdatedUnix)}}

      - {{if .Description}}
      {{.RenderedContent}}
      {{end}} +

      #{{.ID}} {{ctx.Locale.Tr "issues.updated" (DateUtils.TimeSince .UpdatedUnix)}}

      + {{if .Description}}
      {{.RenderedContent}}
      {{end}}
      {{if $canWrite}} {{template "components/common/dropdown/dropdown" dict "Kind" "actions" "Icon" "octicon-kebab-horizontal" "IconOnly" true "Quiet" true "Label" (ctx.Locale.Tr "profile.actions.tooltip") "Items" "components/project/list/actions" "Data" (dict "Context" $data.Context "Link" (.Link ctx) "IsClosed" .IsClosed)}} diff --git a/components/project/list/sort.tmpl b/components/project/list/sort.tmpl index 22edf956..f92587bc 100644 --- a/components/project/list/sort.tmpl +++ b/components/project/list/sort.tmpl @@ -1,5 +1,5 @@ {{$query := print "?state=" .State "&q=" .Keyword}} -{{svg "octicon-check" 16 "theme-project-list-check"}}{{ctx.Locale.Tr "issues.search.sort.latest"}} -{{svg "octicon-check" 16 "theme-project-list-check"}}{{ctx.Locale.Tr "issues.search.sort.oldest"}} -{{svg "octicon-check" 16 "theme-project-list-check"}}{{ctx.Locale.Tr "issues.search.sort.recentupdate"}} -{{svg "octicon-check" 16 "theme-project-list-check"}}{{ctx.Locale.Tr "issues.search.sort.leastupdate"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "issues.search.sort.latest"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "issues.search.sort.oldest"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "issues.search.sort.recentupdate"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "issues.search.sort.leastupdate"}} diff --git a/components/repository/label/actions.tmpl b/components/repository/label/actions.tmpl new file mode 100644 index 00000000..8b8d3e0f --- /dev/null +++ b/components/repository/label/actions.tmpl @@ -0,0 +1,3 @@ +{{$label := .Label}} +{{svg "octicon-pencil" 16}}{{ctx.Locale.Tr "repo.issues.label_edit"}} +{{svg "octicon-trash" 16}}{{ctx.Locale.Tr "repo.issues.label_delete"}} diff --git a/components/repository/label/list.css b/components/repository/label/list.css new file mode 100644 index 00000000..28990e8d --- /dev/null +++ b/components/repository/label/list.css @@ -0,0 +1,86 @@ +/* GitHub's label rows: the label in a quarter of the row, the description, the open issues, then the menu. */ +.theme-label-row { + min-height: 56px; + padding: 12px 0; +} + +.theme-label-name { + display: flex; + flex: 0 0 25%; + align-items: center; + gap: 8px; + min-width: 0; +} + +.theme-label-description { + flex: 1; + min-width: 0; + color: var(--color-text-light-1); + font-size: 12px; + line-height: 18px; + overflow-wrap: anywhere; +} + +.theme-label-issues { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + gap: 4px; + color: var(--color-text-light-1); + font-size: 12px; + line-height: 18px; +} + +.theme-label-issues:hover { + color: var(--github-fgColor-accent); +} + +/* Rows without the menu keep the issue links in line with those that have it. */ +.theme-label-actions-placeholder { + flex: 0 0 32px; +} + +.theme-label-list-note { + min-height: 0; + color: var(--color-text-light-1); + font-size: 12px; + line-height: 18px; +} + +.theme-label-list-template { + padding: 16px; + border-bottom: 1px solid var(--color-light-border); +} + +/* Phones: the label and its menu on one line, the description and the open issues beneath. */ +@media (max-width: 767.98px) { + .theme-label-row { + display: grid; + grid-template-areas: + "name actions" + "description description" + "issues issues"; + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px 16px; + } + + .theme-label-name { + grid-area: name; + } + + .theme-label-description { + grid-area: description; + } + + .theme-label-description:empty { + display: none; + } + + .theme-label-issues { + grid-area: issues; + } + + .theme-label-row > :is(.theme-dropdown, .theme-label-actions-placeholder) { + grid-area: actions; + } +} diff --git a/components/repository/label/list.tmpl b/components/repository/label/list.tmpl new file mode 100644 index 00000000..fdc4adc9 --- /dev/null +++ b/components/repository/label/list.tmpl @@ -0,0 +1,28 @@ +{{- /* GitHub's label list: a box headed by the label count and Sort, with a row per label holding the label, its +description, its open issues and, for writers, a menu with Edit and Delete. The repository labels page and the +organization's label settings both render it; Forgejo's label scripts find the edit and delete buttons by class. */ -}} +{{$canWrite := or .PageIsOrgSettingsLabels (and (not .Repository.IsArchived) (or .CanWriteIssues .CanWritePulls))}} +{{$issuesLink := print .RepoLink "/issues"}}{{if .PageIsOrgSettingsLabels}}{{$issuesLink = print AppSubUrl "/issues"}}{{end}} +
      +
      +

      {{ctx.Locale.Tr "repo.issues.label_count" .NumLabels}}

      + {{template "components/common/dropdown/dropdown" dict "Kind" "actions" "Label" (ctx.Locale.Tr "repo.issues.filter_sort") "Quiet" true "Items" "components/repository/label/sort" "Data" .}} +
      + {{if and $canWrite (eq .NumLabels 0) (or .PageIsOrgSettingsLabels (not .OrgLabels))}} +
      {{template "repo/issue/labels/label_load_template" .}}
      + {{end}} +
        + {{range .Labels}} + {{template "components/repository/label/row" dict "Context" $.Context "Label" . "IssuesLink" $issuesLink "OpenIssues" .NumOpenIssues "CanWrite" $canWrite "DeleteLink" (print $.Link "/delete")}} + {{end}} + {{if and (not .PageIsOrgSettingsLabels) .OrgLabels}} +
      • + {{ctx.Locale.Tr "repo.org_labels_desc"}} + {{if .IsOrganizationOwner}}{{ctx.Locale.Tr "repo.org_labels_desc_manage"}}{{end}} +
      • + {{range .OrgLabels}} + {{template "components/repository/label/row" dict "Context" $.Context "Label" . "IssuesLink" $issuesLink "OpenIssues" .NumOpenRepoIssues}} + {{end}} + {{end}} +
      +
      diff --git a/components/repository/label/row.tmpl b/components/repository/label/row.tmpl new file mode 100644 index 00000000..58275d19 --- /dev/null +++ b/components/repository/label/row.tmpl @@ -0,0 +1,16 @@ +{{- /* One row of components/repository/label/list. Label is the label; IssuesLink and OpenIssues link its open issues; +CanWrite adds the Edit and Delete menu, DeleteLink being where Delete posts. */ -}} +{{$label := .Label}} +
    • +
      + {{template "components/repository/label/label" $label}} + {{template "repo/issue/labels/label_archived" $label}} +
      +
      {{if $label.Description}}{{$label.Description | RenderEmoji ctx}}{{end}}
      + {{svg "octicon-issue-opened" 16}}{{ctx.Locale.Tr "repo.issues.label_open_issues" .OpenIssues}} + {{if .CanWrite}} + {{template "components/common/dropdown/dropdown" dict "Kind" "actions" "Icon" "octicon-kebab-horizontal" "IconOnly" true "Quiet" true "Label" (ctx.Locale.Tr "profile.actions.tooltip") "Items" "components/repository/label/actions" "Data" (dict "Context" .Context "Label" $label "DeleteLink" .DeleteLink)}} + {{else}} + + {{end}} +
    • diff --git a/components/repository/label/sort.tmpl b/components/repository/label/sort.tmpl new file mode 100644 index 00000000..bf19c5b1 --- /dev/null +++ b/components/repository/label/sort.tmpl @@ -0,0 +1,5 @@ +{{$query := print "?state=" .State}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "milestone.filter_sort.least_issues"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "milestone.filter_sort.most_issues"}} diff --git a/components/repository/milestone/list.tmpl b/components/repository/milestone/list.tmpl new file mode 100644 index 00000000..dd7bdaa4 --- /dev/null +++ b/components/repository/milestone/list.tmpl @@ -0,0 +1,61 @@ +{{- /* GitHub's milestone list, for a repository and for the dashboard: a box with the Open and Closed tabs and Sort in +its header, and a row per milestone with its due date and description on the left and its progress on the right. +Data is the page context. Query starts every state and sort link ("?q=..." plus any page filter). ShowRepo names each +milestone's repository, as the dashboard lists several. RepoLink and CanWrite add Edit, Close and Delete for a +repository's writers. */ -}} +{{$data := .Data}} +{{$query := .Query}} +
      +
      + {{template "components/common/box-list/states" dict "Label" (ctx.Locale.Tr "repo.milestones") "OpenLink" (print $query "&state=open&sort=" $data.SortType) "ClosedLink" (print $query "&state=closed&sort=" $data.SortType) "OpenCount" .OpenCount "ClosedCount" .ClosedCount "IsClosed" $data.IsShowClosed}} + {{template "components/common/dropdown/dropdown" dict "Kind" "actions" "Label" (ctx.Locale.Tr "repo.issues.filter_sort") "Quiet" true "Items" "components/repository/milestone/sort" "Data" (dict "Context" $data.Context "Query" (print $query "&state=" $data.State) "SortType" $data.SortType)}} +
      +
        + {{range $data.Milestones}} + {{$link := print $.RepoLink "/milestone/" .ID}} + {{if $.ShowRepo}}{{$link = print .Repo.Link "/milestone/" .ID}}{{end}} +
      • +
        +

        {{if $.ShowRepo}}{{.Repo.FullName}}{{end}}{{.Name}}

        +

        + {{if .IsClosed}} + {{ctx.Locale.Tr "repo.milestones.closed" (DateUtils.TimeSince .ClosedDateUnix)}} + {{else if .DeadlineString}} + {{svg "octicon-calendar" 14}} {{DateUtils.AbsoluteShort (.DeadlineString|DateUtils.ParseLegacy)}} + {{else}} + {{ctx.Locale.Tr "repo.milestones.no_due_date"}} + {{end}} + {{if .UpdatedUnix}} {{ctx.Locale.Tr "repo.milestones.update_ago" (DateUtils.TimeSince .UpdatedUnix)}}{{end}} + {{if .TotalTrackedTime}} {{svg "octicon-clock" 14}} {{.TotalTrackedTime|Sec2Time}}{{end}} +

        + {{if .Content}}
        {{.RenderedContent}}
        {{end}} +
        +
        + +

        + {{ctx.Locale.Tr "repo.milestones.completeness" .Completeness}} + {{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}} + {{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}} +

        + {{if $.CanWrite}} +

        + {{ctx.Locale.Tr "repo.issues.label_edit"}} + {{if .IsClosed}} + {{ctx.Locale.Tr "repo.milestones.open"}} + {{else}} + {{ctx.Locale.Tr "repo.milestones.close"}} + {{end}} + {{ctx.Locale.Tr "repo.issues.label_delete"}} +

        + {{end}} +
        +
      • + {{else}} +
      • + {{svg "octicon-milestone" 24}} +

        {{ctx.Locale.Tr "search.no_results"}}

        +
      • + {{end}} +
      +
      +{{template "base/paginate" $data}} diff --git a/components/repository/milestone/milestone.css b/components/repository/milestone/milestone.css new file mode 100644 index 00000000..85c3e7bb --- /dev/null +++ b/components/repository/milestone/milestone.css @@ -0,0 +1,101 @@ +/* GitHub's milestone row: the title, dates and description on the left; on the right a third of the row for the + progress bar, the completion and issue counts, and the writer's Edit, Close and Delete. */ +.theme-milestone-row { + align-items: flex-start; + gap: 24px; + padding: 16px 0; +} + +.theme-milestone-row .theme-box-list-main h3 { + font-weight: 600; +} + +.theme-milestone-repo { + margin-right: 8px; + color: var(--color-text-light-1); + font-weight: 400; +} + +.theme-milestone-row .theme-box-list-meta { + margin-top: 4px; +} + +.theme-milestone-row .theme-box-list-meta .svg { + vertical-align: text-bottom; +} + +.theme-milestone-overdue { + color: var(--color-red); +} + +.theme-milestone-row .theme-box-list-description { + margin-top: 8px; + color: var(--color-text); + font-size: 14px; + line-height: 21px; +} + +.theme-milestone-progress { + flex: 0 0 33%; + min-width: 0; + padding-top: 4px; +} + +.theme-milestone-stats, +.theme-milestone-actions { + display: flex; + flex-wrap: wrap; + gap: 4px 16px; + margin: 8px 0 0; + color: var(--color-text-light-1); + font-size: 14px; + line-height: 21px; +} + +.theme-milestone-stats strong { + color: var(--color-text); + font-weight: 600; +} + +.theme-milestone-actions > a { + color: var(--github-fgColor-accent); +} + +.theme-milestone-actions > .theme-milestone-delete { + color: var(--color-red); +} + +@media (max-width: 767.98px) { + .theme-milestone-row { + flex-direction: column; + gap: 12px; + } + + .theme-milestone-progress { + flex-basis: auto; + width: 100%; + } +} + +/* One milestone's page: its due date, description and progress between the heading and the issue list. */ +.theme-milestone-summary { + margin-bottom: 16px; +} + +.theme-milestone-summary-meta { + display: flex; + flex-wrap: wrap; + gap: 4px 16px; + margin: 0 0 8px; + color: var(--color-text-light-1); + font-size: 14px; + line-height: 21px; +} + +.theme-milestone-summary-meta .svg { + vertical-align: text-bottom; +} + +.theme-milestone-summary > .markup { + margin-bottom: 16px; +} diff --git a/components/repository/milestone/sort.tmpl b/components/repository/milestone/sort.tmpl new file mode 100644 index 00000000..d0e8430a --- /dev/null +++ b/components/repository/milestone/sort.tmpl @@ -0,0 +1,8 @@ +{{$query := .Query}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "milestone.filter_sort.earliest_due_data"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "milestone.filter_sort.latest_due_date"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "milestone.filter_sort.least_complete"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "milestone.filter_sort.most_complete"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "milestone.filter_sort.most_issues"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "milestone.filter_sort.least_issues"}} +{{svg "octicon-check" 16 "theme-box-list-check"}}{{ctx.Locale.Tr "milestone.filter_sort.name"}} diff --git a/src/build-output.test.ts b/src/build-output.test.ts index 1e069fb5..296e530d 100644 --- a/src/build-output.test.ts +++ b/src/build-output.test.ts @@ -457,7 +457,7 @@ 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:not(#notification_div) > .ui.container,"); + 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;"); @@ -511,6 +511,39 @@ describe("Forgejo 15 native integration", () => { expect(read(...page)).not.toContain('template "repo/header"'); }); + it("lists labels and milestones in the issue sidebar layout with the shared box list", () => { + const read = (...parts: string[]) => fs.readFileSync(path.join(ROOT_DIR, ...parts), "utf-8"); + const sidebar = read("templates", "repo", "issue", "sidebar.tmpl"); + expect(sidebar).toContain('{{if $isMilestonePage}}active{{end}}" href="{{.RepoLink}}/milestones"'); + expect(sidebar).toContain('{{if .PageIsLabels}}active{{end}}" href="{{.RepoLink}}/labels"'); + for (const page of ["labels.tmpl", "milestones.tmpl", "milestone_issues.tmpl", "milestone_new.tmpl"]) { + const template = read("templates", "repo", "issue", page); + expect(template, page).toContain('{{template "repo/issue/sidebar" .}}'); + expect(template, page).not.toContain('template "repo/issue/navbar"'); + } + expect(read("templates", "repo", "issue", "milestones.tmpl")).toContain( + 'template "components/repository/milestone/list"' + ); + expect(read("templates", "user", "dashboard", "milestones.tmpl")).toContain( + 'template "components/repository/milestone/list"' + ); + expect(read("templates", "repo", "issue", "labels", "label_list.tmpl")).toContain( + 'template "components/repository/label/list"' + ); + expect(read("templates", "repo", "issue", "milestone_issues.tmpl")).toContain('"themeList" "issues"'); + // The project, milestone and label lists share GitHub's Box list and its Open/Closed tabs. + for (const list of [ + ["components", "project", "list", "list.tmpl"], + ["components", "repository", "milestone", "list.tmpl"], + ["components", "repository", "label", "list.tmpl"], + ]) + expect(read(...list)).toContain('class="theme-box-list'); + expect(read("components", "repository", "label", "actions.tmpl")).toContain("edit-label-button"); + expect(read("templates", "base", "component_assets.tmpl")).toContain( + "/css/components/common/box-list/box-list.css" + ); + }); + it("matches GitHub's repository About, visibility, and code-search details", () => { const headerStyle = fs.readFileSync(path.join(ROOT_DIR, "styles", "templates", "repo", "header.ts"), "utf-8"); const headerTemplate = fs.readFileSync(path.join(ROOT_DIR, "templates", "repo", "home_header.tmpl"), "utf-8"); diff --git a/src/types/color/github.ts b/src/types/color/github.ts index 8e671727..fc44c463 100644 --- a/src/types/color/github.ts +++ b/src/types/color/github.ts @@ -73,6 +73,7 @@ export const github = { /** 成功的背景颜色 * @issue `babel` 重新开启图标背景颜色 * @issue `prMerge` 合并提交的图标背景色 + * @components `common/progress` GitHub 进度条的填充色 */ emphasis: null, }, @@ -383,6 +384,7 @@ export const github = { track: { /** 进度条轨道背景色, 默认为 #0000, 高对比度专属颜色 * @repo_sidebar `repoSidebarBottom` 语言统计条的背景色 + * @components `common/progress` GitHub 进度条的轨道色 */ bgColor: null, /** 进度条轨道边框色, 默认为 #0000, 高对比度专属颜色 diff --git a/styles/components/issues/issue_list.ts b/styles/components/issues/issue_list.ts index 3e6fff93..66eb880e 100644 --- a/styles/components/issues/issue_list.ts +++ b/styles/components/issues/issue_list.ts @@ -21,8 +21,6 @@ import { css, cssCombine, extractVarName, otherThemeVars, themeVars } from "@lut // 工单&PR 列表 const issueList = css` - /* 仓库页面的里程碑列表菜单栏 */ - .page-content.repository.milestones, .page-content.repository.milestone-issue-list, .page-content.repository.issue-list { ${extractVarName(otherThemeVars.checkbox.size)}: 16px; @@ -109,39 +107,11 @@ const issueList = css` } } } - - /* 里程碑列表 */ - /* [TODO] 暂时排除项目的列表 */ - .page-content.repository.milestones:not(.projects) .milestone-list, - /* 仪表板的里程碑列表 */ - .page-content.repository.milestones.dashboard .milestone-list { - border: 1px solid ${themeVars.color.light.border}; - border-bottom-left-radius: ${otherThemeVars.border.radius}; - border-bottom-right-radius: ${otherThemeVars.border.radius}; - .milestone-card { - padding: 8px 16px 10px; - .milestone-header h3 > a { - font-size: 16px; - font-weight: 500; - } - .milestone-header > .tw-flex > span { - font-size: 14px; - font-weight: 600; - } - } - .milestone-toolbar { - font-size: 12px; - .group > a { - font-size: 13px; - } - } - } `; // 避免手机/平板下菜单错位 const issueListMobile = css` @media (max-width: 1023.98px) { - .page-content.repository.milestones, .page-content.repository.milestone-issue-list, .page-content.repository.issue-list { .issue-list-toolbar { diff --git a/styles/components/repo/index.ts b/styles/components/repo/index.ts index 33e2eece..dc0f0437 100644 --- a/styles/components/repo/index.ts +++ b/styles/components/repo/index.ts @@ -23,7 +23,6 @@ import diff from "./diff"; import fileTree from "../file_tree"; import sourceBrowser from "./source_browser"; import filePicker from "./file_picker"; -import milestones from "./milestones"; import packages from "./packages"; import release from "./release"; import repo from "./repo"; @@ -37,7 +36,6 @@ export default cssCombine( filePicker, fileTree, sourceBrowser, - milestones, packages, release, repo, diff --git a/styles/components/repo/milestones.ts b/styles/components/repo/milestones.ts deleted file mode 100644 index 73d7508c..00000000 --- a/styles/components/repo/milestones.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*! - * Copyright (c) https://github.com/lutinglt - * - * See the NOTICE file distributed with this work for additional - * information regarding copyright ownership. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { css, cssCombine, themeVars } from "@lutinglt/gitea-github-theme/core"; - -const milestone = css` - /* 里程碑头部 */ - .milestone-header { - gap: 16px; - /* 进度条 */ - progress { - height: 5px; - width: 300px; - max-width: 80vw; - } - } - /* 里程碑 Issue 列表的进度条 */ - .milestone-progress-big { - height: 8px; - } - /* 里程碑 Issue 列表 */ - .page-content.repository.milestone-issue-list { - > .ui.container { - > .flex-text-block:first-child { - margin-bottom: 16px; - > h1 { - font-size: 32px; - font-weight: 600; - line-height: 48px; - word-break: keep-all; - } - + .tw-flex { - flex-direction: row !important; - align-items: center; - gap: 8px !important; - padding-top: 8px; - padding-bottom: 10px; - font-size: 14px; - color: ${themeVars.color.text.light.num1}; - strong { - color: ${themeVars.color.text.self}; - } - > .flex-text-block { - gap: 8px !important; - } - } - } - > .divider { - border-top-color: #0000; - } - } - } -`; - -// 避免手机/平板下菜单错位 -const milestoneMobile = css` - @media (max-width: 767.98px) { - .page-content.repository.milestone-issue-list > .ui.container > .flex-text-block:first-child + .tw-flex { - flex-direction: column !important; - } - } -`; - -export default cssCombine(milestone, milestoneMobile); diff --git a/styles/pages/dashboard-lists.css b/styles/pages/dashboard-lists.css index 60dfc133..26822b8e 100644 --- a/styles/pages/dashboard-lists.css +++ b/styles/pages/dashboard-lists.css @@ -1,4 +1,4 @@ -/* The dashboard's own lists: issues, pull requests, milestones and notifications. */ +/* The dashboard's own lists: issues, pull requests and notifications. */ /* The context switcher sits at the top of the sidebar instead of in a bar across the page. */ .dashboard-list-context .secondary-nav { @@ -20,27 +20,6 @@ padding: 6px 8px; } -/* Milestones: the cards form one bordered list under the Open/Closed bar. */ -.github-milestones .milestone-list { - margin: 0; - padding: 0; - border: 1px solid var(--color-light-border); - border-top: 0; - border-radius: 0 0 var(--border-radius) var(--border-radius); - list-style: none; -} - -.github-milestones .milestone-list > .milestone-card { - margin: 0; - padding: 12px 16px; - border: 0; - border-radius: 0; -} - -.github-milestones .milestone-list > .milestone-card + .milestone-card { - border-top: 1px solid var(--color-light-border); -} - /* Notifications */ .github-notifications .theme-list-heading { margin-bottom: 16px; @@ -105,10 +84,3 @@ .dashboard-list-context .ui.secondary.menu > .item { align-self: flex-start; } - -/* Forgejo's milestone page gives the list header its own box; the shared heading has none. */ -.github-milestones .list-header.list-header-issues { - padding: 0 !important; - border: 0 !important; - background: transparent !important; -} diff --git a/styles/public/menu/compact_menu.ts b/styles/public/menu/compact_menu.ts index 414d4be2..6b0e9452 100644 --- a/styles/public/menu/compact_menu.ts +++ b/styles/public/menu/compact_menu.ts @@ -25,13 +25,7 @@ const smallCompactMenu = css` .page-content.repository.file.editor #editor-bar, /* 订阅/关注切换菜单(应只选中订阅/关注页面, 不能选中通知页面) */ /* 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, - /* 里程碑/标签切换菜单(新建里程碑页) */ - .page-content.repository.new.milestone .issue-navbar, - /* 里程碑/标签切换菜单(标签页) */ - .page-content.repository.labels .issue-navbar { + .page-content.user.notification:not(#notification_div) > .ui.container { .switch { --switch-item-min-height: 30px; --switch-padding-inline: 12px; diff --git a/templates/base/component_assets.tmpl b/templates/base/component_assets.tmpl index 399b3ba2..cf2f498e 100644 --- a/templates/base/component_assets.tmpl +++ b/templates/base/component_assets.tmpl @@ -1,3 +1,4 @@ + @@ -5,6 +6,7 @@ + @@ -16,7 +18,8 @@ - + + diff --git a/templates/repo/issue/labels.tmpl b/templates/repo/issue/labels.tmpl new file mode 100644 index 00000000..0f731e3f --- /dev/null +++ b/templates/repo/issue/labels.tmpl @@ -0,0 +1,23 @@ +{{/* GitHub lists a repository's labels inside the issue sidebar layout: a heading with New label, then the label box. */}} +{{template "base/head" .}} +{{$canWrite := and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} +
      + {{template "repo/issue/sidebar" .}} +
      +
      +
      +

      {{ctx.Locale.Tr "repo.labels"}}

      + {{if $canWrite}}{{end}} +
      + {{if $canWrite}} + {{template "repo/issue/labels/label_new" .}} + {{end}} + {{template "base/alert" .}} + {{template "repo/issue/labels/label_list" .}} +
      +
      +
      +{{if $canWrite}} + {{template "repo/issue/labels/edit_delete_label" .}} +{{end}} +{{template "base/footer" .}} diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl new file mode 100644 index 00000000..f82ef116 --- /dev/null +++ b/templates/repo/issue/labels/label_list.tmpl @@ -0,0 +1 @@ +{{template "components/repository/label/list" .}} diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl new file mode 100644 index 00000000..3c64e862 --- /dev/null +++ b/templates/repo/issue/milestone_issues.tmpl @@ -0,0 +1,44 @@ +{{/* One milestone, as GitHub shows it inside the issue sidebar layout: its name with Close, Edit and New issue, its due +date and progress, then the issue list with the issue page's filters. */}} +{{template "base/head" .}} +
      + {{template "repo/issue/sidebar" .}} +
      + {{template "base/alert" .}} +
      +

      {{.Milestone.Name}}

      + {{if not .Repository.IsArchived}} +
      + {{if or .CanWriteIssues .CanWritePulls}} + {{if .Milestone.IsClosed}} + {{ctx.Locale.Tr "repo.milestones.open"}} + {{else}} + {{ctx.Locale.Tr "repo.milestones.close"}} + {{end}} + {{ctx.Locale.Tr "repo.milestones.edit"}} + {{end}} + {{ctx.Locale.Tr "repo.issues.new"}} +
      + {{end}} +
      +
      +

      + {{if .Milestone.IsClosed}} + {{ctx.Locale.Tr "repo.milestones.closed" (DateUtils.TimeSince .Milestone.ClosedDateUnix)}} + {{else if .Milestone.DeadlineString}} + {{svg "octicon-calendar" 16}} {{DateUtils.AbsoluteShort (.Milestone.DeadlineString|DateUtils.ParseLegacy)}} + {{else}} + {{ctx.Locale.Tr "repo.milestones.no_due_date"}} + {{end}} + {{if .TotalTrackedTime}}{{svg "octicon-clock" 16}} {{.TotalTrackedTime | Sec2Time}}{{end}} +

      + {{if .Milestone.RenderedContent}}
      {{.Milestone.RenderedContent}}
      {{end}} + +

      {{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness}}

      +
      + {{template "repo/issue/filters" dict "." . "themeList" "issues"}} + {{template "shared/issuelist" dict "." . "listType" "milestone"}} +
      +
      + +{{template "base/footer" .}} diff --git a/templates/repo/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl new file mode 100644 index 00000000..0015fc94 --- /dev/null +++ b/templates/repo/issue/milestone_new.tmpl @@ -0,0 +1,58 @@ +{{/* Forgejo 17's milestone form inside the issue sidebar layout, where GitHub shows it; Milestones in the sidebar +replaces the Labels and Milestones switch. */}} +{{template "base/head" .}} +
      + {{template "repo/issue/sidebar" .}} +
      +

      + {{if .PageIsEditMilestone}} + {{ctx.Locale.Tr "repo.milestones.edit"}} +
      {{ctx.Locale.Tr "repo.milestones.edit_subheader"}}
      + {{else}} + {{ctx.Locale.Tr "repo.milestones.new"}} +
      {{ctx.Locale.Tr "repo.milestones.new_subheader"}}
      + {{end}} +

      + {{template "base/alert" .}} +
      +
      + + +
      +
      + + +
      +
      + {{template "shared/combomarkdowneditor" (dict + "MarkdownPreviewUrl" (print .Repository.Link "/markup") + "MarkdownPreviewContext" .RepoLink + "TextareaName" "content" + "TextareaPlaceholder" (ctx.Locale.Tr "repo.milestones.desc") + "TextareaAriaLabel" (ctx.Locale.Tr "repo.milestones.desc") + "TextareaContent" .content + "EasyMDE" true + )}} +
      +
      +
      + {{if .PageIsEditMilestone}} + + {{ctx.Locale.Tr "repo.milestones.cancel"}} + + + {{else}} + + {{end}} +
      +
      +
      +
      +{{template "base/footer" .}} diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl new file mode 100644 index 00000000..4ad9a095 --- /dev/null +++ b/templates/repo/issue/milestones.tmpl @@ -0,0 +1,37 @@ +{{/* GitHub lists a repository's milestones inside the issue sidebar layout: a heading with New milestone, the search, +then the milestone box. */}} +{{template "base/head" .}} +{{$canWrite := and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} +
      + {{template "repo/issue/sidebar" .}} +
      + {{template "base/alert" .}} +
      +
      +

      {{ctx.Locale.Tr "repo.milestones"}}

      + {{if $canWrite}}{{ctx.Locale.Tr "repo.milestones.new"}}{{end}} +
      +
      + +
      + {{template "components/repository/milestone/list" dict "Data" . "Query" (print "?q=" (QueryEscape .Keyword)) "OpenCount" .OpenCount "ClosedCount" .ClosedCount "RepoLink" .RepoLink "CanWrite" $canWrite}} +
      +
      +
      +{{if $canWrite}} + +{{end}} +{{template "base/footer" .}} diff --git a/templates/repo/issue/sidebar.tmpl b/templates/repo/issue/sidebar.tmpl index c712497a..e167db6a 100644 --- a/templates/repo/issue/sidebar.tmpl +++ b/templates/repo/issue/sidebar.tmpl @@ -1,15 +1,18 @@ {{/* The issue list and the pull request list share this sidebar; only the first entry and "Review requested" differ. */}} {{$list := "issues"}}{{if .PageIsPullList}}{{$list = "pulls"}}{{end}} +{{/* Labels, milestones (the list, one milestone and its form) and projects are pages of their own, not a filter of the list. */}} +{{$isMilestonePage := or .PageIsMilestones .Milestone}} +{{$isListPage := not (or .IsProjectsPage $isMilestonePage .PageIsLabels)}}