Skip to content

Fix #276: explore search results sections, landing page centering and search reset - #335

Merged
taoeffect merged 5 commits into
masterfrom
fix/276-landing-search
Aug 25, 2026
Merged

Fix #276: explore search results sections, landing page centering and search reset#335
taoeffect merged 5 commits into
masterfrom
fix/276-landing-search

Conversation

@pieer

@pieer pieer commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Closes #276.

Fixes the three cases in the issue, plus a landing-page regression found while reproducing them.

1. The landing page was no longer vertically centered

The logo, heading and search bar were centered with height: 100% on the landing <main>, which only works when every ancestor has a definite height. #316 relaxed body from height: 100% to min-height: 100% so tall pages stop pushing the footer out of view (#242). With an indefinite body height, height: 100% resolved to auto, <main> shrank to its content, justify-content: center had no free space to distribute, and the block sat directly under the navbar.

Replaced the percentage height with flexbox, the same way #316 fixed the auth pages: .full.height already grows to fill the viewport via flex: 1 0 auto, so making it a flex column and giving the landing flex: 1 0 auto hands <main> a real height to center inside. flex-shrink: 0 keeps the old top-aligned behaviour on viewports too short for the content, so the page scrolls instead of clipping the logo.

Measured at 1456x836: the gap above and below the block went from 0px / 373px to 187px / 187px, with no viewport overflow.

2. Landing search went to the wrong tab (issue cases 1 and 2)

The landing form submitted to /explore/articles, while the search on /explore submits to /explore/subjects — the tab the navbar defaults to, and the one /explore itself redirects to. The same query produced a different page depending on where it was typed, and the landing one arrived on a page whose navbar shows no active tab and which renders no "Search results for ..." section.

Pointed the form at /explore/subjects and dropped three hidden inputs (only_show_relevant, sort=score, fork=0) that explore.Subjects never reads — sort=score isn't a valid subject sort and silently fell back to recentupdate. A landing search now produces exactly the ?q=... URL the explore subject search produces.

3. No "Search results for ..." / "Similar" on the users tab (issue case 3)

The users tab returned a bare list, or a bare "No matching results found.". It now splits results the way the subjects tab does.

  • models: added SearchUserOptions.ExactMatchOnly, mirroring repo.FindSubjectsOptions.ExactMatchOnly. It swaps the keyword's LIKE for Eq on both lower_name and LOWER(full_name), so user1 stops dragging in user10..user18. The email condition stays a substring match on purpose — an email is never the thing a visitor typed the name of.
  • routers: splitExactUserMatch runs the exact lookup as its own query rather than picking the match out of the current page. The list is paginated and ordered by name or sign-up date, not relevance, so the exactly-named user can sit on any page — someone searching "anastasia" should find her on the first one. Reusing the page's own options makes the lookup inherit every filter already applied (user type, active, visibility to the viewer, repo role), so it cannot surface a user the paginated list would have hidden. When the exact match also falls on the current page it is dropped from the "Similar" list; the pagination total is left alone so page boundaries stay stable.
  • templates: custom/templates/explore/user_list.tmpl overrides the stock list. Row markup moved to custom/templates/shared/user/explore_item.tmpl, taking a dict rather than a bare user because Go rebinds $ to whatever each {{template}} call passes.

The split is gated on PageIsExploreUsers. /explore/organizations shares not just the handler but the template (explore.Organizations calls RenderUserSearch with tplExploreUsers), so without the gate an org listing would answer a search with "No user named exactly ..." — caught in review, fixed, and pinned by TestExploreOrganizationsKeepsPlainList. templates/admin/user/view.tmpl shares the handler too but sets no search data, so it renders the plain list exactly as before.

All exact matches are rendered, not just the first: lower_name is unique but full_name is not, so a name two accounts share would otherwise promote one arbitrarily and file its twin under "Similar".

4. Border framed the headings, not just the rows

The box came from .explore-users-list-container, which wraps everything the list template emits, so the new headings ended up inside it. Moved the border, radius, overflow and background onto an .explore-users-rows wrapper that only ever holds user rows; the headings and the no-exact-match note are siblings of those wrappers. A search renders two boxes — exact match and similar list — each framed on its own.

flex-list had to move onto the same element as the rows: the divider rule is .flex-list > .flex-item + .flex-item, a direct-child selector.

5. Clearing the search box now resets the page

Emptying the keyword field left the results on screen next to an empty search box. Clicking the field's own clear button (the x that <input type="search"> renders) did nothing at all — it produces an input event indistinguishable from typing, and no change until the field is left. Clearing by keyboard and pressing Enter did reload the list, but submitted q= and landed on ?q=&sort=alphabetically.

