Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion components/common/button/button.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,51 @@
}

.button.theme-control-small {
gap: 4px;
height: 28px;
min-height: 28px;
padding: 0 8px;
font-size: 12px;
line-height: 18px;
}

/* A main action and its menu joined into one shape, like GitHub's Fork button. */
.theme-control-split {
display: inline-flex;
align-items: stretch;
}
.theme-control-split > .theme-control {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.theme-control-split > .theme-dropdown > summary.theme-control {
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* An icon-only small menu trigger is a 28px square, like GitHub's "…" and split carets. */
.theme-dropdown > summary.theme-control-small:not(:has(> span)) {
width: 28px;
padding: 0;
}

.theme-control:hover {
background: var(--color-hover);
}

/* GitHub draws a default button's icons muted; a filled star keeps its own colour. */
.theme-control:not(.primary) > svg:not(.octicon-star-fill) {
color: var(--color-text-light-1);
}

/* Forgejo sizes buttons in dropdown summaries through --button-min-height on more specific rules. */
details > summary.button.theme-control {
min-height: 32px;
}
details > summary.button.theme-control-small {
min-height: 28px;
}

.theme-control:focus-visible {
outline: 2px solid var(--github-fgColor-accent);
outline-offset: 2px;
Expand Down Expand Up @@ -87,7 +121,9 @@
.comment-code-cloud .field.footer .ui.ui.ui.button,
.comment-code-cloud :is(.button-row, .button-sequence):has(.comment-form-reply) .ui.ui.ui.button,
.page-content.repository.new.issue.github-issue-new .issue-content-left .text.right .ui.ui.ui.button,
.page-content.repository.new.release .ui.form .ui.ui.ui.button:not(.mini)
.page-content.repository.new.release .ui.form .ui.ui.ui.button:not(.mini),
.repo-button-row .ui.ui.ui.button.repo-add-file,
.js-branch-tag-selector .ui.ui.ui.button.branch-dropdown-button
) {
height: 32px;
min-height: 32px;
Expand All @@ -96,3 +132,22 @@
font-weight: 500;
line-height: 20px;
}

/* The branch selector is a Forgejo Vue component: GitHub sets its branch name in the button's weight and uses a
16px caret. */
/* Its spacing comes from Forgejo's Tailwind utilities, which are !important. */
.js-branch-tag-selector .ui.ui.ui.ui.button.branch-dropdown-button {
padding: 5px 12px !important;
}
.js-branch-tag-selector .branch-dropdown-button > .text {
gap: 8px;
margin-right: 0 !important;
}
.js-branch-tag-selector .branch-dropdown-button strong {
margin-left: 0 !important;
font-weight: inherit;
}
.js-branch-tag-selector .branch-dropdown-button > svg:last-child {
width: 16px;
height: 16px;
}
4 changes: 2 additions & 2 deletions components/common/dropdown/dropdown.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{{/* Kind: single (radio), multiple (checkbox), or actions (links/buttons). Items owns page-specific content. */}}
{{/* Kind: single (radio), multiple (checkbox), or actions (links/buttons). Items owns page-specific content. Small uses the 28px control. */}}
<details class="theme-dropdown{{if .Quiet}} theme-dropdown-quiet{{end}}" data-theme-dropdown="{{.Kind}}">
<summary class="theme-control"{{if .IconOnly}} aria-label="{{.Label}}" data-tooltip-content="{{.Label}}"{{end}}>
<summary class="theme-control{{if .Small}} button theme-control-small{{end}}"{{if .IconOnly}} aria-label="{{.Label}}" data-tooltip-content="{{.Label}}"{{end}}>
{{if .Icon}}{{svg .Icon 16}}{{end}}
{{if not .IconOnly}}<span{{if .UpdateLabel}} data-theme-dropdown-label{{end}}>{{.Label}}</span>{{svg "octicon-triangle-down" 12}}{{end}}
</summary>
Expand Down
22 changes: 22 additions & 0 deletions components/common/search/search.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@
display: none;
}

/* A shortcut hint, like the "/" in GitHub's header search; it gives way to the clear button once there is text. */
.theme-search-field > .theme-search-shortcut {
flex: none;
display: inline-grid;
place-items: center;
box-sizing: border-box;
min-width: 16px;
height: 16px;
margin-right: 8px;
padding: 0 3px;
border: 1px solid var(--color-text-light-1);
border-radius: 3px;
box-shadow: none;
background: transparent;
color: var(--color-text-light-1);
font: 12px/1 var(--fonts-monospace);
}

.theme-search-field:not(:has(> input:placeholder-shown)) > .theme-search-shortcut {
display: none;
}

/* Syntax fields: a mirror behind the transparent input text colours qualifier values (author:<value>). */
.theme-search-syntax-control {
position: relative;
Expand Down
10 changes: 10 additions & 0 deletions components/common/search/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
if (applied && input.form) input.form.requestSubmit();
});

