* fix port in server address * fix: invalid args for path * fix page loading * hide labels
842 lines
21 KiB
Vue
842 lines
21 KiB
Vue
<template>
|
|
<div v-if="instance">
|
|
<div class="p-6 pr-2 pb-4" @contextmenu.prevent.stop="(event) => handleRightClick(event)">
|
|
<ExportModal ref="exportModal" :instance="instance" />
|
|
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
|
|
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
|
|
<ContentPageHeader>
|
|
<template #icon>
|
|
<Avatar
|
|
:src="icon ? icon : undefined"
|
|
:alt="instance.name"
|
|
size="64px"
|
|
:tint-by="instance.path"
|
|
/>
|
|
</template>
|
|
<template #title>
|
|
{{ instance.name }}
|
|
</template>
|
|
<template #stats>
|
|
<div class="flex items-center flex-wrap gap-2">
|
|
<template v-if="!isServerInstance">
|
|
<div class="flex items-center gap-2 capitalize font-medium">
|
|
{{ instance.loader }} {{ instance.game_version }}
|
|
</div>
|
|
|
|
<div class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
|
|
|
|
<div class="flex items-center gap-2 font-medium">
|
|
<template v-if="timePlayed > 0">
|
|
{{ timePlayedHumanized }}
|
|
</template>
|
|
<template v-else> Never played </template>
|
|
</div>
|
|
|
|
<div v-if="linkedProjectV3" class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
|
|
|
|
<div
|
|
v-if="linkedProjectV3"
|
|
class="flex gap-1.5 items-center font-medium text-primary"
|
|
>
|
|
Linked to
|
|
<Avatar
|
|
:src="linkedProjectV3.icon_url"
|
|
:alt="linkedProjectV3.name"
|
|
:tint-by="instance.path"
|
|
size="24px"
|
|
/>
|
|
<router-link
|
|
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
|
|
class="hover:underline text-primary truncate"
|
|
>
|
|
{{ linkedProjectV3.name }}
|
|
</router-link>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<ServerOnlinePlayers
|
|
v-if="playersOnline !== undefined"
|
|
:online="playersOnline"
|
|
:status-online="statusOnline"
|
|
hide-label
|
|
/>
|
|
|
|
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
|
|
|
|
<div
|
|
v-if="
|
|
(playersOnline !== undefined || recentPlays !== undefined) &&
|
|
(minecraftServer?.region || ping)
|
|
"
|
|
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
|
></div>
|
|
|
|
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
|
|
|
|
<ServerPing v-if="ping" :ping="ping" />
|
|
|
|
<div
|
|
v-if="minecraftServer?.region || ping"
|
|
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
|
></div>
|
|
|
|
<div
|
|
v-if="linkedProjectV3"
|
|
class="flex gap-1.5 items-center font-medium text-primary"
|
|
>
|
|
Linked to
|
|
<Avatar
|
|
:src="linkedProjectV3.icon_url"
|
|
:alt="linkedProjectV3.name"
|
|
:tint-by="instance.path"
|
|
size="24px"
|
|
/>
|
|
<router-link
|
|
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
|
|
class="hover:underline text-primary truncate"
|
|
>
|
|
{{ linkedProjectV3.name }}
|
|
</router-link>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
<template #actions>
|
|
<div class="flex gap-2">
|
|
<ButtonStyled
|
|
v-if="
|
|
['installing', 'pack_installing', 'minecraft_installing'].includes(
|
|
instance.install_stage,
|
|
)
|
|
"
|
|
color="brand"
|
|
size="large"
|
|
>
|
|
<button disabled>Installing...</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled
|
|
v-else-if="instance.install_stage !== 'installed'"
|
|
color="brand"
|
|
size="large"
|
|
>
|
|
<button @click="repairInstance()">
|
|
<DownloadIcon />
|
|
Repair
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled v-else-if="playing === true" color="red" size="large">
|
|
<button @click="stopInstance('InstancePage')">
|
|
<StopCircleIcon />
|
|
Stop
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled
|
|
v-else-if="playing === false && loading === false && !isServerInstance"
|
|
color="brand"
|
|
size="large"
|
|
>
|
|
<button @click="startInstance('InstancePage')">
|
|
<PlayIcon />
|
|
Play
|
|
</button>
|
|
</ButtonStyled>
|
|
<div
|
|
v-else-if="playing === false && loading === false && isServerInstance"
|
|
class="joined-buttons"
|
|
>
|
|
<ButtonStyled color="brand" size="large">
|
|
<button @click="handlePlayServer()">
|
|
<PlayIcon />
|
|
Play
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled color="brand" size="large">
|
|
<OverflowMenu
|
|
:options="[
|
|
{
|
|
id: 'join_server',
|
|
action: () => handlePlayServer(),
|
|
},
|
|
{
|
|
id: 'launch_instance',
|
|
action: () => startInstance('InstancePage'),
|
|
},
|
|
]"
|
|
>
|
|
<div class="w-0 text-xl relative top-0.5 right-2.5">
|
|
<DropdownIcon />
|
|
</div>
|
|
|
|
<template #join_server>
|
|
<PlayIcon />
|
|
Join server
|
|
</template>
|
|
<template #launch_instance>
|
|
<PlayIcon />
|
|
Launch instance
|
|
</template>
|
|
</OverflowMenu>
|
|
</ButtonStyled>
|
|
</div>
|
|
<ButtonStyled
|
|
v-else-if="loading === true && playing === false"
|
|
color="brand"
|
|
size="large"
|
|
>
|
|
<button disabled>Loading...</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled circular size="large">
|
|
<button v-tooltip="'Instance settings'" @click="settingsModal?.show()">
|
|
<SettingsIcon />
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled type="transparent" circular size="large">
|
|
<OverflowMenu
|
|
:options="[
|
|
{
|
|
id: 'open-folder',
|
|
action: () => {
|
|
if (instance) showProfileInFolder(instance.path)
|
|
},
|
|
},
|
|
{
|
|
id: 'export-mrpack',
|
|
action: () => exportModal?.show(),
|
|
},
|
|
]"
|
|
>
|
|
<MoreVerticalIcon />
|
|
<template #share-instance> <UserPlusIcon /> Share instance </template>
|
|
<template #host-a-server> <ServerIcon /> Create a server </template>
|
|
<template #open-folder> <FolderOpenIcon /> Open folder </template>
|
|
<template #export-mrpack> <PackageIcon /> Export modpack </template>
|
|
</OverflowMenu>
|
|
</ButtonStyled>
|
|
</div>
|
|
</template>
|
|
</ContentPageHeader>
|
|
</div>
|
|
<div class="px-6">
|
|
<NavTabs :links="tabs" />
|
|
</div>
|
|
<div v-if="!!instance" class="p-6 pt-4">
|
|
<RouterView
|
|
v-if="route.path.startsWith('/instance')"
|
|
v-slot="{ Component }"
|
|
:key="instance.path"
|
|
>
|
|
<template v-if="Component">
|
|
<Suspense
|
|
:key="instance.path"
|
|
@pending="loadingBar.startLoading()"
|
|
@resolve="loadingBar.stopLoading()"
|
|
>
|
|
<component
|
|
:is="Component"
|
|
:instance="instance"
|
|
:options="options"
|
|
:offline="offline"
|
|
:playing="playing"
|
|
:versions="modrinthVersions"
|
|
:installed="instance.install_stage !== 'installed'"
|
|
:is-server-instance="isServerInstance"
|
|
@play="updatePlayState"
|
|
@stop="() => stopInstance('InstanceSubpage')"
|
|
></component>
|
|
<template #fallback>
|
|
<LoadingIndicator />
|
|
</template>
|
|
</Suspense>
|
|
</template>
|
|
</RouterView>
|
|
</div>
|
|
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
|
<template #play> <PlayIcon /> Play </template>
|
|
<template #stop> <StopCircleIcon /> Stop </template>
|
|
<template #add_content> <PlusIcon /> Add content </template>
|
|
<template #edit> <EditIcon /> Edit </template>
|
|
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
|
<template #open_folder> <FolderOpenIcon /> Open folder </template>
|
|
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
|
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
|
<template #copy_names><EditIcon />Copy names</template>
|
|
<template #copy_slugs><HashIcon />Copy slugs</template>
|
|
<template #copy_links><GlobeIcon />Copy links</template>
|
|
<template #toggle><EditIcon />Toggle selected</template>
|
|
<template #disable><XIcon />Disable selected</template>
|
|
<template #enable><CheckCircleIcon />Enable selected</template>
|
|
<template #hide_show><EyeIcon />Show/Hide unselected</template>
|
|
<template #update_all
|
|
><UpdatedIcon />Update {{ selected.length > 0 ? 'selected' : 'all' }}</template
|
|
>
|
|
<template #filter_update><UpdatedIcon />Select Updatable</template>
|
|
</ContextMenu>
|
|
</div>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import type { Labrinth } from '@modrinth/api-client'
|
|
import {
|
|
CheckCircleIcon,
|
|
ClipboardCopyIcon,
|
|
DownloadIcon,
|
|
DropdownIcon,
|
|
EditIcon,
|
|
ExternalIcon,
|
|
EyeIcon,
|
|
FolderOpenIcon,
|
|
GlobeIcon,
|
|
HashIcon,
|
|
MoreVerticalIcon,
|
|
PackageIcon,
|
|
PlayIcon,
|
|
PlusIcon,
|
|
ServerIcon,
|
|
SettingsIcon,
|
|
StopCircleIcon,
|
|
UpdatedIcon,
|
|
UserPlusIcon,
|
|
XIcon,
|
|
} from '@modrinth/assets'
|
|
import {
|
|
Avatar,
|
|
ButtonStyled,
|
|
ContentPageHeader,
|
|
injectNotificationManager,
|
|
LoadingIndicator,
|
|
OverflowMenu,
|
|
ServerOnlinePlayers,
|
|
ServerPing,
|
|
ServerRecentPlays,
|
|
ServerRegion,
|
|
} from '@modrinth/ui'
|
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
import dayjs from 'dayjs'
|
|
import duration from 'dayjs/plugin/duration'
|
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
import ExportModal from '@/components/ui/ExportModal.vue'
|
|
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
|
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
|
import { trackEvent } from '@/helpers/analytics'
|
|
import { get_project_v3, get_version_many } from '@/helpers/cache.js'
|
|
import { process_listener, profile_listener } from '@/helpers/events'
|
|
import { get_by_profile_path } from '@/helpers/process'
|
|
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
|
|
import type { GameInstance } from '@/helpers/types'
|
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
|
import { get_server_status, getServerLatency } from '@/helpers/worlds'
|
|
import { handleSevereError } from '@/store/error.js'
|
|
import { playServerProject } from '@/store/install.js'
|
|
import { useBreadcrumbs, useLoading } from '@/store/state'
|
|
|
|
dayjs.extend(duration)
|
|
dayjs.extend(relativeTime)
|
|
|
|
const { handleError } = injectNotificationManager()
|
|
const route = useRoute()
|
|
|
|
const router = useRouter()
|
|
const breadcrumbs = useBreadcrumbs()
|
|
|
|
const offline = ref(!navigator.onLine)
|
|
window.addEventListener('offline', () => {
|
|
offline.value = true
|
|
})
|
|
window.addEventListener('online', () => {
|
|
offline.value = false
|
|
})
|
|
|
|
const instance = ref<GameInstance>()
|
|
const modrinthVersions = ref<Labrinth.Versions.v2.Version[]>([])
|
|
const playing = ref(false)
|
|
const loading = ref(false)
|
|
const exportModal = ref<InstanceType<typeof ExportModal>>()
|
|
const updateToPlayModal = ref<InstanceType<typeof UpdateToPlayModal>>()
|
|
|
|
const isServerInstance = ref(false)
|
|
const linkedProjectV3 = ref<Labrinth.Projects.v3.Project>()
|
|
const selected = ref<unknown[]>([])
|
|
|
|
const minecraftServer = computed(() => linkedProjectV3.value?.minecraft_server)
|
|
const javaServerPingData = computed(() => linkedProjectV3.value?.minecraft_java_server?.ping?.data)
|
|
const statusOnline = computed(() => !!javaServerPingData.value)
|
|
const recentPlays = computed(
|
|
() => linkedProjectV3.value?.minecraft_java_server?.verified_plays_2w ?? undefined,
|
|
)
|
|
const playersOnline = ref<number | undefined>(undefined)
|
|
const ping = ref<number | undefined>(undefined)
|
|
|
|
async function fetchInstance() {
|
|
isServerInstance.value = false
|
|
linkedProjectV3.value = undefined
|
|
modrinthVersions.value = []
|
|
ping.value = undefined
|
|
playersOnline.value = undefined
|
|
|
|
instance.value = await get(route.params.id as string).catch(handleError)
|
|
|
|
if (!offline.value && instance.value?.linked_data && instance.value.linked_data.project_id) {
|
|
try {
|
|
linkedProjectV3.value = await get_project_v3(
|
|
instance.value.linked_data.project_id,
|
|
'must_revalidate',
|
|
)
|
|
|
|
if (linkedProjectV3.value?.minecraft_server != null) {
|
|
isServerInstance.value = true
|
|
}
|
|
|
|
if (linkedProjectV3.value && linkedProjectV3.value.versions) {
|
|
const versions = await get_version_many(linkedProjectV3.value.versions, 'must_revalidate')
|
|
modrinthVersions.value = versions.sort(
|
|
(a: Labrinth.Versions.v2.Version, b: Labrinth.Versions.v2.Version) =>
|
|
dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf(),
|
|
)
|
|
}
|
|
} catch (error) {
|
|
handleError(error as Error)
|
|
}
|
|
}
|
|
|
|
fetchDeferredData()
|
|
}
|
|
|
|
function fetchDeferredData() {
|
|
const serverAddress = linkedProjectV3.value?.minecraft_java_server?.address
|
|
if (isServerInstance.value && serverAddress) {
|
|
Promise.all([get_server_status(serverAddress), getServerLatency(serverAddress)])
|
|
.then(([status, latency]) => {
|
|
ping.value = latency
|
|
playersOnline.value = status.players?.online
|
|
})
|
|
.catch((err) => {
|
|
console.error(`Failed to ping server ${serverAddress}:`, err)
|
|
})
|
|
}
|
|
|
|
updatePlayState()
|
|
}
|
|
|
|
async function updatePlayState() {
|
|
if (!route.params.id) return
|
|
const runningProcesses = await get_by_profile_path(route.params.id as string).catch(handleError)
|
|
|
|
playing.value = Array.isArray(runningProcesses) && runningProcesses.length > 0
|
|
}
|
|
|
|
await fetchInstance()
|
|
watch(
|
|
() => route.params.id,
|
|
async () => {
|
|
if (route.params.id && route.path.startsWith('/instance')) {
|
|
await fetchInstance()
|
|
}
|
|
},
|
|
)
|
|
|
|
const basePath = computed(() => `/instance/${encodeURIComponent(route.params.id as string)}`)
|
|
|
|
const tabs = computed(() => [
|
|
{
|
|
label: 'Content',
|
|
href: `${basePath.value}`,
|
|
},
|
|
{
|
|
label: 'Worlds',
|
|
href: `${basePath.value}/worlds`,
|
|
},
|
|
{
|
|
label: 'Logs',
|
|
href: `${basePath.value}/logs`,
|
|
},
|
|
])
|
|
|
|
if (instance.value) {
|
|
breadcrumbs.setName(
|
|
'Instance',
|
|
instance.value.name.length > 40
|
|
? instance.value.name.substring(0, 40) + '...'
|
|
: instance.value.name,
|
|
)
|
|
breadcrumbs.setContext({
|
|
name: instance.value.name,
|
|
link: route.path,
|
|
query: route.query,
|
|
})
|
|
}
|
|
|
|
const loadingBar = useLoading()
|
|
|
|
const options = ref<InstanceType<typeof ContextMenu> | null>(null)
|
|
|
|
const startInstance = async (context: string) => {
|
|
if (!instance.value) return
|
|
if (updateToPlayModal.value?.hasUpdate) {
|
|
updateToPlayModal.value.show(instance.value)
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
try {
|
|
await run(route.params.id as string)
|
|
playing.value = true
|
|
} catch (err) {
|
|
handleSevereError(err, { profilePath: route.params.id as string })
|
|
}
|
|
loading.value = false
|
|
|
|
trackEvent('InstancePlay', {
|
|
loader: instance.value.loader,
|
|
game_version: instance.value.game_version,
|
|
source: context,
|
|
})
|
|
}
|
|
|
|
const stopInstance = async (context: string) => {
|
|
playing.value = false
|
|
await kill(route.params.id as string).catch(handleError)
|
|
|
|
if (!instance.value) return
|
|
trackEvent('InstanceStop', {
|
|
loader: instance.value.loader,
|
|
game_version: instance.value.game_version,
|
|
source: context,
|
|
})
|
|
}
|
|
|
|
const handlePlayServer = async () => {
|
|
if (!instance.value?.linked_data?.project_id) return
|
|
loading.value = true
|
|
try {
|
|
await playServerProject(instance.value.linked_data.project_id)
|
|
} finally {
|
|
await updatePlayState()
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const repairInstance = async () => {
|
|
await finish_install(instance.value).catch(handleError)
|
|
}
|
|
|
|
const handleRightClick = (event: MouseEvent) => {
|
|
const baseOptions = [
|
|
{ name: 'add_content' },
|
|
{ type: 'divider' },
|
|
{ name: 'edit' },
|
|
{ name: 'open_folder' },
|
|
{ name: 'copy_path' },
|
|
]
|
|
|
|
options.value?.showMenu(
|
|
event,
|
|
instance.value,
|
|
playing.value
|
|
? [
|
|
{
|
|
name: 'stop',
|
|
color: 'danger',
|
|
},
|
|
...baseOptions,
|
|
]
|
|
: [
|
|
{
|
|
name: 'play',
|
|
color: 'primary',
|
|
},
|
|
...baseOptions,
|
|
],
|
|
)
|
|
}
|
|
|
|
const handleOptionsClick = async (args: { option: string; item: unknown }) => {
|
|
switch (args.option) {
|
|
case 'play':
|
|
await startInstance('InstancePageContextMenu')
|
|
break
|
|
case 'stop':
|
|
await stopInstance('InstancePageContextMenu')
|
|
break
|
|
case 'add_content':
|
|
await router.push({
|
|
path: `/browse/${instance.value?.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
|
query: { i: route.params.id },
|
|
})
|
|
break
|
|
case 'edit':
|
|
await router.push({
|
|
path: `/instance/${encodeURIComponent(route.params.id as string)}/options`,
|
|
})
|
|
break
|
|
case 'open_folder':
|
|
if (instance.value) await showProfileInFolder(instance.value.path)
|
|
break
|
|
case 'copy_path': {
|
|
if (instance.value) {
|
|
const fullPath = await get_full_path(instance.value?.path)
|
|
await navigator.clipboard.writeText(fullPath)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
const unlistenProfiles = await profile_listener(
|
|
async (event: { profile_path_id: string; event: string }) => {
|
|
if (event.profile_path_id === route.params.id) {
|
|
if (event.event === 'removed') {
|
|
await router.push({
|
|
path: '/',
|
|
})
|
|
return
|
|
}
|
|
instance.value = await get(route.params.id as string).catch(handleError)
|
|
}
|
|
},
|
|
)
|
|
|
|
const unlistenProcesses = await process_listener(
|
|
(e: { event: string; profile_path_id: string }) => {
|
|
if (e.event === 'finished' && e.profile_path_id === route.params.id) {
|
|
playing.value = false
|
|
}
|
|
},
|
|
)
|
|
|
|
const icon = computed(() =>
|
|
instance.value?.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
|
)
|
|
|
|
const settingsModal = ref<InstanceType<typeof InstanceSettingsModal>>()
|
|
|
|
const timePlayed = computed(() => {
|
|
return instance.value
|
|
? instance.value.recent_time_played + instance.value.submitted_time_played
|
|
: 0
|
|
})
|
|
|
|
const timePlayedHumanized = computed(() => {
|
|
const duration = dayjs.duration(timePlayed.value, 'seconds')
|
|
const hours = Math.floor(duration.asHours())
|
|
if (hours >= 1) {
|
|
return hours + ' hour' + (hours > 1 ? 's' : '')
|
|
}
|
|
|
|
const minutes = Math.floor(duration.asMinutes())
|
|
if (minutes >= 1) {
|
|
return minutes + ' minute' + (minutes > 1 ? 's' : '')
|
|
}
|
|
|
|
const seconds = Math.floor(duration.asSeconds())
|
|
return seconds + ' second' + (seconds > 1 ? 's' : '')
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
unlistenProcesses()
|
|
unlistenProfiles()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.instance-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
Button {
|
|
width: 100%;
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.side-cards {
|
|
position: fixed;
|
|
width: 300px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
min-height: calc(100vh - 3.25rem);
|
|
max-height: calc(100vh - 3.25rem);
|
|
overflow-y: auto;
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
|
|
&::-webkit-scrollbar {
|
|
width: 0;
|
|
background: transparent;
|
|
}
|
|
|
|
.card {
|
|
min-height: unset;
|
|
margin-bottom: 0;
|
|
}
|
|
}
|
|
|
|
.instance-nav {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding: 1rem;
|
|
gap: 0.5rem;
|
|
background: var(--color-raised-bg);
|
|
height: 100%;
|
|
}
|
|
|
|
.name {
|
|
font-size: 1.25rem;
|
|
color: var(--color-contrast);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.metadata {
|
|
text-transform: capitalize;
|
|
}
|
|
|
|
.instance-container {
|
|
display: flex;
|
|
flex-direction: row;
|
|
overflow: auto;
|
|
gap: 1rem;
|
|
min-height: 100%;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.instance-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
}
|
|
|
|
.badge {
|
|
display: flex;
|
|
align-items: center;
|
|
font-weight: bold;
|
|
width: fit-content;
|
|
color: var(--color-orange);
|
|
}
|
|
|
|
.pages-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--gap-xs);
|
|
|
|
.btn {
|
|
font-size: 100%;
|
|
font-weight: 400;
|
|
background: inherit;
|
|
transition: all ease-in-out 0.1s;
|
|
width: 100%;
|
|
color: var(--color-primary);
|
|
box-shadow: none;
|
|
|
|
&.router-link-exact-active {
|
|
box-shadow: var(--shadow-inset-lg);
|
|
background: var(--color-button-bg);
|
|
color: var(--color-contrast);
|
|
}
|
|
|
|
&:hover {
|
|
background-color: var(--color-button-bg);
|
|
color: var(--color-contrast);
|
|
box-shadow: var(--shadow-inset-lg);
|
|
text-decoration: none;
|
|
}
|
|
|
|
svg {
|
|
width: 1.3rem;
|
|
height: 1.3rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
.instance-nav {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: flex-start;
|
|
justify-content: left;
|
|
padding: 1rem;
|
|
gap: 0.5rem;
|
|
height: min-content;
|
|
width: 100%;
|
|
}
|
|
|
|
.instance-button {
|
|
width: fit-content;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: flex-start;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.content {
|
|
margin: 0 1rem 0.5rem 20rem;
|
|
width: calc(100% - 20rem);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: auto;
|
|
}
|
|
|
|
.stats {
|
|
grid-area: stats;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-wrap: wrap;
|
|
gap: var(--gap-md);
|
|
|
|
.stat {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
width: fit-content;
|
|
gap: var(--gap-xs);
|
|
--stat-strong-size: 1.25rem;
|
|
|
|
strong {
|
|
font-size: var(--stat-strong-size);
|
|
}
|
|
|
|
p {
|
|
margin: 0;
|
|
}
|
|
|
|
svg {
|
|
height: var(--stat-strong-size);
|
|
width: var(--stat-strong-size);
|
|
}
|
|
}
|
|
|
|
.date {
|
|
margin-top: auto;
|
|
}
|
|
|
|
@media screen and (max-width: 750px) {
|
|
flex-direction: row;
|
|
column-gap: var(--gap-md);
|
|
margin-top: var(--gap-xs);
|
|
}
|
|
|
|
@media screen and (max-width: 600px) {
|
|
margin-top: 0;
|
|
|
|
.stat-label {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
</style>
|