Files
AstralRinth/pages/search/[searchProjectType].vue
MMK21Hub 247be0c11f Fix search filters being hidden (#1205)
* Fix search filters being hidden (#1165)

Previously, `max-width` was used to hide the sidebar on mobile, which
meant that at exactly 1024 pixels wide, the sidebar would be hidden.
However, this breakpoint is also as a
`min-width` other media queries, notably the ones used to enable viewing
filters on mobile sizes. This has caused issue #1024.

To fix this, I have swapped the logic of the rule that hides the filters
on mobile: it is now hidden by default and a `min-width` query is used
to show it on wider viewports. This is more consistent with similar media
queries in the same file.

Looking to the future, these issues should become less common if we
switch to range-based media queries:
https://caniuse.com/css-media-range-syntax

* Delete package-lock.json

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2023-06-20 14:37:39 -07:00

1046 lines
31 KiB
Vue

<template>
<div
:class="{
'search-page': true,
'normal-page': true,
'alt-layout': $cosmetics.searchLayout,
}"
>
<Head>
<Title>Search {{ $formatProjectType(projectType.display) }}s - Modrinth</Title>
<Meta name="og:title" :content="`Search ${$formatProjectType(projectType.display)}s`" />
<Meta name="description" :content="metaDescription" />
<Meta
name="apple-mobile-web-app-title"
:content="`Search ${$formatProjectType(projectType.display)}s`"
/>
<Meta name="og:description" :content="metaDescription" />
</Head>
<aside
:class="{
'normal-page__sidebar': true,
open: sidebarMenuOpen,
}"
aria-label="Filters"
>
<section class="card filters-card" role="presentation">
<div class="sidebar-menu" :class="{ 'sidebar-menu_open': sidebarMenuOpen }">
<button
:disabled="
onlyOpenSource === false &&
selectedEnvironments.length === 0 &&
selectedVersions.length === 0 &&
facets.length === 0 &&
orFacets.length === 0
"
class="iconified-button"
@click="clearFilters"
>
<ClearIcon aria-hidden="true" />
Clear filters
</button>
<section aria-label="Category filters">
<div v-for="(categories, header) in categoriesMap" :key="header">
<h3
v-if="categories.filter((x) => x.project_type === projectType.actual).length > 0"
class="sidebar-menu-heading"
>
{{ $formatCategoryHeader(header) }}
</h3>
<SearchFilter
v-for="category in categories.filter((x) => x.project_type === projectType.actual)"
:key="category.name"
:active-filters="facets"
:display-name="$formatCategory(category.name)"
:facet-name="`categories:'${encodeURIComponent(category.name)}'`"
:icon="header === 'resolutions' ? null : category.icon"
@toggle="toggleFacet"
/>
</div>
</section>
<section
v-if="projectType.id !== 'resourcepack' && projectType.id !== 'datapack'"
aria-label="Loader filters"
>
<h3
v-if="
$tag.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
.length > 0
"
class="sidebar-menu-heading"
>
Loaders
</h3>
<SearchFilter
v-for="loader in $tag.loaders.filter((x) => {
if (
projectType.id === 'mod' &&
!showAllLoaders &&
x.name !== 'forge' &&
x.name !== 'fabric' &&
x.name !== 'quilt'
) {
return false
} else if (projectType.id === 'mod' && showAllLoaders) {
return $tag.loaderData.modLoaders.includes(x.name)
} else if (projectType.id === 'plugin') {
return $tag.loaderData.pluginLoaders.includes(x.name)
} else if (projectType.id === 'datapack') {
return $tag.loaderData.dataPackLoaders.includes(x.name)
} else {
return x.supported_project_types.includes(projectType.actual)
}
})"
:key="loader.name"
ref="loaderFilters"
:active-filters="orFacets"
:display-name="$formatCategory(loader.name)"
:facet-name="`categories:'${encodeURIComponent(loader.name)}'`"
:icon="loader.icon"
@toggle="toggleOrFacet"
/>
<Checkbox
v-if="projectType.id === 'mod'"
v-model="showAllLoaders"
:label="showAllLoaders ? 'Less' : 'More'"
description="Show all loaders"
style="margin-bottom: 0.5rem"
:border="false"
:collapsing-toggle-style="true"
/>
</section>
<section v-if="projectType.id === 'plugin'" aria-label="Platform loader filters">
<h3
v-if="
$tag.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
.length > 0
"
class="sidebar-menu-heading"
>
Proxies
</h3>
<SearchFilter
v-for="loader in $tag.loaders.filter((x) =>
$tag.loaderData.pluginPlatformLoaders.includes(x.name)
)"
:key="loader.name"
ref="platformFilters"
:active-filters="orFacets"
:display-name="$formatCategory(loader.name)"
:facet-name="`categories:'${encodeURIComponent(loader.name)}'`"
:icon="loader.icon"
@toggle="toggleOrFacet"
/>
</section>
<section
v-if="!['resourcepack', 'plugin', 'shader', 'datapack'].includes(projectType.id)"
aria-label="Environment filters"
>
<h3 class="sidebar-menu-heading">Environments</h3>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Client"
facet-name="client"
@toggle="toggleEnv"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Server"
facet-name="server"
@toggle="toggleEnv"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</section>
<h3 class="sidebar-menu-heading">Minecraft versions</h3>
<Checkbox
v-model="showSnapshots"
label="Show all versions"
description="Show all versions"
style="margin-bottom: 0.5rem"
:border="false"
/>
<multiselect
v-model="selectedVersions"
:options="
showSnapshots
? $tag.gameVersions.map((x) => x.version)
: $tag.gameVersions
.filter((it) => it.version_type === 'release')
.map((x) => x.version)
"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-search-on-select="false"
:show-labels="false"
:selectable="() => selectedVersions.length <= 6"
placeholder="Choose versions..."
@update:model-value="onSearchChange(1)"
/>
<h3 class="sidebar-menu-heading">Open source</h3>
<Checkbox
v-model="onlyOpenSource"
label="Open source only"
style="margin-bottom: 0.5rem"
:border="false"
@update:model-value="onSearchChange(1)"
/>
</div>
</section>
</aside>
<section class="normal-page__content">
<div
v-if="projectType.id === 'modpack' && $orElse($cosmetics.modpacksAlphaNotice, true)"
class="card information"
aria-label="Information"
>
Modpack support is currently in alpha, and modpacks can only be created and installed
through third party tools. Our documentation includes instructions on
<a href="https://docs.modrinth.com/docs/modpacks/playing_modpacks/" :target="$external()"
>playing modpacks</a
>
with
<a rel="noopener" href="https://atlauncher.com/about" :target="$external()">ATLauncher</a>,
<a rel="noopener" href="https://multimc.org/" :target="$external()">MultiMC</a>, and
<a rel="noopener" href="https://prismlauncher.org" :target="$external()"> Prism Launcher</a
>. Pack creators can reference our documentation on
<a href="https://docs.modrinth.com/docs/modpacks/creating_modpacks/" :target="$external()"
>creating modpacks</a
>. Join us on
<a rel="noopener" href="https://discord.gg/EUHuJHt" :target="$external()">Discord</a>
for support.
</div>
<Promotion />
<div class="card search-controls">
<div class="search-filter-container">
<button
class="iconified-button sidebar-menu-close-button"
:class="{ open: sidebarMenuOpen }"
@click="sidebarMenuOpen = !sidebarMenuOpen"
>
<FilterIcon aria-hidden="true" />
Filters...
</button>
<div class="iconified-input">
<label class="hidden" for="search">Search</label>
<SearchIcon aria-hidden="true" />
<input
id="search"
v-model="query"
type="search"
name="search"
:placeholder="`Search ${projectType.display}s...`"
autocomplete="off"
@input="onSearchChange(1)"
/>
</div>
</div>
<div class="sort-controls">
<div class="labeled-control">
<span class="labeled-control__label">Sort by</span>
<Multiselect
v-model="sortType"
placeholder="Select one"
class="search-controls__sorting labeled-control__control"
track-by="display"
label="display"
:options="sortTypes"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@update:model-value="onSearchChange(1)"
>
<template #singleLabel="{ option }">
{{ option.display }}
</template>
</Multiselect>
</div>
<div class="labeled-control">
<span class="labeled-control__label">Show per page</span>
<Multiselect
v-model="maxResults"
placeholder="Select one"
class="labeled-control__control"
:options="maxResultsForView[$cosmetics.searchDisplayMode[projectType.id]]"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@update:model-value="onMaxResultsChange(currentPage)"
/>
</div>
<button
v-tooltip="$capitalizeString($cosmetics.searchDisplayMode[projectType.id]) + ' view'"
:aria-label="$capitalizeString($cosmetics.searchDisplayMode[projectType.id]) + ' view'"
class="square-button"
@click="cycleSearchDisplayMode()"
>
<GridIcon v-if="$cosmetics.searchDisplayMode[projectType.id] === 'grid'" />
<ImageIcon v-else-if="$cosmetics.searchDisplayMode[projectType.id] === 'gallery'" />
<ListIcon v-else />
</button>
</div>
</div>
<Pagination
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-before"
@switch-page="onSearchChange"
/>
<LogoAnimated v-if="searchLoading && !noLoad"></LogoAnimated>
<div v-else-if="results && results.hits && results.hits.length === 0" class="no-results">
<p>No results found for your query!</p>
</div>
<div v-else class="search-results-container">
<div
id="search-results"
class="project-list"
:class="'display-mode--' + $cosmetics.searchDisplayMode[projectType.id]"
role="list"
aria-label="Search results"
>
<ProjectCard
v-for="result in results?.hits"
:id="result.slug ? result.slug : result.project_id"
:key="result.project_id"
:display="$cosmetics.searchDisplayMode[projectType.id]"
:featured-image="result.featured_gallery ? result.featured_gallery : result.gallery[0]"
:type="result.project_type"
:author="result.author"
:name="result.title"
:description="result.description"
:created-at="result.date_created"
:updated-at="result.date_modified"
:downloads="result.downloads.toString()"
:follows="result.follows.toString()"
:icon-url="result.icon_url"
:client-side="result.client_side"
:server-side="result.server_side"
:categories="result.display_categories"
:search="true"
:show-updated-date="sortType.name !== 'newest'"
:hide-loaders="['resourcepack', 'datapack'].includes(projectType.id)"
:color="result.color"
/>
</div>
</div>
<pagination
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-after"
@switch-page="onSearchChangeToTop"
/>
</section>
</div>
</template>
<script>
import { Multiselect } from 'vue-multiselect'
import ProjectCard from '~/components/ui/ProjectCard.vue'
import Pagination from '~/components/ui/Pagination.vue'
import SearchFilter from '~/components/ui/search/SearchFilter.vue'
import Checkbox from '~/components/ui/Checkbox.vue'
import LogoAnimated from '~/components/brand/LogoAnimated.vue'
import ClientIcon from '~/assets/images/categories/client.svg'
import ServerIcon from '~/assets/images/categories/server.svg'
import SearchIcon from '~/assets/images/utils/search.svg'
import ClearIcon from '~/assets/images/utils/clear.svg'
import FilterIcon from '~/assets/images/utils/filter.svg'
import GridIcon from '~/assets/images/utils/grid.svg'
import ListIcon from '~/assets/images/utils/list.svg'
import ImageIcon from '~/assets/images/utils/image.svg'
import Promotion from '~/components/ads/Promotion.vue'
export default defineNuxtComponent({
components: {
LogoAnimated,
Promotion,
ProjectCard,
Pagination,
Multiselect,
SearchFilter,
Checkbox,
ClientIcon,
ServerIcon,
SearchIcon,
ClearIcon,
FilterIcon,
GridIcon,
ListIcon,
ImageIcon,
},
data() {
return {
previousMaxResults: 20,
maxResultsForView: {
list: [5, 10, 15, 20, 50, 100],
grid: [6, 12, 18, 24, 48, 96],
gallery: [6, 10, 16, 20, 50, 100],
},
sidebarMenuOpen: false,
showAllLoaders: false,
}
},
setup() {
const data = useNuxtApp()
const route = useRoute()
const query = ref('')
const facets = ref([])
const orFacets = ref([])
const selectedVersions = ref([])
const onlyOpenSource = ref(false)
const showSnapshots = ref(false)
const selectedEnvironments = ref([])
const sortTypes = shallowReadonly([
{ display: 'Relevance', name: 'relevance' },
{ display: 'Download count', name: 'downloads' },
{ display: 'Follow count', name: 'follows' },
{ display: 'Recently published', name: 'newest' },
{ display: 'Recently updated', name: 'updated' },
])
const sortType = ref({ display: 'Relevance', name: 'relevance' })
const maxResults = ref(20)
const currentPage = ref(1)
const projectType = ref({ id: 'mod', display: 'mod', actual: 'mod' })
const metaDescription = computed(
() =>
`Search and browse thousands of Minecraft ${data.$formatProjectType(
projectType.value.display
)}s on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft ${data.$formatProjectType(
projectType.value.display
)}s.`
)
if (route.query.q) {
query.value = route.query.q
}
if (route.query.f) {
facets.value = getArrayOrString(route.query.f)
}
if (route.query.g) {
orFacets.value = getArrayOrString(route.query.g)
}
if (route.query.v) {
selectedVersions.value = getArrayOrString(route.query.v)
}
if (route.query.l) {
onlyOpenSource.value = route.query.l === 'true'
}
if (route.query.h) {
showSnapshots.value = route.query.h === 'true'
}
if (route.query.e) {
selectedEnvironments.value = getArrayOrString(route.query.e)
}
if (route.query.s) {
sortType.value.name = route.query.s
switch (sortType.value.name) {
case 'relevance':
sortType.value.display = 'Relevance'
break
case 'downloads':
sortType.value.display = 'Downloads'
break
case 'newest':
sortType.value.display = 'Recently published'
break
case 'updated':
sortType.value.display = 'Recently updated'
break
case 'follows':
sortType.value.display = 'Follow count'
break
}
}
if (route.query.m) {
maxResults.value = route.query.m
}
if (route.query.o) {
currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1
}
projectType.value = data.$tag.projectTypes.find(
(x) => x.id === route.path.substring(1, route.path.length - 1)
)
const noLoad = ref(false)
const {
data: rawResults,
refresh: refreshSearch,
pending: searchLoading,
} = useLazyFetch(
() => {
const config = useRuntimeConfig()
const base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl
const params = [`limit=${maxResults.value}`, `index=${sortType.value.name}`]
if (query.value.length > 0) {
params.push(`query=${query.value.replace(/ /g, '+')}`)
}
if (
facets.value.length > 0 ||
orFacets.value.length > 0 ||
selectedVersions.value.length > 0 ||
selectedEnvironments.value.length > 0 ||
projectType.value
) {
let formattedFacets = []
for (const facet of facets.value) {
formattedFacets.push([facet])
}
// loaders specifier
if (orFacets.value.length > 0) {
formattedFacets.push(orFacets.value)
} else if (projectType.value.id === 'plugin') {
formattedFacets.push(
data.$tag.loaderData.allPluginLoaders.map(
(x) => `categories:'${encodeURIComponent(x)}'`
)
)
} else if (projectType.value.id === 'mod') {
formattedFacets.push(
data.$tag.loaderData.modLoaders.map((x) => `categories:'${encodeURIComponent(x)}'`)
)
} else if (projectType.value.id === 'datapack') {
formattedFacets.push(
data.$tag.loaderData.dataPackLoaders.map(
(x) => `categories:'${encodeURIComponent(x)}'`
)
)
}
if (selectedVersions.value.length > 0) {
const versionFacets = []
for (const facet of selectedVersions.value) {
versionFacets.push('versions:' + facet)
}
formattedFacets.push(versionFacets)
}
if (onlyOpenSource.value) {
formattedFacets.push(['open_source:true'])
}
if (selectedEnvironments.value.length > 0) {
let environmentFacets = []
const includesClient = selectedEnvironments.value.includes('client')
const includesServer = selectedEnvironments.value.includes('server')
if (includesClient && includesServer) {
environmentFacets = [['client_side:required'], ['server_side:required']]
} else {
if (includesClient) {
environmentFacets = [
['client_side:optional', 'client_side:required'],
['server_side:optional', 'server_side:unsupported'],
]
}
if (includesServer) {
environmentFacets = [
['client_side:optional', 'client_side:unsupported'],
['server_side:optional', 'server_side:required'],
]
}
}
formattedFacets = [...formattedFacets, ...environmentFacets]
}
if (projectType.value) {
formattedFacets.push([`project_type:${projectType.value.actual}`])
}
params.push(`facets=${JSON.stringify(formattedFacets)}`)
}
const offset = (currentPage.value - 1) * maxResults.value
if (currentPage.value !== 1) {
params.push(`offset=${offset}`)
}
let url = 'search'
if (params.length > 0) {
for (let i = 0; i < params.length; i++) {
url += i === 0 ? `?${params[i]}` : `&${params[i]}`
}
}
return `${base}${url}`
},
{
transform(hits) {
noLoad.value = false
return hits
},
}
)
const results = shallowRef(toRaw(rawResults))
const pageCount = computed(() =>
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1
)
const onSearchChange = (newPageNumber) => {
noLoad.value = true
currentPage.value = newPageNumber
if (query.value === null) {
return
}
refreshSearch()
if (process.client) {
const router = useRouter()
const obj = getSearchUrl((currentPage.value - 1) * maxResults.value, true)
router.replace({ path: route.path, query: obj })
}
}
const getSearchUrl = (offset, useObj) => {
const queryItems = []
const obj = {}
if (query.value) {
queryItems.push(`q=${encodeURIComponent(query.value)}`)
obj.q = query.value
}
if (offset > 0) {
queryItems.push(`o=${offset}`)
obj.o = offset
}
if (facets.value.length > 0) {
queryItems.push(`f=${encodeURIComponent(facets.value)}`)
obj.f = facets.value
}
if (orFacets.value.length > 0) {
queryItems.push(`g=${encodeURIComponent(orFacets.value)}`)
obj.g = orFacets.value
}
if (selectedVersions.value.length > 0) {
queryItems.push(`v=${encodeURIComponent(selectedVersions.value)}`)
obj.v = selectedVersions.value
}
if (onlyOpenSource.value) {
queryItems.push('l=true')
obj.l = true
}
if (showSnapshots.value) {
queryItems.push('h=true')
obj.h = true
}
if (selectedEnvironments.value.length > 0) {
queryItems.push(`e=${encodeURIComponent(selectedEnvironments.value)}`)
obj.e = selectedEnvironments.value
}
if (sortType.value.name !== 'relevance') {
queryItems.push(`s=${encodeURIComponent(sortType.value.name)}`)
obj.s = sortType.value.name
}
if (maxResults.value !== 20) {
queryItems.push(`m=${encodeURIComponent(maxResults.value)}`)
obj.m = maxResults.value
}
let url = `${route.path}`
if (queryItems.length > 0) {
url += `?${queryItems[0]}`
for (let i = 1; i < queryItems.length; i++) {
url += `&${queryItems[i]}`
}
}
return useObj ? obj : url
}
return {
query,
results,
facets,
orFacets,
selectedVersions,
onlyOpenSource,
showSnapshots,
selectedEnvironments,
sortTypes,
sortType,
maxResults,
currentPage,
pageCount,
projectType,
onSearchChange,
getSearchUrl,
searchLoading,
noLoad,
metaDescription,
}
},
computed: {
categoriesMap() {
const categories = {}
for (const category of this.$sortedCategories) {
if (categories[category.header]) {
categories[category.header].push(category)
} else {
categories[category.header] = [category]
}
}
const newVals = Object.keys(categories).reduce((obj, key) => {
obj[key] = categories[key]
return obj
}, {})
return newVals
},
},
methods: {
clearFilters() {
for (const facet of [...this.facets]) {
this.toggleFacet(facet, true)
}
for (const facet of [...this.orFacets]) {
this.toggleOrFacet(facet, true)
}
this.onlyOpenSource = false
this.selectedVersions = []
this.selectedEnvironments = []
this.onSearchChange(1)
},
toggleFacet(elementName, doNotSendRequest = false) {
const index = this.facets.indexOf(elementName)
if (index !== -1) {
this.facets.splice(index, 1)
} else {
this.facets.push(elementName)
}
if (!doNotSendRequest) {
this.onSearchChange(1)
}
},
toggleOrFacet(elementName, doNotSendRequest) {
const index = this.orFacets.indexOf(elementName)
if (index !== -1) {
this.orFacets.splice(index, 1)
} else {
if (elementName === 'categories:purpur') {
if (!this.orFacets.includes('categories:paper')) {
this.orFacets.push('categories:paper')
}
if (!this.orFacets.includes('categories:spigot')) {
this.orFacets.push('categories:spigot')
}
if (!this.orFacets.includes('categories:bukkit')) {
this.orFacets.push('categories:bukkit')
}
} else if (elementName === 'categories:paper') {
if (!this.orFacets.includes('categories:spigot')) {
this.orFacets.push('categories:spigot')
}
if (!this.orFacets.includes('categories:bukkit')) {
this.orFacets.push('categories:bukkit')
}
} else if (elementName === 'categories:spigot') {
if (!this.orFacets.includes('categories:bukkit')) {
this.orFacets.push('categories:bukkit')
}
} else if (elementName === 'categories:waterfall') {
if (!this.orFacets.includes('categories:bungeecord')) {
this.orFacets.push('categories:bungeecord')
}
}
this.orFacets.push(elementName)
}
if (!doNotSendRequest) {
this.onSearchChange(1)
}
},
toggleEnv(environment, sendRequest) {
const index = this.selectedEnvironments.indexOf(environment)
if (index !== -1) {
this.selectedEnvironments.splice(index, 1)
} else {
this.selectedEnvironments.push(environment)
}
if (!sendRequest) {
this.onSearchChange(1)
}
},
onSearchChangeToTop(newPageNumber) {
if (process.client) {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
this.onSearchChange(newPageNumber)
},
onMaxResultsChange(newPageNumber) {
newPageNumber = Math.max(
1,
Math.min(
Math.floor(newPageNumber / (this.maxResults / this.previousMaxResults)),
this.pageCount
)
)
this.previousMaxResults = this.maxResults
this.onSearchChange(newPageNumber)
},
cycleSearchDisplayMode() {
this.$cosmetics.searchDisplayMode[this.projectType.id] = this.$cycleValue(
this.$cosmetics.searchDisplayMode[this.projectType.id],
this.$tag.projectViewModes
)
saveCosmetics()
this.setClosestMaxResults()
},
setClosestMaxResults() {
const view = this.$cosmetics.searchDisplayMode[this.projectType.id]
const maxResultsOptions = this.maxResultsForView[view] ?? [20]
const currentMax = this.maxResults
if (!maxResultsOptions.includes(currentMax)) {
this.maxResults = maxResultsOptions.reduce(function (prev, curr) {
return Math.abs(curr - currentMax) <= Math.abs(prev - currentMax) ? curr : prev
})
}
},
},
})
</script>
<style lang="scss" scoped>
.normal-page__content {
// Passthrough children as grid items on mobile
display: contents;
@media screen and (min-width: 1024px) {
display: block;
}
}
// Move the filters "sidebar" on mobile underneath the search card
.normal-page__sidebar {
grid-row: 3;
// Always show on desktop
@media screen and (min-width: 1024px) {
display: block;
}
// Hide on mobile unless open
display: none;
&.open {
display: block;
}
}
.filters-card {
padding: var(--spacing-card-md);
@media screen and (min-width: 1024px) {
padding: var(--spacing-card-lg);
}
}
.sidebar-menu {
display: none;
}
.sidebar-menu_open {
display: block;
}
.sidebar-menu-heading {
margin: 1.5rem 0 0.5rem 0;
}
// EthicalAds
.content-wrapper {
grid-row: 1;
}
.search-controls {
display: flex;
flex-direction: row;
gap: var(--spacing-card-md);
flex-wrap: wrap;
padding: var(--spacing-card-md);
grid-row: 2;
.search-filter-container {
display: flex;
width: 100%;
align-items: center;
.sidebar-menu-close-button {
max-height: none;
// match height of the search field
height: 40px;
transition: box-shadow 0.1s ease-in-out;
margin-right: var(--spacing-card-md);
&.open {
color: var(--color-button-text-active);
background-color: var(--color-brand-highlight);
box-shadow: inset 0 0 0 transparent, 0 0 0 2px var(--color-brand);
}
}
.iconified-input {
flex: 1;
input {
width: 100%;
margin: 0;
}
}
}
.sort-controls {
width: 100%;
display: flex;
flex-direction: row;
gap: var(--spacing-card-md);
flex-wrap: wrap;
align-items: center;
.labeled-control {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
.labeled-control__label {
white-space: nowrap;
}
}
.square-button {
margin-top: auto;
// match height of search dropdowns
height: 40px;
width: 40px; // make it square!
}
}
}
.search-controls__sorting {
min-width: 14rem;
}
.labeled-control__label,
.labeled-control__control {
display: block;
}
.pagination-before {
grid-row: 4;
}
.search-results-container {
grid-row: 5;
}
.pagination-after {
grid-row: 6;
}
.no-results {
text-align: center;
display: flow-root;
}
.loading-logo {
margin: 2rem;
}
#search-results {
min-height: 20vh;
}
@media screen and (min-width: 750px) {
.search-controls {
flex-wrap: nowrap;
flex-direction: row;
}
.sort-controls {
min-width: fit-content;
max-width: fit-content;
flex-wrap: nowrap;
}
.labeled-control {
align-items: center;
display: flex;
flex-direction: column !important;
flex-wrap: wrap;
gap: 0.5rem;
max-width: fit-content;
}
.labeled-control__label {
flex-shrink: 0;
margin-bottom: 0 !important;
}
}
@media screen and (min-width: 860px) {
.labeled-control {
flex-wrap: nowrap !important;
flex-direction: row !important;
}
}
@media screen and (min-width: 1024px) {
.sidebar-menu {
display: block;
margin-top: 0;
}
.sidebar-menu-close-button {
display: none;
}
.labeled-control {
flex-wrap: wrap !important;
flex-direction: column !important;
}
}
@media screen and (min-width: 1100px) {
.labeled-control {
flex-wrap: nowrap !important;
flex-direction: row !important;
}
}
</style>