// A field with a shortcut takes focus when its key is pressed outside another text field.
document.addEventListener("keydown", event => {
if (event.ctrlKey || event.metaKey || event.altKey || event.isComposing) return;
if (event.target.closest("input, textarea, select, [contenteditable]")) return;
const input = document.querySelector(`input[data-search-shortcut="${CSS.escape(event.key)}"]`);
if (!input || !input.checkVisibility()) return;
event.preventDefault();
input.focus();
});

// Qualifiers Forgejo's issue search understands; their values are highlighted like GitHub's.
const qualifiers = new Set(["is", "author", "assignee", "review", "mentions", "sort", "modified"]);

Expand Down
5 changes: 3 additions & 2 deletions components/common/search/search.tmpl
Original file line number Diff line number Diff line change
@@ -1,10 +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. Syntax adds GitHub's qualifier popup and value highlighting (issue and pull request searches). */}}
{{/* 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). Shortcut shows a key that focuses the field from anywhere on the page. */}}
{{$icon := eq .Kind "icon"}}
{{if .Syntax}}<div class="theme-search-syntax-control">{{end}}
{{if $icon}}<label class="theme-search-field theme-search-field-icon{{if .Class}} {{.Class}}{{end}}">{{svg "octicon-search" 16}}{{else}}<div class="theme-search-field{{if .Class}} {{.Class}}{{end}}">{{end}}
{{if .Syntax}}<span class="theme-search-input-wrap"><span class="theme-search-highlight" aria-hidden="true"></span>{{end}}
<input type="search"{{if .ID}} id="{{.ID}}"{{end}}{{if .Local}} autocomplete="off"{{else}} name="{{or .Name "q"}}" value="{{.Value}}" maxlength="255"{{end}}{{if .Syntax}} autocomplete="off" data-search-syntax role="combobox" aria-autocomplete="list" aria-expanded="false"{{end}} placeholder="{{.Placeholder}}" aria-label="{{or .Label .Placeholder}}" spellcheck="false" dir="auto">
<input type="search"{{if .ID}} id="{{.ID}}"{{end}}{{if .Local}} autocomplete="off"{{else}} name="{{or .Name "q"}}" value="{{.Value}}" maxlength="255"{{end}}{{if .Syntax}} autocomplete="off" data-search-syntax role="combobox" aria-autocomplete="list" aria-expanded="false"{{end}} placeholder="{{.Placeholder}}" aria-label="{{or .Label .Placeholder}}"{{if .Shortcut}} data-search-shortcut="{{.Shortcut}}" aria-keyshortcuts="{{.Shortcut}}"{{end}} spellcheck="false" dir="auto">
{{if .Syntax}}</span>{{end}}
{{if .Shortcut}}<kbd class="theme-search-shortcut" aria-hidden="true">{{.Shortcut}}</kbd>{{end}}
<button type="button" class="theme-search-clear" aria-label="Clear search" tabindex="-1">{{svg "octicon-x-circle-fill" 16}}</button>
{{if not (or $icon (eq .Kind "plain"))}}<button type="submit" class="theme-search-submit" aria-label="{{ctx.Locale.Tr "search.search"}}">{{svg "octicon-search" 16}}</button>{{end}}
{{if $icon}}</label>{{else}}</div>{{end}}
Expand Down
16 changes: 9 additions & 7 deletions public/assets/js/navbar-drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@
extraNodes.push(node);
}

