Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
73b3871
feat: add norisk client source support (#6707)
orb1n Jul 13, 2026
3ac3b95
changelog
Prospector Jul 13, 2026
39b4cf6
feat: implement depends on search filter
tdgao Jul 13, 2026
b574d32
Merge branch 'main' into truman/dependents-search-in-discovery
tdgao Jul 14, 2026
94f74b6
feat: add modpack "includes content" filter and update depends on
tdgao Jul 14, 2026
1f66ea1
feat: style pass
tdgao Jul 14, 2026
5c0061a
Merge branch 'main' into truman/dependents-search-in-discovery
tdgao Jul 27, 2026
26b90ae
feat: show included modpack projects filter tag better
tdgao Jul 27, 2026
63741f6
pnpm prepr
tdgao Jul 27, 2026
21c11b8
Merge branch 'main' into truman/dependents-search-in-discovery
tdgao Aug 14, 2026
e2d5d4d
feat: polish depends on filter
tdgao Aug 14, 2026
ee74727
fix: clear button, page scroll, and metadata loading state
tdgao Aug 14, 2026
3ce9c6d
feat: hook up excludes modpack
tdgao Aug 14, 2026
e51529c
fix: combobox closing race
tdgao Aug 14, 2026
76dab13
Revert "fix: combobox closing race"
tdgao Aug 14, 2026
ca37f94
fix scrolling after applying depends on
tdgao Aug 14, 2026
4101d01
Merge branch 'main' into truman/dependents-search-in-discovery
tdgao Aug 20, 2026
71260bf
feedback
tdgao Aug 20, 2026
1dd1804
pnpm prepr
tdgao Aug 20, 2026
79a3b1f
slightly make buttons larger for touch devices
tdgao Aug 20, 2026
0e8cba7
feat: add view dependents/view modpacks in project overflow menu
tdgao Aug 20, 2026
0bae0ae
pnpm prepr
tdgao Aug 20, 2026
65dfe63
fix: divider
tdgao Aug 20, 2026
73701c7
add icon
tdgao Aug 20, 2026
e9a0858
fix: project search combobox background in app
tdgao Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -3494,6 +3494,15 @@
"project.actions.servers-promo.title": {
"message": "Create a server"
},
"project.actions.view-dependents": {
"message": "View dependents"
},
"project.actions.view-modpacks": {
"message": "View modpacks"
},
"project.actions.view-project-type-dependents": {
"message": "View {projectType} dependents"
},
"project.ai-image-warning-modal.body.1": {
"message": "Using AI-generated images to represent your project is not allowed. Attempting to work around detection may lead to your Modrinth account being suspended."
},
Expand Down
79 changes: 78 additions & 1 deletion apps/frontend/src/pages/[type]/[project].vue
Original file line number Diff line number Diff line change
Expand Up @@ -564,10 +564,12 @@ import {
HeartIcon,
LeftArrowIcon,
MoreVerticalIcon,
PackageSearchIcon,
PlayIcon,
ReportIcon,
ScaleIcon,
ScanEyeIcon,
SearchIcon,
ServerPlusIcon,
SettingsIcon,
XIcon,
Expand All @@ -582,6 +584,8 @@ import {
ButtonLink,
commonMessages,
defineMessages,
formatDependencyProjectFilterOption,
formatProjectTypeSentence,
getActiveDisclosures,
IconButton,
injectModrinthClient,
Expand Down Expand Up @@ -846,6 +850,18 @@ const messages = defineMessages({
id: 'project.actions.review-project',
defaultMessage: 'Review project',
},
viewDependents: {
id: 'project.actions.view-dependents',
defaultMessage: 'View dependents',
},
viewProjectTypeDependents: {
id: 'project.actions.view-project-type-dependents',
defaultMessage: 'View {projectType} dependents',
},
viewModpacks: {
id: 'project.actions.view-modpacks',
defaultMessage: 'View modpacks',
},
rescanModpack: {
id: 'project.actions.rescan-modpack',
defaultMessage: 'Rescan modpack',
Expand Down Expand Up @@ -1904,6 +1920,30 @@ async function checkModpackArchives() {

const projectHeaderMoreActions = computed(() => {
const isStaff = !!(auth.value.user && tags.value.staffRoles.includes(auth.value.user.role))
const projectId = project.value?.id
const dependentSearchTypes = getDependentSearchTypes()
const dependentSearchActions = dependentSearchTypes
.filter((projectType) => projectType !== 'modpack')
.map((projectType) => ({
id: `view-${projectType}-dependents`,
label: formatMessage(
dependentSearchTypes.length === 1
? messages.viewDependents
: messages.viewProjectTypeDependents,
{
projectType: formatProjectTypeSentence(formatMessage, projectType),
},
),
icon: SearchIcon,
type: 'link',
to: {
path: `/discover/${projectType}s`,
query: {
dep: formatDependencyProjectFilterOption(projectId, ['required']),
},
},
}))
const isPluginOnly = dependentSearchTypes.length === 1 && dependentSearchTypes[0] === 'plugin'

return [
{
Expand All @@ -1914,7 +1954,21 @@ const projectHeaderMoreActions = computed(() => {
to: `${projectPath.value}/settings/analytics`,
shown: !!auth.value.user && !!currentMember.value,
},
{ type: 'divider', shown: !!auth.value.user && !!currentMember.value },
...dependentSearchActions,
{
id: 'view-modpacks',
label: formatMessage(messages.viewModpacks),
icon: PackageSearchIcon,
type: 'link',
to: {
path: '/discover/modpacks',
query: {
dep: formatDependencyProjectFilterOption(projectId, ['required']),
},
},
shown: !isPluginOnly && project.value?.actualProjectType !== 'modpack',
},
{ type: 'divider' },
{
id: 'moderation-checklist',
label: formatMessage(messages.reviewProject),
Expand Down Expand Up @@ -1977,6 +2031,29 @@ const projectHeaderMoreActions = computed(() => {
]
})

function getDependentSearchTypes() {
if (!project.value) return []

if (project.value.actualProjectType !== 'mod') {
return [isServerProject.value ? 'server' : project.value.actualProjectType]
}

const loaders = project.value.loaders ?? []
const projectTypes = []

if (loaders.some((loader) => tags.value.loaderData.modLoaders.includes(loader))) {
projectTypes.push('mod')
}
if (loaders.some((loader) => tags.value.loaderData.allPluginLoaders.includes(loader))) {
projectTypes.push('plugin')
}
if (loaders.some((loader) => tags.value.loaderData.dataPackLoaders.includes(loader))) {
projectTypes.push('datapack')
}

return projectTypes.length > 0 ? projectTypes : ['mod']
}

const createCanonicalUrl = () =>
project.value ? `https://modrinth.com/project/${project.value.id}` : undefined

Expand Down
2 changes: 2 additions & 0 deletions packages/assets/generated-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ import _PackageIcon from './icons/package.svg?component'
import _PackageClosedIcon from './icons/package-closed.svg?component'
import _PackageOpenIcon from './icons/package-open.svg?component'
import _PackagePlusIcon from './icons/package-plus.svg?component'
import _PackageSearchIcon from './icons/package-search.svg?component'
import _PageRoundIcon from './icons/page-round.svg?component'
import _PaintbrushIcon from './icons/paintbrush.svg?component'
import _PaletteIcon from './icons/palette.svg?component'
Expand Down Expand Up @@ -656,6 +657,7 @@ export const PackageIcon = _PackageIcon
export const PackageClosedIcon = _PackageClosedIcon
export const PackageOpenIcon = _PackageOpenIcon
export const PackagePlusIcon = _PackagePlusIcon
export const PackageSearchIcon = _PackageSearchIcon
export const PageRoundIcon = _PageRoundIcon
export const PaintbrushIcon = _PaintbrushIcon
export const PaletteIcon = _PaletteIcon
Expand Down
20 changes: 20 additions & 0 deletions packages/assets/icons/package-search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 23 additions & 5 deletions packages/ui/src/components/base/Combobox.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<template>
<div ref="containerRef" class="relative inline-block w-full">
<!-- Searchable mode: input trigger -->
<div v-if="searchable" class="relative w-full rounded-xl bg-surface-4">
<div
v-if="searchable"
class="relative w-full rounded-xl"
:class="{ 'bg-surface-4': searchInputVariant === 'surface' }"
>
<!--
Selection mirror: horizontal padding must match StyledInput (filled + left icon uses `pl-10`,
else `pl-3`) and `searchableInputClass` when the chevron is shown (`!pr-9`), or the overlay
else `pl-3`) and `resolvedSearchInputClass` when the chevron is shown (`!pr-9`), or the overlay
text will not line up with the transparent input text / caret.
-->
<div
Expand All @@ -24,20 +28,22 @@
:name="searchName"
:placeholder="searchPlaceholder || placeholder"
:disabled="disabled"
:clearable="clearable"
:autocomplete="searchAutocomplete"
:autocorrect="searchAutocorrect"
:autocapitalize="searchAutocapitalize"
:spellcheck="searchSpellcheck"
:inputmode="searchInputmode"
:input-attrs="searchInputAttrs"
wrapper-class="w-full !bg-transparent"
:input-class="searchableInputClass"
:input-class="resolvedSearchInputClass"
class="relative z-[1]"
@input="handleSearchInput"
@keydown="handleSearchKeydown"
@focusin="handleSearchFocus"
@focusout="handleSearchFocusout"
@click="handleSearchClick"
@clear="handleSearchClear"
>
<template v-if="showChevron" #right>
<ChevronLeftIcon
Expand Down Expand Up @@ -234,6 +240,8 @@ export interface ComboboxOption<T> {
searchTerms?: string[]
}

export type ComboboxSearchInputVariant = 'surface' | 'button'

type OverlayScrollbarsInstance = NonNullable<ReturnType<typeof OverlayScrollbars>>
type ViewportRect = {
width: number
Expand Down Expand Up @@ -274,6 +282,7 @@ const props = withDefaults(
placeholder?: string
disabled?: boolean
searchable?: boolean
clearable?: boolean
searchPlaceholder?: string
listbox?: boolean
showChevron?: boolean
Expand Down Expand Up @@ -311,11 +320,13 @@ const props = withDefaults(
searchAutocapitalize?: 'none' | 'off' | 'sentences' | 'words' | 'characters'
searchSpellcheck?: boolean
searchInputAttrs?: Record<string, string | number | boolean | undefined>
searchInputVariant?: ComboboxSearchInputVariant
}>(),
{
placeholder: 'Select an option',
disabled: false,
searchable: false,
clearable: false,
searchPlaceholder: 'Search...',
listbox: true,
showChevron: true,
Expand All @@ -327,6 +338,7 @@ const props = withDefaults(
selectSearchTextOnFocus: false,
showSearchIcon: false,
searchType: 'text',
searchInputVariant: 'surface',
triggerType: 'base',
triggerSize: 'md',
triggerInteraction: 'surface',
Expand Down Expand Up @@ -397,8 +409,8 @@ const searchSelectionOverlayVisible = computed(() => {
return true
})

const searchableInputClass = computed(() => {
const parts = ['!bg-transparent']
const resolvedSearchInputClass = computed(() => {
const parts = [props.searchInputVariant === 'button' ? '!bg-button-bg' : '!bg-transparent']
if (props.showChevron) parts.push('!pr-9')
if (searchSelectionOverlayVisible.value) {
parts.push('!text-transparent [caret-color:var(--color-text-primary)] selection:bg-transparent')
Expand Down Expand Up @@ -838,6 +850,12 @@ function handleSearchInput() {
}
}

function handleSearchClear() {
userHasTyped.value = true
emit('searchInput', searchQuery.value)
closeDropdown()
}

function handleSearchFocus(event: FocusEvent) {
const target = event.target
if (props.selectSearchTextOnFocus && target instanceof HTMLInputElement) {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export { default as Collapsible } from './Collapsible.vue'
export type { CollapsibleAdmonitionItem } from './CollapsibleAdmonition.vue'
export { default as CollapsibleAdmonition } from './CollapsibleAdmonition.vue'
export { default as CollapsibleRegion } from './CollapsibleRegion.vue'
export type { ComboboxOption } from './Combobox.vue'
export type { ComboboxOption, ComboboxSearchInputVariant } from './Combobox.vue'
export { default as Combobox } from './Combobox.vue'
export { default as CopyCode } from './CopyCode.vue'
export { default as DatePicker } from './DatePicker.vue'
Expand Down
34 changes: 30 additions & 4 deletions packages/ui/src/components/project/ProjectCombobox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
:no-options-message="searchLoading ? loadingMessage : noResultsMessage"
:disable-search-filter="true"
:disabled="disabled"
:clearable="clearable"
:search-input-variant="searchInputVariant"
:sync-with-selection="syncWithSelection"
:select-search-text-on-focus="selectSearchTextOnFocus"
show-icon-in-selected
@search-input="(query) => handleSearch(query)"
/>
Expand All @@ -20,7 +24,7 @@ import Fuse from 'fuse.js'
import { defineAsyncComponent, h, markRaw, ref, watch } from 'vue'
import { injectModrinthClient, injectNotificationManager } from '../../providers'
import type { ComboboxOption } from '../base/Combobox.vue'
import type { ComboboxOption, ComboboxSearchInputVariant } from '../base/Combobox.vue'
import Combobox from '../base/Combobox.vue'
export type ProjectType =
Expand All @@ -32,12 +36,13 @@ export type ProjectType =
| 'plugin'
| 'server'
interface SearchHit {
export interface SearchHit {
project_id: string
title: string
icon_url?: string
project_type: string
slug: string
author?: string
}
const props = withDefaults(
Expand All @@ -54,6 +59,14 @@ const props = withDefaults(
noResultsMessage?: string
/** Whether the combobox is disabled */
disabled?: boolean
/** Whether to show a button for clearing the search input */
clearable?: boolean
/** Visual treatment for the search input */
searchInputVariant?: ComboboxSearchInputVariant
/** Keep the selected project's label in the search input */
syncWithSelection?: boolean
/** Select all search text when the input receives focus */
selectSearchTextOnFocus?: boolean
/** Maximum number of results to show */
limit?: number
/** Project IDs to exclude from results */
Expand All @@ -69,6 +82,8 @@ const props = withDefaults(
loadingMessage: 'Loading...',
noResultsMessage: 'No results found',
disabled: false,
searchInputVariant: 'surface',
syncWithSelection: true,
limit: 20,
},
)
Expand All @@ -83,6 +98,10 @@ const searchResultsCache = ref<Map<string, SearchHit>>(new Map())
const { labrinth } = injectModrinthClient()
function isAllowedProjectType(projectType: string): boolean {
return !props.projectTypes || props.projectTypes.includes(projectType as ProjectType)
}
const userProjectHits = ref<SearchHit[]>([])
const userProjectsFuse = ref<Fuse<SearchHit> | null>(null)
Expand Down Expand Up @@ -163,7 +182,7 @@ watch(
} else {
try {
const project = await labrinth.projects_v2.get(newId)
if (project) {
if (project && isAllowedProjectType(project.project_type)) {
hit = {
project_id: project.id,
title: project.title,
Expand All @@ -178,6 +197,9 @@ watch(
return
}
}
if (hit && !isAllowedProjectType(hit.project_type)) {
hit = null
}
selectedProject.value = hit
if (hit && !options.value.some((o) => o.value === hit!.project_id)) {
Expand Down Expand Up @@ -220,7 +242,11 @@ const search = async (query: string) => {
const uniqueHits: SearchHit[] = []
for (const hit of allHits) {
if (!seenIds.has(hit.project_id) && !excludeSet.has(hit.project_id)) {
if (
isAllowedProjectType(hit.project_type) &&
!seenIds.has(hit.project_id) &&
!excludeSet.has(hit.project_id)
) {
seenIds.add(hit.project_id)
uniqueHits.push(hit)
searchResultsCache.value.set(hit.project_id, hit)
Expand Down
Loading
Loading