feat: better tooltips for mods in content tab hosting panel (#5679)

* feat: better tooltips for mods in content tab hosting panel

* feat: qa
This commit is contained in:
Calum H.
2026-03-26 22:55:08 +00:00
committed by GitHub
parent ef1ffa6577
commit 4394092928
17 changed files with 223 additions and 86 deletions

View File

@@ -25,7 +25,12 @@ import { useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages'
import { truncatedTooltip } from '#ui/utils/truncate'
import type { ContentCardProject, ContentCardVersion, ContentOwner } from '../types'
import type {
ClientWarningType,
ContentCardProject,
ContentCardVersion,
ContentOwner,
} from '../types'
const { formatMessage } = useVIntl()
@@ -39,6 +44,8 @@ interface Props {
installing?: boolean
hasUpdate?: boolean
isClientOnly?: boolean
clientWarning?: ClientWarningType | null
hideSwitchVersion?: boolean
overflowOptions?: OverflowMenuOption[]
disabled?: boolean
showCheckbox?: boolean
@@ -55,6 +62,8 @@ const props = withDefaults(defineProps<Props>(), {
installing: false,
hasUpdate: false,
isClientOnly: false,
clientWarning: null,
hideSwitchVersion: false,
overflowOptions: undefined,
disabled: false,
showCheckbox: false,
@@ -83,6 +92,17 @@ const fileNameRef = ref<HTMLElement | null>(null)
const isDisabled = computed(() => props.disabled || props.installing)
const clientWarningMessage = computed(() => {
switch (props.clientWarning) {
case 'retained':
return commonMessages.clientRetainedWarning
case 'depends':
return commonMessages.clientDependsWarning
default:
return commonMessages.clientOnlyWarning
}
})
const { shift: shiftHeld } = useMagicKeys()
const deleteHovered = ref(false)
</script>
@@ -147,7 +167,7 @@ const deleteHovered = ref(false)
<TriangleAlertIcon class="size-4 shrink-0 text-orange" />
<template #popper>
<div class="max-w-[18rem] text-sm">
{{ formatMessage(commonMessages.clientOnlyWarning) }}
{{ formatMessage(clientWarningMessage) }}
</div>
</template>
</Tooltip>
@@ -260,7 +280,11 @@ const deleteHovered = ref(false)
<DownloadIcon class="size-5" />
</button>
</ButtonStyled>
<ButtonStyled v-else-if="hasSwitchVersionListener && version" circular type="transparent">
<ButtonStyled
v-else-if="hasSwitchVersionListener && version && !hideSwitchVersion"
circular
type="transparent"
>
<button
v-tooltip="formatMessage(commonMessages.switchVersionButton)"
:disabled="isDisabled"

View File

@@ -276,6 +276,8 @@ function handleSort(column: ContentCardTableSortColumn) {
:installing="item.installing"
:has-update="item.hasUpdate"
:is-client-only="item.isClientOnly"
:client-warning="item.clientWarning"
:hide-switch-version="item.hideSwitchVersion"
:overflow-options="item.overflowOptions"
:disabled="item.disabled"
:show-checkbox="showSelection"
@@ -329,6 +331,8 @@ function handleSort(column: ContentCardTableSortColumn) {
:enabled="item.enabled"
:installing="item.installing"
:has-update="item.hasUpdate"
:is-client-only="item.isClientOnly"
:client-warning="item.clientWarning"
:overflow-options="item.overflowOptions"
:disabled="item.disabled"
:show-checkbox="showSelection"

View File

@@ -138,19 +138,28 @@ onUnmounted(() => {
class="@container flex flex-col gap-4 rounded-[20px] bg-bg-raised p-6 shadow-md"
:class="{ 'opacity-50': disabled }"
>
<div class="flex flex-wrap items-start justify-between gap-4">
<div class="flex min-w-0 flex-1 items-start gap-4">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex min-w-0 flex-1 items-center gap-4">
<AutoLink :to="projectLink" class="shrink-0">
<Avatar :src="project.icon_url" :alt="project.title" size="5rem" no-shadow raised />
</AutoLink>
<div class="flex flex-col gap-1.5">
<AutoLink
:to="projectLink"
class="text-xl font-semibold leading-8 text-contrast hover:underline"
<div class="flex min-w-0 flex-col gap-1.5">
<div class="flex min-w-0 flex-col">
<AutoLink
:to="projectLink"
class="truncate text-xl font-semibold text-contrast"
:class="projectLink ? 'hover:underline' : ''"
>
{{ project.title }}
</AutoLink>
<span v-if="project.filename" class="truncate text-secondary mb-2">
{{ project.filename }}
</span>
</div>
<div
v-if="owner || version"
class="flex flex-nowrap items-center gap-2 overflow-hidden text-secondary"
>
{{ project.title }}
</AutoLink>
<div class="flex flex-nowrap items-center gap-2 overflow-hidden text-secondary">
<AutoLink
v-if="owner"
:to="owner.link"
@@ -346,13 +355,16 @@ onUnmounted(() => {
{{ project.description }}
</span>
<div class="flex flex-wrap items-center gap-3">
<div v-if="project.downloads !== undefined" class="flex items-center gap-2 text-secondary">
<div
v-if="project.downloads != null || project.followers != null || categories?.length"
class="flex flex-wrap items-center gap-3"
>
<div v-if="project.downloads != null" class="flex items-center gap-2 text-secondary">
<DownloadIcon class="size-5" />
<span class="font-medium">{{ formatCompact(project.downloads) }}</span>
</div>
<div v-if="project.followers !== undefined" class="flex items-center gap-2 text-secondary">
<div v-if="project.followers != null" class="flex items-center gap-2 text-secondary">
<HeartIcon class="size-5" />
<span class="font-medium">{{ formatCompact(project.followers) }}</span>
</div>

View File

@@ -25,7 +25,7 @@ import {
normalizeProjectType,
} from '#ui/utils/common-messages'
import { isClientOnlyEnvironment } from '../../composables/content-filtering'
import { getClientWarningType, isClientOnlyEnvironment } from '../../composables/content-filtering'
import type { ContentCardTableItem, ContentItem } from '../../types'
import ContentCardTable from '../ContentCardTable.vue'
import ContentSelectionBar from '../ContentSelectionBar.vue'
@@ -239,7 +239,11 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
}
: undefined,
...(props.enableToggle ? { enabled: item.enabled } : {}),
isClientOnly: isClientOnlyEnvironment(item.environment),
isClientOnly:
isClientOnlyEnvironment(item.environment) ||
!!item.pack_client_retained ||
!!item.pack_client_depends,
clientWarning: getClientWarningType(item),
disabled: disabledIds.value.has(item.file_name),
overflowOptions: [
...(props.switchVersion

View File

@@ -5,7 +5,7 @@ import { computed, ref, watch } from 'vue'
import { useVIntl } from '#ui/composables/i18n'
import { commonProjectTypeCategoryMessages, normalizeProjectType } from '#ui/utils/common-messages'
import type { ContentItem } from '../types'
import type { ClientWarningType, ContentItem } from '../types'
const CLIENT_ONLY_ENVIRONMENTS = new Set(['client_only', 'singleplayer_only'])
@@ -13,6 +13,13 @@ export function isClientOnlyEnvironment(env?: string | null): boolean {
return !!env && CLIENT_ONLY_ENVIRONMENTS.has(env)
}
export function getClientWarningType(item: ContentItem): ClientWarningType | null {
if (item.pack_client_retained) return 'retained'
if (item.pack_client_depends) return 'depends'
if (isClientOnlyEnvironment(item.environment)) return 'environment'
return null
}
export interface ContentFilterOption {
id: string
label: string
@@ -55,10 +62,7 @@ export function useContentFilters(items: Ref<ContentItem[]>, config?: ContentFil
options.push({ id: 'updates', label: 'Updates' })
}
if (
config?.showClientOnlyFilter &&
items.value.some((m) => isClientOnlyEnvironment(m.environment))
) {
if (config?.showClientOnlyFilter && items.value.some((m) => getClientWarningType(m) !== null)) {
options.push({ id: 'client-only', label: 'Client-only' })
}
@@ -102,7 +106,7 @@ export function useContentFilters(items: Ref<ContentItem[]>, config?: ContentFil
for (const filter of activeAttributes) {
if (filter === 'updates' && !item.has_update) return false
if (filter === 'disabled' && item.enabled) return false
if (filter === 'client-only' && !isClientOnlyEnvironment(item.environment)) return false
if (filter === 'client-only' && getClientWarningType(item) === null) return false
}
return true

View File

@@ -40,6 +40,7 @@ import ConfirmBulkUpdateModal from './components/modals/ConfirmBulkUpdateModal.v
import ConfirmDeletionModal from './components/modals/ConfirmDeletionModal.vue'
import ConfirmUnlinkModal from './components/modals/ConfirmUnlinkModal.vue'
import {
getClientWarningType,
isClientOnlyEnvironment,
useBulkOperation,
useChangingItems,
@@ -279,7 +280,12 @@ const tableItems = computed<ContentCardTableItem[]>(() => {
item.installing === true,
installing: item.installing === true,
hasUpdate: item.has_update,
isClientOnly: isClientOnlyEnvironment(item.environment),
isClientOnly:
isClientOnlyEnvironment(item.environment) ||
!!item.pack_client_retained ||
!!item.pack_client_depends,
clientWarning: getClientWarningType(item),
hideSwitchVersion: !base.versionLink,
overflowOptions: ctx.getOverflowOptions?.(item),
}
})

View File

@@ -21,6 +21,8 @@ export interface ContentOwner {
link?: string | RouteLocationRaw | (() => void)
}
export type ClientWarningType = 'retained' | 'depends' | 'environment'
export interface ContentCardTableItem {
id: string
project: ContentCardProject
@@ -33,6 +35,8 @@ export interface ContentCardTableItem {
installing?: boolean
hasUpdate?: boolean
isClientOnly?: boolean
clientWarning?: ClientWarningType | null
hideSwitchVersion?: boolean
overflowOptions?: OverflowMenuOption[]
}
@@ -53,13 +57,19 @@ export interface ContentItem extends Omit<
update_version_id: string | null
date_added?: string
environment?: string
pack_client_retained?: boolean
pack_client_depends?: boolean
installing?: boolean
}
export type ContentModpackCardProject = Pick<
Labrinth.Projects.v2.Project,
'id' | 'slug' | 'title' | 'icon_url' | 'description' | 'downloads' | 'followers'
>
'id' | 'slug' | 'title' | 'icon_url' | 'description'
> & {
downloads?: number | null
followers?: number | null
filename?: string | null
}
export type ContentModpackCardVersion = Pick<
Labrinth.Versions.v2.Version,