bunch more fixes lol

This commit is contained in:
Prospector
2026-06-24 00:20:02 -07:00
parent 14c728c8e3
commit 623e01dbff
10 changed files with 315 additions and 97 deletions
@@ -36,7 +36,7 @@ const { hierarchicalSidebarAvailable } = injectPageContext()
</template>
<style scoped>
.ui-normal-page {
@apply grid gap-6 mx-auto py-4;
@apply grid gap-x-6 gap-y-4 mx-auto py-4;
width: min(calc(100% - 2rem), calc(80rem - 3rem));
grid-template:
@@ -9,12 +9,13 @@ defineProps<{
</script>
<template>
<div
class="flex flex-col gap-3 p-4"
class="flex flex-col gap-3 p-4 pt-3"
:class="{
'card-shadow mb-4 last:mb-0 rounded-2xl bg-bg-raised': !hierarchicalSidebarAvailable,
'card-shadow mb-4 last:mb-0 rounded-2xl bg-surface-3 border border-solid border-surface-4':
!hierarchicalSidebarAvailable,
}"
>
<span class="font-semibold">{{ title }}</span>
<h2 class="text-lg font-semibold m-0">{{ title }}</h2>
<slot />
</div>
</template>
@@ -10,10 +10,10 @@
>
<Avatar :src="organization.icon_url" :alt="organization.name" size="32px" />
<div class="flex flex-col flex-nowrap justify-center">
<span class="group-hover:underline font-normal text-contrast">
<span class="group-hover:underline font-medium">
{{ organization.name }}
</span>
<span class="text-sm font-normal flex items-center gap-1"
<span class="text-sm font-normal text-secondary flex items-center gap-1"
><OrganizationIcon /> {{ formatMessage(messages.organization) }}</span
>
</div>
@@ -30,9 +30,7 @@
<Avatar :src="member.user.avatar_url" :alt="member.user.username" size="32px" circle />
<div class="flex flex-col">
<span class="flex w-full flex-nowrap items-center gap-1 group-hover:underline">
<span class="min-w-0 overflow-hidden font-normal text-contrast truncate">{{
member.user.username
}}</span>
<span class="min-w-0 overflow-hidden truncate">{{ member.user.username }}</span>
<CrownIcon
v-if="member.is_owner"
v-tooltip="formatMessage(messages.owner)"
@@ -40,7 +38,7 @@
/>
<ExternalIcon v-if="linkTarget === '_blank'" />
</span>
<span class="text-sm font-normal">{{ member.role }}</span>
<span class="text-sm font-normal text-secondary">{{ member.role }}</span>
</div>
</AutoLink>
</div>
@@ -483,7 +483,7 @@ const earnedBadges = computed(() => {
<template>
<div v-if="earnedBadges.length > 0 || !!downloadsBadge" class="flex flex-col">
<h2 class="text-lg text-contrast m-0 mb-2">
<h2 class="text-lg font-semibold text-contrast m-0 mb-2">
{{ formatMessage(messages.title) }}
</h2>
<div class="grid grid-cols-[repeat(auto-fill,minmax(64px,1fr))] gap-2">
@@ -1,7 +1,12 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { DownloadIcon, FileIcon, SearchIcon } from '@modrinth/assets'
import { capitalizeString, renderHighlightedString } from '@modrinth/utils'
import { DownloadIcon, ExternalIcon, FileIcon, SearchIcon } from '@modrinth/assets'
import {
capitalizeString,
formatVersionsForDisplay,
type GameVersionTag,
renderHighlightedString,
} from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
@@ -11,6 +16,7 @@ import { useCompactNumber, useFormatNumber } from '#ui/composables/format-number
import { useRelativeTime } from '#ui/composables/how-ago.ts'
import { defineMessage, defineMessages, useVIntl } from '#ui/composables/i18n.ts'
import { injectModrinthClient } from '#ui/providers/api-client.ts'
import { injectTags } from '#ui/providers/tags.ts'
import {
commonMessages,
fileTypeMessages,
@@ -41,12 +47,23 @@ const { formatCompactNumber } = useCompactNumber()
const props = defineProps<{
version: Labrinth.Versions.v3.Version
enrichment?: Labrinth.Projects.v2.DependencyInfo
enrichmentLoading?: boolean
dependencyLinkCreator: (context: DependencyContext) => string | undefined
members?: Labrinth.Projects.v3.TeamMember[]
userLinkCreator?: (user: Labrinth.Users.v3.User) => string | undefined
}>()
const api = injectModrinthClient()
const tags = injectTags(null)
const gameVersionsToDisplay = computed(() =>
tags?.gameVersions.value?.length
? formatVersionsForDisplay(
props.version.game_versions,
tags.gameVersions.value as GameVersionTag[],
)
: props.version.game_versions,
)
const versionNumber = computed(() => props.version.version_number)
const versionSubtitle = computed(() => props.version.name)
@@ -101,22 +118,82 @@ const optionalContent = computed(() =>
const incompatibleContent = computed(() =>
dependencies.value.filter((dep) => dep.dependency.dependency_type === 'incompatible'),
)
const includedContent = computed(() =>
dependencies.value
.filter((context) => context.dependency.dependency_type === 'embedded')
.map((context) => ({
icon_url: context.project
? context.project.icon_url
: context.dependency.attribution?.icon_url,
name: context.project ? context.project.title : context.dependency.file_name,
version: context.version ? context.version.version_number : undefined,
link: context.project
? props.dependencyLinkCreator(context)
: context.dependency.attribution?.link,
hasProject: !!context.project,
})),
const embeddedDependencies = computed(() =>
dependencies.value.filter((context) => context.dependency.dependency_type === 'embedded'),
)
const includedContentLoading = computed(
() =>
!!props.enrichmentLoading &&
embeddedDependencies.value.some((context) => context.dependency.project_id && !context.project),
)
function getEmbeddedDependencyId(context: DependencyContext) {
return (
context.dependency.project_id ??
context.dependency.file_name ??
context.dependency.version_id ??
'included'
)
}
type IncludedContentLoadingRow = { loading: true; id: string }
type IncludedContentRow = {
id: string
icon_url?: string
name?: string
version?: string
link?: string
hasProject: boolean
}
function isIncludedContentLoadingRow(
row: IncludedContentLoadingRow | IncludedContentRow,
): row is IncludedContentLoadingRow {
return 'loading' in row
}
const includedContent = computed((): (IncludedContentLoadingRow | IncludedContentRow)[] => {
if (includedContentLoading.value) {
return embeddedDependencies.value.map((context) => ({
loading: true as const,
id: getEmbeddedDependencyId(context),
}))
}
return embeddedDependencies.value
.map((context) => {
const resolution = context.dependency.attribution?.resolution
return {
id: getEmbeddedDependencyId(context),
icon_url: context.project
? context.project.icon_url
: context.dependency.attribution?.flame_project?.icon_url,
name: context.project ? context.project.title : context.dependency.file_name,
version: context.version ? context.version.version_number : undefined,
link: context.project
? props.dependencyLinkCreator(context)
: resolution && 'link_to_work' in resolution
? resolution.link_to_work
: undefined,
hasProject: !!context.project,
}
})
.sort((a, b) => {
const priority = (row: typeof a) => {
if (row.hasProject) return 0
if (row.link) return 1
return 2
}
const priorityDiff = priority(a) - priority(b)
if (priorityDiff !== 0) {
return priorityDiff
}
const sortName = (name: string | undefined) => (name ?? '').replace(/[^a-zA-Z0-9]/g, '')
return sortName(a.name).localeCompare(sortName(b.name))
})
})
const contentTableColumns = computed(() => {
const cols = [
{
@@ -128,7 +205,11 @@ const contentTableColumns = computed(() => {
label: formatMessage(defineMessage({ id: 'version.content.name', defaultMessage: 'Name' })),
},
]
if (includedContent.value.some((x) => x.version)) {
if (
includedContentLoading.value
? embeddedDependencies.value.some((context) => context.dependency.version_id)
: includedContent.value.some((row) => 'version' in row && row.version)
) {
cols.push({
key: 'version',
label: formatMessage(
@@ -318,7 +399,7 @@ const authorLink = computed(() =>
{{ formatMessage(projectCompatibilityMessages.minecraftJava) }}
<div class="flex gap-1 flex-wrap mt-2">
<TagItem
v-for="gameVersion in version.game_versions"
v-for="gameVersion in gameVersionsToDisplay"
:key="`version-compat-game-version-${gameVersion}`"
>{{ gameVersion }}</TagItem
>
@@ -414,7 +495,7 @@ const authorLink = computed(() =>
/>
</div>
</section>
<section v-if="includedContent?.length > 0" id="content">
<section v-if="embeddedDependencies.length > 0" id="content">
<h3 class="mt-0 mb-2 text-lg font-semibold">
{{
formatMessage(
@@ -422,36 +503,60 @@ const authorLink = computed(() =>
)
}}
</h3>
<Table :columns="contentTableColumns" :data="includedContent">
<Table :columns="contentTableColumns" :data="includedContent" row-key="id">
<template #header-actions>
<StyledInput
v-model="contentSearchQuery"
:icon="SearchIcon"
:placeholder="formatMessage(messages.searchContent)"
:disabled="includedContentLoading"
clearable
wrapper-class="w-full sm:w-64"
/>
</template>
<template #cell-icon="{ row }">
<AutoLink :to="row.link" tabindex="-1" class="flex" target="_blank">
<Avatar v-if="row.icon_url" :src="row.icon_url" alt="" size="2rem" />
<div
v-if="isIncludedContentLoadingRow(row)"
class="size-[2rem] shrink-0 rounded-[16%] bg-surface-3 animate-pulse"
/>
<AutoLink v-else :to="row.link" tabindex="-1" class="flex" target="_blank">
<Avatar v-if="row.hasProject || row.icon_url" :src="row.icon_url" alt="" size="2rem" />
<div v-else class="size-[2rem] flex items-center justify-center">
<FileIcon class="size-5 text-secondary" />
</div>
</AutoLink>
</template>
<template #cell-name="{ row }">
<div
v-if="isIncludedContentLoadingRow(row)"
class="h-4 max-w-[12rem] rounded-lg bg-surface-3 animate-pulse"
/>
<AutoLink
v-else
:to="row.link"
class="flex w-fit"
link-class="hover:underline hover:text-contrast"
target="_blank"
>
{{ row.name }}
<ExternalIcon v-if="row.link?.startsWith('http')" class="shrink-0 ml-2" />
</AutoLink>
</template>
<template #cell-version="{ row }">
{{ (row.version ?? row.hasProject) ? formatMessage(commonMessages.unknownLabel) : '—' }}
<div
v-if="isIncludedContentLoadingRow(row)"
class="h-4 w-16 rounded-lg bg-surface-3 animate-pulse"
/>
<AutoLink
v-else
:to="row.version ? row.link : undefined"
class="flex w-fit"
tabindex="-1"
link-class="hover:underline hover:text-contrast"
target="_blank"
>
{{ row.version ?? '—' }}
</AutoLink>
</template>
</Table>
</section>