fix(webui): shared SearchInput/useSearchQuery, correct expand rationale

Address review of the user/group search filter (feat/user-search):

- Correct the "group name match does not auto-expand" rationale in
  UsageView.vue and lib/search-expand.ts. The prior comment claimed
  Overview has no provider-name search — false: OverviewView's
  providerMatches checks each model's full ROUTABLE id
  (`${provider}/${model}`, lib/format.ts's routableModelId), so a
  provider-name query does match and auto-expand there. The actual
  reason Usage withholds auto-expand from a name-only match: a group's
  name is already legible on its collapsed trigger row, so expanding
  every name match too would dump full member rosters for no reason;
  a member-only match has no such shortcut and still needs the reveal.
  Behavior is unchanged — only the (now accurate) justification.

- Extract components/SearchInput.vue and composables/useSearchQuery.ts.
  OverviewView's model search, UsageView's user/group search, and
  ChartsView's scope-picker search were three ~22-line copies of the
  same icon+Input+clear-button markup, plus three copies of the
  query/normalized/hasQuery/clear quartet. All three views now consume
  the shared component/composable. SearchInput's clear button is a real
  shadcn Button (variant="ghost", size="icon-xs" — 24x24px) instead of a
  hand-rolled <button>, fixing a WCAG 2.2 SC 2.5.8 hit-target/
  focus-visible gap once instead of three times.

- ChartsView's search input: w-48 (clipped the 22-char placeholder) to
  w-56, matching the sibling SelectTrigger's width.

- UsageView.vue's visibleMembers now calls the exported, tested
  matchingMembersOfGroup helper instead of re-implementing the same
  filter inline with userMatches.

- Reworded the "shared with ChartsView.vue" claims on membersOfGroup
  (usage-search.ts) and in UsageView.vue's visibleMembers doc comment:
  ChartsView never calls membersOfGroup directly, only indirectly via
  groupMatches -> matchingMembersOfGroup.

- UsageView's groups/users empty-state messages now follow Overview's
  aliasEmptyMessage three-way pattern ("none configured" vs "no
  groups/users match ..." vs "none"), distinguishing nothing-configured
  from nothing-matches instead of collapsing both to the same text.

Gate unchanged: npm run build + npm test + vue-tsc, make admin-ui
(separate regen commit), go test ./... -count=1, gofmt/vet, make
yaegi-check.
This commit is contained in:
2026-08-21 12:17:04 +01:00
parent 46537fa16a
commit 24809ed25a
7 changed files with 173 additions and 132 deletions
+24
View File
@@ -0,0 +1,24 @@
import { computed, ref } from 'vue'
/**
* useSearchQuery is the one raw-query → {normalized, hasQuery, clear}
* adapter every search filter in this panel uses — OverviewView's model
* search, UsageView's user/group search, ChartsView's scope-picker search
* all had their own copy of this identical query/normalizedQuery/hasQuery/
* clearQuery quartet; extracted so it exists once (vue.md: "if you've
* written it twice, you owe an abstraction"). Callers still own their own
* matching logic entirely (OverviewView's modelMatches/providerMatches,
* lib/usage-search.ts's helpers) — this only normalizes the raw input the
* same way every call site already agreed on: trimmed, lowercased.
*/
export function useSearchQuery() {
const query = ref('')
const normalized = computed(() => query.value.trim().toLowerCase())
const hasQuery = computed(() => normalized.value.length > 0)
function clear(): void {
query.value = ''
}
return { query, normalized, hasQuery, clear }
}