// The drawer opens on every width, like GitHub's. Custom navbar links only move into it on mobile, where the
// navbar hides them; on desktop they stay in the navbar.
let movedExtras = false;
toggle.addEventListener("click", () => {
if (!mobile.matches || drawer.open) return;
if (extraSlot && extraEnd) {
if (drawer.open) return;
movedExtras = mobile.matches && Boolean(extraSlot && extraEnd);
if (movedExtras) {
extraSlot.hidden = !extraNodes.some(node => node.nodeType === 1 || node.textContent.trim());
extraSlot.append(...extraNodes);
}
Expand All @@ -41,11 +45,12 @@
}
});
drawer.addEventListener("close", () => {
extraEnd?.before(...extraNodes);
if (movedExtras) extraEnd.before(...extraNodes);
movedExtras = false;
toggle.setAttribute("aria-expanded", "false");
document.documentElement.classList.remove("navbar-drawer-open");
drawer.querySelectorAll("details[open]").forEach(details => (details.open = false));
if (mobile.matches) toggle.focus();
toggle.focus();
});

// A modal dialog reports backdrop clicks on the dialog itself. Check both
Expand All @@ -64,9 +69,6 @@
if (backdropPress && event.target === drawer && outside(event)) drawer.close();
backdropPress = false;
});
mobile.addEventListener("change", () => {
if (!mobile.matches && drawer.open) drawer.close();
});
window.addEventListener("pagehide", () => {
if (drawer.open) drawer.close();
});
Expand Down
7 changes: 7 additions & 0 deletions public/navbar-drawer.css
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,10 @@ html.navbar-drawer-open {
animation: none;
}
}

