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
11 changes: 8 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,14 @@ controls.
`shared/theme/sidebar_toggle` inside the sidebar and a button with `data-theme-sidebar-toggle` and
`data-theme-sidebar-expand` beside the page title. The controller preserves desktop collapse preferences and starts
collapsed on mobile. See `templates/repo/issue/sidebar.tmpl` and `templates/org/repository_nav.tmpl`.
- **Search:** call `components/common/search/search` with `Value` and `Placeholder` inside the existing form. Keep
hidden filter inputs in that form. The common search field should be a direct child of the form or have a parent that
fills the available width; an unconstrained wrapper can shrink it.
- **Search:** call `components/common/search/search` with `Value` and `Placeholder` inside the existing form; do not
write a search input in a page template. `Kind` selects the variant: `submit` (default, trailing search button),
`icon` (leading search icon, as on releases and Actions), or `plain`. `Name` defaults to `q`, `Label` overrides the
accessible name, and `Class` adds a page layout hook to the field. For a client-side filter that is never submitted,
pass `Local` with an `ID` for its controller. Keep hidden filter inputs in the form and size the field from the page
stylesheet through its `Class` or parent; an unconstrained wrapper can shrink it. A control built by a script uses the
`theme-search-field` class directly. Forgejo's native query/mode/submit group (`shared/search/combo_multi`) opts in
with `theme-search-control` on its form.
- **Dropdowns:** call `components/common/dropdown/dropdown` with `Kind` (`single`, `multiple`, or `actions`), `Label`,
`Items` (the content template name), and `Data` (the page context). `Title`, `Icon`, `IconOnly`, `UpdateLabel`, and
`Quiet` customize the header and trigger. Use `components/common/dropdown/option` for radio or checkbox rows; action
Expand Down
117 changes: 116 additions & 1 deletion components/common/search/search.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
height: 32px;
width: 100%;
min-width: 0;
margin: 0;
border: 1px solid var(--color-light-border);
border-radius: var(--border-radius);
background: var(--color-body);
Expand All @@ -15,10 +16,12 @@
outline-offset: -1px;
}

