From 52d573d03fe99b47a3b7efa7375a650bf441b5cf Mon Sep 17 00:00:00 2001
From: Noor A
Date: Thu, 24 Sep 2026 16:15:40 +0200
Subject: [PATCH 1/4] feat(filters): reorder sections, remove source search,
add select all per category
Sources, SDGs, Languages order in both the filter panel and the selected-filter pills.
Each source category gets a Select all checkbox (partial state shows a dash).
Co-Authored-By: Claude Opus 5.5 (1M context)
---
src/components/CheckboxCompnent.vue | 26 ++-
src/components/FiltersComponent.vue | 76 +++-----
.../__snapshots__/QandA.spec.ts.snap | 180 +++++++++---------
src/components/dropdowns/SourcesSelector.vue | 98 +++++++++-
src/localisation/en.ts | 1 -
src/localisation/fr.ts | 1 -
src/stores/filters.ts | 10 +
7 files changed, 225 insertions(+), 167 deletions(-)
diff --git a/src/components/CheckboxCompnent.vue b/src/components/CheckboxCompnent.vue
index f7be416..909130d 100644
--- a/src/components/CheckboxCompnent.vue
+++ b/src/components/CheckboxCompnent.vue
@@ -10,6 +10,7 @@ const props = defineProps<{
selectAll?: () => void;
isDisabled?: boolean;
isCapitalized?: boolean;
+ isIndeterminate?: boolean;
}>();
const emit = defineEmits(['update:modelValue', 'selectAll']);
@@ -39,7 +40,7 @@ const model = computed({
:checked="checked"
:disabled="isDisabled"
/>
-
+
diff --git a/src/components/FiltersComponent.vue b/src/components/FiltersComponent.vue
index 3973939..69a2307 100644
--- a/src/components/FiltersComponent.vue
+++ b/src/components/FiltersComponent.vue
@@ -15,29 +15,8 @@ const props = defineProps<{
}>();
const sourcesStore = useSourcesStore();
-const availableSources = ref(sourcesStore.sourcesList);
const { shouldClose } = toRefs(props);
-const handleSearchFilters = (event: Event) => {
- if (!(event.target instanceof HTMLInputElement))
- availableSources.value = sourcesStore.sourcesList;
- const target = event.target as HTMLInputElement;
- const value = target.value;
- let temp = undefined;
-
- for (const [category, sources] of Object.entries(sourcesStore.sourcesList)) {
- const filteredSources = sources.filter((source) =>
- source.name.toLowerCase().includes(value.toLowerCase())
- );
- if (filteredSources.length > 0) {
- temp = temp || {};
- temp[category] = filteredSources;
- }
- }
-
- availableSources.value = temp || sourcesStore.sourcesList;
-};
-
const hideFilters = ref(false);
watch(shouldClose, (close) => {
if (close) {
@@ -45,10 +24,6 @@ watch(shouldClose, (close) => {
}
});
-watch(sourcesStore, () => {
- availableSources.value = sourcesStore.sourcesList;
-});
-
const filters = useFiltersStore();
const toggleFilters = () => (hideFilters.value = !hideFilters.value);
@@ -70,22 +45,6 @@ const clearFilters = () => {
-
-
{{ $t('languages') }}{{ $t(':') }}
-
-
-
- x
-
-
-
-
{{ $t('sources') }}{{ $t(':') }}
{
+
+
{{ $t('languages') }}{{ $t(':') }}
+
+
+
+ x
+
+
+
+
{{ $t('removeAll') }}
@@ -121,29 +96,22 @@ const clearFilters = () => {
{{ $t('noFiltersSelected') }}
-
-
- {{ $t('languages') }}
+ {{ $t('sources') }}
-
+
- {{ $t('sources') }}
+ {{ $t('sdgsAcronym') }}
-
+
- {{ $t('sdgsAcronym') }}
+ {{ $t('languages') }}
-
+
diff --git a/src/components/__tests__/__snapshots__/QandA.spec.ts.snap b/src/components/__tests__/__snapshots__/QandA.spec.ts.snap
index e62cc25..8d41cbc 100644
--- a/src/components/__tests__/__snapshots__/QandA.spec.ts.snap
+++ b/src/components/__tests__/__snapshots__/QandA.spec.ts.snap
@@ -271,99 +271,6 @@ exports[`QandA > renders properly 1`] = `
class="pr-5 filters"
data-v-e843c1dd=""
>
-
-
-
- languages
-
-
-
renders properly 1`] = `
+
+
+ languages
+
+
+
diff --git a/src/components/dropdowns/SourcesSelector.vue b/src/components/dropdowns/SourcesSelector.vue
index 4c85956..c012393 100644
--- a/src/components/dropdowns/SourcesSelector.vue
+++ b/src/components/dropdowns/SourcesSelector.vue
@@ -1,18 +1,53 @@
-
-
-
- {{ $t(`categories.${category}`) }}
-
+
+
+
+
+
+ {{ $t(`categories.${category}`) }}
+
+
+ {{ selectedCount(sources) }}/{{ sources.length }}
+
+
+
diff --git a/src/localisation/en.ts b/src/localisation/en.ts
index 39dcf99..eecaafb 100644
--- a/src/localisation/en.ts
+++ b/src/localisation/en.ts
@@ -292,7 +292,6 @@ export const en = {
removeAll: 'Clear all',
removeBookmark: 'Remove bookmark',
removeSelection: 'Clear',
- searchBarPlaceholder: 'Search...',
sdgsAcronym: 'SDGs',
sdgs: {
1: 'No Poverty',
diff --git a/src/localisation/fr.ts b/src/localisation/fr.ts
index 29807da..fbcb91f 100644
--- a/src/localisation/fr.ts
+++ b/src/localisation/fr.ts
@@ -294,7 +294,6 @@ export const fr = {
removeAll: 'Tout effacer',
removeBookmark: 'Retirer des favoris',
removeSelection: 'Effacer',
- searchBarPlaceholder: 'Rechercher...',
sdgsAcronym: 'ODD',
sdgs: {
1: 'Pas de pauvreté',
diff --git a/src/stores/filters.ts b/src/stores/filters.ts
index 322ee61..e238b18 100644
--- a/src/stores/filters.ts
+++ b/src/stores/filters.ts
@@ -45,6 +45,15 @@ export const useFiltersStore = defineStore('filters', () => {
}
};
+ // "Select all" for one category: untick all if every source is ticked, otherwise tick the missing ones
+ const toggleCategorySources = (names: Corpus['name'][]) => {
+ if (names.every((name) => sourcesFilters.value.includes(name))) {
+ sourcesFilters.value = sourcesFilters.value.filter((item) => !names.includes(item));
+ } else {
+ sourcesFilters.value = [...new Set([...sourcesFilters.value, ...names])];
+ }
+ };
+
const handleResetFilters = () => {
sdgFilters.value = [];
sourcesFilters.value = [];
@@ -58,6 +67,7 @@ export const useFiltersStore = defineStore('filters', () => {
sourcesFilters,
handleSdgFilterChange,
handleSourcesFilterChange,
+ toggleCategorySources,
handleLanguageFilterChange,
languageList,
languageFilters,
From 5de8c1ef6958a55e866a1270855a5eb2c19c8e14 Mon Sep 17 00:00:00 2001
From: Noor A
Date: Thu, 24 Sep 2026 16:19:29 +0200
Subject: [PATCH 2/4] fix(search): give results the full column and collapse
the search box on scroll
Only the results area scrolls now, below the search box. Scrolling down collapses the box
into a one-line bar showing the query; clicking it (or scrolling back to the top) expands it.
Collapsing never changes the scroll position, so there is no flicker. On stacked
(mobile/tablet) layouts the search column fills the screen and filters sit below.
Co-Authored-By: Claude Opus 5.5 (1M context)
---
src/views/SearchSDG.vue | 141 ++++++++++++++++++++++++++++++++++------
1 file changed, 121 insertions(+), 20 deletions(-)
diff --git a/src/views/SearchSDG.vue b/src/views/SearchSDG.vue
index 00aad62..05b4d80 100644
--- a/src/views/SearchSDG.vue
+++ b/src/views/SearchSDG.vue
@@ -10,14 +10,49 @@ import Pill from '@/components/PillComponent.vue';
import FiltersComponent from '@/components/FiltersComponent.vue';
import ColumnTemplate from '@/components/ColumnTemplate.vue';
+import ChevronDownIcon from '@/components/icons/ChevronDown.vue';
+import { nextTick, ref } from 'vue';
+
const store = useSearchStore();
+
+// The search box collapses into a one-line bar while the user scrolls down through the results,
+// and comes back when they click it or scroll back to the top.
+// Only the results area scrolls, so collapsing never moves the scroll position (no flicker loop).
+const wrapper = ref(null);
+const isCompact = ref(false);
+let lastScrollTop = 0;
+
+const onResultsScroll = (event: Event) => {
+ const top = (event.target as HTMLElement).scrollTop;
+ if (top === 0) isCompact.value = false;
+ else if (top > 24 && top > lastScrollTop) isCompact.value = true;
+ lastScrollTop = top;
+};
+
+const expandSearch = async () => {
+ isCompact.value = false;
+ await nextTick();
+ wrapper.value?.querySelector('textarea')?.focus();
+};
-
+
-
-
+
+
+
+
+
@@ -83,12 +121,68 @@ const store = useSearchStore();