doxa-maps/library/components/search
2026-09-19 17:24:30 +00:00
..
FilterDropdown.vue Brand-neutral shared VerticalTabsLayout with lazy nested-element hosting; fix cap-5 prayer tab leaf path; neutralize library identifiers; add Forgejo CI build-gate workflow 2026-09-19 17:24:30 +00:00
fuzzy.js Brand-neutral shared VerticalTabsLayout with lazy nested-element hosting; fix cap-5 prayer tab leaf path; neutralize library identifiers; add Forgejo CI build-gate workflow 2026-09-19 17:24:30 +00:00
README.md Brand-neutral shared VerticalTabsLayout with lazy nested-element hosting; fix cap-5 prayer tab leaf path; neutralize library identifiers; add Forgejo CI build-gate workflow 2026-09-19 17:24:30 +00:00
SearchBar.vue Brand-neutral shared VerticalTabsLayout with lazy nested-element hosting; fix cap-5 prayer tab leaf path; neutralize library identifiers; add Forgejo CI build-gate workflow 2026-09-19 17:24:30 +00:00

@map/components/search — shared search + filter primitives

ONE source of truth for the search-bar, the searchable filter-dropdown, and the fuzzy matcher. Consumed by BOTH plugins so there is no duplication:

  • cap-2 plugin-designs/people-groups-grid/ — its own header (default) drives the grid.
  • cap-1 plugin-designs/big-picture/ — the Unified Research Dashboard header (BpResearchHeader.vue) can adopt these to replace its hand-rolled <input> + native <select>s (card BUILD-G1-CAT4-BIG-PICTURE-DASHBOARD wires them in).

All three are props-driven, emit-based, framework-agnostic — no Nuxt context, no i18n composables, no plugin-specific hardcoding. Styles are inlined + scoped so each self-styles inside a standalone custom-element shadow root.

Import via the @map alias (resolves to library/):

import SearchBar      from '@map/components/search/SearchBar.vue'
import FilterDropdown from '@map/components/search/FilterDropdown.vue'
import Fuse           from '@map/components/search/fuzzy.js'

SearchBar.vue

A magnifier-icon + <input type="search">.

Prop Type Default Purpose
modelValue String '' Bound value; supports v-model.
placeholder String 'Search…' Input placeholder text.
ariaLabel String 'Search' Accessible label.
maxWidth String '' Optional CSS max-width (e.g. '480px'); empty = fill container.
Event Payload When
update:modelValue String On every input (enables v-model).
search { value } On every input; value is the raw string (read in-control, so shadow-DOM retargeting never loses it).
<SearchBar :model-value="term" placeholder="Search people groups…" max-width="480px" @search="onSearch" />
<!-- onSearch(payload) { term = payload.value } -->

FilterDropdown.vue

A searchable single-select with per-option counts (the four axes: Region / Block / People-Group / Religion in the grid; Region / Status in the dashboard).

Prop Type Required Default Purpose
label String yes — Control label, e.g. "Region".
name String yes — Axis/field key echoed back in events.
options Array no [] [{ value, label, count?, type? }].
value String no '' Currently-selected value (controlled).
placeholder String no 'Type to search...' In-menu search placeholder.
Event Payload When
filter-change { name, value, label, type } An option was picked.
filter-clear { name } Selection was cleared.
<FilterDropdown label="Region" name="wagf_region" :options="opts" :value="sel"
  @filter-change="onFilterChange" @filter-clear="onFilterClear" />

fuzzy.js

A zero-dependency Fuse.js-shaped matcher (so no npm dep is added to the shared package.json/bun.lock).

const fuse = new Fuse(list, {
  keys: ['name', 'country_code.label', 'religion.label'], // dotted paths
  threshold: 0,          // 0 = exact substring only; >0 = fuzzy subsequence
  minMatchCharLength: 3, // shortest run counted as a match
})
fuse.search(term) // -> [{ item, matches: [{ key, indices: [[start,end], …] }] }]
  • Empty term → every item with matches: [].
  • matches[].indices drive <span class="search-highlight"> wrapping (same shape as Fuse's includeMatches).

Verification (headless)

  • fuzzy.js: exact + fuzzy + empty-term contracts asserted via a node harness.
  • SearchBar / FilterDropdown: SSR-mounted (@vue/server-renderer) — render icon + input, trigger + label, and reflect a selected value.
  • bun run build builds the people-groups-grid bundle green consuming these shared imports (all guards pass).