|
|
||
|---|---|---|
| .. | ||
| FilterDropdown.vue | ||
| fuzzy.js | ||
| README.md | ||
| SearchBar.vue | ||
@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 (cardBUILD-G1-CAT4-BIG-PICTURE-DASHBOARDwires 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 withmatches: []. matches[].indicesdrive<span class="search-highlight">wrapping (same shape as Fuse'sincludeMatches).
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 buildbuilds thepeople-groups-gridbundle green consuming these shared imports (all guards pass).