mirror of
https://github.com/modrinth/code.git
synced 2026-09-03 05:25:58 +00:00
feat: PageHeader migration start
This commit is contained in:
@@ -1,11 +1,399 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-[min-content_1fr_auto] gap-4">
|
||||
<slot name="icon" />
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<slot name="title" />
|
||||
<slot name="summary" />
|
||||
<slot name="stats" />
|
||||
<div
|
||||
class="flex flex-col gap-2"
|
||||
:class="[
|
||||
props.divider ? 'border-0 border-b border-solid border-divider' : '',
|
||||
props.bottomPadding ? 'pb-4' : '',
|
||||
props.headerClass,
|
||||
]"
|
||||
>
|
||||
<div class="flex flex-wrap items-start gap-4 max-md:flex-col" :class="props.rowClass">
|
||||
<div class="flex min-w-0 flex-1 gap-4" :class="props.mainClass">
|
||||
<div v-if="leadingItems.length" class="contents">
|
||||
<template v-for="(leadingItem, index) in leadingItems" :key="leadingItem.id ?? index">
|
||||
<div
|
||||
v-if="leadingItem.type === 'button'"
|
||||
:class="
|
||||
leadingWrapperClass(
|
||||
leadingItem,
|
||||
'flex size-16 shrink-0 items-center justify-center',
|
||||
)
|
||||
"
|
||||
>
|
||||
<ButtonStyled
|
||||
circular
|
||||
:color="leadingItem.color ?? 'standard'"
|
||||
:size="leadingItem.size ?? 'large'"
|
||||
:type="leadingItem.buttonType ?? 'standard'"
|
||||
>
|
||||
<AutoLink
|
||||
v-if="leadingItem.to"
|
||||
v-tooltip="leadingItem.tooltip"
|
||||
:to="leadingItem.to"
|
||||
:aria-label="leadingLabel(leadingItem)"
|
||||
>
|
||||
<component
|
||||
:is="leadingItem.icon"
|
||||
v-if="leadingItem.icon"
|
||||
aria-hidden="true"
|
||||
v-bind="leadingItem.iconProps"
|
||||
/>
|
||||
</AutoLink>
|
||||
<button
|
||||
v-else
|
||||
v-tooltip="leadingItem.tooltip"
|
||||
type="button"
|
||||
:aria-label="leadingLabel(leadingItem)"
|
||||
@click="leadingItem.onClick"
|
||||
>
|
||||
<component
|
||||
:is="leadingItem.icon"
|
||||
v-if="leadingItem.icon"
|
||||
aria-hidden="true"
|
||||
v-bind="leadingItem.iconProps"
|
||||
/>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<Avatar
|
||||
v-else-if="leadingItem.type === 'avatar'"
|
||||
:src="leadingItem.src"
|
||||
:alt="leadingItem.alt ?? ''"
|
||||
:size="leadingItem.avatarSize ?? '64px'"
|
||||
:tint-by="leadingItem.tintBy ?? null"
|
||||
:circle="leadingItem.circle ?? false"
|
||||
:no-shadow="leadingItem.noShadow ?? false"
|
||||
:raised="leadingItem.raised ?? false"
|
||||
:loading="leadingItem.loading ?? 'eager'"
|
||||
:class="leadingItem.class"
|
||||
/>
|
||||
<component
|
||||
:is="leadingItem.component"
|
||||
v-else-if="leadingItem.component"
|
||||
:class="leadingItem.class"
|
||||
v-bind="leadingItem.componentProps"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-2 justify-center">
|
||||
<div class="flex flex-col gap-1.5 justify-center">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h1
|
||||
class="m-0 min-w-0 max-w-full text-2xl font-semibold leading-none text-contrast"
|
||||
:class="[props.truncateTitle ? 'truncate' : '', props.titleClass]"
|
||||
>
|
||||
{{ props.header }}
|
||||
</h1>
|
||||
</div>
|
||||
<p
|
||||
v-if="props.summary"
|
||||
class="m-0 max-w-[44rem] empty:hidden"
|
||||
:class="[props.disableLineClamp ? '' : 'line-clamp-2']"
|
||||
>
|
||||
{{ props.summary }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="props.metadata.length" class="flex flex-wrap gap-3 empty:hidden max-md:hidden">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<template v-for="(item, index) in props.metadata" :key="item.id">
|
||||
<BulletDivider v-if="index > 0" class="shrink-0" />
|
||||
<AutoLink
|
||||
v-if="item.to"
|
||||
v-tooltip="item.tooltip"
|
||||
:to="item.to"
|
||||
:aria-label="item.ariaLabel ?? item.label"
|
||||
:class="metadataClass(item, true)"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
v-if="item.icon"
|
||||
class="flex size-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
v-bind="item.iconProps"
|
||||
/>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</AutoLink>
|
||||
<button
|
||||
v-else-if="item.onClick"
|
||||
v-tooltip="item.tooltip"
|
||||
type="button"
|
||||
:aria-label="item.ariaLabel ?? item.label"
|
||||
:class="metadataClass(item, true)"
|
||||
@click="item.onClick"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
v-if="item.icon"
|
||||
class="flex size-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
v-bind="item.iconProps"
|
||||
/>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
v-tooltip="item.tooltip"
|
||||
:aria-label="item.ariaLabel"
|
||||
:class="metadataClass(item)"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
v-if="item.icon"
|
||||
class="flex size-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
v-bind="item.iconProps"
|
||||
/>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.actions.length" class="flex flex-wrap gap-2 items-center">
|
||||
<ButtonStyled
|
||||
v-for="action in props.actions"
|
||||
:key="action.id"
|
||||
:color="action.color ?? 'standard'"
|
||||
:size="action.size ?? 'large'"
|
||||
:type="action.type ?? 'standard'"
|
||||
:circular="action.circular ?? action.labelHidden ?? false"
|
||||
>
|
||||
<AutoLink
|
||||
v-if="action.to"
|
||||
v-tooltip="action.tooltip"
|
||||
:to="action.to"
|
||||
:aria-label="actionLabel(action)"
|
||||
>
|
||||
<component
|
||||
:is="action.icon"
|
||||
v-if="action.icon"
|
||||
aria-hidden="true"
|
||||
v-bind="action.iconProps"
|
||||
/>
|
||||
<span v-if="!action.labelHidden && !action.circular">{{ action.label }}</span>
|
||||
</AutoLink>
|
||||
<button
|
||||
v-else
|
||||
v-tooltip="action.tooltip"
|
||||
type="button"
|
||||
:disabled="action.disabled"
|
||||
:aria-label="actionLabel(action)"
|
||||
@click="action.onClick"
|
||||
>
|
||||
<component
|
||||
:is="action.icon"
|
||||
v-if="action.icon"
|
||||
aria-hidden="true"
|
||||
v-bind="action.iconProps"
|
||||
/>
|
||||
<span v-if="!action.labelHidden && !action.circular">{{ action.label }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.metadata.length" class="flex justify-between md:hidden">
|
||||
<div class="flex flex-wrap gap-3 empty:hidden">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<template v-for="(item, index) in props.metadata" :key="item.id">
|
||||
<BulletDivider v-if="index > 0" class="shrink-0" />
|
||||
<AutoLink
|
||||
v-if="item.to"
|
||||
v-tooltip="item.tooltip"
|
||||
:to="item.to"
|
||||
:aria-label="item.ariaLabel ?? item.label"
|
||||
:class="metadataClass(item, true)"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
v-if="item.icon"
|
||||
class="flex size-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
v-bind="item.iconProps"
|
||||
/>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</AutoLink>
|
||||
<button
|
||||
v-else-if="item.onClick"
|
||||
v-tooltip="item.tooltip"
|
||||
type="button"
|
||||
:aria-label="item.ariaLabel ?? item.label"
|
||||
:class="metadataClass(item, true)"
|
||||
@click="item.onClick"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
v-if="item.icon"
|
||||
class="flex size-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
v-bind="item.iconProps"
|
||||
/>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
v-tooltip="item.tooltip"
|
||||
:aria-label="item.ariaLabel"
|
||||
:class="metadataClass(item)"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
v-if="item.icon"
|
||||
class="flex size-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
v-bind="item.iconProps"
|
||||
/>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
import AutoLink from './AutoLink.vue'
|
||||
import Avatar from './Avatar.vue'
|
||||
import BulletDivider from './BulletDivider.vue'
|
||||
import ButtonStyled from './ButtonStyled.vue'
|
||||
|
||||
type PageHeaderTarget = string | RouteLocationRaw
|
||||
type ButtonColor =
|
||||
| 'standard'
|
||||
| 'brand'
|
||||
| 'red'
|
||||
| 'orange'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'purple'
|
||||
| 'medal-promo'
|
||||
type ButtonSize = 'standard' | 'large' | 'small'
|
||||
type ButtonType =
|
||||
| 'standard'
|
||||
| 'outlined'
|
||||
| 'transparent'
|
||||
| 'highlight'
|
||||
| 'highlight-colored-text'
|
||||
| 'chip'
|
||||
|
||||
type PageHeaderLeading = {
|
||||
id?: string
|
||||
type: 'avatar' | 'button' | 'component'
|
||||
icon?: Component
|
||||
iconProps?: Record<string, unknown>
|
||||
component?: Component
|
||||
componentProps?: Record<string, unknown>
|
||||
src?: string | null
|
||||
alt?: string
|
||||
tintBy?: string | null
|
||||
avatarSize?: string
|
||||
circle?: boolean
|
||||
noShadow?: boolean
|
||||
raised?: boolean
|
||||
loading?: 'eager' | 'lazy'
|
||||
class?: string
|
||||
wrapperClass?: string
|
||||
to?: PageHeaderTarget
|
||||
onClick?: () => void | Promise<void>
|
||||
tooltip?: string
|
||||
ariaLabel?: string
|
||||
color?: ButtonColor
|
||||
size?: ButtonSize
|
||||
buttonType?: ButtonType
|
||||
}
|
||||
|
||||
type PageHeaderMetadataItem = {
|
||||
id: string
|
||||
label: string
|
||||
icon?: Component
|
||||
iconProps?: Record<string, unknown>
|
||||
tooltip?: string
|
||||
ariaLabel?: string
|
||||
to?: PageHeaderTarget
|
||||
onClick?: () => void | Promise<void>
|
||||
class?: string
|
||||
}
|
||||
|
||||
type PageHeaderAction = {
|
||||
id: string
|
||||
label: string
|
||||
icon?: Component
|
||||
iconProps?: Record<string, unknown>
|
||||
tooltip?: string
|
||||
ariaLabel?: string
|
||||
to?: PageHeaderTarget
|
||||
onClick?: () => void | Promise<void>
|
||||
disabled?: boolean
|
||||
labelHidden?: boolean
|
||||
circular?: boolean
|
||||
color?: ButtonColor
|
||||
size?: ButtonSize
|
||||
type?: ButtonType
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
header: string
|
||||
summary?: string | null
|
||||
leading?: PageHeaderLeading | PageHeaderLeading[] | null
|
||||
metadata?: PageHeaderMetadataItem[]
|
||||
actions?: PageHeaderAction[]
|
||||
headerClass?: string
|
||||
rowClass?: string
|
||||
mainClass?: string
|
||||
titleClass?: string
|
||||
truncateTitle?: boolean
|
||||
divider?: boolean
|
||||
bottomPadding?: boolean
|
||||
disableLineClamp?: boolean
|
||||
}>(),
|
||||
{
|
||||
summary: null,
|
||||
leading: null,
|
||||
metadata: () => [],
|
||||
actions: () => [],
|
||||
headerClass: '',
|
||||
rowClass: '',
|
||||
mainClass: '',
|
||||
titleClass: '',
|
||||
truncateTitle: false,
|
||||
divider: true,
|
||||
bottomPadding: true,
|
||||
disableLineClamp: false,
|
||||
},
|
||||
)
|
||||
|
||||
const leadingItems = computed(() => {
|
||||
if (!props.leading) return []
|
||||
return Array.isArray(props.leading) ? props.leading : [props.leading]
|
||||
})
|
||||
|
||||
function leadingLabel(item: PageHeaderLeading) {
|
||||
return item.ariaLabel ?? item.tooltip ?? 'Header action'
|
||||
}
|
||||
|
||||
function leadingWrapperClass(item: PageHeaderLeading, defaultClass: string) {
|
||||
return [item.wrapperClass ?? defaultClass]
|
||||
}
|
||||
|
||||
function metadataClass(item: PageHeaderMetadataItem, interactive = false) {
|
||||
return [
|
||||
'flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap',
|
||||
interactive ? 'm-0 cursor-pointer border-0 bg-transparent p-0 hover:underline' : '',
|
||||
item.class,
|
||||
]
|
||||
}
|
||||
|
||||
function actionLabel(action: PageHeaderAction) {
|
||||
return action.ariaLabel ?? action.tooltip ?? action.label
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -63,6 +63,7 @@ export { default as OptionGroup } from './OptionGroup.vue'
|
||||
export type { Option as OverflowMenuOption } from './OverflowMenu.vue'
|
||||
export { default as OverflowMenu } from './OverflowMenu.vue'
|
||||
export { default as Page } from './Page.vue'
|
||||
export { default as PageHeader } from './PageHeader.vue'
|
||||
export { default as Pagination } from './Pagination.vue'
|
||||
export { default as PopoutMenu } from './PopoutMenu.vue'
|
||||
export { default as PreviewSelectButton } from './PreviewSelectButton.vue'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { LeftArrowIcon } from '@modrinth/assets'
|
||||
import { LeftArrowIcon, TagCategoryGamepad2Icon as Gamepad2Icon } from '@modrinth/assets'
|
||||
import type { Component } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import Avatar from '#ui/components/base/Avatar.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import PageHeader from '#ui/components/base/PageHeader.vue'
|
||||
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
|
||||
import { useServerImage } from '#ui/composables/servers/use-server-image.ts'
|
||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||
|
||||
@@ -20,6 +21,13 @@ const props = defineProps<{
|
||||
installContext?: BrowseInstallContext | null
|
||||
}>()
|
||||
type SelectedProjectsLeaveResult = 'cancel' | 'discard' | 'install'
|
||||
type BrowseHeaderMetadataItem = {
|
||||
id: string
|
||||
label: string
|
||||
icon?: Component
|
||||
iconProps?: Record<string, unknown>
|
||||
class?: string
|
||||
}
|
||||
|
||||
const ctx = injectBrowseManager(null)
|
||||
const installContext = computed(() => props.installContext ?? ctx?.installContext?.value ?? null)
|
||||
@@ -37,14 +45,66 @@ const iconSrc = computed(() => {
|
||||
return fetchedIcon.value ?? installContext.value?.iconSrc ?? null
|
||||
})
|
||||
|
||||
const leadingItems = computed(() => {
|
||||
const context = installContext.value
|
||||
if (!context) return []
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'back',
|
||||
type: 'button' as const,
|
||||
icon: LeftArrowIcon,
|
||||
ariaLabel: context.backLabel,
|
||||
tooltip: context.backLabel,
|
||||
onClick: handleBack,
|
||||
wrapperClass: 'flex size-12 shrink-0 items-center justify-center',
|
||||
},
|
||||
...(iconSrc.value
|
||||
? [
|
||||
{
|
||||
id: 'icon',
|
||||
type: 'avatar' as const,
|
||||
src: iconSrc.value,
|
||||
alt: context.name,
|
||||
avatarSize: '48px',
|
||||
class: 'shrink-0',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
})
|
||||
|
||||
const metadataItems = computed(() => {
|
||||
const context = installContext.value
|
||||
if (!context) return []
|
||||
return [
|
||||
context.heading,
|
||||
context.gameVersion ? `MC ${context.gameVersion}` : '',
|
||||
context.loader ? formatLoaderLabel(context.loader) : '',
|
||||
].filter(Boolean)
|
||||
|
||||
const items: BrowseHeaderMetadataItem[] = []
|
||||
if (context.heading) {
|
||||
items.push({
|
||||
id: 'heading',
|
||||
label: context.heading,
|
||||
class: '!text-primary',
|
||||
})
|
||||
}
|
||||
if (context.gameVersion) {
|
||||
items.push({
|
||||
id: 'game-version',
|
||||
label: context.gameVersion,
|
||||
icon: Gamepad2Icon,
|
||||
class: '!text-primary',
|
||||
})
|
||||
}
|
||||
if (context.loader) {
|
||||
const loaderLabel = formatLoaderLabel(context.loader)
|
||||
items.push({
|
||||
id: 'loader',
|
||||
label: loaderLabel,
|
||||
icon: LoaderIcon,
|
||||
iconProps: { loader: loaderLabel },
|
||||
class: '!text-primary',
|
||||
})
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const selectedCount = computed(() => installContext.value?.selectedProjects?.length ?? 0)
|
||||
@@ -99,37 +159,16 @@ async function handleSelectedProjectsLeaveResult(
|
||||
:count="selectedCount"
|
||||
:installing="isInstallingSelected"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex min-w-0 items-center gap-4">
|
||||
<ButtonStyled circular size="large">
|
||||
<button :aria-label="installContext.backLabel" @click="handleBack">
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<Avatar v-if="iconSrc" :src="iconSrc" size="48px" class="shrink-0" />
|
||||
|
||||
<div class="flex min-w-0 flex-col justify-center gap-1">
|
||||
<h1 class="m-0 truncate text-2xl font-semibold leading-8 text-contrast">
|
||||
{{ installContext.name }}
|
||||
</h1>
|
||||
<div
|
||||
v-if="metadataItems.length"
|
||||
class="flex flex-wrap items-center gap-2 text-base font-medium leading-6 text-primary"
|
||||
>
|
||||
<template v-for="(item, index) in metadataItems" :key="item">
|
||||
<span
|
||||
v-if="index > 0"
|
||||
class="h-1.5 w-1.5 shrink-0 rounded-full bg-current opacity-60"
|
||||
/>
|
||||
<span>{{ item }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader
|
||||
:header="installContext.name"
|
||||
:leading="leadingItems"
|
||||
:metadata="metadataItems"
|
||||
:divider="false"
|
||||
:bottom-padding="false"
|
||||
main-class="items-center"
|
||||
title-class="leading-8"
|
||||
truncate-title
|
||||
/>
|
||||
<Admonition v-if="installContext.warning" type="warning" class="mb-1">
|
||||
{{ installContext.warning }}
|
||||
</Admonition>
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { SortType } from '#ui/utils/search'
|
||||
import SelectedProjectsFloatingBar from './components/SelectedProjectsFloatingBar.vue'
|
||||
import BrowseInstallHeader from './header.vue'
|
||||
import { injectBrowseManager } from './providers/browse-manager'
|
||||
import BrowseSidebar from './sidebar.vue'
|
||||
|
||||
const ctx = injectBrowseManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -44,6 +45,21 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
const installHeaderClass = computed(() => [
|
||||
'sticky top-0 z-20 border-0 bg-surface-1',
|
||||
ctx.variant === 'web'
|
||||
? 'normal-page__header browse-install-header-bleed mb-4 flex flex-col gap-2 py-3'
|
||||
: '-mx-6 -mt-6 rounded-tl-[--radius-xl] border-b border-solid p-3 border-surface-5',
|
||||
ctx.variant !== 'web' && isInstallHeaderStuck.value ? 'border-t' : '',
|
||||
])
|
||||
const contentFrameTag = computed(() => (ctx.variant === 'web' ? 'section' : 'div'))
|
||||
const contentFrameClass = computed(() =>
|
||||
ctx.variant === 'web' ? 'normal-page__content browse-page-layout__content' : 'contents',
|
||||
)
|
||||
const contentInnerClass = computed(() =>
|
||||
ctx.variant === 'web' ? 'flex flex-col gap-3' : 'contents',
|
||||
)
|
||||
|
||||
const messages = defineMessages({
|
||||
searchPlaceholder: {
|
||||
id: 'browse.search.placeholder',
|
||||
@@ -69,253 +85,308 @@ const messages = defineMessages({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="ctx.installContext?.value && ctx.variant !== 'web'">
|
||||
<div
|
||||
ref="stickyInstallHeaderRef"
|
||||
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid bg-surface-1 p-3 border-surface-5"
|
||||
:class="[isInstallHeaderStuck ? 'border-t' : '']"
|
||||
<div v-if="ctx.installContext?.value" ref="stickyInstallHeaderRef" :class="installHeaderClass">
|
||||
<BrowseInstallHeader />
|
||||
</div>
|
||||
<SelectedProjectsFloatingBar v-if="ctx.installContext?.value" />
|
||||
|
||||
<template v-if="ctx.variant === 'web'">
|
||||
<aside
|
||||
class="normal-page__sidebar browse-page-layout__sidebar"
|
||||
:aria-label="formatMessage(commonMessages.filtersLabel)"
|
||||
>
|
||||
<BrowseInstallHeader />
|
||||
</div>
|
||||
<slot name="sidebar-prepend" />
|
||||
<BrowseSidebar />
|
||||
</aside>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Teleport to="#sidebar-teleport-target">
|
||||
<BrowseSidebar>
|
||||
<template #prepend>
|
||||
<slot name="sidebar-prepend" />
|
||||
</template>
|
||||
</BrowseSidebar>
|
||||
</Teleport>
|
||||
</template>
|
||||
<SelectedProjectsFloatingBar v-if="ctx.installContext?.value && ctx.variant !== 'web'" />
|
||||
|
||||
<NavTabs v-if="ctx.showProjectTypeTabs.value" :links="ctx.selectableProjectTypes.value" />
|
||||
<component :is="contentFrameTag" :class="contentFrameClass">
|
||||
<div :class="contentInnerClass">
|
||||
<NavTabs v-if="ctx.showProjectTypeTabs.value" :links="ctx.selectableProjectTypes.value" />
|
||||
|
||||
<StyledInput
|
||||
v-model="ctx.query.value"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="
|
||||
formatMessage(messages.searchPlaceholder, {
|
||||
projectType: formatProjectTypeSentence(formatMessage, ctx.projectType.value, 2),
|
||||
})
|
||||
"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
:input-class="ctx.variant === 'web' ? '!h-12' : 'h-12'"
|
||||
@clear="ctx.clearSearch()"
|
||||
/>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Combobox
|
||||
:model-value="ctx.effectiveCurrentSortType.value"
|
||||
:options="sortOptions"
|
||||
:class="
|
||||
ctx.variant === 'web'
|
||||
? '!w-[16rem] min-w-max max-w-full flex-grow md:flex-grow-0'
|
||||
: '!w-[16rem] min-w-max max-w-full'
|
||||
"
|
||||
@update:model-value="(val: SortType) => (ctx.effectiveCurrentSortType.value = val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="font-semibold text-primary">{{
|
||||
formatMessage(commonMessages.sortByLabel)
|
||||
}}</span>
|
||||
</template>
|
||||
</Combobox>
|
||||
|
||||
<Combobox
|
||||
:model-value="ctx.maxResults.value"
|
||||
:options="maxResultsOptions"
|
||||
:class="
|
||||
ctx.variant === 'web'
|
||||
? '!w-[9rem] min-w-max max-w-full flex-grow md:flex-grow-0'
|
||||
: '!w-[9rem] min-w-max max-w-full'
|
||||
"
|
||||
:placeholder="formatMessage(commonMessages.viewLabel)"
|
||||
@update:model-value="(val: number) => (ctx.maxResults.value = val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="font-semibold text-primary">{{ formatMessage(messages.viewPrefix) }}</span>
|
||||
</template>
|
||||
</Combobox>
|
||||
|
||||
<div v-if="ctx.filtersMenuOpen && !ctx.filtersMenuOpen.value" class="lg:hidden">
|
||||
<ButtonStyled>
|
||||
<button @click="ctx.filtersMenuOpen.value = true">
|
||||
{{ formatMessage(messages.filterResults) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<ButtonStyled v-if="ctx.cycleDisplayMode" circular>
|
||||
<button @click="ctx.cycleDisplayMode!()">
|
||||
<slot name="display-mode-icon" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<Pagination
|
||||
:page="ctx.currentPage.value"
|
||||
:count="ctx.pageCount.value"
|
||||
:class="ctx.variant === 'web' ? 'mx-auto sm:ml-auto sm:mr-0' : 'ml-auto'"
|
||||
@switch-page="ctx.setPage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SearchFilterControl
|
||||
v-if="ctx.isServerType.value"
|
||||
v-model:selected-filters="ctx.serverCurrentFilters.value"
|
||||
:filters="ctx.serverFilterTypes.value"
|
||||
:provided-filters="[]"
|
||||
:overridden-provided-filter-types="[]"
|
||||
/>
|
||||
<SearchFilterControl
|
||||
v-else
|
||||
v-model:selected-filters="ctx.currentFilters.value"
|
||||
:filters="ctx.filters.value.filter((f) => f.display !== 'none')"
|
||||
:provided-filters="ctx.providedFilters?.value ?? []"
|
||||
:overridden-provided-filter-types="ctx.overriddenProvidedFilterTypes.value"
|
||||
:provided-message="lockedMessages?.providedBy"
|
||||
/>
|
||||
|
||||
<div class="search">
|
||||
<section v-if="ctx.loading.value" class="offline">
|
||||
<component :is="ctx.loadingComponent ?? LoadingIndicator" />
|
||||
</section>
|
||||
<section v-else-if="ctx.offline?.value && ctx.totalHits.value === 0" class="offline">
|
||||
{{ formatMessage(messages.offline) }}
|
||||
</section>
|
||||
<section
|
||||
v-else-if="
|
||||
ctx.isServerType.value
|
||||
? ctx.serverHits.value.length === 0
|
||||
: ctx.projectHits.value.length === 0
|
||||
"
|
||||
class="offline"
|
||||
>
|
||||
<p>{{ formatMessage(messages.noResults) }}</p>
|
||||
</section>
|
||||
|
||||
<ProjectCardList v-else :layout="ctx.effectiveLayout.value">
|
||||
<template v-if="ctx.isServerType.value">
|
||||
<ProjectCard
|
||||
v-for="result in ctx.serverHits.value"
|
||||
:key="`server-card-${result.project_id}`"
|
||||
:title="result.name"
|
||||
:icon-url="result.icon_url || undefined"
|
||||
:summary="result.summary"
|
||||
:tags="result.categories"
|
||||
:link="ctx.getServerProjectLink(result)"
|
||||
:server-online-players="result.minecraft_java_server?.ping?.data?.players_online ?? 0"
|
||||
:server-region="result.minecraft_server?.region"
|
||||
:server-recent-plays="result.minecraft_java_server?.verified_plays_2w ?? 0"
|
||||
:server-modpack-content="ctx.getServerModpackContent?.(result)"
|
||||
:server-ping="ctx.serverPings?.value?.[result.project_id]"
|
||||
:server-status-online="!!result.minecraft_java_server?.ping?.data"
|
||||
:hide-online-players-label="ctx.variant === 'app'"
|
||||
:hide-recent-plays-label="ctx.variant === 'app'"
|
||||
:layout="ctx.effectiveLayout.value"
|
||||
:max-tags="2"
|
||||
is-server-project
|
||||
exclude-loaders
|
||||
:color="result.color ?? undefined"
|
||||
:banner="result.featured_gallery ?? undefined"
|
||||
@contextmenu.prevent.stop="(event: MouseEvent) => ctx.onContextMenu?.(event, result)"
|
||||
@mouseenter="ctx.onServerProjectHover?.(result)"
|
||||
@mouseleave="ctx.onProjectHoverEnd?.()"
|
||||
>
|
||||
<template v-if="ctx.getCardActions?.(result, ctx.projectType.value)?.length" #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-for="action in ctx.getCardActions(result, ctx.projectType.value)"
|
||||
:key="action.key"
|
||||
:color="action.color"
|
||||
:type="action.type"
|
||||
:circular="action.circular"
|
||||
>
|
||||
<button
|
||||
v-tooltip="action.tooltip"
|
||||
:disabled="action.disabled"
|
||||
@click.stop="action.onClick"
|
||||
>
|
||||
<component :is="action.icon" :class="action.iconClass" />
|
||||
<template v-if="!action.circular">{{ action.label }}</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</ProjectCard>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ProjectCard
|
||||
v-for="result in ctx.projectHits.value"
|
||||
:key="result.project_id"
|
||||
:link="ctx.getProjectLink(result)"
|
||||
:title="result.title"
|
||||
:icon-url="result.icon_url"
|
||||
:author="{
|
||||
name: result.organization == null ? result.author : result.organization,
|
||||
link:
|
||||
result.organization_id == null
|
||||
? ctx.variant === 'web'
|
||||
? `/user/${result.author_id ?? result.author}`
|
||||
: `https://modrinth.com/user/${result.author_id ?? result.author}`
|
||||
: ctx.variant === 'web'
|
||||
? `/organization/${result.organization_id}`
|
||||
: `https://modrinth.com/organization/${result.organization_id}`,
|
||||
}"
|
||||
:date-updated="result.date_modified"
|
||||
:date-published="result.date_created"
|
||||
:displayed-date="
|
||||
ctx.effectiveCurrentSortType.value.name === 'newest' ? 'published' : 'updated'
|
||||
"
|
||||
:downloads="result.downloads"
|
||||
:summary="result.description"
|
||||
:tags="result.display_categories"
|
||||
:all-tags="result.categories"
|
||||
:deprioritized-tags="ctx.deprioritizedTags.value"
|
||||
:exclude-loaders="ctx.excludeLoaders.value"
|
||||
:followers="result.follows"
|
||||
:banner="result.featured_gallery ?? undefined"
|
||||
:color="result.color ?? undefined"
|
||||
:environment="
|
||||
['mod', 'modpack'].includes(ctx.projectType.value)
|
||||
? {
|
||||
clientSide: result.client_side as Labrinth.Projects.v2.Environment,
|
||||
serverSide: result.server_side as Labrinth.Projects.v2.Environment,
|
||||
}
|
||||
: undefined
|
||||
"
|
||||
:layout="ctx.effectiveLayout.value"
|
||||
@contextmenu.prevent.stop="(event: MouseEvent) => ctx.onContextMenu?.(event, result)"
|
||||
@mouseenter="ctx.onProjectHover?.(result)"
|
||||
@mouseleave="ctx.onProjectHoverEnd?.()"
|
||||
>
|
||||
<template v-if="ctx.getCardActions?.(result, ctx.projectType.value)?.length" #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-for="action in ctx.getCardActions(result, ctx.projectType.value)"
|
||||
:key="action.key"
|
||||
:color="action.color"
|
||||
:type="action.type"
|
||||
:circular="action.circular"
|
||||
>
|
||||
<button
|
||||
v-tooltip="action.tooltip"
|
||||
:disabled="action.disabled"
|
||||
@click.stop="action.onClick"
|
||||
>
|
||||
<component :is="action.icon" :class="action.iconClass" />
|
||||
<template v-if="!action.circular">{{ action.label }}</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</ProjectCard>
|
||||
</template>
|
||||
</ProjectCardList>
|
||||
|
||||
<div :class="ctx.variant === 'web' ? 'pagination-after mt-3' : 'flex justify-end mt-3'">
|
||||
<Pagination
|
||||
:page="ctx.currentPage.value"
|
||||
:count="ctx.pageCount.value"
|
||||
:class="ctx.variant === 'web' ? 'justify-end' : 'pagination-after'"
|
||||
@switch-page="ctx.setPage"
|
||||
<StyledInput
|
||||
v-model="ctx.query.value"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="
|
||||
formatMessage(messages.searchPlaceholder, {
|
||||
projectType: formatProjectTypeSentence(formatMessage, ctx.projectType.value, 2),
|
||||
})
|
||||
"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
:input-class="ctx.variant === 'web' ? '!h-12' : 'h-12'"
|
||||
@clear="ctx.clearSearch()"
|
||||
/>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Combobox
|
||||
:model-value="ctx.effectiveCurrentSortType.value"
|
||||
:options="sortOptions"
|
||||
:class="
|
||||
ctx.variant === 'web'
|
||||
? '!w-[16rem] min-w-max max-w-full flex-grow md:flex-grow-0'
|
||||
: '!w-[16rem] min-w-max max-w-full'
|
||||
"
|
||||
@update:model-value="(val: SortType) => (ctx.effectiveCurrentSortType.value = val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="font-semibold text-primary">{{
|
||||
formatMessage(commonMessages.sortByLabel)
|
||||
}}</span>
|
||||
</template>
|
||||
</Combobox>
|
||||
|
||||
<Combobox
|
||||
:model-value="ctx.maxResults.value"
|
||||
:options="maxResultsOptions"
|
||||
:class="
|
||||
ctx.variant === 'web'
|
||||
? '!w-[9rem] min-w-max max-w-full flex-grow md:flex-grow-0'
|
||||
: '!w-[9rem] min-w-max max-w-full'
|
||||
"
|
||||
:placeholder="formatMessage(commonMessages.viewLabel)"
|
||||
@update:model-value="(val: number) => (ctx.maxResults.value = val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="font-semibold text-primary">{{ formatMessage(messages.viewPrefix) }}</span>
|
||||
</template>
|
||||
</Combobox>
|
||||
|
||||
<div v-if="ctx.filtersMenuOpen && !ctx.filtersMenuOpen.value" class="lg:hidden">
|
||||
<ButtonStyled>
|
||||
<button @click="ctx.filtersMenuOpen.value = true">
|
||||
{{ formatMessage(messages.filterResults) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<ButtonStyled v-if="ctx.cycleDisplayMode" circular>
|
||||
<button @click="ctx.cycleDisplayMode!()">
|
||||
<slot name="display-mode-icon" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<Pagination
|
||||
:page="ctx.currentPage.value"
|
||||
:count="ctx.pageCount.value"
|
||||
:class="ctx.variant === 'web' ? 'mx-auto sm:ml-auto sm:mr-0' : 'ml-auto'"
|
||||
@switch-page="ctx.setPage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SearchFilterControl
|
||||
v-if="ctx.isServerType.value"
|
||||
v-model:selected-filters="ctx.serverCurrentFilters.value"
|
||||
:filters="ctx.serverFilterTypes.value"
|
||||
:provided-filters="[]"
|
||||
:overridden-provided-filter-types="[]"
|
||||
/>
|
||||
<SearchFilterControl
|
||||
v-else
|
||||
v-model:selected-filters="ctx.currentFilters.value"
|
||||
:filters="ctx.filters.value.filter((f) => f.display !== 'none')"
|
||||
:provided-filters="ctx.providedFilters?.value ?? []"
|
||||
:overridden-provided-filter-types="ctx.overriddenProvidedFilterTypes.value"
|
||||
:provided-message="lockedMessages?.providedBy"
|
||||
/>
|
||||
|
||||
<div class="search">
|
||||
<section v-if="ctx.loading.value" class="offline">
|
||||
<component :is="ctx.loadingComponent ?? LoadingIndicator" />
|
||||
</section>
|
||||
<section v-else-if="ctx.offline?.value && ctx.totalHits.value === 0" class="offline">
|
||||
{{ formatMessage(messages.offline) }}
|
||||
</section>
|
||||
<section
|
||||
v-else-if="
|
||||
ctx.isServerType.value
|
||||
? ctx.serverHits.value.length === 0
|
||||
: ctx.projectHits.value.length === 0
|
||||
"
|
||||
class="offline"
|
||||
>
|
||||
<p>{{ formatMessage(messages.noResults) }}</p>
|
||||
</section>
|
||||
|
||||
<ProjectCardList v-else :layout="ctx.effectiveLayout.value">
|
||||
<template v-if="ctx.isServerType.value">
|
||||
<ProjectCard
|
||||
v-for="result in ctx.serverHits.value"
|
||||
:key="`server-card-${result.project_id}`"
|
||||
:title="result.name"
|
||||
:icon-url="result.icon_url || undefined"
|
||||
:summary="result.summary"
|
||||
:tags="result.categories"
|
||||
:link="ctx.getServerProjectLink(result)"
|
||||
:server-online-players="result.minecraft_java_server?.ping?.data?.players_online ?? 0"
|
||||
:server-region="result.minecraft_server?.region"
|
||||
:server-recent-plays="result.minecraft_java_server?.verified_plays_2w ?? 0"
|
||||
:server-modpack-content="ctx.getServerModpackContent?.(result)"
|
||||
:server-ping="ctx.serverPings?.value?.[result.project_id]"
|
||||
:server-status-online="!!result.minecraft_java_server?.ping?.data"
|
||||
:hide-online-players-label="ctx.variant === 'app'"
|
||||
:hide-recent-plays-label="ctx.variant === 'app'"
|
||||
:layout="ctx.effectiveLayout.value"
|
||||
:max-tags="2"
|
||||
is-server-project
|
||||
exclude-loaders
|
||||
:color="result.color ?? undefined"
|
||||
:banner="result.featured_gallery ?? undefined"
|
||||
@contextmenu.prevent.stop="(event: MouseEvent) => ctx.onContextMenu?.(event, result)"
|
||||
@mouseenter="ctx.onServerProjectHover?.(result)"
|
||||
@mouseleave="ctx.onProjectHoverEnd?.()"
|
||||
>
|
||||
<template v-if="ctx.getCardActions?.(result, ctx.projectType.value)?.length" #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-for="action in ctx.getCardActions(result, ctx.projectType.value)"
|
||||
:key="action.key"
|
||||
:color="action.color"
|
||||
:type="action.type"
|
||||
:circular="action.circular"
|
||||
>
|
||||
<button
|
||||
v-tooltip="action.tooltip"
|
||||
:disabled="action.disabled"
|
||||
@click.stop="action.onClick"
|
||||
>
|
||||
<component :is="action.icon" :class="action.iconClass" />
|
||||
<template v-if="!action.circular">{{ action.label }}</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</ProjectCard>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ProjectCard
|
||||
v-for="result in ctx.projectHits.value"
|
||||
:key="result.project_id"
|
||||
:link="ctx.getProjectLink(result)"
|
||||
:title="result.title"
|
||||
:icon-url="result.icon_url"
|
||||
:author="{
|
||||
name: result.organization == null ? result.author : result.organization,
|
||||
link:
|
||||
result.organization_id == null
|
||||
? ctx.variant === 'web'
|
||||
? `/user/${result.author_id ?? result.author}`
|
||||
: `https://modrinth.com/user/${result.author_id ?? result.author}`
|
||||
: ctx.variant === 'web'
|
||||
? `/organization/${result.organization_id}`
|
||||
: `https://modrinth.com/organization/${result.organization_id}`,
|
||||
}"
|
||||
:date-updated="result.date_modified"
|
||||
:date-published="result.date_created"
|
||||
:displayed-date="
|
||||
ctx.effectiveCurrentSortType.value.name === 'newest' ? 'published' : 'updated'
|
||||
"
|
||||
:downloads="result.downloads"
|
||||
:summary="result.description"
|
||||
:tags="result.display_categories"
|
||||
:all-tags="result.categories"
|
||||
:deprioritized-tags="ctx.deprioritizedTags.value"
|
||||
:exclude-loaders="ctx.excludeLoaders.value"
|
||||
:followers="result.follows"
|
||||
:banner="result.featured_gallery ?? undefined"
|
||||
:color="result.color ?? undefined"
|
||||
:environment="
|
||||
['mod', 'modpack'].includes(ctx.projectType.value)
|
||||
? {
|
||||
clientSide: result.client_side as Labrinth.Projects.v2.Environment,
|
||||
serverSide: result.server_side as Labrinth.Projects.v2.Environment,
|
||||
}
|
||||
: undefined
|
||||
"
|
||||
:layout="ctx.effectiveLayout.value"
|
||||
@contextmenu.prevent.stop="(event: MouseEvent) => ctx.onContextMenu?.(event, result)"
|
||||
@mouseenter="ctx.onProjectHover?.(result)"
|
||||
@mouseleave="ctx.onProjectHoverEnd?.()"
|
||||
>
|
||||
<template v-if="ctx.getCardActions?.(result, ctx.projectType.value)?.length" #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-for="action in ctx.getCardActions(result, ctx.projectType.value)"
|
||||
:key="action.key"
|
||||
:color="action.color"
|
||||
:type="action.type"
|
||||
:circular="action.circular"
|
||||
>
|
||||
<button
|
||||
v-tooltip="action.tooltip"
|
||||
:disabled="action.disabled"
|
||||
@click.stop="action.onClick"
|
||||
>
|
||||
<component :is="action.icon" :class="action.iconClass" />
|
||||
<template v-if="!action.circular">{{ action.label }}</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</ProjectCard>
|
||||
</template>
|
||||
</ProjectCardList>
|
||||
|
||||
<div :class="ctx.variant === 'web' ? 'pagination-after mt-3' : 'flex justify-end mt-3'">
|
||||
<Pagination
|
||||
:page="ctx.currentPage.value"
|
||||
:count="ctx.pageCount.value"
|
||||
:class="ctx.variant === 'web' ? 'justify-end' : 'pagination-after'"
|
||||
@switch-page="ctx.setPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot name="content-after" />
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
|
||||
<slot name="after" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.browse-install-header-bleed {
|
||||
grid-column: 1 / -1;
|
||||
margin-inline: -1.5rem;
|
||||
padding-inline: 0.75rem !important;
|
||||
}
|
||||
|
||||
.browse-install-header-bleed::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
border-bottom: 1px solid var(--surface-5);
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.browse-page-layout__content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.browse-page-layout__sidebar {
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.browse-page-layout__content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.browse-page-layout__sidebar {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
GlobeIcon,
|
||||
LeftArrowIcon,
|
||||
LinkIcon,
|
||||
MoreVerticalIcon,
|
||||
PlayIcon,
|
||||
SettingsIcon,
|
||||
TagCategoryGamepad2Icon as Gamepad2Icon,
|
||||
TimerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import PageHeader from '../../components/base/PageHeader.vue'
|
||||
import LoaderIcon from '../../components/servers/icons/LoaderIcon.vue'
|
||||
import ServerIcon from '../../components/servers/icons/ServerIcon.vue'
|
||||
|
||||
const noop = () => undefined
|
||||
|
||||
const meta = {
|
||||
title: 'Base/PageHeader Prototype',
|
||||
component: PageHeader,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: '<div class="w-full"><story /></div>',
|
||||
}),
|
||||
],
|
||||
} satisfies Meta<typeof PageHeader>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const AppInstanceHeader: Story = {
|
||||
args: {
|
||||
header: 'Create: Astral',
|
||||
leading: {
|
||||
type: 'avatar',
|
||||
src: null,
|
||||
alt: 'Create: Astral',
|
||||
tintBy: '/Users/calum/Library/Application Support/com.modrinth.theseus/profiles/astral',
|
||||
},
|
||||
metadata: [
|
||||
{
|
||||
id: 'game-version',
|
||||
label: '1.20.1',
|
||||
icon: Gamepad2Icon,
|
||||
tooltip: 'Minecraft version',
|
||||
},
|
||||
{
|
||||
id: 'loader',
|
||||
label: 'Fabric 0.16.14',
|
||||
icon: LoaderIcon,
|
||||
iconProps: { loader: 'Fabric' },
|
||||
tooltip: 'Mod loader',
|
||||
},
|
||||
{
|
||||
id: 'playtime',
|
||||
label: '12 hours',
|
||||
icon: TimerIcon,
|
||||
tooltip: 'Total playtime',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 'play',
|
||||
label: 'Play',
|
||||
icon: PlayIcon,
|
||||
color: 'brand',
|
||||
onClick: noop,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Instance settings',
|
||||
icon: SettingsIcon,
|
||||
labelHidden: true,
|
||||
tooltip: 'Instance settings',
|
||||
onClick: noop,
|
||||
},
|
||||
{
|
||||
id: 'more',
|
||||
label: 'More actions',
|
||||
icon: MoreVerticalIcon,
|
||||
labelHidden: true,
|
||||
type: 'transparent',
|
||||
tooltip: 'More actions',
|
||||
onClick: noop,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const BrowseHeader: Story = {
|
||||
args: {
|
||||
header: 'Survival SMP',
|
||||
leading: [
|
||||
{
|
||||
id: 'back',
|
||||
type: 'button',
|
||||
icon: LeftArrowIcon,
|
||||
to: '/instance/my-world',
|
||||
ariaLabel: 'Back to instance',
|
||||
tooltip: 'Back to instance',
|
||||
wrapperClass: 'flex size-12 shrink-0 items-center justify-center',
|
||||
},
|
||||
{
|
||||
id: 'target',
|
||||
type: 'avatar',
|
||||
src: null,
|
||||
alt: 'Survival SMP',
|
||||
avatarSize: '48px',
|
||||
tintBy: 'survival-smp',
|
||||
},
|
||||
],
|
||||
metadata: [
|
||||
{
|
||||
id: 'heading',
|
||||
label: 'Installing content',
|
||||
class: '!text-primary',
|
||||
},
|
||||
{
|
||||
id: 'game-version',
|
||||
label: '1.20.1',
|
||||
icon: Gamepad2Icon,
|
||||
tooltip: 'Minecraft version',
|
||||
class: '!text-primary',
|
||||
},
|
||||
{
|
||||
id: 'loader',
|
||||
label: 'Fabric',
|
||||
icon: LoaderIcon,
|
||||
iconProps: { loader: 'Fabric' },
|
||||
tooltip: 'Mod loader',
|
||||
class: '!text-primary',
|
||||
},
|
||||
],
|
||||
divider: false,
|
||||
bottomPadding: false,
|
||||
mainClass: 'items-center',
|
||||
titleClass: 'leading-8',
|
||||
truncateTitle: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const ServerPanelRootHeader: Story = {
|
||||
args: {
|
||||
header: 'Survival SMP',
|
||||
leading: {
|
||||
type: 'component',
|
||||
component: ServerIcon,
|
||||
componentProps: { image: undefined },
|
||||
class: 'size-15 !rounded-xl',
|
||||
},
|
||||
metadata: [
|
||||
{
|
||||
id: 'active-world',
|
||||
label: 'My World',
|
||||
icon: GlobeIcon,
|
||||
tooltip: 'Active instance',
|
||||
},
|
||||
{
|
||||
id: 'address',
|
||||
label: 'play.modrinth.gg',
|
||||
icon: LinkIcon,
|
||||
tooltip: 'Copy server address',
|
||||
onClick: noop,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 'start',
|
||||
label: 'Start server',
|
||||
icon: PlayIcon,
|
||||
color: 'brand',
|
||||
onClick: noop,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Server settings',
|
||||
icon: SettingsIcon,
|
||||
labelHidden: true,
|
||||
tooltip: 'Server settings',
|
||||
onClick: noop,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const ServerPanelInstanceHeader: Story = {
|
||||
args: {
|
||||
header: 'My World',
|
||||
leading: {
|
||||
type: 'button',
|
||||
icon: LeftArrowIcon,
|
||||
to: '/hosting/manage/demo-server/instances',
|
||||
ariaLabel: 'All instances',
|
||||
tooltip: 'All instances',
|
||||
},
|
||||
metadata: [
|
||||
{
|
||||
id: 'game-version',
|
||||
label: '1.20.1',
|
||||
icon: Gamepad2Icon,
|
||||
tooltip: 'Minecraft version',
|
||||
},
|
||||
{
|
||||
id: 'loader',
|
||||
label: 'Fabric 0.19.2',
|
||||
icon: LoaderIcon,
|
||||
iconProps: { loader: 'Fabric' },
|
||||
tooltip: 'Mod loader',
|
||||
},
|
||||
{
|
||||
id: 'last-active',
|
||||
label: 'Last active 2 weeks ago',
|
||||
icon: TimerIcon,
|
||||
tooltip: 'Last activity',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 'start',
|
||||
label: 'Start instance',
|
||||
icon: PlayIcon,
|
||||
color: 'brand',
|
||||
onClick: noop,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Instance settings',
|
||||
icon: SettingsIcon,
|
||||
labelHidden: true,
|
||||
tooltip: 'Instance settings',
|
||||
onClick: noop,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user