feat: add compact mode for library

This commit is contained in:
tdgao
2026-08-18 23:16:50 -06:00
parent 8308e73994
commit 7502e9b315
6 changed files with 75 additions and 18 deletions
@@ -30,6 +30,7 @@ import type {
} from '@/components/ui/library/use-library' } from '@/components/ui/library/use-library'
import { useLibrary } from '@/components/ui/library/use-library' import { useLibrary } from '@/components/ui/library/use-library'
import { FAVORITES_GROUP_ID, MAX_INSTANCE_GROUP_NAME_LENGTH } from '@/helpers/instance-groups' import { FAVORITES_GROUP_ID, MAX_INSTANCE_GROUP_NAME_LENGTH } from '@/helpers/instance-groups'
import { useTheming } from '@/store/state'
const INSTANCE_GRID_OBSERVER_ACTIVATION_DELAY = 500 const INSTANCE_GRID_OBSERVER_ACTIVATION_DELAY = 500
@@ -48,6 +49,8 @@ const props = withDefaults(
) )
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const themeStore = useTheming()
const compactMode = computed(() => themeStore.getFeatureFlag('compact_instance_cards'))
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const { const {
isSectionCollapsed, isSectionCollapsed,
@@ -475,7 +478,12 @@ onMounted(startInstanceGridResizeObserver)
<div ref="instanceGridContent"> <div ref="instanceGridContent">
<TransitionGroup <TransitionGroup
tag="section" tag="section"
class="grid min-h-[45px] w-full grid-cols-[repeat(auto-fill,minmax(min(10rem,100%),1fr))] max-xl:grid-cols-[repeat(auto-fill,minmax(min(8rem,100%),1fr))] gap-3 overflow-y-auto scroll-smooth" class="grid min-h-[45px] w-full gap-3 overflow-y-auto scroll-smooth"
:class="
compactMode
? 'grid-cols-[repeat(auto-fill,minmax(min(15rem,100%),1fr))]'
: 'grid-cols-[repeat(auto-fill,minmax(min(10rem,100%),1fr))] max-xl:grid-cols-[repeat(auto-fill,minmax(min(8rem,100%),1fr))]'
"
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none" move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
enter-active-class="transition-[opacity,transform] duration-[150ms] ease-out motion-reduce:transition-none" enter-active-class="transition-[opacity,transform] duration-[150ms] ease-out motion-reduce:transition-none"
enter-from-class="opacity-0" enter-from-class="opacity-0"
@@ -4,6 +4,7 @@ import { computed, ref } from 'vue'
import { getInstanceIconUrl } from '@/helpers/instance' import { getInstanceIconUrl } from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types' import type { GameInstance } from '@/helpers/types'
import { useTheming } from '@/store/state'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -16,6 +17,8 @@ const props = withDefaults(
) )
const iconSrc = computed(() => getInstanceIconUrl(props.instance.icon_path)) const iconSrc = computed(() => getInstanceIconUrl(props.instance.icon_path))
const themeStore = useTheming()
const compactMode = computed(() => themeStore.getFeatureFlag('compact_instance_cards'))
const nameRef = ref<HTMLElement | null>(null) const nameRef = ref<HTMLElement | null>(null)
const versionRef = ref<HTMLElement | null>(null) const versionRef = ref<HTMLElement | null>(null)
@@ -23,30 +26,40 @@ const versionRef = ref<HTMLElement | null>(null)
<template> <template>
<div <div
class="relative flex w-full min-w-0 select-none flex-col items-start justify-end gap-3 overflow-clip rounded-[20px] border border-solid bg-surface-3 p-3 text-left transition-all" class="relative flex w-full min-w-0 select-none overflow-clip border border-solid bg-surface-3 text-left transition-all"
:class="{ :class="{
'flex-row items-center justify-start gap-2.5 rounded-xl p-2.5': compactMode,
'flex-col items-start justify-end gap-3 rounded-[20px] p-3': !compactMode,
'[border-color:color-mix(in_srgb,var(--color-text-primary)_40%,transparent)] brightness-110': '[border-color:color-mix(in_srgb,var(--color-text-primary)_40%,transparent)] brightness-110':
selected, selected,
'border-surface-4': !selected, 'border-surface-4': !selected,
}" }"
> >
<div <div
class="relative flex aspect-square min-w-full shrink-0 items-center overflow-clip rounded-2xl" class="relative flex shrink-0 items-center overflow-clip"
:class="compactMode ? 'size-10 rounded-lg' : 'aspect-square min-w-full rounded-2xl'"
> >
<Avatar <Avatar
class="pointer-events-none !rounded-2xl outline-none" class="pointer-events-none outline-none"
:class="compactMode ? '!rounded-lg' : '!rounded-2xl'"
size="100%" size="100%"
:src="iconSrc" :src="iconSrc"
:tint-by="instance.id" :tint-by="instance.id"
alt="" alt=""
no-shadow no-shadow
/> />
<slot name="loading" /> <slot name="loading" :compact="compactMode" />
<div class="absolute bottom-1.5 right-1.5 z-[1] flex size-12 items-center justify-center"> <div
<slot name="leading" /> class="absolute z-[1] flex items-center justify-center"
:class="compactMode ? 'inset-0' : 'bottom-1.5 right-1.5 size-12'"
>
<slot name="leading" :compact="compactMode" />
</div> </div>
</div> </div>
<div class="flex min-w-0 w-full flex-col items-start justify-center gap-1 px-0.5"> <div
class="flex min-w-0 w-full flex-col items-start justify-center gap-1 px-0.5"
:class="{ 'pr-10': compactMode }"
>
<p <p
ref="nameRef" ref="nameRef"
v-tooltip="truncatedTooltip(nameRef, instance.name)" v-tooltip="truncatedTooltip(nameRef, instance.name)"
@@ -62,6 +75,6 @@ const versionRef = ref<HTMLElement | null>(null)
{{ instance.loader }} {{ instance.game_version }} {{ instance.loader }} {{ instance.game_version }}
</p> </p>
</div> </div>
<slot name="overlay" /> <slot name="overlay" :compact="compactMode" />
</div> </div>
</template> </template>
@@ -285,7 +285,7 @@ onMounted(() => {
@mouseenter="checkProcess" @mouseenter="checkProcess"
@pointerdown="handlePointerDown" @pointerdown="handlePointerDown"
> >
<template #loading> <template #loading="{ compact }">
<div <div
v-if="loadingIndicatorVisible" v-if="loadingIndicatorVisible"
class="pointer-events-none absolute inset-0 z-[1] flex items-center justify-center" class="pointer-events-none absolute inset-0 z-[1] flex items-center justify-center"
@@ -293,13 +293,17 @@ onMounted(() => {
<div class="absolute inset-0 bg-surface-1 opacity-30" /> <div class="absolute inset-0 bg-surface-1 opacity-30" />
<SpinnerIcon <SpinnerIcon
v-tooltip="formatMessage(modLoading ? messages.loading : messages.installing)" v-tooltip="formatMessage(modLoading ? messages.loading : messages.installing)"
class="relative size-[30%] animate-spin text-contrast" class="relative animate-spin text-contrast"
:class="compact ? 'size-5' : 'size-[30%]'"
tabindex="-1" tabindex="-1"
/> />
</div> </div>
</template> </template>
<template #leading> <template #leading="{ compact }">
<div class="relative flex size-12 shrink-0 items-center justify-center"> <div
class="relative flex shrink-0 items-center justify-center"
:class="compact ? 'size-10' : 'size-12'"
>
<div class="absolute inset-0 flex items-center justify-center"> <div class="absolute inset-0 flex items-center justify-center">
<IconButton <IconButton
v-if="playing" v-if="playing"
@@ -307,7 +311,7 @@ onMounted(() => {
:label="formatMessage(messages.stop)" :label="formatMessage(messages.stop)"
type="colored" type="colored"
color="red" color="red"
size="lg" :size="compact ? 'md' : 'lg'"
@click="(e) => stop(e, 'InstanceCard')" @click="(e) => stop(e, 'InstanceCard')"
@mouseenter="checkProcess" @mouseenter="checkProcess"
> >
@@ -325,7 +329,7 @@ onMounted(() => {
:label="formatMessage(messages.repair)" :label="formatMessage(messages.repair)"
type="colored" type="colored"
color="brand" color="brand"
size="lg" :size="compact ? 'md' : 'lg'"
class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100" class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
@click="(e) => repair(e)" @click="(e) => repair(e)"
> >
@@ -342,7 +346,7 @@ onMounted(() => {
:label="formatMessage(messages.play)" :label="formatMessage(messages.play)"
type="colored" type="colored"
color="brand" color="brand"
size="lg" :size="compact ? 'md' : 'lg'"
class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100" class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
@click="(e) => play(e, 'InstanceCard')" @click="(e) => play(e, 'InstanceCard')"
@mouseenter="checkProcess" @mouseenter="checkProcess"
@@ -352,10 +356,11 @@ onMounted(() => {
</div> </div>
</div> </div>
</template> </template>
<template #overlay> <template #overlay="{ compact }">
<button <button
type="button" type="button"
class="selection-button group/selection absolute right-2 top-1.5 z-[2] flex size-[50px] cursor-pointer items-start pt-4 justify-center border-0 bg-transparent p-0" class="selection-button group/selection absolute z-[2] flex size-[50px] cursor-pointer items-start pt-4 justify-center border-0 bg-transparent p-0"
:class="compact ? '-right-1 -top-1' : 'right-2 top-1.5'"
:aria-label="formatMessage(selected ? messages.deselect : messages.select)" :aria-label="formatMessage(selected ? messages.deselect : messages.select)"
:aria-pressed="selected" :aria-pressed="selected"
@click.stop="toggleSelection" @click.stop="toggleSelection"
@@ -10,6 +10,7 @@ const themeStore = useTheming()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home' const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
const compactInstanceCardsFlag: FeatureFlag = 'compact_instance_cards'
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings' const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning' const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time' const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
@@ -60,6 +61,14 @@ const messages = defineMessages({
defaultMessage: defaultMessage:
'Show recently played worlds or instances in the "Jump in" section on the Home page.', 'Show recently played worlds or instances in the "Jump in" section on the Home page.',
}, },
compactModeTitle: {
id: 'app.appearance-settings.compact-mode.title',
defaultMessage: 'Compact mode',
},
compactModeDescription: {
id: 'app.appearance-settings.compact-mode.description',
defaultMessage: 'Display library instances in a compact row layout.',
},
showPlayTimeTitle: { showPlayTimeTitle: {
id: 'app.appearance-settings.show-play-time.title', id: 'app.appearance-settings.show-play-time.title',
defaultMessage: 'Show play time', defaultMessage: 'Show play time',
@@ -172,6 +181,26 @@ watch(
/> />
</div> </div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.compactModeTitle) }}
</h3>
<p class="m-0 mt-1">{{ formatMessage(messages.compactModeDescription) }}</p>
</div>
<Toggle
id="compact-mode"
:model-value="themeStore.getFeatureFlag(compactInstanceCardsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(compactInstanceCardsFlag)
themeStore.featureFlags[compactInstanceCardsFlag] = newValue
settings.feature_flags[compactInstanceCardsFlag] = newValue
}
"
/>
</div>
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div> <div>
<h3 class="m-0 text-lg font-semibold text-contrast"> <h3 class="m-0 text-lg font-semibold text-contrast">
+1
View File
@@ -15,6 +15,7 @@ export const DEFAULT_FEATURE_FLAGS = {
pride_fundraiser: true, pride_fundraiser: true,
i18n_debug: false, i18n_debug: false,
show_instance_play_time: true, show_instance_play_time: true,
compact_instance_cards: false,
advanced_filters_collapsed: true, advanced_filters_collapsed: true,
always_show_copy_details: false, always_show_copy_details: false,
hide_installed_modpacks: false, hide_installed_modpacks: false,
+1
View File
@@ -60,6 +60,7 @@ pub enum FeatureFlag {
ServerProjectQa, ServerProjectQa,
I18nDebug, I18nDebug,
ShowInstancePlayTime, ShowInstancePlayTime,
CompactInstanceCards,
SkipNonEssentialWarnings, SkipNonEssentialWarnings,
AdvancedFiltersCollapsed, AdvancedFiltersCollapsed,
AlwaysShowCopyDetails, AlwaysShowCopyDetails,