mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
change toggle filters to just use exclude-only filters, same design as the others.
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
:option="option"
|
||||
:included="included(option)"
|
||||
:excluded="excluded(option)"
|
||||
:supports-negative-filter="supportsNegativeFilter"
|
||||
:supports="supports"
|
||||
@toggle="(o) => emit('toggle', o)"
|
||||
@toggle-exclude="(o) => emit('toggleExclude', o)"
|
||||
>
|
||||
@@ -44,13 +44,13 @@
|
||||
import { DropdownIcon } from '@modrinth/assets'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { FilterOption } from '../../utils/search'
|
||||
import type { FilterMode, FilterOption } from '../../utils/search'
|
||||
import SearchFilterOption from './SearchFilterOption.vue'
|
||||
|
||||
defineProps<{
|
||||
groupName: string
|
||||
options: FilterOption[]
|
||||
supportsNegativeFilter: boolean
|
||||
supports: FilterMode[]
|
||||
included: (option: FilterOption) => boolean
|
||||
excluded: (option: FilterOption) => boolean
|
||||
}>()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="search-filter-option group flex gap-1 items-center">
|
||||
<button
|
||||
:class="`flex border-none cursor-pointer !w-full items-center gap-2 truncate rounded-xl px-2 py-2 [@media(hover:hover)]:py-1 text-sm font-semibold transition-all hover:text-contrast focus-visible:text-contrast active:scale-[0.98] ${included ? 'bg-brand-highlight text-contrast hover:brightness-125' : excluded ? 'bg-highlight-red text-contrast hover:brightness-125' : 'bg-transparent text-secondary hover:bg-button-bg focus-visible:bg-button-bg [&>svg.check-icon]:hover:text-brand [&>svg.check-icon]:focus-visible:text-brand'}`"
|
||||
@click="() => emit('toggle', option)"
|
||||
:class="`flex border-none cursor-pointer !w-full items-center gap-2 truncate rounded-xl px-2 py-2 [@media(hover:hover)]:py-1 text-sm font-semibold transition-all hover:text-contrast focus-visible:text-contrast active:scale-[0.98] ${included ? 'bg-brand-highlight text-contrast hover:brightness-125' : excluded ? 'bg-highlight-red text-contrast hover:brightness-125' : 'bg-transparent text-secondary hover:bg-button-bg focus-visible:bg-button-bg [&>svg.check-icon]:hover:text-brand [&>svg.check-icon]:focus-visible:text-brand [&>svg.ban-icon]:hover:text-red [&>svg.ban-icon]:focus-visible:text-red'}`"
|
||||
@click="() => emit(primaryAction === 'exclude' ? 'toggleExclude' : 'toggle', option)"
|
||||
>
|
||||
<slot> </slot>
|
||||
<BanIcon
|
||||
v-if="excluded"
|
||||
:class="`filter-action-icon ml-auto h-4 w-4 shrink-0 transition-opacity group-hover:opacity-100 ${excluded ? '' : '[@media(hover:hover)]:opacity-0'}`"
|
||||
v-if="excluded || primaryAction === 'exclude'"
|
||||
:class="`filter-action-icon ban-icon ml-auto h-4 w-4 shrink-0 transition-opacity group-hover:opacity-100 ${excluded ? '' : '[@media(hover:hover)]:opacity-0'}`"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon
|
||||
@@ -17,12 +17,12 @@
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-if="supportsNegativeFilter && !excluded"
|
||||
v-if="showExcludeButton"
|
||||
class="w-px h-[1.75rem] bg-button-bg [@media(hover:hover)]:contents"
|
||||
:class="{ 'opacity-0': included }"
|
||||
></div>
|
||||
<button
|
||||
v-if="supportsNegativeFilter && !excluded"
|
||||
v-if="showExcludeButton"
|
||||
v-tooltip="formatMessage(messages.excludeTooltip)"
|
||||
class="flex border-none cursor-pointer items-center justify-center gap-2 rounded-xl bg-transparent px-2 py-1 text-sm font-semibold text-secondary [@media(hover:hover)]:opacity-0 transition-all hover:bg-button-bg hover:text-red focus-visible:bg-button-bg focus-visible:text-red active:scale-[0.96]"
|
||||
@click="() => emit('toggleExclude', option)"
|
||||
@@ -34,22 +34,30 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BanIcon, CheckIcon } from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { defineMessages, useVIntl } from '../../composables/i18n'
|
||||
import type { FilterOption } from '../../utils/search'
|
||||
import type { FilterMode, FilterOption } from '../../utils/search'
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
option: FilterOption
|
||||
included: boolean
|
||||
excluded: boolean
|
||||
supportsNegativeFilter?: boolean
|
||||
supports?: FilterMode[]
|
||||
}>(),
|
||||
{
|
||||
supportsNegativeFilter: false,
|
||||
supports: () => ['include'],
|
||||
},
|
||||
)
|
||||
|
||||
const supportsInclude = computed(() => props.supports.includes('include'))
|
||||
const supportsExclude = computed(() => props.supports.includes('exclude'))
|
||||
const primaryAction = computed<FilterMode>(() => (supportsInclude.value ? 'include' : 'exclude'))
|
||||
const showExcludeButton = computed(
|
||||
() => supportsInclude.value && supportsExclude.value && !props.excluded,
|
||||
)
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -71,105 +71,82 @@
|
||||
</template>
|
||||
<template v-else #default>
|
||||
<slot name="prefix" />
|
||||
<div
|
||||
v-if="filterType.display === 'toggle'"
|
||||
:class="innerPanelClass ? innerPanelClass : ''"
|
||||
class="flex flex-col gap-3"
|
||||
>
|
||||
<label
|
||||
v-for="option in filterType.options"
|
||||
:key="`${filterType.id}-toggle-${option.id}`"
|
||||
class="flex cursor-pointer items-center justify-between text-secondary gap-3 font-semibold"
|
||||
>
|
||||
<span class="text-sm">{{ option.formatted_name ?? option.id }}</span>
|
||||
<Toggle
|
||||
:model-value="isExcluded(option)"
|
||||
small
|
||||
class="shrink-0"
|
||||
@update:model-value="toggleNegativeFilter(option)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<template v-if="filterType.display !== 'toggle'">
|
||||
<StyledInput
|
||||
v-if="filterType.searchable"
|
||||
:id="`search-${filterType.id}`"
|
||||
v-model="query"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchPlaceholder)"
|
||||
autocomplete="off"
|
||||
clearable
|
||||
size="small"
|
||||
input-class="!bg-button-bg"
|
||||
wrapper-class="mx-2 my-1 w-[calc(100%-1rem)]"
|
||||
/>
|
||||
<ScrollablePanel :class="{ 'h-[16rem]': scrollable }" :disable-scrolling="!scrollable">
|
||||
<div :class="innerPanelClass ? innerPanelClass : ''" class="flex flex-col gap-1">
|
||||
<template v-if="groupedOptions">
|
||||
<SearchFilterGroup
|
||||
v-for="[groupName, options] in groupedOptions"
|
||||
:key="`${filterType.id}-group-${groupName}`"
|
||||
:group-name="groupName"
|
||||
:options="options"
|
||||
:supports-negative-filter="filterType.supports_negative_filter"
|
||||
:included="isIncluded"
|
||||
:excluded="isExcluded"
|
||||
@toggle="toggleFilter"
|
||||
@toggle-exclude="toggleNegativeFilter"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<SearchFilterOption
|
||||
v-for="option in visibleOptions"
|
||||
:key="`${filterType.id}-${option}`"
|
||||
:option="option"
|
||||
:included="isIncluded(option)"
|
||||
:excluded="isExcluded(option)"
|
||||
:supports-negative-filter="filterType.supports_negative_filter"
|
||||
:class="{
|
||||
'mr-3': scrollable,
|
||||
}"
|
||||
@toggle="toggleFilter"
|
||||
@toggle-exclude="toggleNegativeFilter"
|
||||
>
|
||||
<slot name="option" :filter="filterType" :option="option">
|
||||
<span
|
||||
v-if="option.icon"
|
||||
class="inline-flex items-center justify-center shrink-0 h-4 w-4"
|
||||
:style="iconStyle(option)"
|
||||
>
|
||||
<div
|
||||
v-if="typeof option.icon === 'string'"
|
||||
class="h-4 w-4"
|
||||
v-html="option.icon"
|
||||
/>
|
||||
<component :is="option.icon" v-else class="h-4 w-4" />
|
||||
</span>
|
||||
<span class="truncate text-sm" :style="iconStyle(option)">
|
||||
{{ option.formatted_name ?? option.id }}
|
||||
</span>
|
||||
</slot>
|
||||
</SearchFilterOption>
|
||||
</template>
|
||||
<button
|
||||
v-if="filterType.display === 'expandable'"
|
||||
class="flex bg-transparent text-secondary border-none cursor-pointer !w-full items-center gap-2 truncate rounded-xl px-2 py-1 text-sm font-semibold transition-all hover:text-contrast focus-visible:text-contrast active:scale-[0.98]"
|
||||
@click="showMore = !showMore"
|
||||
<StyledInput
|
||||
v-if="filterType.searchable"
|
||||
:id="`search-${filterType.id}`"
|
||||
v-model="query"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchPlaceholder)"
|
||||
autocomplete="off"
|
||||
clearable
|
||||
size="small"
|
||||
input-class="!bg-button-bg"
|
||||
wrapper-class="mx-2 my-1 w-[calc(100%-1rem)]"
|
||||
/>
|
||||
<ScrollablePanel :class="{ 'h-[16rem]': scrollable }" :disable-scrolling="!scrollable">
|
||||
<div :class="innerPanelClass ? innerPanelClass : ''" class="flex flex-col gap-1">
|
||||
<template v-if="groupedOptions">
|
||||
<SearchFilterGroup
|
||||
v-for="[groupName, options] in groupedOptions"
|
||||
:key="`${filterType.id}-group-${groupName}`"
|
||||
:group-name="groupName"
|
||||
:options="options"
|
||||
:supports="filterType.supports"
|
||||
:included="isIncluded"
|
||||
:excluded="isExcluded"
|
||||
@toggle="toggleFilter"
|
||||
@toggle-exclude="toggleNegativeFilter"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<SearchFilterOption
|
||||
v-for="option in visibleOptions"
|
||||
:key="`${filterType.id}-${option}`"
|
||||
:option="option"
|
||||
:included="isIncluded(option)"
|
||||
:excluded="isExcluded(option)"
|
||||
:supports="filterType.supports"
|
||||
:class="{
|
||||
'mr-3': scrollable,
|
||||
}"
|
||||
@toggle="toggleFilter"
|
||||
@toggle-exclude="toggleNegativeFilter"
|
||||
>
|
||||
<DropdownIcon
|
||||
class="h-4 w-4 transition-transform"
|
||||
:class="{ 'rotate-180': showMore }"
|
||||
/>
|
||||
<span class="truncate text-sm">
|
||||
{{
|
||||
showMore ? formatMessage(messages.showFewer) : formatMessage(messages.showMore)
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</ScrollablePanel>
|
||||
</template>
|
||||
<slot name="option" :filter="filterType" :option="option">
|
||||
<span
|
||||
v-if="option.icon"
|
||||
class="inline-flex items-center justify-center shrink-0 h-4 w-4"
|
||||
:style="iconStyle(option)"
|
||||
>
|
||||
<div
|
||||
v-if="typeof option.icon === 'string'"
|
||||
class="h-4 w-4"
|
||||
v-html="option.icon"
|
||||
/>
|
||||
<component :is="option.icon" v-else class="h-4 w-4" />
|
||||
</span>
|
||||
<span class="truncate text-sm" :style="iconStyle(option)">
|
||||
{{ option.formatted_name ?? option.id }}
|
||||
</span>
|
||||
</slot>
|
||||
</SearchFilterOption>
|
||||
</template>
|
||||
<button
|
||||
v-if="filterType.display === 'expandable'"
|
||||
class="flex bg-transparent text-secondary border-none cursor-pointer !w-full items-center gap-2 truncate rounded-xl px-2 py-1 text-sm font-semibold transition-all hover:text-contrast focus-visible:text-contrast active:scale-[0.98]"
|
||||
@click="showMore = !showMore"
|
||||
>
|
||||
<DropdownIcon
|
||||
class="h-4 w-4 transition-transform"
|
||||
:class="{ 'rotate-180': showMore }"
|
||||
/>
|
||||
<span class="truncate text-sm">
|
||||
{{ showMore ? formatMessage(messages.showFewer) : formatMessage(messages.showMore) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</ScrollablePanel>
|
||||
<div :class="innerPanelClass ? innerPanelClass : ''" class="empty:hidden">
|
||||
<Checkbox
|
||||
v-for="group in filterType.toggle_groups"
|
||||
@@ -213,7 +190,6 @@ import { defineMessages, useVIntl } from '../../composables/i18n'
|
||||
import type { FilterOption, FilterType, FilterValue } from '../../utils/search'
|
||||
import Accordion from '../base/Accordion.vue'
|
||||
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||
import Toggle from '../base/Toggle.vue'
|
||||
import { Checkbox, ScrollablePanel, StyledInput } from '../index'
|
||||
import SearchFilterGroup from './SearchFilterGroup.vue'
|
||||
import SearchFilterOption from './SearchFilterOption.vue'
|
||||
|
||||
@@ -29,13 +29,15 @@ export type FilterOption = BaseOption &
|
||||
| { method: 'environment'; environment: 'client' | 'server' }
|
||||
)
|
||||
|
||||
export type FilterMode = 'include' | 'exclude'
|
||||
|
||||
export type FilterType = {
|
||||
id: string
|
||||
formatted_name: string
|
||||
options: FilterOption[]
|
||||
supported_project_types: ProjectType[]
|
||||
query_param: string
|
||||
supports_negative_filter: boolean
|
||||
supports: FilterMode[]
|
||||
toggle_groups?: {
|
||||
id: string
|
||||
formatted_name: string
|
||||
@@ -46,7 +48,7 @@ export type FilterType = {
|
||||
ordering?: number
|
||||
} & (
|
||||
| {
|
||||
display: 'all' | 'scrollable' | 'none' | 'toggle'
|
||||
display: 'all' | 'scrollable' | 'none'
|
||||
}
|
||||
| {
|
||||
display: 'expandable'
|
||||
@@ -191,7 +193,7 @@ export function useSearch(
|
||||
: ([category.project_type] as ProjectType[]),
|
||||
display: 'all',
|
||||
query_param: category.header === 'resolutions' ? 'g' : 'f',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
searchable: false,
|
||||
options: [],
|
||||
}
|
||||
@@ -227,7 +229,7 @@ export function useSearch(
|
||||
supported_project_types: ['mod', 'modpack'],
|
||||
display: 'all',
|
||||
query_param: 'e',
|
||||
supports_negative_filter: false,
|
||||
supports: ['include'],
|
||||
searchable: false,
|
||||
options: [
|
||||
{
|
||||
@@ -267,7 +269,7 @@ export function useSearch(
|
||||
supported_project_types: ALL_PROJECT_TYPES,
|
||||
display: 'scrollable',
|
||||
query_param: 'v',
|
||||
supports_negative_filter: false,
|
||||
supports: ['include'],
|
||||
toggle_groups: [
|
||||
{
|
||||
id: 'all_versions',
|
||||
@@ -305,7 +307,7 @@ export function useSearch(
|
||||
supported_project_types: ['mod'],
|
||||
display: 'expandable',
|
||||
query_param: 'g',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
default_values: DEFAULT_MOD_LOADERS,
|
||||
searchable: false,
|
||||
options: tags.value.loaders
|
||||
@@ -337,7 +339,7 @@ export function useSearch(
|
||||
supported_project_types: ['modpack'],
|
||||
display: 'all',
|
||||
query_param: 'g',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
searchable: false,
|
||||
options: tags.value.loaders
|
||||
.filter((loader) => loader.supported_project_types.includes('modpack'))
|
||||
@@ -363,7 +365,7 @@ export function useSearch(
|
||||
display: 'expandable',
|
||||
default_values: DEFAULT_PLUGIN_LOADERS,
|
||||
query_param: 'g',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
searchable: false,
|
||||
options: tags.value.loaders
|
||||
.filter(
|
||||
@@ -392,7 +394,7 @@ export function useSearch(
|
||||
supported_project_types: ['plugin'],
|
||||
display: 'all',
|
||||
query_param: 'g',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
searchable: false,
|
||||
options: tags.value.loaders
|
||||
.filter((loader) => PLUGIN_PLATFORMS.includes(loader.name))
|
||||
@@ -416,7 +418,7 @@ export function useSearch(
|
||||
),
|
||||
supported_project_types: ['shader'],
|
||||
query_param: 'g',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
searchable: false,
|
||||
display: 'expandable',
|
||||
default_values: DEFAULT_SHADER_LOADERS,
|
||||
@@ -439,7 +441,7 @@ export function useSearch(
|
||||
),
|
||||
supported_project_types: ['mod', 'modpack', 'resourcepack', 'shader', 'plugin', 'datapack'],
|
||||
query_param: 'l',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
display: 'all',
|
||||
searchable: false,
|
||||
options: [
|
||||
@@ -466,7 +468,7 @@ export function useSearch(
|
||||
),
|
||||
supported_project_types: ALL_PROJECT_TYPES,
|
||||
query_param: 'pid',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
display: 'none',
|
||||
searchable: false,
|
||||
options: [],
|
||||
@@ -481,8 +483,9 @@ export function useSearch(
|
||||
}),
|
||||
),
|
||||
supported_project_types: ['mod', 'plugin', 'datapack'],
|
||||
display: 'toggle',
|
||||
display: 'all',
|
||||
query_param: 'a',
|
||||
supports: ['exclude'],
|
||||
searchable: false,
|
||||
ordering: -1000,
|
||||
options: excludeableProjectTypes.map((target) => ({
|
||||
|
||||
@@ -157,7 +157,7 @@ export function useServerSearch(opts: {
|
||||
supported_project_types: ['server'],
|
||||
display: 'all',
|
||||
query_param: 'sc',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
searchable: false,
|
||||
options: [],
|
||||
}
|
||||
@@ -204,7 +204,7 @@ export function useServerSearch(opts: {
|
||||
supported_project_types: ['server'],
|
||||
display: 'all',
|
||||
query_param: 'sct',
|
||||
supports_negative_filter: false,
|
||||
supports: ['include'],
|
||||
searchable: false,
|
||||
options: [
|
||||
{
|
||||
@@ -250,7 +250,7 @@ export function useServerSearch(opts: {
|
||||
supported_project_types: ['server'],
|
||||
display: 'scrollable',
|
||||
query_param: 'sgv',
|
||||
supports_negative_filter: false,
|
||||
supports: ['include'],
|
||||
searchable: true,
|
||||
options: (tags.value?.gameVersions ?? []).map((gv) => ({
|
||||
id: gv.version,
|
||||
@@ -271,7 +271,7 @@ export function useServerSearch(opts: {
|
||||
supported_project_types: ['server'],
|
||||
display: 'all',
|
||||
query_param: 'sr',
|
||||
supports_negative_filter: true,
|
||||
supports: ['include', 'exclude'],
|
||||
searchable: false,
|
||||
options: sortedRegions.map(([code, name]) => ({
|
||||
id: code,
|
||||
@@ -291,7 +291,7 @@ export function useServerSearch(opts: {
|
||||
supported_project_types: ['server'],
|
||||
display: 'scrollable',
|
||||
query_param: 'sl',
|
||||
supports_negative_filter: false,
|
||||
supports: ['include'],
|
||||
searchable: true,
|
||||
options: sortedLanguages.map(([code, name]) => ({
|
||||
id: code,
|
||||
@@ -312,7 +312,7 @@ export function useServerSearch(opts: {
|
||||
supported_project_types: ['server'],
|
||||
display: 'all',
|
||||
query_param: 'sst',
|
||||
supports_negative_filter: false,
|
||||
supports: ['include'],
|
||||
searchable: false,
|
||||
options: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user