fix: header issues

This commit is contained in:
Calum H. (IMB11)
2026-06-26 17:09:22 +01:00
parent 8fca23e66a
commit b72c319569
6 changed files with 257 additions and 74 deletions
@@ -4,7 +4,7 @@
<template #icon>
<ServerIcon
:image="headerImage"
:class="isNuxt ? 'size-20 !rounded-2xl' : 'size-16 !rounded-xl'"
:class="isNuxt ? 'size-15 !rounded-2xl' : 'size-15 !rounded-xl'"
/>
</template>
<template #title>
@@ -0,0 +1,127 @@
<template>
<div class="w-full flex flex-col gap-4" :class="{ 'mt-4': isNuxt }">
<ContentPageHeader :class="props.headerClass">
<template #icon>
<div class="flex size-16 shrink-0 items-center justify-center">
<ButtonStyled circular size="large">
<button
type="button"
:aria-label="props.backLabel"
@click="router.push(props.backHref)"
>
<LeftArrowIcon aria-hidden="true" />
</button>
</ButtonStyled>
</div>
</template>
<template #title>
{{ props.name || props.fallbackName }}
</template>
<template #stats>
<div v-if="headerMetadata.length" class="flex min-w-0 flex-wrap items-center gap-2">
<template v-for="(item, index) in headerMetadata" :key="item.id">
<BulletDivider v-if="index > 0" />
<div class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap">
<component
:is="item.icon"
class="flex size-5 shrink-0"
aria-hidden="true"
v-bind="item.iconProps"
/>
<span class="truncate">{{ item.label }}</span>
</div>
</template>
</div>
</template>
<template #actions>
<slot name="actions" />
</template>
</ContentPageHeader>
</div>
</template>
<script setup lang="ts">
import { NuxtModrinthClient } from '@modrinth/api-client'
import { LeftArrowIcon, TagCategoryGamepad2Icon as Gamepad2Icon, TimerIcon } from '@modrinth/assets'
import { type Component, computed } from 'vue'
import { useRouter } from 'vue-router'
import BulletDivider from '#ui/components/base/BulletDivider.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import ContentPageHeader from '#ui/components/base/ContentPageHeader.vue'
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
import { injectModrinthClient } from '#ui/providers'
import { formatLoaderLabel } from '#ui/utils/loaders'
type MetadataItem = {
id: string
label: string
icon: Component
iconProps?: Record<string, unknown>
}
const props = withDefaults(
defineProps<{
name?: string | null
metadataItems?: MetadataItem[]
gameVersion?: string | null
loader?: string | null
loaderVersion?: string | null
lastActive?: string | null
backHref: string
backLabel: string
fallbackName?: string
headerClass?: string
}>(),
{
name: null,
metadataItems: () => [],
gameVersion: null,
loader: null,
loaderVersion: null,
lastActive: null,
fallbackName: 'World',
headerClass: '',
},
)
const client = injectModrinthClient()
const router = useRouter()
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
const loaderLabel = computed(() => {
if (!props.loader) return null
const label = formatLoaderLabel(props.loader.toLowerCase())
return [label, props.loaderVersion].filter(Boolean).join(' ')
})
const headerMetadata = computed<MetadataItem[]>(() => {
if (props.metadataItems.length) return props.metadataItems
const items: MetadataItem[] = []
if (props.gameVersion) {
items.push({
id: 'game-version',
label: props.gameVersion,
icon: Gamepad2Icon,
})
}
if (props.loader && loaderLabel.value) {
items.push({
id: 'loader',
label: loaderLabel.value,
icon: LoaderIcon,
iconProps: {
loader: formatLoaderLabel(props.loader.toLowerCase()),
},
})
}
if (props.lastActive) {
items.push({
id: 'last-active',
label: props.lastActive,
icon: TimerIcon,
})
}
return items
})
</script>
@@ -1,2 +1,3 @@
export { default as PanelServerActionButton } from './PanelServerActionButton.vue'
export { default as ServerManageHeader } from './ServerManageHeader.vue'
export { default as WorldManageHeader } from './WorldManageHeader.vue'
@@ -1,42 +1,27 @@
<template>
<div class="flex min-h-[36rem] flex-col gap-6 text-primary">
<div class="flex flex-col gap-2">
<RouterLink
:to="instancesPath"
class="flex w-fit items-center gap-1 text-base font-medium text-blue hover:underline"
>
<ChevronLeftIcon class="size-4" aria-hidden="true" />
{{ formatMessage(messages.allInstances) }}
</RouterLink>
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="flex min-w-0 flex-col gap-1">
<h1 class="m-0 truncate text-2xl font-semibold leading-8 text-contrast">
{{ worldName }}
</h1>
<div class="flex flex-wrap items-center gap-2 text-base font-medium text-secondary">
<template v-for="(item, index) in worldMetadata" :key="item">
<span>{{ item }}</span>
<BulletDivider v-if="index < worldMetadata.length - 1" />
</template>
</div>
</div>
<div class="flex shrink-0 items-center gap-2">
<PanelServerActionButton size="large" start-label="Start instance" />
<ButtonStyled size="large" circular>
<button
v-tooltip="formatMessage(messages.instanceSettings)"
@click="openServerSettings({ tabId: 'installation' })"
>
<SettingsIcon aria-hidden="true" />
</button>
</ButtonStyled>
</div>
</div>
</div>
<div class="h-px w-full bg-surface-5" />
<div class="flex min-h-[36rem] flex-col gap-4 text-primary">
<WorldManageHeader
:name="worldName"
:game-version="gameVersion"
:loader="loader"
:loader-version="loaderVersion"
:last-active="lastActiveLabel"
:back-href="instancesPath"
:back-label="formatMessage(messages.allInstances)"
:fallback-name="formatMessage(messages.worldFallbackName)"
>
<template #actions>
<PanelServerActionButton size="large" start-label="Start instance" />
<ButtonStyled size="large" circular>
<button
v-tooltip="formatMessage(messages.instanceSettings)"
@click="openServerSettings({ tabId: 'installation' })"
>
<SettingsIcon aria-hidden="true" />
</button>
</ButtonStyled>
</template>
</WorldManageHeader>
<NavTabs :links="worldTabLinks" replace />
@@ -46,21 +31,14 @@
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import {
BoxesIcon,
ChevronLeftIcon,
DatabaseBackupIcon,
FolderOpenIcon,
SettingsIcon,
} from '@modrinth/assets'
import { BoxesIcon, DatabaseBackupIcon, FolderOpenIcon, SettingsIcon } from '@modrinth/assets'
import { useQuery } from '@tanstack/vue-query'
import { computed, watch } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { useRouter } from 'vue-router'
import BulletDivider from '#ui/components/base/BulletDivider.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import NavTabs from '#ui/components/base/NavTabs.vue'
import { PanelServerActionButton } from '#ui/components/servers/server-header'
import { PanelServerActionButton, WorldManageHeader } from '#ui/components/servers/server-header'
import { useRelativeTime } from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import {
@@ -68,7 +46,6 @@ import {
injectModrinthServerContext,
injectServerSettingsModal,
} from '#ui/providers'
import { formatLoaderLabel } from '#ui/utils/loaders'
interface Tab {
label: string
@@ -138,24 +115,18 @@ const worldName = computed(
() => currentWorld.value?.name ?? server.value?.name ?? formatMessage(messages.worldFallbackName),
)
const worldMetadata = computed(() =>
[gameVersionLabel.value, loaderLabel.value, lastActiveLabel.value].filter(
(item): item is string => !!item,
),
const gameVersion = computed(() => {
const version = currentWorld.value?.content?.game_version ?? server.value?.mc_version
return version ?? null
})
const loader = computed(
() => currentWorld.value?.content?.modloader ?? server.value?.loader ?? null,
)
const gameVersionLabel = computed(() => {
const version = currentWorld.value?.content?.game_version ?? server.value?.mc_version
return version ? `MC ${version}` : null
})
const loaderLabel = computed(() => {
const loader = currentWorld.value?.content?.modloader ?? server.value?.loader?.toLowerCase()
if (!loader) return null
const loaderVersion =
currentWorld.value?.content?.modloader_version ?? server.value?.loader_version ?? null
return [formatLoaderLabel(loader), loaderVersion].filter(Boolean).join(' ')
})
const loaderVersion = computed(
() => currentWorld.value?.content?.modloader_version ?? server.value?.loader_version ?? null,
)
const lastActiveLabel = computed(() => {
const latestBackup = currentWorld.value ? latestDate(currentWorld.value.backups) : null
@@ -0,0 +1,51 @@
import { SettingsIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import WorldManageHeader from '../../components/servers/server-header/WorldManageHeader.vue'
const meta = {
title: 'Servers/WorldManageHeader',
component: WorldManageHeader,
parameters: {
layout: 'padded',
},
decorators: [
(story) => ({
components: { story },
template: '<div style="max-width: 920px;"><story /></div>',
}),
],
} satisfies Meta<typeof WorldManageHeader>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
name: 'My World',
gameVersion: '1.20.1',
loader: 'fabric',
loaderVersion: '0.19.2',
lastActive: 'Last active 2 weeks ago',
backHref: '/hosting/manage/demo-server/instances',
backLabel: 'All instances',
},
render: (args) => ({
components: { ButtonStyled, SettingsIcon, WorldManageHeader },
setup() {
return { args }
},
template: `
<WorldManageHeader v-bind="args">
<template #actions>
<ButtonStyled size="large" circular>
<button type="button" aria-label="Instance settings">
<SettingsIcon aria-hidden="true" />
</button>
</ButtonStyled>
</template>
</WorldManageHeader>
`,
}),
}