* fix: files.vue bugs before styling changes * feat: move files tab to shared layout structure * fix: qa * fix: qa * fix: bugs * fix: lint * fix: admonition cleanup with progress + actions * fix: cleanup * fix: modals * fix: admon title * fix: i18n standard * fix: lint + i18n pass * fix: remove transition * fix: type errors * feat: files tab in app * fix: qa * fix: backup item minmax * fix: use ContentPageHeader for server panel * fix: lint * fix: lint * fix: lint * feat: page leave safety * fix: lint * fix: cargo fmt fix * fix: blank in prod * fix: content card table stuff * Revert "fix: blank in prod" This reverts commit 74758fe185cf85a4a20355857f889cb091b97ace. * fix: import * feat: browse worlds/servers flow * fix: worlds tab parity with content tab * fix: perf bug + shader filter pill copy * feat: singleplayer filter * fix: ordering * fix: breadcrumbs * fix: lint * fix: qa * feat: store server proj id when adding to a non-linked instance * fix: lint * fix: i18n + qa * fix: conflict * qa: already installed modal + placeholders not server-specific * fix: qa * fix: add + edit server modals * fix: qa * fix: security * fix: devin flags * fix: lint * chore: change file to break build cache * fix: admon * fix: import path stuff * feat: qa * fix: fmt fmt idiot --------- Signed-off-by: Calum H. <calum@modrinth.com>
717 lines
18 KiB
Vue
717 lines
18 KiB
Vue
<template>
|
|
<div v-if="data">
|
|
<Teleport to="#sidebar-teleport-target">
|
|
<ProjectSidebarCompatibility
|
|
v-if="!isServerProject"
|
|
:project="data"
|
|
:tags="{ loaders: allLoaders, gameVersions: allGameVersions }"
|
|
:project-v3="projectV3"
|
|
class="project-sidebar-section"
|
|
/>
|
|
<ProjectSidebarServerInfo
|
|
v-if="isServerProject"
|
|
:project-v3="projectV3"
|
|
:tags="{ loaders: allLoaders, gameVersions: allGameVersions }"
|
|
:required-content="serverRequiredContent"
|
|
:recommended-version="serverRecommendedVersion"
|
|
:supported-versions="serverSupportedVersions"
|
|
:loaders="serverModpackLoaders"
|
|
:ping="serverPing"
|
|
:status-online="serverStatusOnline"
|
|
class="project-sidebar-section"
|
|
/>
|
|
<ProjectSidebarLinks
|
|
link-target="_blank"
|
|
:project="data"
|
|
:project-v3="projectV3"
|
|
class="project-sidebar-section"
|
|
/>
|
|
<ProjectSidebarTags :project="data" class="project-sidebar-section" />
|
|
<ProjectSidebarCreators
|
|
:organization="organization"
|
|
:members="members"
|
|
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
|
|
:user-link="(username) => `https://modrinth.com/user/${username}`"
|
|
link-target="_blank"
|
|
class="project-sidebar-section"
|
|
/>
|
|
<ProjectSidebarDetails
|
|
:project="data"
|
|
:has-versions="versions.length > 0"
|
|
:link-target="`_blank`"
|
|
:hide-license="isServerProject"
|
|
:show-followers="isServerProject"
|
|
class="project-sidebar-section"
|
|
/>
|
|
</Teleport>
|
|
<div class="flex flex-col gap-4 p-6">
|
|
<InstanceIndicator v-if="instance" :instance="instance" />
|
|
<template v-if="data">
|
|
<Teleport
|
|
v-if="themeStore.featureFlags.project_background"
|
|
to="#background-teleport-target"
|
|
>
|
|
<ProjectBackgroundGradient :project="data" />
|
|
</Teleport>
|
|
<ProjectHeader
|
|
v-else
|
|
:project="data"
|
|
:project-v3="projectV3"
|
|
:ping="serverPing"
|
|
@contextmenu.prevent.stop="handleRightClick"
|
|
>
|
|
<template v-if="isServerProject" #actions>
|
|
<ButtonStyled v-if="serverPlaying" size="large" color="red">
|
|
<button @click="handleStopServer">
|
|
<StopCircleIcon />
|
|
Stop
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled v-else size="large" color="brand">
|
|
<button
|
|
:disabled="data && installingServerProjects.includes(data.id)"
|
|
@click="handleClickPlay"
|
|
>
|
|
<PlayIcon />
|
|
{{ data && installingServerProjects.includes(data.id) ? 'Installing...' : 'Play' }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled size="large" circular>
|
|
<button v-tooltip="'Add server to instance'" @click="handleAddServerToInstance">
|
|
<PlusIcon />
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled size="large" circular type="transparent">
|
|
<OverflowMenu
|
|
:tooltip="`More options`"
|
|
:options="[
|
|
{
|
|
id: 'open-in-browser',
|
|
link: `https://modrinth.com/project/${data.slug}`,
|
|
external: true,
|
|
},
|
|
{
|
|
divider: true,
|
|
},
|
|
{
|
|
id: 'report',
|
|
color: 'red',
|
|
hoverFilled: true,
|
|
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
|
},
|
|
]"
|
|
aria-label="More options"
|
|
>
|
|
<MoreVerticalIcon aria-hidden="true" />
|
|
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
|
<template #report> <ReportIcon /> Report </template>
|
|
</OverflowMenu>
|
|
</ButtonStyled>
|
|
</template>
|
|
<template v-else #actions>
|
|
<ButtonStyled size="large" color="brand">
|
|
<button
|
|
v-tooltip="installed ? `This project is already installed` : null"
|
|
:disabled="installed || installing"
|
|
@click="install(null)"
|
|
>
|
|
<DownloadIcon v-if="!installed && !installing" />
|
|
<CheckIcon v-else-if="installed" />
|
|
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled size="large" circular type="transparent">
|
|
<OverflowMenu
|
|
:tooltip="`More options`"
|
|
:options="[
|
|
{
|
|
id: 'follow',
|
|
disabled: true,
|
|
tooltip: 'Coming soon',
|
|
action: () => {},
|
|
},
|
|
{
|
|
id: 'save',
|
|
disabled: true,
|
|
tooltip: 'Coming soon',
|
|
action: () => {},
|
|
},
|
|
{
|
|
id: 'open-in-browser',
|
|
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
|
|
external: true,
|
|
},
|
|
{
|
|
divider: true,
|
|
},
|
|
{
|
|
id: 'report',
|
|
color: 'red',
|
|
hoverFilled: true,
|
|
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
|
},
|
|
]"
|
|
aria-label="More options"
|
|
>
|
|
<MoreVerticalIcon aria-hidden="true" />
|
|
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
|
<template #follow> <HeartIcon /> Follow </template>
|
|
<template #save> <BookmarkIcon /> Save </template>
|
|
<template #report> <ReportIcon /> Report </template>
|
|
</OverflowMenu>
|
|
</ButtonStyled>
|
|
</template>
|
|
</ProjectHeader>
|
|
<NavTabs
|
|
:links="[
|
|
{
|
|
label: 'Description',
|
|
href: `/project/${$route.params.id}`,
|
|
},
|
|
{
|
|
label: 'Versions',
|
|
href: versionsHref,
|
|
subpages: ['version'],
|
|
shown: projectV3?.minecraft_server == null,
|
|
},
|
|
{
|
|
label: 'Gallery',
|
|
href: `/project/${$route.params.id}/gallery`,
|
|
shown: data.gallery.length > 0,
|
|
},
|
|
]"
|
|
/>
|
|
<RouterView
|
|
v-if="route.path.startsWith('/project')"
|
|
:project="data"
|
|
:versions="versions"
|
|
:members="members"
|
|
:instance="instance"
|
|
:install="install"
|
|
:installed="installed"
|
|
:installing="installing"
|
|
:installed-version="installedVersion"
|
|
/>
|
|
</template>
|
|
<template v-else> Project data couldn't not be loaded. </template>
|
|
</div>
|
|
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
|
<template #install> <DownloadIcon /> Install </template>
|
|
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
|
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
|
</ContextMenu>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import {
|
|
BookmarkIcon,
|
|
CheckIcon,
|
|
ClipboardCopyIcon,
|
|
DownloadIcon,
|
|
ExternalIcon,
|
|
GlobeIcon,
|
|
HeartIcon,
|
|
MoreVerticalIcon,
|
|
PlayIcon,
|
|
PlusIcon,
|
|
ReportIcon,
|
|
StopCircleIcon,
|
|
} from '@modrinth/assets'
|
|
import {
|
|
ButtonStyled,
|
|
injectNotificationManager,
|
|
NavTabs,
|
|
OverflowMenu,
|
|
ProjectBackgroundGradient,
|
|
ProjectHeader,
|
|
ProjectSidebarCompatibility,
|
|
ProjectSidebarCreators,
|
|
ProjectSidebarDetails,
|
|
ProjectSidebarLinks,
|
|
ProjectSidebarServerInfo,
|
|
ProjectSidebarTags,
|
|
} from '@modrinth/ui'
|
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
import dayjs from 'dayjs'
|
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
|
import {
|
|
get_organization,
|
|
get_project,
|
|
get_project_v3,
|
|
get_team,
|
|
get_version,
|
|
get_version_many,
|
|
} from '@/helpers/cache.js'
|
|
import { process_listener } from '@/helpers/events'
|
|
import { get_by_profile_path } from '@/helpers/process'
|
|
import {
|
|
get as getInstance,
|
|
get_projects as getInstanceProjects,
|
|
kill,
|
|
list as listInstances,
|
|
} from '@/helpers/profile'
|
|
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
|
import { getServerLatency } from '@/helpers/worlds'
|
|
import { injectContentInstall } from '@/providers/content-install'
|
|
import { injectServerInstall } from '@/providers/server-install'
|
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
import { getServerAddress } from '@/store/install.js'
|
|
import { useTheming } from '@/store/state.js'
|
|
|
|
dayjs.extend(relativeTime)
|
|
|
|
const { handleError } = injectNotificationManager()
|
|
const { install: installVersion } = injectContentInstall()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const breadcrumbs = useBreadcrumbs()
|
|
const themeStore = useTheming()
|
|
|
|
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
|
|
injectServerInstall()
|
|
const installing = ref(false)
|
|
const data = shallowRef(null)
|
|
const versions = shallowRef([])
|
|
const members = shallowRef([])
|
|
const categories = shallowRef([])
|
|
const organization = shallowRef(null)
|
|
const instance = ref(null)
|
|
const instanceProjects = ref(null)
|
|
|
|
const installed = ref(false)
|
|
const installedVersion = ref(null)
|
|
const isServerProject = ref(false)
|
|
const projectV3 = shallowRef(null)
|
|
const serverRequiredContent = shallowRef(null)
|
|
const serverRecommendedVersion = shallowRef(null)
|
|
const serverSupportedVersions = shallowRef([])
|
|
const serverModpackLoaders = shallowRef([])
|
|
const serverPing = ref(undefined)
|
|
const serverStatusOnline = ref(false)
|
|
const serverInstancePath = ref(null)
|
|
const serverPlaying = ref(false)
|
|
|
|
const instanceFilters = computed(() => {
|
|
if (!instance.value) {
|
|
return {}
|
|
}
|
|
|
|
const loaders = []
|
|
if (data.value.project_type === 'mod') {
|
|
if (instance.value.loader !== 'vanilla') {
|
|
loaders.push(instance.value.loader)
|
|
}
|
|
if (instance.value.loader === 'vanilla' || data.value.loaders.includes('datapack')) {
|
|
loaders.push('datapack')
|
|
}
|
|
}
|
|
|
|
return { l: loaders, g: instance.value.game_version }
|
|
})
|
|
|
|
const versionsHref = computed(() => {
|
|
const base = `/project/${route.params.id}/versions`
|
|
const filters = instanceFilters.value
|
|
const params = new URLSearchParams()
|
|
for (const [key, val] of Object.entries(filters)) {
|
|
if (Array.isArray(val)) {
|
|
for (const v of val) params.append(key, v)
|
|
} else if (val) {
|
|
params.append(key, String(val))
|
|
}
|
|
}
|
|
const qs = params.toString()
|
|
return qs ? `${base}?${qs}` : base
|
|
})
|
|
|
|
const [allLoaders, allGameVersions] = await Promise.all([
|
|
get_loaders().catch(handleError).then(ref),
|
|
get_game_versions().catch(handleError).then(ref),
|
|
])
|
|
|
|
async function handleClickPlay() {
|
|
if (!isServerProject.value) return
|
|
await playServerProject(data.value.id).catch(handleError)
|
|
await updateServerPlayState()
|
|
}
|
|
|
|
async function updateServerPlayState() {
|
|
if (!isServerProject.value || !data.value) return
|
|
const packs = await listInstances()
|
|
const inst = packs.find((p) => p.linked_data?.project_id === data.value.id)
|
|
if (inst) {
|
|
serverInstancePath.value = inst.path
|
|
const processes = await get_by_profile_path(inst.path).catch(() => [])
|
|
serverPlaying.value = Array.isArray(processes) && processes.length > 0
|
|
} else {
|
|
serverInstancePath.value = null
|
|
serverPlaying.value = false
|
|
}
|
|
}
|
|
|
|
async function handleStopServer() {
|
|
if (!serverInstancePath.value) return
|
|
await kill(serverInstancePath.value).catch(() => {})
|
|
serverPlaying.value = false
|
|
}
|
|
|
|
function handleAddServerToInstance() {
|
|
const address = getServerAddress(projectV3.value?.minecraft_java_server)
|
|
if (!address || !data.value) return
|
|
showAddServerToInstanceModal(data.value.title, address)
|
|
}
|
|
|
|
async function fetchProjectData() {
|
|
const [project, projectV3Result] = await Promise.all([
|
|
get_project(route.params.id, 'must_revalidate').catch(handleError),
|
|
get_project_v3(route.params.id, 'must_revalidate').catch(handleError),
|
|
])
|
|
projectV3.value = projectV3Result
|
|
|
|
if (!project) {
|
|
handleError('Error loading project')
|
|
return
|
|
}
|
|
|
|
data.value = project
|
|
;[versions.value, members.value, categories.value, instance.value, instanceProjects.value] =
|
|
await Promise.all([
|
|
get_version_many(project.versions, 'must_revalidate').catch(handleError),
|
|
get_team(project.team).catch(handleError),
|
|
get_categories().catch(handleError),
|
|
route.query.i ? getInstance(route.query.i).catch(handleError) : Promise.resolve(),
|
|
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
|
|
])
|
|
|
|
versions.value = versions.value.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published))
|
|
|
|
if (instanceProjects.value) {
|
|
const installedFile = Object.values(instanceProjects.value).find(
|
|
(x) => x.metadata && x.metadata.project_id === data.value.id,
|
|
)
|
|
if (installedFile) {
|
|
installed.value = true
|
|
installedVersion.value = installedFile.metadata.version_id
|
|
}
|
|
}
|
|
|
|
if (project.organization) {
|
|
organization.value = await get_organization(project.organization).catch(handleError)
|
|
}
|
|
|
|
isServerProject.value = projectV3.value?.minecraft_server != null
|
|
serverStatusOnline.value = !!projectV3.value?.minecraft_java_server?.ping?.data
|
|
|
|
breadcrumbs.setName('Project', data.value.title)
|
|
|
|
fetchDeferredServerData(project)
|
|
}
|
|
|
|
function fetchDeferredServerData(project) {
|
|
const serverAddress = projectV3.value?.minecraft_java_server?.address
|
|
if (serverAddress) {
|
|
serverPing.value = undefined
|
|
getServerLatency(serverAddress)
|
|
.then((latency) => {
|
|
serverPing.value = latency
|
|
})
|
|
.catch((error) => {
|
|
console.error(`Failed to ping server ${serverAddress}:`, error)
|
|
})
|
|
}
|
|
|
|
const content = projectV3.value?.minecraft_java_server?.content
|
|
if (content?.kind === 'modpack' && content.version_id) {
|
|
get_version(content.version_id, 'bypass')
|
|
.catch(handleError)
|
|
.then(async (modpackVersion) => {
|
|
if (!modpackVersion) return
|
|
serverRecommendedVersion.value = modpackVersion.game_versions?.[0] ?? null
|
|
serverModpackLoaders.value = modpackVersion.mrpack_loaders ?? []
|
|
if (modpackVersion.project_id) {
|
|
const modpackProject = await get_project_v3(
|
|
modpackVersion.project_id,
|
|
'must_revalidate',
|
|
).catch(handleError)
|
|
if (modpackProject) {
|
|
const primaryFile =
|
|
modpackVersion.files?.find((f) => f.primary) ?? modpackVersion.files?.[0]
|
|
|
|
serverRequiredContent.value = {
|
|
name: modpackProject.name,
|
|
versionNumber: modpackVersion.version_number ?? '',
|
|
icon: modpackProject.icon_url,
|
|
onclickName:
|
|
modpackProject.id !== project.id
|
|
? () => router.push(`/project/${modpackProject.id}`)
|
|
: undefined,
|
|
onclickVersion:
|
|
modpackProject.id !== project.id
|
|
? () => router.push(`/project/${modpackProject.id}/version/${modpackVersion.id}`)
|
|
: undefined,
|
|
onclickDownload: primaryFile?.url ? () => openUrl(primaryFile.url) : undefined,
|
|
showCustomModpackTooltip: modpackProject.id === project.id,
|
|
}
|
|
}
|
|
}
|
|
})
|
|
} else if (content?.kind === 'vanilla') {
|
|
serverRecommendedVersion.value = content.recommended_game_version ?? null
|
|
const supported = content.supported_game_versions ?? []
|
|
serverSupportedVersions.value = supported.filter((v) => !!v)
|
|
}
|
|
|
|
updateServerPlayState()
|
|
}
|
|
|
|
await fetchProjectData()
|
|
|
|
let unlistenProcesses
|
|
process_listener((e) => {
|
|
if (
|
|
e.event === 'finished' &&
|
|
serverInstancePath.value &&
|
|
e.profile_path_id === serverInstancePath.value
|
|
) {
|
|
serverPlaying.value = false
|
|
}
|
|
}).then((unlisten) => {
|
|
unlistenProcesses = unlisten
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
unlistenProcesses?.()
|
|
})
|
|
|
|
watch(
|
|
() => route.params.id,
|
|
async () => {
|
|
if (route.params.id && route.path.startsWith('/project')) {
|
|
await fetchProjectData()
|
|
}
|
|
},
|
|
)
|
|
|
|
async function install(version) {
|
|
installing.value = true
|
|
await installVersion(
|
|
data.value.id,
|
|
version,
|
|
instance.value ? instance.value.path : null,
|
|
'ProjectPage',
|
|
(version) => {
|
|
installing.value = false
|
|
|
|
if (instance.value && version) {
|
|
installed.value = true
|
|
installedVersion.value = version
|
|
}
|
|
},
|
|
(profile) => {
|
|
router.push(`/instance/${profile}`)
|
|
},
|
|
).catch(handleError)
|
|
}
|
|
|
|
const options = ref(null)
|
|
const handleRightClick = (event) => {
|
|
options.value.showMenu(event, data.value, [
|
|
{
|
|
name: 'install',
|
|
},
|
|
{
|
|
type: 'divider',
|
|
},
|
|
{
|
|
name: 'open_link',
|
|
},
|
|
{
|
|
name: 'copy_link',
|
|
},
|
|
])
|
|
}
|
|
const handleOptionsClick = (args) => {
|
|
switch (args.option) {
|
|
case 'install':
|
|
install(null)
|
|
break
|
|
case 'open_link':
|
|
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
|
break
|
|
case 'copy_link':
|
|
navigator.clipboard.writeText(
|
|
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
|
)
|
|
break
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.root-container {
|
|
display: flex;
|
|
flex-direction: row;
|
|
min-height: 100%;
|
|
}
|
|
|
|
.project-sidebar {
|
|
position: fixed;
|
|
width: calc(300px + 1.5rem);
|
|
min-height: calc(100vh - 3.25rem);
|
|
height: fit-content;
|
|
max-height: calc(100vh - 3.25rem);
|
|
padding: 1rem 0.5rem 1rem 1rem;
|
|
overflow-y: auto;
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
|
|
&::-webkit-scrollbar {
|
|
width: 0;
|
|
background: transparent;
|
|
}
|
|
}
|
|
|
|
.sidebar-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.content-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
padding: 1rem;
|
|
margin-left: calc(300px + 1rem);
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
flex-direction: row;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.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 {
|
|
min-height: var(--stat-strong-size);
|
|
min-width: var(--stat-strong-size);
|
|
}
|
|
}
|
|
|
|
.date {
|
|
margin-top: auto;
|
|
}
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: 1rem;
|
|
margin-bottom: var(--gap-md);
|
|
justify-content: space-between;
|
|
|
|
.tab {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
border-radius: var(--border-radius);
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease-in-out;
|
|
|
|
&:hover {
|
|
background-color: var(--color-raised-bg);
|
|
}
|
|
|
|
&.router-view-active {
|
|
background-color: var(--color-raised-bg);
|
|
}
|
|
}
|
|
}
|
|
|
|
.links {
|
|
a {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
border-radius: 1rem;
|
|
color: var(--color-text);
|
|
|
|
svg,
|
|
img {
|
|
height: 1rem;
|
|
width: 1rem;
|
|
}
|
|
|
|
span {
|
|
margin-left: 0.25rem;
|
|
text-decoration: underline;
|
|
line-height: 2rem;
|
|
}
|
|
|
|
&:focus-visible,
|
|
&:hover {
|
|
svg,
|
|
img,
|
|
span {
|
|
color: var(--color-heading);
|
|
}
|
|
}
|
|
|
|
&:active {
|
|
svg,
|
|
img,
|
|
span {
|
|
color: var(--color-text-dark);
|
|
}
|
|
}
|
|
|
|
&:not(:last-child)::after {
|
|
content: '•';
|
|
margin: 0 0.25rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
.install-loading {
|
|
scale: 0.2;
|
|
height: 1rem;
|
|
width: 1rem;
|
|
margin-right: -1rem;
|
|
|
|
:deep(svg) {
|
|
color: var(--color-contrast);
|
|
}
|
|
}
|
|
|
|
.project-sidebar-section {
|
|
@apply p-4 flex flex-col gap-2 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid;
|
|
}
|
|
</style>
|