Now reset on both gestures:

  • listen for search, which Blink and WebKit fire when the clear button or Escape empties the field (it bubbles, so one listener per form).
  • intercept submit while the field is empty — the path Enter and the search button take, and the only one Firefox has, since it renders no clear button and does not implement search.

Both only intervene while the field is empty, so a real keyword still submits natively, and both drop only q — sort and filters are separate controls and survive. Dropping an empty q moved into a shared helper, so the existing filter/sort handler stops emitting a stray q= too.

Deliberately not hooked: input turning empty. Retyping a query starts by deleting the old one (step 2 of the issue's own repro), and navigating away on the last backspace would make the field unusable. Typing still just updates the tab links, so the keyword carry-over from #273 is untouched.

Testing

Verified in a browser against a throwaway instance seeded from a dev database:

case result
landing search "forest" lands on /explore/subjects?q=forest, Subjects tab active, "Search results for forest" renders
users pierre exact pierre + Similar pierre_user
users PIERRE same — case insensitive
users Ada Lorne exact match on full name, no Similar section
users bubble no-exact-match note + 11 similar
users anastasia heading + "No matching results found." only
users, no keyword unchanged plain list
clear button / clear + Enter ?sort=..., no q, full list, heading gone (users and subjects)
real keyword still submits natively
type then clear without submitting no navigation; tab links gain then lose q
change sort still navigates, now without a stray empty q=

Also checked every rows box contains only .flex-item children, with headings as siblings outside.

TestSearchUsers gains four cases for ExactMatchOnly: username, full name, case folding, and a substring-only keyword matching nobody.

tests/integration/explore_user_test.go gains TestExploreUserSearchSplit (exact user promoted into its own box, "Similar" populated, the note for a substring-only keyword, no split without a keyword) and TestExploreOrganizationsKeepsPlainList (verified to fail without the users-tab gate). eslint, vue-tsc and stylelint are clean.

Note

The branch is based on a master that is 5 commits behind origin/master. It merges cleanly — the two overlapping files (models/user/search.go, custom/options/locale/locale_en-US.ini) received purely additive upstream changes in different regions, and ExactMatchOnly composes with the new ExcludeUserIDs / ExcludeOwnersOfSubjectID conditions as independent AND-ed clauses.

🤖 Generated with Claude Code

pieer and others added 4 commits August 24, 2026 18:14
…jects

Two regressions on the signed-out landing page.

1. The logo, heading and search bar were no longer vertically centered.

   They were centered with "height: 100%" on the landing <main>, which only
   works when every ancestor has a definite height. #316 relaxed "body" from
   "height: 100%" to "min-height: 100%" so that pages taller than the viewport
   stop pushing the footer out of view (#242). With an indefinite body height,
   "height: 100%" on <main> resolves to "auto", the element shrinks to its
   content, "justify-content: center" has no free space left to distribute, and
   the block ends up stuck directly under the navbar.

   Replace the percentage height with flexbox, the same way the auth pages were
   fixed in #316: ".full.height" already grows to fill the viewport via
   "flex: 1 0 auto", so making it a flex column and giving the landing
   "flex: 1 0 auto" hands <main> a real height to center inside. "flex-shrink: 0"
   keeps the old top-aligned behavior on viewports too short for the content, so
   the page scrolls instead of clipping the logo. The 64px "--page-space-bottom"
   that ".full.height" reserves is dropped here: with one centered block it is
   dead space that pulls the content 32px above the true center.

   Verified at 1456x836 against the running instance: the gap above and below
   the block is 187px each (was 0px / 373px) and the page still does not
   overflow the viewport.

2. The landing search submitted to "/explore/articles" while the search on
   /explore submits to "/explore/subjects" -- the tab the explore navbar
   defaults to, and the one "/explore" itself redirects to. So the same query
   produced a different page depending on where it was typed, and the landing
   one arrived on a page whose navbar shows no active tab and which renders no
   "Search results for ..." section (#276 cases 1 and 2).

   Point the form at "/explore/subjects" and drop the three hidden inputs
   ("only_show_relevant", "sort=score", "fork=0"): they are repo-search
   parameters that explore.Subjects does not read ("sort=score" is not a valid
   subject sort type and silently falls back to "recentupdate"). Removing them
   makes a landing search produce exactly the "?q=..." URL that the explore
   subject search produces.

Case 3 of the issue -- no "Search results for ..." / "Similar" sections on the
/explore/users tab -- is a separate gap in the users template and is not
addressed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Case 3 of the issue: searching on /explore/users returned a bare list (or a
bare "No matching results found."), while the subjects tab splits the same
search into the subject the keyword names and a "Similar" section below it.
The users tab now does the same, so a search reads the same way on both tabs.

models: add SearchUserOptions.ExactMatchOnly, mirroring
repo.FindSubjectsOptions.ExactMatchOnly. It swaps the keyword's LIKE for an Eq
on both lower_name and LOWER(full_name), so "user1" stops dragging in
user10..user18. The email condition is deliberately left as a substring match:
an email is never the thing a visitor typed the name of.

routers: splitExactUserMatch runs that exact lookup as its own query rather
than picking the match out of the current page. The list is paginated and
ordered by name or sign-up date, not relevance, so the user named exactly like
the keyword can sit on any page -- someone searching "anastasia" should find
her on the first one. Reusing the page's own options makes the lookup inherit
every filter already applied (user type, active, visibility to the viewer,
repo role), so it cannot surface a user the paginated list would have hidden.
When the exact match also falls on the current page it is dropped from the
"Similar" list so it is not rendered twice; the pagination total is left alone
so page boundaries stay stable while browsing.

templates: custom/templates/explore/user_list.tmpl overrides the stock list
with the three sections. The row markup moves to
custom/templates/shared/user/explore_item.tmpl so all sections render a user
identically. It takes a dict rather than a bare user because Go rebinds "$"
to whatever each {{template}} call passes, so a sub-template cannot reach the
page data the way the markup did while inlined.

When there is neither an exact match nor a similar user, only "No matching
results found." is shown -- no dangling "no user named exactly" note above it.
templates/admin/user/view.tmpl shares this template but sets no search data, so
HasSearchKeyword is false there and it renders the plain list exactly as before.

Verified against a throwaway instance seeded from the dev database:
  - "pierre"    -> exact "pierre" + Similar "pierre_user"
  - "PIERRE"    -> same (case insensitive)
  - "Ada Lorne" -> exact match on full name, no Similar section
  - "bubble"    -> no exact match note + 11 similar
  - "anastasia" -> heading + "No matching results found." only
  - no keyword  -> unchanged plain list
TestSearchUsers covers ExactMatchOnly on username, full name, case folding, and
a substring-only keyword matching nobody.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bordered box on /explore/users comes from ".explore-users-list-container",
which wraps everything the list template emits. Once the "Search results for ..."
heading, the "Similar" label and the no-exact-match note were added inside that
template, the border swallowed them too.

Move the box (border, radius, overflow, background) off the outer container and
onto a ".explore-users-rows" wrapper that only ever holds user rows. The headings
and the note become siblings of those wrappers, so they sit outside the frame. A
search now renders two boxes, the exact match and the similar list, each framed on
its own with the heading above it.

"flex-list" moves onto the same element as the rows rather than staying on an outer
wrapper: the row separator rule is ".flex-list > .flex-item + .flex-item"
(web_src/css/shared/flex-list.css:80), a direct-child selector, so the rows have to
be direct children of the element carrying the class or they lose their dividers.
The ".flex-item" padding rules are rescoped to ".explore-users-rows" for the same
reason -- ":first-child" / ":last-child" now resolve within each box.

The empty-search result keeps its border: "No matching results found." was framed
before this feature existed, and it stands in for the rows rather than labelling
them.

Verified on a throwaway instance that every box contains only ".flex-item"
children, with the headings as siblings outside:
  - "pierre"    -> heading, box(1 row), "Similar", box(1 row)
  - "bubble"    -> heading, note, "Similar", box(11 rows)
  - "anastasia" -> heading, box(1 row: no matching results)
  - no keyword  -> single box of 16 rows, unchanged

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Emptying the keyword field left the results on screen next to an empty search box.
Clicking the field's own clear button -- the "x" that <input type="search"> renders
-- did nothing at all: it produces an "input" event indistinguishable from typing,
and no "change" until the field is left, so no handler noticed. Clearing by keyboard
and pressing Enter did reload the list, but submitted "q=" and landed on
"?q=&sort=alphabetically": the right page behind a URL that says it is searching for
nothing.

Reset the page on both gestures:

- listen for "search", which Blink and WebKit fire when the clear button or Escape
  empties the field. It bubbles, so one listener per form is enough (verified).
- intercept "submit" while the field is empty. This is the path Enter and the search
  button take, and the only one Firefox has: it renders no clear button and does not
  implement the "search" event.

Both only intervene while the field is empty -- a real keyword still submits
natively -- and both drop only "q". Sort and filters are separate controls and
survive: clearing the search clears the search, not the way results are arranged.

Dropping an empty "q" moved into buildSearchParams, shared with the existing filter
and sort handler, so those navigations stop carrying a stray "q=" too.

Deliberately not hooked: "input" turning empty. Retyping a query starts by deleting
the old one (this is step 2 of the issue's own repro), and navigating away mid-edit
on the last backspace would make the field unusable. Only the explicit clear
gestures reset; typing still just updates the tab links, so the keyword carry-over
from #273 is untouched.

Verified on a throwaway instance, users and subjects tabs:
  - clear button      -> "?sort=..." , no "q", full list, search heading gone
  - clear + Enter     -> same
  - real keyword      -> still submits natively ("?q=bubble&sort=alphabetically")
  - type then clear without submitting -> no navigation; tab links gain then lose "q"
  - changing sort     -> still navigates, now without a stray empty "q="

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pieer

pieer commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

/review


AI review started.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

Advanced AI Review

  • Type: Agentic (crush)
  • Model: glm-5.3
Click to expand review

Review

I read the full files touched by the diff (routers/web/explore/user.go, models/user/search.go, both new templates, the CSS, explore-search.ts) plus every caller of RenderUserSearch, the subjects-tab equivalent in routers/web/explore/repo.go:239-318, and the base layout CSS. go vet passes on the changed packages. The change is solid overall; the issues below are ordered by importance.

1. 🟡 The split (and its user-specific wording) also renders on the /explore/organizations page, which the PR says it doesn't

  • Addressed
  • Dismissed

The PR comment in routers/web/explore/user.go:158-161 claims:

	// Split the results the way the subjects tab does: the user the keyword actually
	// names goes into its own "Search results for ..." section, everything else is
	// listed under "Similar" (#276). Only the explore user list renders these; the
	// admin pages share this handler but their templates ignore them.

But the organizations page shares not just the handler, the template: routers/web/explore/org.go:47-54 calls RenderUserSearch(..., tplExploreUsers), and templates/explore/users.tmpl:7 includes the overridden explore/user_list template. The route is still registered and enabled by default (routers/web/web.go:561, DisableOrganizationsPage defaults to false), only the navbar tab is commented out (custom/templates/explore/navbar.tmpl:14-18). So GET /explore/organizations?q=foo now renders the split, including:

{{ctx.Locale.Tr "explore.user.no_exact_match" .Keyword}}

which produces "No user named exactly 'foo'." on a page listing organizations — misleading wording, and an untested/unintended surface for the new feature. Either gate the split on the users tab, or make the wording entity-neutral:

// option A: only the users tab gets the split
ctx.Data["HasSearchKeyword"] = opts.Keyword != "" && ctx.Data["PageIsExploreUsers"] == true
; option B: neutral wording in custom/options/locale/locale_en-US.ini
user.no_exact_match = No account named exactly '%s'.

Option A is closer to what the PR description promises, and avoids the extra exact-match query on the orgs page.

2. ⚪️ Several users can share the same full name, but only one is promoted to the exact-match section

  • Addressed
  • Dismissed

routers/web/explore/user.go:58 caps the exact-match query at one row:

	exactOpts.ListOptions = db.ListOptions{Page: 1, PageSize: 1}

Usernames (lower_name) are unique, but full_name is not, so searching a name borne by two users arbitrarily promotes whichever comes first under the inherited sort order and lists the other(s) under "Similar". (The subjects tab doesn't have this problem; subject names are unique.) Fetching all exact matches and excluding all of them from "Similar" keeps the single exact-match display but stops twins from being mislabeled:

	exactOpts.ListOptions = db.ListOptions{Page: 1, PageSize: 50}
	exactUsers, _, err := user_model.SearchUsers(ctx, exactOpts)
	if err != nil {
		return nil, nil, err
	}
	if len(exactUsers) == 0 {
		return nil, users, nil
	}

	exactMatchIDs := container.SetOf[int64]()
	for _, u := range exactUsers {
		exactMatchIDs.Add(u.ID)
	}
	exactMatch := exactUsers[0]
	similar := make([]*user_model.User, 0, len(users))
	for _, u := range users {
		if !exactMatchIDs.Contains(u.ID) {
			similar = append(similar, u)
		}
	}
	return exactMatch, similar, nil

(container is already imported in this file.)

3. ⚪️ DRY: the Eq/Like branches in toSearchQueryBase duplicate the whole builder.Or

  • Addressed
  • Dismissed

models/user/search.go:96-107 repeats the identical two-column Or with only the comparison kind differing:

		var keywordCond builder.Cond
		if opts.ExactMatchOnly {
			keywordCond = builder.Or(
				builder.Eq{"lower_name": lowerKeyword},
				builder.Eq{"LOWER(full_name)": lowerKeyword},
			)
		} else {
			keywordCond = builder.Or(
				builder.Like{"lower_name", lowerKeyword},
				builder.Like{"LOWER(full_name)": lowerKeyword},
			)
		}

A small helper removes the duplication and makes the two columns impossible to drift apart:

		matchName := func(col string) builder.Cond {
			if opts.ExactMatchOnly {
				return builder.Eq{col: lowerKeyword}
			}
			return builder.Like{col, lowerKeyword}
		}
		var keywordCond builder.Cond = builder.Or(
			matchName("lower_name"),
			matchName("LOWER(full_name)"),
		)

4. ⚪️ CSS: the margin: 0 rule now targets the exact same element as the border rule and can be folded in

  • Addressed
  • Dismissed

Since flex-list and explore-users-rows are now on the same element (by design, per the template comment), custom/public/assets/css/explore-users.custom.css:35-37 is redundant as a separate rule:

.page-content.explore.users .explore-users-list-container .flex-list {
  margin: 0;
}

Merge it into the box rule at custom/public/assets/css/explore-users.custom.css:28-33:

.page-content.explore.users .explore-users-list-container .explore-users-rows {
  border: 1px solid var(--color-secondary);
  border-radius: 8px;
  overflow: hidden;
  background: var(--color-body);
  margin: 0;
}

5. ⚪️ Locale: user.similar is a duplicate of subject.similar

  • Addressed
  • Dismissed

custom/options/locale/locale_en-US.ini:440-441 adds a key whose value is identical to the existing one:

subject.similar = Similar
user.similar = Similar

A single shared key avoids two strings to keep in sync for translators:

similar = Similar

with both templates using {{ctx.Locale.Tr "explore.similar"}}. (Keeping them separate is defensible if you expect the wording to diverge, but at present they are the same sentence.)

6. ⚪️ Template: nested {{if .Keyword}} inside {{if .HasSearchKeyword}} is dead logic

  • Addressed
  • Dismissed

custom/templates/explore/user_list.tmpl:17-22:

{{if .HasSearchKeyword}}
	{{if .Keyword}}
		<div class="tw-font-bold tw-text-14 tw-mb-4">

HasSearchKeyword is set to opts.Keyword != "" in the only handlers that set it (routers/web/explore/user.go:167, routers/web/explore/repo.go:318), so the inner check can never be false. It was copied from custom/templates/shared/subject/list.tmpl:2-4, which has the same redundancy; dropping it in both keeps them consistent:

{{if .HasSearchKeyword}}
	<div class="tw-font-bold tw-text-14 tw-mb-4">
		{{ctx.Locale.Tr "explore.search_results_for" .Keyword}}
	</div>

7. ⚪️ No automated coverage for the rendered split, which is what issue #276 is about

  • Addressed
  • Dismissed

The new unit tests cover ExactMatchOnly at the model layer, but nothing pins the router/template behavior (exact user promoted, "Similar" section, no-exact-match note). tests/integration/explore_user_test.go is the natural home and already parses explore pages:

func TestExploreUserSearchSplit(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	// "user1" is the exact username; user10..user18 are only substrings
	req := NewRequest(t, "GET", "/explore/users?q=user1")
	resp := MakeRequest(t, req, http.StatusOK)
	h := NewHTMLParser(t, resp.Body)
	assert.Equal(t, "Search results for user1", h.Find(".explore-users-list-container .tw-font-bold").Text())
	// the exact match box holds exactly one row, the "Similar" box the rest
	assert.Len(t, h.Find(".explore-users-rows").Eq(0).Find(".flex-item").Nodes, 1)
	assert.True(t, h.Find(".explore-users-rows").Eq(1).Find(".flex-item").Length() > 0)

	// a keyword matching nobody exactly gets the note instead
	req = NewRequest(t, "GET", "/explore/users?q=ser1")
	resp = MakeRequest(t, req, http.StatusOK)
	h = NewHTMLParser(t, resp.Body)
	assert.Contains(t, resp.Body.String(), "No user named exactly &#39;ser1&#39;.")
}

Everything else I checked holds up: the exact-match query inherits the visibility/type/role conditions of the page query (no leak of users the list would hide), the SQL stays parameterized, the keyword is auto-escaped in the new strings, admin/user/view.tmpl degrades to the plain list as claimed, and the JS submit/search interception only fires on an empty field so native submits and the #273 tab-link carry-over are untouched.


Review generated using glm-5.3 via Z.AI. Comment /review to re-run.

The organizations page does not just share the handler, it shares the template:
explore.Organizations calls RenderUserSearch with tplExploreUsers, and
templates/explore/users.tmpl includes the overridden explore/user_list. The route
is registered and enabled by default; only the navbar tab is commented out. So
"GET /explore/organizations?q=acme" answered a search with "No user named exactly
'acme'." on a listing of organizations. Reproduced against a throwaway instance
with two seeded orgs. The claim to the contrary in the code comment, the template
comment and the PR description was simply wrong.

Gate the split on PageIsExploreUsers, so the organizations page falls back to the
plain list it rendered before and does not pay for the extra exact-match query
either. TestExploreOrganizationsKeepsPlainList pins it, and fails without the gate.

Also from the review:

- Return every exact match, not just the first. "lower_name" is unique but
  "full_name" is not, so a name two accounts share used to promote one of them
  arbitrarily and file its twin under "Similar" as though it were a near miss.
  Note this is not the fix the review proposed: excluding all exact matches from
  "Similar" while still rendering only exactMatches[0] would have dropped the twin
  from the page entirely. Rendering all of them keeps every result visible.
  Bounded at 50 by maxExactUserMatches.
- Derive both name columns from one matchName helper in toSearchQueryBase, so the
  exact and fuzzy variants cannot drift apart.
- Drop the "{{if .Keyword}}" nested inside "{{if .HasSearchKeyword}}": the latter
  is set to "Keyword != \"\"", so the inner test could never be false.
- Fold the ".flex-list { margin: 0 }" reset into the rows-box rule, now that both
  classes are on the same element by design.
- Cover the rendered split in tests/integration/explore_user_test.go: exact user
  promoted into its own box, "Similar" populated, the note for a substring-only
  keyword, and no split without a keyword.

Not taken: merging "user.similar" into a shared "explore.similar". Per-context
locale keys are better for translators, not worse -- "Similar" can need different
agreement or gender for users and for subjects -- and collapsing them locks both
to one string.

The integration tests assert on the parsed document rather than resp.Body.String():
NewHTMLParser drains the buffer, so body assertions made after it read "" and pass
no matter what. Two of them did exactly that before this was caught.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pieer

pieer commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Approved

@pieer pieer self-assigned this Aug 25, 2026
@taoeffect
taoeffect merged commit 01ce5e0 into master Aug 25, 2026
33 checks passed
@taoeffect
taoeffect deleted the fix/276-landing-search branch August 25, 2026 15:18
pieer added a commit that referenced this pull request Aug 25, 2026
PR #335 (#276: explore search sections, landing centering, search reset) landed on
master and touches the same two landing-page files as this branch. Both conflicts
are additive; both sides are kept.

custom/templates/home.tmpl -- most of the file merged on its own: the "home-landing"
class on <main> and the removal of the three dead hidden inputs came from master, the
combobox input id and aria attributes and the suggestions container from this branch.
The one conflicting hunk was the wrapper and the opening form tag:

- wrapper: took "home-search" over master's "dropdown-anchor". "home-search" carries
  "position: relative", which is what anchors the absolutely positioned dropdown;
  "dropdown-anchor" has no CSS and no JS anywhere and was dead markup.
- form: kept this branch's "ignore-dirty" (and the comment explaining it), and took
  master's action of "/explore/subjects" over "/explore/articles". That change is the
  point of #276 cases 1 and 2, and it is what the suggestions already assume:
  home-search.ts queries /explore/subjects/suggestions and each suggestion links to
  /subject/..., so submitting the form now lands on the same kind of page.

web_src/css/home.css -- both sides appended a block after ".home .logo" and shared the
next closing brace. Master's landing-centering block is kept first, then this branch's
search-suggestion styles.

Verified on the merged tree against a throwaway instance:
  - the landing block is still vertically centered (gap 430px above and below, no
    viewport overflow) and <main> still has flex-grow 1, so #276 survives
  - ".home-search" resolves to position: relative and the suggestions box to absolute
  - typing "sub" returns 4 suggestions, the box unhides, aria-expanded flips to true,
    and the first entry links to /subject/Test%20Subject
  - form action is /explore/subjects and still carries ignore-dirty
  - go build, webpack, eslint and stylelint clean; home-search vitest passes;
    TestExploreSubjectSuggestions, TestExploreSubjectsListMarkup,
    TestExploreUserSearchSplit and TestExploreOrganizationsKeepsPlainList all pass
pieer added a commit that referenced this pull request Aug 25, 2026
PR #335 (#276) landed on master and touches the same two landing-page files. Both
conflicts are additive and both sides are kept.

custom/templates/home.tmpl -- the only conflicting hunk was the opening form tag.
Kept this branch's id="home-search-form" and "ignore-dirty", took master's action of
"/explore/subjects" over "/explore/articles": that redirect is the point of #276 cases
1 and 2, and nothing here depends on the action, since home-search.ts only guards the
submit. The rest merged on its own -- the "home-landing" class on <main> and the
removal of the three dead hidden inputs from master, the error element from here.

web_src/css/home.css -- both sides appended a block after ".home .logo" and shared the
next closing brace. Master's landing-centering block first, then the search-validation
styles.

Verified on the merged tree against a throwaway instance:
  - the landing block is still centered (187px above and below, no overflow), so #276
    survives
  - submitting blank stays on the landing page, shows "Please type a subject to
    search", sets aria-invalid and aria-describedby, and turns the border red
    (rgb(207, 34, 46))
  - typing clears the message again, and a real search submits through to
    /explore/subjects?q=forest
  - go build, webpack, eslint and stylelint clean; home-search vitest (3 tests),
    ./models/repo, TestExploreRepos, TestExploreReposBlankKeyword and
    TestExploreUserSearchSplit all pass
pieer added a commit that referenced this pull request Sep 2, 2026
The Subjects tab was reported as neither highlighted nor underlined in the
brand colour. Most of that report no longer applies. The tab-activation half
is already fixed on master: /explore/subjects sets PageIsExploreSubjects and
the custom navbar keys the Subjects tab on it. The reporter landed on
/explore/articles, which sets PageIsExploreRepositories, a flag the custom
navbar never tests, so no tab was active there at all; PR #335 retargeted the
landing form at /explore/subjects. The underline colour was already correct
too: menu.custom.css has carried `.ui.secondary.pointing.menu .active.item
{ border-color: var(--color-primary) }` for a while, and custom/header is
included after base/head_style, so it already beat upstream's `currentcolor`.

What was actually broken is the alignment. custom/templates/explore/navbar.tmpl
had no <div class="overflow-menu-items"> wrapper, which upstream's navbar has.
The <overflow-menu> web component's connectedCallback parks on a
MutationObserver waiting for that element, so init() never ran and the menu
never collapsed responsively or grew an overflow button. It also meant
base.css's `overflow-menu .overflow-menu-items .item { margin-bottom: 0
!important }` never applied, so the tabs kept fomantic's `margin: 0 0 -2px`
while <overflow-menu>'s own `border-bottom: 1px solid` won over the menu's 2px
rule — the active tab's underline sat a pixel below the rail, which is what the
screenshot shows.

Restoring the wrapper makes the component initialise for the first time, and
that exposes a second, previously unreachable colour bug: once a tab can
collapse into the kebab button, base.css draws that button's active underline
with `border-bottom: 2px solid currentcolor` — the near-black this issue is
about, now at narrow viewports. menu.custom.css brands it to match the tab it
stands in for, using base.css's own selector so the specificities match and
mere sheet order decides it.

TestExploreNavbarSubjectsTabActive covers the markup: it asserts exactly one
tab is active inside .overflow-menu-items on /explore/subjects and
/explore/users, that it is the tab matching the page, and that the sibling tabs
still render unmarked. The colours are CSS only and are not asserted.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pieer added a commit that referenced this pull request Sep 2, 2026
On the explore tabs the sort radios and the filter radios are rendered
inside the same form as the keyword field, checked to match the results
on screen. Typing a new keyword and pressing Enter submitted them along
with it, so a brand new set of results came back arranged the way the
previous one had been left, with the previously picked button still
active.

The submit listener now compares the typed keyword with the one the page
was rendered for (the field's defaultValue, so no new markup is needed).
A changed keyword takes over the submit and navigates with the keyword
alone, dropping the sort, the filters and the page. Re-submitting the
same keyword still submits natively: there the form is the only record
of a sort or a filter just picked, and dropping it would undo that
choice. Clearing the field is unchanged -- clearing the search clears
the search, not the way the results are arranged (#335).

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pieer added a commit that referenced this pull request Sep 2, 2026
The Subjects tab was reported as neither highlighted nor underlined in the
brand colour. Most of that report no longer applies. The tab-activation half
is already fixed on master: /explore/subjects sets PageIsExploreSubjects and
the custom navbar keys the Subjects tab on it. The reporter landed on
/explore/articles, which sets PageIsExploreRepositories, a flag the custom
navbar never tests, so no tab was active there at all; PR #335 retargeted the
landing form at /explore/subjects. The underline colour was already correct
too: menu.custom.css has carried `.ui.secondary.pointing.menu .active.item
{ border-color: var(--color-primary) }` for a while, and custom/header is
included after base/head_style, so it already beat upstream's `currentcolor`.

What was actually broken is the alignment. custom/templates/explore/navbar.tmpl
had no <div class="overflow-menu-items"> wrapper, which upstream's navbar has.
The <overflow-menu> web component's connectedCallback parks on a
MutationObserver waiting for that element, so init() never ran and the menu
never collapsed responsively or grew an overflow button. It also meant
base.css's `overflow-menu .overflow-menu-items .item { margin-bottom: 0
!important }` never applied, so the tabs kept fomantic's `margin: 0 0 -2px`
while <overflow-menu>'s own `border-bottom: 1px solid` won over the menu's 2px
rule — the active tab's underline sat a pixel below the rail, which is what the
screenshot shows.

Restoring the wrapper makes the component initialise for the first time, and
that exposes a second, previously unreachable colour bug: once a tab can
collapse into the kebab button, base.css draws that button's active underline
with `border-bottom: 2px solid currentcolor` — the near-black this issue is
about, now at narrow viewports. menu.custom.css brands it to match the tab it
stands in for, using base.css's own selector so the specificities match and
mere sheet order decides it.

TestExploreNavbarSubjectsTabActive covers the markup: it asserts exactly one
tab is active inside .overflow-menu-items on /explore/subjects and
/explore/users, that it is the tab matching the page, and that the sibling tabs
still render unmarked. The colours are CSS only and are not asserted.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pieer added a commit that referenced this pull request Sep 3, 2026
On the explore tabs the sort radios and the filter radios are rendered
inside the same form as the keyword field, checked to match the results
on screen. Typing a new keyword and pressing Enter submitted them along
with it, so a brand new set of results came back arranged the way the
previous one had been left, with the previously picked button still
active.

The submit listener now compares the typed keyword with the one the page
was rendered for (the field's defaultValue, so no new markup is needed).
A changed keyword takes over the submit and navigates with the keyword
alone, dropping the sort, the filters and the page. Re-submitting the
same keyword still submits natively: there the form is the only record
of a sort or a filter just picked, and dropping it would undo that
choice. Clearing the field is unchanged -- clearing the search clears
the search, not the way the results are arranged (#335).

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pieer added a commit that referenced this pull request Sep 3, 2026
The Subjects tab was reported as neither highlighted nor underlined in the
brand colour. Most of that report no longer applies. The tab-activation half
is already fixed on master: /explore/subjects sets PageIsExploreSubjects and
the custom navbar keys the Subjects tab on it. The reporter landed on
/explore/articles, which sets PageIsExploreRepositories, a flag the custom
navbar never tests, so no tab was active there at all; PR #335 retargeted the
landing form at /explore/subjects. The underline colour was already correct
too: menu.custom.css has carried `.ui.secondary.pointing.menu .active.item
{ border-color: var(--color-primary) }` for a while, and custom/header is
included after base/head_style, so it already beat upstream's `currentcolor`.

What was actually broken is the alignment. custom/templates/explore/navbar.tmpl
had no <div class="overflow-menu-items"> wrapper, which upstream's navbar has.
The <overflow-menu> web component's connectedCallback parks on a
MutationObserver waiting for that element, so init() never ran and the menu
never collapsed responsively or grew an overflow button. It also meant
base.css's `overflow-menu .overflow-menu-items .item { margin-bottom: 0
!important }` never applied, so the tabs kept fomantic's `margin: 0 0 -2px`
while <overflow-menu>'s own `border-bottom: 1px solid` won over the menu's 2px
rule — the active tab's underline sat a pixel below the rail, which is what the
screenshot shows.

Restoring the wrapper makes the component initialise for the first time, and
that exposes a second, previously unreachable colour bug: once a tab can
collapse into the kebab button, base.css draws that button's active underline
with `border-bottom: 2px solid currentcolor` — the near-black this issue is
about, now at narrow viewports. menu.custom.css brands it to match the tab it
stands in for, using base.css's own selector so the specificities match and
mere sheet order decides it.

TestExploreNavbarSubjectsTabActive covers the markup: it asserts exactly one
tab is active inside .overflow-menu-items on /explore/subjects and
/explore/users, that it is the tab matching the page, and that the sibling tabs
still render unmarked. The colours are CSS only and are not asserted.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Explore page/Landing page/Users; Landing page/Subjects: add the Search results for... and Similar section after making a search request

2 participants