.ui.form .theme-search-field > input[type="search"] {
/* :is() keeps one specificity for fields inside and outside Forgejo's .ui.form. */
:is(.ui.form, body) .theme-search-field > input[type="search"] {
flex: 1;
width: 100%;
min-width: 0;
height: 30px;
margin: 0;
padding: 5px 12px;
border: 0;
Expand All @@ -30,6 +33,27 @@
outline: none;
}

:is(.ui.form, body) .theme-search-field > input[type="search"]::placeholder {
color: var(--color-text-light-1);
opacity: 1;
}

.theme-search-field-icon {
align-items: center;
padding-left: 7px;
color: var(--color-text-light-1);
cursor: text;
}

.theme-search-field-icon > svg {
flex: none;
margin-right: 9px;
}

:is(.ui.form, body) .theme-search-field-icon > input[type="search"] {
padding-left: 0;
}

.theme-search-field > .theme-search-submit {
flex: none;
width: 32px;
Expand All @@ -47,3 +71,94 @@
.theme-search-submit:hover {
background: var(--color-hover);
}

/* 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;
align-items: stretch;
height: 32px;
min-width: 0;
border: 1px solid var(--color-light-border);
border-radius: var(--border-radius);
background: var(--color-body);
box-shadow: none;
&:has(> input:focus-visible) {
outline: 2px solid var(--github-fgColor-accent);
outline-offset: -1px;
}
& > input[type="search"] {
min-width: 0;
width: 0;
flex: 1 1 0;
height: 30px;
padding: 5px 12px;
font-size: 14px;
line-height: 20px;
background: transparent;
border: 0;
border-radius: inherit;
box-shadow: none;
outline: none;
}
& > .ui.selection.dropdown,
& > .ui.button {
flex: 0 0 auto;
height: 30px;
min-height: 30px;
margin: 0;
border: 0;
border-left: 1px solid var(--color-light-border);
border-radius: 0;
background: transparent;
box-shadow: none;
color: var(--color-text-light-1);
&:hover {
background: var(--color-hover);
}
&:focus-visible {
outline: 2px solid var(--github-fgColor-accent);
outline-offset: -2px;
}
}
& > .ui.selection.dropdown {
display: flex;
align-items: center;
width: auto;
min-width: 88px;
padding: 5px 28px 5px 8px;
font-size: 12px;
line-height: 20px;
& > .text {
margin: 0;
line-height: 20px;
}
& > .dropdown.icon {
top: 50%;
transform: translateY(-50%);
right: 8px;
padding: 0;
margin: 0;
width: 16px;
height: 16px;
}
& > .menu {
left: auto;
right: 0;
width: 200px;
min-width: 200px;
margin-top: 4px;
}
}
& > .ui.button {
display: grid;
place-items: center;
width: 32px;
padding: 0 !important;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
& > svg {
width: 16px;
height: 16px;
margin: 0;
}
}
}
28 changes: 28 additions & 0 deletions components/common/search/search.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

const root = fileURLToPath(new URL("../../..", import.meta.url));
const component = "components/common/search/search.tmpl";

const templates = ["templates", "components"].flatMap(directory =>
readdirSync(join(root, directory), { recursive: true })
.map(file => `${directory}/${String(file)}`)
.filter(file => file.endsWith(".tmpl"))
);

describe("shared search field", () => {
it("is the only template that writes a search input", () => {
const own = templates.filter(file => /type="search"/.test(readFileSync(join(root, file), "utf8")));
expect(own).toEqual([component]);
});

it("renders every kind from one field", () => {
const source = readFileSync(join(root, component), "utf8");
expect(source.match(/<input /g)).toHaveLength(1);
expect(source).toContain('eq .Kind "icon"');
expect(source).toContain('eq .Kind "plain"');
expect(source).toContain("theme-search-submit");
});
});
10 changes: 6 additions & 4 deletions components/common/search/search.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<div class="theme-search-field">
<input type="search" name="q" value="{{.Value}}" placeholder="{{.Placeholder}}" aria-label="{{.Placeholder}}" maxlength="255" spellcheck="false" dir="auto">
<button type="submit" class="theme-search-submit" aria-label="{{ctx.Locale.Tr "search.search"}}">{{svg "octicon-search" 16}}</button>
</div>
{{/* 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. */}}
{{$icon := eq .Kind "icon"}}
{{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}}
<input type="search"{{if .ID}} id="{{.ID}}"{{end}}{{if .Local}} autocomplete="off"{{else}} name="{{or .Name "q"}}" value="{{.Value}}" maxlength="255"{{end}} placeholder="{{.Placeholder}}" aria-label="{{or .Label .Placeholder}}" spellcheck="false" dir="auto">
{{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}}
22 changes: 0 additions & 22 deletions public/actions-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,30 +117,8 @@
margin: 0;
}
.actions-search {
display: flex;
align-items: center;
gap: 8px;
width: 300px;
flex-shrink: 0;
height: 32px;
padding: 0 8px;
border: 1px solid var(--color-light-border);
border-radius: var(--border-radius);
color: var(--color-text-light-1);
}
.actions-search:focus-within {
outline: 2px solid var(--color-primary);
outline-offset: -1px;
}
.actions-search input {
background: transparent;
padding: 0;
border: 0;
box-shadow: none;
outline: none;
width: 100%;
min-width: 0;
font: 14px/21px var(--fonts-proportional);
}
.actions-filters.ui.menu {
display: flex;
Expand Down
12 changes: 2 additions & 10 deletions public/actions-run.css
Original file line number Diff line number Diff line change
Expand Up @@ -250,17 +250,9 @@
align-items: center;
gap: 8px;
}
.actions-log-search input {
.actions-log-search > .theme-search-field {
width: 256px;
max-width: 100%;
height: 32px;
padding: 5px 12px;
border: 1px solid var(--color-light-border);
border-radius: var(--border-radius);
background: var(--color-body);
color: var(--color-text);
font-size: 14px;
box-shadow: none;
}
.actions-log-search span {
font-size: 12px;
Expand Down Expand Up @@ -440,7 +432,7 @@
.actions-job-card {
width: 100%;
}
.actions-log-search input {
.actions-log-search > .theme-search-field {
width: 170px;
}
.actions-log-search span {
Expand Down
6 changes: 4 additions & 2 deletions public/assets/js/actions-run.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@
const tools = right.querySelector(".job-info-header-right");
if (tools && !tools.querySelector(".actions-log-search")) {
const search = make("div", "actions-log-search");
const field = make("div", "theme-search-field");
const input = make("input", "");
input.type = "search";
input.placeholder = "Search loaded logs";
input.setAttribute("aria-label", "Search loaded logs");
const result = make("span", "");
result.setAttribute("aria-live", "polite");
search.append(input, result);
field.append(input);
search.append(field, result);
tools.prepend(search);
}
filterLogs();
Expand Down Expand Up @@ -124,7 +126,7 @@
line.hidden = !!query && !line.textContent.toLowerCase().includes(query);
if (!line.hidden) matches++;
}
const result = input.nextElementSibling;
const result = input.closest(".actions-log-search").querySelector("[aria-live]");
const text = query ? `${matches} ${matches === 1 ? "match" : "matches"}` : "";
if (result.textContent !== text) result.textContent = text;
}
Expand Down
4 changes: 2 additions & 2 deletions styles/components/repo/diff_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default css`
align-items: center;
min-width: 0;
}
.diff-code-search > .diff-search-field {
.diff-code-search > .theme-search-field {
flex: 0 1 300px;
width: 300px;
height: 34px;
Expand Down Expand Up @@ -277,7 +277,7 @@ export default css`
outline-offset: -1px;
}
@media (max-width: 767.98px) {
.diff-code-search > .diff-search-field {
.diff-code-search > .theme-search-field {
width: min(264px, calc(100vw - 112px));
}
.repository #diff-file-boxes .diff-change-summary,
Expand Down
48 changes: 0 additions & 48 deletions styles/components/repo/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,30 +509,6 @@ const releaseRefinements = css`
width: 248px;
flex: 0 0 248px;
}
.github-release-search .ui.input {
position: relative;
width: 100%;
}
.github-release-search .ui.input input {
width: 100%;
min-width: 0;
height: 32px;
padding: 5px 12px 5px 32px;
border-radius: 6px !important;
border-right: 1px solid var(--color-light-border) !important;
background: transparent;
}
.github-release-search .ui.input button {
position: absolute;
left: 0;
top: 0;
width: 32px;
height: 32px;
padding: 0;
border: 0;
background: transparent;
color: var(--color-text-light-1);
}
.github-release-toc {
align-self: start;
position: sticky;
Expand Down Expand Up @@ -873,30 +849,6 @@ const githubTagPage = css`
.github-tags-search {
width: 248px;
}
.github-tags-search .ui.input {
position: relative;
width: 100%;
}
.github-tags-search .ui.input input {
width: 100%;
min-width: 0;
background: transparent;
height: 32px;
padding: 5px 12px 5px 32px;
border-radius: 6px !important;
border-right: 1px solid var(--color-light-border) !important;
}
.github-tags-search .ui.input button {
position: absolute;
left: 0;
top: 0;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: 0;
color: var(--color-text-light-1);
}
.github-tags-box {
border: 1px solid var(--color-light-border);
border-radius: 6px;
Expand Down
10 changes: 5 additions & 5 deletions styles/pages/profile-subpages.css
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,10 @@
gap: 8px;
}

:is(.github-profile, .github-org) .profile-search-controls input[type="search"],
:is(.github-profile, .github-org) .profile-project-search input[type="search"],
:is(.github-profile, .github-org) .profile-search-controls > .theme-search-field {
flex: 1;
}

:is(.github-profile, .github-org) .profile-code input[type="search"] {
flex: 1;
min-width: 0;
Expand Down Expand Up @@ -525,7 +527,7 @@
flex-wrap: wrap;
}

:is(.github-profile, .github-org) .profile-search-controls input[type="search"] {
:is(.github-profile, .github-org) .profile-search-controls > .theme-search-field {
flex-basis: 100%;
}

Expand All @@ -547,8 +549,6 @@
}
}

:is(.github-profile, .github-org) .profile-search-controls input[type="search"]:focus,
:is(.github-profile, .github-org) .profile-project-search input[type="search"]:focus,
:is(.github-profile, .github-org) .profile-code input[type="search"]:focus {
border-color: var(--github-borderColor-accent-emphasis);
}
Expand Down
Loading
Loading