/* On desktop the avatar and create menus stay in the header, as on GitHub, so the drawer only navigates. */
@media (min-width: 768px) {
.navbar-drawer .navbar-drawer-group:has(> .navbar-drawer-create, > .navbar-drawer-account) {
display: none;
}
}
16 changes: 12 additions & 4 deletions src/build-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,17 @@ describe("Forgejo 15 native integration", () => {
"utf-8"
);
const navbarTemplate = fs.readFileSync(path.join(ROOT_DIR, "templates", "base", "head_navbar.tmpl"), "utf-8");
const navbarStyle = fs.readFileSync(path.join(ROOT_DIR, "styles", "components", "navbar.ts"), "utf-8");

expect(header).toContain("height: 52px;");
expect(header).toContain("padding-top: 16px;");
expect(header).toContain("border-bottom: 0;");
expect(header).toContain(".navbar-left > #navbar-logo.item");
// GitHub: the menu button, 12px, a bare 32px logo, 8px, "owner / repo" with a repository icon and no avatar.
expect(navbarStyle).toContain("&#navbar-logo {");
expect(navbarStyle).toContain("min-width: 32px;");
expect(navbarTemplate.indexOf('id="navbar-drawer-toggle"')).toBeLessThan(
navbarTemplate.indexOf('id="navbar-logo"')
);
expect(header).toContain("height: 48px;");
expect(header).toContain("gap: 8px;");
expect(header).toContain("margin: 0 7px;");
Expand All @@ -401,8 +407,8 @@ describe("Forgejo 15 native integration", () => {
expect(header).toContain("line-height: 21px;");
expect(header).toContain("bottom: -8px;");
expect(secondaryMenu).toContain(".overflow-menu-items {\n gap: 8px;");
expect(navbarTemplate).toContain("{{ctx.AvatarUtils.Avatar .Owner 16}}");
expect(navbarTemplate).not.toContain('class="repository-navbar-owner-avatar"');
expect(navbarTemplate).not.toContain("{{ctx.AvatarUtils.Avatar .Owner 16}}");
expect(navbarTemplate).toContain("{{svg $repoIcon 16}}");
});

it("ships the mobile drawer assets and keeps its toggle independent of Forgejo's inline menu", () => {
Expand Down Expand Up @@ -615,7 +621,9 @@ describe("Forgejo 15 native integration", () => {
expect(style).toContain("@media (max-width: 767.98px)");
}

expect(headerStyle).toContain("padding-left: 8px;");
// GitHub keeps its 16px gutter on phones and drops the tab icons there.
expect(headerStyle).not.toContain("padding-left: 8px;");
expect(headerStyle).toContain(".overflow-menu-items > .item > svg {");
expect(headerStyle).toContain(".repo-buttons");
expect(viewContentStyle).toContain("> .repo-button-row-left");
expect(viewContentStyle).toContain("> .repo-button-row-right");
Expand Down
15 changes: 7 additions & 8 deletions src/navbar-drawer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function fixture() {
};
}

describe("mobile navigation drawer", () => {
describe("navigation drawer", () => {
it("opens once as a modal and restores focus, scroll, and disclosure state on close", () => {
const f = fixture();
f.toggle.emit("click");
Expand Down Expand Up @@ -132,16 +132,15 @@ describe("mobile navigation drawer", () => {
expect(preventDefault).toHaveBeenCalledTimes(2);
});

it("closes when crossing to desktop and does not reopen there", () => {
it("opens on desktop too, leaving the custom links in the navbar", () => {
const f = fixture();
f.toggle.emit("click");
f.media.matches = false;
f.media.emit("change");
f.toggle.emit("click");
expect(f.drawer.open).toBe(false);
expect(f.drawer.showModal).toHaveBeenCalledTimes(1);
expect(f.classes.has("navbar-drawer-open")).toBe(false);
expect(f.toggle.focus).not.toHaveBeenCalled();
expect(f.drawer.showModal).toHaveBeenCalledOnce();
expect(f.slot.append).not.toHaveBeenCalled();
f.closeButton.emit("click");
expect(f.end.before).not.toHaveBeenCalled();
expect(f.toggle.focus).toHaveBeenCalledOnce();
});

it("moves existing custom links into the drawer and restores the same nodes", () => {
Expand Down
50 changes: 48 additions & 2 deletions styles/components/navbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,44 @@ const navbarRight = css`
position: relative;
text-decoration: none;
}
/* GitHub: the menu button, 12px, the logo, 8px, the page's name. Header controls are transparent with a border. */
#navbar-drawer-toggle {
background: transparent;
&:hover {
background: ${themeVars.github.control.transparent.bgColor.hover};
}
}
/* GitHub's header search: a 197px field first on the right from 1012px, a search button below that. */
.navbar-search {
width: 197px;
}
.navbar-search-field {
padding-left: 12px;
background: transparent;
}
.navbar-search-button {
order: -1;
}
@media (min-width: 1012px) {
.navbar-search-button {
display: none !important;
}
}
@media (max-width: 1011.98px) {
.navbar-search {
display: none;
}
}
/* On mobile GitHub keeps only search and the inbox on the right; the drawer has the dashboard links. */
.navbar-mobile-right > .navbar-dashboard-link {
display: none !important;
}
/* GitHub drops the logo below 600px. */
@media (max-width: 599.98px) {
.navbar-left > #navbar-logo.item {
display: none;
}
}
.navbar-left {
gap: 8px;
> .item {
Expand All @@ -48,10 +86,18 @@ const navbarRight = css`
font-weight: 600;
}
&#navbar-logo {
/* 与下方的用户切换头像对齐 */
padding-left: 6px;
/* GitHub: a bare 32px mark 12px after the menu button. */
flex: none;
box-sizing: border-box;
width: 32px;
min-width: 32px;
height: 32px;
min-height: 32px;
margin-left: 4px;
padding: 0;
&:hover {
background: unset;
box-shadow: none;
}
img {
height: 32px;
Expand Down
2 changes: 2 additions & 0 deletions styles/pages/user-profile.css
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@
}

#navbar .profile-navbar-name {
/* Like the repository breadcrumb: the name starts 6px into its link. */
padding-inline: 6px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
Expand Down
Loading
Loading