feat:移除了弹窗,服务器添加sls

This commit is contained in:
2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 changed files with 1322534 additions and 0 deletions

View File

@ -0,0 +1,15 @@
<template>
<span class="inline-flex items-center gap-2.5 text-[var(--color-contrast)] text-[1.375rem] font-extrabold tracking-[-0.035em] leading-none">
<img src="/axolotl.png" alt="" />
<span>Axolotl</span>
</span>
</template>
<style scoped lang="scss">
img {
width: 2rem;
height: 2rem;
object-fit: contain;
filter: drop-shadow(0 0.35rem 0.7rem rgb(245 139 174 / 22%));
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="flex items-center justify-center h-20 mt-4">
<img src="/axolotl.png" alt="" />
</div>
</template>
<style lang="scss" scoped>
img {
width: 5rem;
height: 5rem;
object-fit: contain;
filter: drop-shadow(0 0.75rem 1.5rem rgb(245 139 174 / 28%));
animation: float 3.5s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateY(0) rotate(-2deg);
}
50% {
transform: translateY(-0.4rem) rotate(2deg);
}
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 44 44" fill="none">
<path
d="M33.5752 37.7889C31.3308 39.8664 28.8802 39.5384 26.5212 38.5543C24.0248 37.5484 21.7345 37.5046 19.1007 38.5543C15.8027 39.9102 14.0621 39.5165 12.0924 37.7889C0.915868 26.789 2.56487 10.0377 15.253 9.42536C18.3449 9.57844 20.4978 11.0436 22.3071 11.1748C25.0096 10.65 27.5976 9.14107 30.4834 9.33789C33.9417 9.60031 36.5526 10.9124 38.2703 13.2742C31.1247 17.3637 32.8195 26.3516 39.3697 28.8665C38.0642 32.1468 36.3694 35.4052 33.5523 37.8108L33.5752 37.7889ZM22.078 9.29415C21.7345 4.41745 25.8799 0.393635 30.6437 0C31.3079 5.6421 25.2844 9.84086 22.078 9.29415Z"
fill="currentColor"
/>
</svg>
</template>
<style scoped>
svg {
width: 1.75rem;
height: 1.75rem;
}
</style>

View File

@ -0,0 +1,245 @@
<script setup lang="ts">
import CheckIcon from '@modrinth/assets/icons/check.svg?component'
import CopyIcon from '@modrinth/assets/icons/copy.svg?component'
import ExternalIcon from '@modrinth/assets/icons/external.svg?component'
import { defineMessages, useVIntl } from '@modrinth/ui/src/composables/i18n.ts'
import QqChannelIcon from '~/components/landing/QqChannelIcon.vue'
import QqIcon from '~/components/landing/QqIcon.vue'
const { formatMessage } = useVIntl()
const QQ_GROUP_NUMBER = '737601250'
const QQ_CHANNEL_URL = 'https://pd.qq.com/s/9nfp5rlz0'
const copied = ref(false)
let copyTimer: ReturnType<typeof setTimeout> | undefined
async function copyQqGroupNumber() {
await navigator.clipboard.writeText(QQ_GROUP_NUMBER)
copied.value = true
clearTimeout(copyTimer)
copyTimer = setTimeout(() => {
copied.value = false
}, 2000)
}
const messages = defineMessages({
eyebrow: {
id: 'axolotl-site.community.eyebrow',
defaultMessage: 'Community',
},
title: {
id: 'axolotl-site.community.title',
defaultMessage: 'Join our community',
},
description: {
id: 'axolotl-site.community.description',
defaultMessage:
'Join the official QQ group and QQ channel to get help, share feedback, and stay up to date with new releases.',
},
qqGroup: {
id: 'axolotl-site.community.qq-group',
defaultMessage: 'Official QQ group',
},
qqGroupDescription: {
id: 'axolotl-site.community.qq-group-description',
defaultMessage: 'Group number {number}. Get help and share feedback with other players.',
},
copyQqGroup: {
id: 'axolotl-site.community.copy-qq-group',
defaultMessage: 'Copy group number',
},
copiedQqGroup: {
id: 'axolotl-site.community.copied-qq-group',
defaultMessage: 'Copied!',
},
qqChannel: {
id: 'axolotl-site.community.qq-channel',
defaultMessage: 'QQ channel',
},
qqChannelDescription: {
id: 'axolotl-site.community.qq-channel-description',
defaultMessage: 'Official announcements, release updates, and channel events.',
},
qqChannelJoin: {
id: 'axolotl-site.community.qq-channel-join',
defaultMessage: 'Join the channel',
},
})
</script>
<template>
<section id="community" class="community-section" aria-labelledby="community-title">
<div class="community-intro">
<span class="text-xs font-extrabold uppercase tracking-[0.1em] text-brand">{{ formatMessage(messages.eyebrow) }}</span>
<h2 id="community-title">{{ formatMessage(messages.title) }}</h2>
<p>{{ formatMessage(messages.description) }}</p>
</div>
<div class="community-cards flex items-stretch justify-center w-[min(44rem,100%)] mx-auto mt-10">
<button
type="button"
class="community-card"
:aria-label="formatMessage(messages.copyQqGroup)"
@click="copyQqGroupNumber"
>
<span class="grid place-items-center w-12 h-12 text-[var(--color-contrast)]"><QqIcon /></span>
<span class="text-[var(--color-contrast)] text-[1.05rem] font-bold">{{ formatMessage(messages.qqGroup) }}</span>
<span class="m-0 text-[var(--color-secondary)] text-sm leading-[1.6]">
{{ formatMessage(messages.qqGroupDescription, { number: QQ_GROUP_NUMBER }) }}
</span>
<span class="community-action" :class="{ copied }" aria-live="polite">
<CheckIcon v-if="copied" />
<CopyIcon v-else />
{{ copied ? formatMessage(messages.copiedQqGroup) : formatMessage(messages.copyQqGroup) }}
</span>
</button>
<a class="community-card" :href="QQ_CHANNEL_URL" target="_blank" rel="noopener">
<span class="grid place-items-center w-12 h-12 text-[var(--color-contrast)]"><QqChannelIcon /></span>
<span class="text-[var(--color-contrast)] text-[1.05rem] font-bold">{{ formatMessage(messages.qqChannel) }}</span>
<span class="m-0 text-[var(--color-secondary)] text-sm leading-[1.6]">
{{ formatMessage(messages.qqChannelDescription) }}
</span>
<span class="community-action">
{{ formatMessage(messages.qqChannelJoin) }}
<ExternalIcon />
</span>
</a>
</div>
</section>
</template>
<style scoped lang="scss">
.community-section {
display: grid;
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
gap: 4rem;
width: min(76rem, calc(100% - 3rem));
margin: 0 auto;
padding: 7rem 0;
}
.community-intro {
h2 {
margin: 0.75rem 0 1rem;
color: var(--color-contrast);
font-size: clamp(2rem, 4vw, 3.25rem);
line-height: 1.08;
}
p {
max-width: 32rem;
margin: 0;
color: var(--color-secondary);
font-size: 1.05rem;
line-height: 1.7;
}
}
.community-card {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.7rem;
flex: 0 1 calc(50% - 0.75rem);
max-width: 21.5rem;
padding: 1.5rem;
border: 1px solid var(--landing-border-color);
border-radius: var(--radius-card);
background: var(--landing-card-bg);
box-shadow: var(--landing-card-shadow);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
color: inherit;
font: inherit;
text-align: left;
text-decoration: none;
cursor: pointer;
transition:
background 160ms ease,
border-color 160ms ease,
box-shadow 160ms ease,
transform 160ms ease;
/* 错落叠放:左卡左倾、右卡右倾并压在上面 */
&:first-child {
z-index: 1;
transform: rotate(-2deg);
}
&:last-child {
z-index: 2;
margin-left: -1.5rem;
transform: rotate(4deg);
}
&:hover {
border-color: color-mix(in srgb, var(--color-brand) 50%, var(--landing-border-color));
box-shadow:
var(--landing-card-shadow),
0 0.45rem 1rem rgb(0 0 0 / 12%);
transform: rotate(0deg);
}
&:focus-visible {
outline: 2px solid var(--color-brand);
outline-offset: 2px;
}
}
.community-action {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin-top: auto;
padding: 0.5rem 0.9rem;
border: 1px solid var(--color-divider);
border-radius: 0.75rem;
background: var(--surface-2);
color: var(--color-contrast);
font-size: 0.82rem;
font-weight: 600;
transition:
background 140ms ease,
border-color 140ms ease,
color 140ms ease;
&.copied {
border-color: color-mix(in srgb, var(--color-green) 50%, var(--color-divider));
color: var(--color-green);
}
}
@media (max-width: 800px) {
.community-section {
grid-template-columns: 1fr;
gap: 2rem;
}
}
@media (max-width: 560px) {
.community-section {
width: calc(100% - 2rem);
padding: 4.5rem 0;
}
.community-cards {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.community-card {
width: 100%;
max-width: 24rem;
&:first-child,
&:last-child {
margin-left: 0;
transform: none;
}
}
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<svg
class="dark-icon w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="url(#import-gradient-dark)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 3v12" />
<path d="m8 11 4 4 4-4" />
<path d="M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4" />
<defs>
<linearGradient
id="import-gradient-dark"
x1="12"
y1="0"
x2="12"
y2="24"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#C1E1B1" />
<stop offset="1" stop-color="#A7BDE6" />
</linearGradient>
</defs>
</svg>
<svg
class="light-icon w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="url(#import-gradient-light)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 3v12" />
<path d="m8 11 4 4 4-4" />
<path d="M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4" />
<defs>
<linearGradient
id="import-gradient-light"
x1="12"
y1="0"
x2="12"
y2="24"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#A7D0FF" />
<stop offset="0.414928" stop-color="#F472B6" />
</linearGradient>
</defs>
</svg>
</template>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,63 @@
<template>
<svg
class="dark-icon w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="url(#multiplayer-gradient-dark)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<defs>
<linearGradient
id="multiplayer-gradient-dark"
x1="12"
y1="0"
x2="12"
y2="24"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#C1E1B1" />
<stop offset="1" stop-color="#A7BDE6" />
</linearGradient>
</defs>
</svg>
<svg
class="light-icon w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="url(#multiplayer-gradient-light)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<defs>
<linearGradient
id="multiplayer-gradient-light"
x1="12"
y1="0"
x2="12"
y2="24"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#A7D0FF" />
<stop offset="0.414928" stop-color="#F472B6" />
</linearGradient>
</defs>
</svg>
</template>

View File

@ -0,0 +1,102 @@
<template>
<svg
class="dark-icon w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<g clip-path="url(#clip0_897_3802)">
<path
d="M4.39313 8.58984C2.31984 9.28359 0.75 10.9266 0.75 13.5C0.75 16.5938 3.28125 18.75 6.375 18.75H15.0173M21.9291 17.7066C22.7456 17.0297 23.25 16.013 23.25 14.625C23.25 11.8209 20.7656 10.605 18.75 10.5C18.3333 6.30281 15.4219 3.75 12 3.75C10.7738 3.75 9.71297 4.07484 8.83125 4.60031"
stroke="url(#paint0_linear_897_3802)"
stroke-width="2.34783"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21 21L3 3"
stroke="url(#paint1_linear_897_3802)"
stroke-width="2.34783"
stroke-miterlimit="10"
stroke-linecap="round"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_897_3802"
x1="12"
y1="3.75"
x2="12"
y2="18.75"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#C1E1B1" />
<stop offset="1" stop-color="#A7BDE6" />
</linearGradient>
<linearGradient
id="paint1_linear_897_3802"
x1="12"
y1="3"
x2="12"
y2="21"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#C1E1B1" />
<stop offset="1" stop-color="#A7BDE6" />
</linearGradient>
<clipPath id="clip0_897_3802">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
<svg
class="light-icon w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 26 24"
fill="none"
>
<path
d="M5.39313 8.58984C3.31984 9.28359 1.75 10.9266 1.75 13.5C1.75 16.5938 4.28125 18.75 7.375 18.75H16.0173M22.9291 17.7066C23.7456 17.0297 24.25 16.013 24.25 14.625C24.25 11.8209 21.7656 10.605 19.75 10.5C19.3333 6.30281 16.4219 3.75 13 3.75C11.7738 3.75 10.713 4.07484 9.83125 4.60031"
stroke="url(#paint0_linear_944_4973)"
stroke-width="2.34783"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M22 21L4 3"
stroke="url(#paint1_linear_944_4973)"
stroke-width="2.34783"
stroke-miterlimit="10"
stroke-linecap="round"
/>
<defs>
<linearGradient
id="paint0_linear_944_4973"
x1="13"
y1="3.75"
x2="13"
y2="18.75"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#A7D0FF" />
<stop offset="0.414928" stop-color="#F472B6" />
</linearGradient>
<linearGradient
id="paint1_linear_944_4973"
x1="13"
y1="3"
x2="13"
y2="21"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#A7D0FF" />
<stop offset="0.414928" stop-color="#F472B6" />
</linearGradient>
</defs>
</svg>
</template>

View File

@ -0,0 +1,336 @@
<script setup lang="ts">
import Avatar from '@modrinth/ui/src/components/base/Avatar.vue'
import { defineMessages, useVIntl } from '@modrinth/ui/src/composables/i18n.ts'
const { formatMessage } = useVIntl()
interface ShowcaseProject {
id: string
iconUrl: string
title: string
description: string
}
// 真实 Modrinth 项目id 与 CDN 图标来自公开 API
const projects: ShowcaseProject[] = [
{
id: 'AANobbMI',
title: 'Sodium',
iconUrl:
'https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp',
},
{
id: 'YL57xq9U',
title: 'Iris Shaders',
iconUrl:
'https://cdn.modrinth.com/data/YL57xq9U/18d0e7f076d3d6ed5bedd472b853909aac5da202_96.webp',
},
{
id: 'gvQqBUqZ',
title: 'Lithium',
iconUrl:
'https://cdn.modrinth.com/data/gvQqBUqZ/bcc8686c13af0143adf4285d741256af824f70b7_96.webp',
},
{
id: 'mOgUt4GM',
title: 'Mod Menu',
iconUrl: 'https://cdn.modrinth.com/data/mOgUt4GM/5a20ed1450a0e1e79a1fe04e61bb4e5878bf1d20.png',
},
{
id: 'P7dR8mSH',
title: 'Fabric API',
iconUrl: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
},
{
id: '9s6osm5g',
title: 'Cloth Config API',
iconUrl:
'https://cdn.modrinth.com/data/9s6osm5g/ed8a2316cbb6f4fc5f510e8e13a59a85cbbbff4d_96.webp',
},
{
id: 'lhGA9TYQ',
title: 'Architectury API',
iconUrl:
'https://cdn.modrinth.com/data/lhGA9TYQ/05fe3a61c28faaccaec3533b92e1b321edde7bf6_96.webp',
},
{
id: 'nrJ2NpD0',
title: 'Craftify',
iconUrl: 'https://cdn.modrinth.com/data/nrJ2NpD0/4f21214db060ed4542b1f3983c4113d293480a1b.webp',
},
{
id: 'u6dRKJwZ',
title: 'Just Enough Items (JEI)',
iconUrl:
'https://cdn.modrinth.com/data/u6dRKJwZ/4a3f18ac0d096c9f8e9176984c44be4e58f94c89_96.webp',
},
{
id: 'LNytGWDc',
title: 'Create',
iconUrl:
'https://cdn.modrinth.com/data/LNytGWDc/61d716699bcf1ec42ed4926a9e1c7311be6087e2_96.webp',
},
{
id: '1bokaNcj',
title: "Xaero's Minimap",
iconUrl:
'https://cdn.modrinth.com/data/1bokaNcj/354080f65407e49f486fcf9c4580e82c45ae63b8_96.webp',
},
{
id: '8oi3bsk5',
title: 'Terralith',
iconUrl:
'https://cdn.modrinth.com/data/8oi3bsk5/1959d924a1088944bbf07a06ba523726112d7e7a_96.webp',
},
{
id: 'NNAgCjsB',
title: 'Entity Culling',
iconUrl:
'https://cdn.modrinth.com/data/NNAgCjsB/7873452d6cede4daed12da3d7d8c193ab88b4fd6_96.webp',
},
{
id: 'uXXizFIs',
title: 'FerriteCore',
iconUrl:
'https://cdn.modrinth.com/data/uXXizFIs/222a126f26f8f9ae1eb339f3b767677f18bff31f_96.webp',
},
{
id: 'fRiHVvU7',
title: 'EMI',
iconUrl: 'https://cdn.modrinth.com/data/fRiHVvU7/395fe5302b2bab612ef0623509f768f3c5a5ee0f.webp',
},
{
id: '9eGKb6K1',
title: 'Simple Voice Chat',
iconUrl: 'https://cdn.modrinth.com/data/9eGKb6K1/icon.png',
},
{
id: 'gu7yAYhd',
title: 'CC: Tweaked',
iconUrl: 'https://cdn.modrinth.com/data/gu7yAYhd/icon.png',
},
{
id: 'EsAfCjCV',
title: 'AppleSkin',
iconUrl: 'https://cdn.modrinth.com/data/EsAfCjCV/icon.png',
},
{
id: 'w7ThoJFB',
title: 'Zoomify (Zoom)',
iconUrl:
'https://cdn.modrinth.com/data/w7ThoJFB/e2de67a0bfb9e8aa2347982ab3ec5463f26cca31_96.webp',
},
{
id: 'nvQzSEkH',
title: 'Jade',
iconUrl:
'https://cdn.modrinth.com/data/nvQzSEkH/b04217bc2b7dc524c4d12f81ff42cc1cefb9b0fc_96.webp',
},
].map((project) => ({
...project,
description: `${project.title} is available through Axolotl's content browser.`,
}))
// 每行渲染两份内容保证动画无缝衔接;后半轮是视觉重复,对屏幕阅读器隐藏
const showcaseProjects = [
...projects.map((project) => ({ ...project, isVisualDuplicate: false })),
...projects.map((project) => ({
...project,
id: `${project.id}-repeat`,
isVisualDuplicate: true,
})),
]
const rowCount = 5
const perRow = Math.ceil(showcaseProjects.length / rowCount)
const rows = Array.from({ length: rowCount }, (_, index) =>
showcaseProjects.slice(index * perRow, (index + 1) * perRow),
)
const messages = defineMessages({
title: {
id: 'app-marketing.features.website.title',
defaultMessage: 'From discovery to install',
},
description: {
id: 'app-marketing.features.website.description',
defaultMessage:
'Use project details and version selection to move from Modrinth or CurseForge discovery to an installed instance, with dependencies and updates handled in place.',
},
})
</script>
<template>
<div class="projects-showcase">
<img class="website-logo" src="/axolotl.png" alt="" aria-hidden="true" />
<div v-for="(row, index) in rows" :key="index" class="row">
<div v-for="n in 2" :key="n" class="row__content" :class="{ offset: index % 2 }">
<div
v-for="project in row"
:key="project.id"
class="project"
:aria-hidden="project.isVisualDuplicate ? 'true' : undefined"
>
<Avatar :src="project.iconUrl" alt="" size="sm" />
<div class="project-info">
<span class="title">{{ project.title }}</span>
<span class="description">{{ project.description }}</span>
</div>
</div>
</div>
</div>
<h3>{{ formatMessage(messages.title) }}</h3>
<p>{{ formatMessage(messages.description) }}</p>
</div>
</template>
<style lang="scss" scoped>
.projects-showcase {
margin: calc(5rem + var(--gap-xl)) 0 var(--gap-xl);
z-index: 3;
text-align: left;
.row {
--gap: var(--gap-md);
// 100vw 在桌面 Firefox经典滚动条下包含滚动条宽度会横向溢出改用容器宽度
width: 100%;
gap: var(--gap);
margin-bottom: var(--gap);
display: flex;
overflow: hidden;
user-select: none;
&:hover {
.row__content {
animation-play-state: paused !important;
}
}
.row__content {
flex-shrink: 0;
display: flex;
min-width: 100%;
gap: var(--gap);
animation: scroll 40s linear infinite;
@media (prefers-reduced-motion: reduce) {
animation-play-state: paused !important;
}
@keyframes scroll {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(-100%));
}
}
&.offset {
transform: translateX(-100%);
animation: scroll-inverse 40s linear infinite;
@keyframes scroll-inverse {
from {
transform: translateX(calc(-100%));
}
to {
transform: translateX(0);
}
}
}
}
.project {
position: relative;
display: flex;
cursor: default;
padding: 1rem;
gap: 1rem;
border-radius: 1rem;
border: 1px solid var(--landing-border-color);
transition:
background 0.5s ease-in-out,
transform 0.05s ease-in-out;
// hover 位移在移动端卡顿,与 Modrinth 原版一致只保留背景渐变
background: var(--landing-blob-gradient);
user-select: none;
&:hover {
background: var(--landing-hover-card-gradient);
}
img {
height: 3rem;
}
.project-info {
box-sizing: border-box;
}
.title {
color: var(--landing-color-heading);
max-width: 13.75rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0;
font-weight: 600;
font-size: 1.25rem;
line-height: 110%;
display: block;
}
.description {
width: 13.75rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 500;
font-size: 0.875rem;
line-height: 125%;
margin: 0.25rem 0 0;
}
}
}
.website-logo {
position: absolute;
top: 1.5rem;
left: 50%;
width: 3.5rem;
height: 3.5rem;
transform: translateX(-50%);
z-index: 4;
object-fit: contain;
}
h3 {
font-weight: 500;
font-size: var(--font-size-xl);
color: var(--landing-color-heading);
margin-bottom: 0.375rem;
text-align: center;
}
p {
font-size: var(--font-size-md);
color: var(--landing-color-subheading);
padding: var(--gap-xl);
padding-top: 0;
text-align: center;
}
}
// 亮色模式项目卡背景(与页面其它卡片一致)
:global(html.light-mode) .project {
background: rgba(255, 255, 255, 0.8) !important;
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<!-- QQ 频道官方图标pd.qq.com favicon.svg原样引用无明暗切换 -->
<svg
class="w-[2.8rem] h-[2.8rem]"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 128 128"
fill="none"
aria-hidden="true"
>
<path
class="qq-channel-glyph"
d="M105.534 43.3071H90.7396L92.1344 33.8696C92.2086 33.3651 91.8228 32.9199 91.3183 32.9199H81.7918C81.3763 32.9199 81.035 33.2167 80.9756 33.6322L79.5363 43.3071H54.696L56.0909 33.8696C56.1651 33.3651 55.7792 32.9199 55.2747 32.9199H45.763C45.3475 32.9199 45.0062 33.2167 44.9469 33.6322L43.5075 43.3071H28.4757C28.0603 43.3071 27.719 43.6039 27.6596 44.0194L26.3686 52.7446C26.2944 53.2492 26.6802 53.6943 27.1848 53.6943H41.9643L40.1984 65.5802C35.4055 64.6602 30.4642 64.126 25.4041 64.0815C24.9886 64.0815 24.6325 64.3931 24.5731 64.7938L23.2821 73.519C23.2079 74.0087 23.6086 74.4539 24.0983 74.4539C30.479 74.4984 34.7823 75.0326 38.67 75.8487L35.9397 94.2638C35.8655 94.7683 36.2513 95.2135 36.7558 95.2135H46.2675C46.683 95.2135 47.0243 94.9167 47.0837 94.5012L49.4134 78.8462C58.9844 82.3778 67.6058 87.8979 74.7433 94.9018C77.7704 97.8696 82.8898 96.1038 83.5131 91.9192L83.884 89.4115L84.5666 84.8263H99.5984C100.014 84.8263 100.355 84.5295 100.415 84.114L101.706 75.3887C101.78 74.8842 101.394 74.439 100.889 74.439H86.095L89.1815 53.6795H104.213C104.629 53.6795 104.97 53.3827 105.029 52.9672L106.32 44.242C106.395 43.7375 106.009 43.2923 105.504 43.2923L105.534 43.3071ZM77.7111 55.6234L74.402 77.8668C74.2091 79.1281 72.7697 79.7068 71.731 78.9797C65.7806 74.7803 59.2367 71.3674 52.2624 68.8745C51.5057 68.6074 51.0456 67.8506 51.1644 67.0493L52.9302 55.1337C53.0489 54.3027 53.7612 53.6943 54.607 53.6943H76.0343C77.073 53.6943 77.8595 54.6143 77.7111 55.6382V55.6234Z"
/>
<path
d="M126.783 45.5479C124.721 34.2258 120.269 28.1716 114.333 23.2451C108.249 18.4373 100.786 14.8314 86.7925 13.1695C80.4563 12.4127 72.7401 12.1901 64 12.1901C55.2599 12.1901 47.5437 12.4127 41.2075 13.1695C27.2293 14.8463 19.7505 18.4521 13.6666 23.2451C7.7459 28.1716 3.2794 34.2258 1.2168 45.5479C0.281954 50.6821 1.58835e-05 56.9293 1.52647e-05 64.0074C1.46459e-05 71.0855 0.281952 77.3475 1.2168 82.4669C3.2794 93.789 7.73106 99.8432 13.6666 104.77C19.7505 109.578 27.2145 113.183 41.2075 114.845C47.5437 115.602 55.2599 115.825 64 115.825C72.7401 115.825 80.4563 115.602 86.7925 114.845C100.771 113.169 108.249 109.563 114.333 104.77C120.254 99.8433 124.721 93.789 126.783 82.467C127.718 77.3327 128 71.0856 128 64.0074C128 56.9293 127.718 50.6673 126.783 45.5479ZM117.331 80.7605C115.728 89.5599 112.686 93.6406 108.294 97.3207C104.139 100.585 98.4855 103.79 85.6647 105.334C80.4266 105.957 73.5414 106.254 64.0148 106.254C54.4883 106.254 47.6031 105.957 42.365 105.334C29.5442 103.805 23.8906 100.585 19.7357 97.3206C15.3434 93.6406 12.3014 89.5599 10.6988 80.7605C9.97173 76.7837 9.63044 71.4714 9.63044 64.0222C9.63044 56.5731 9.97173 51.2608 10.6988 47.284C12.3014 38.4846 15.3434 34.4039 19.7357 30.7239C23.8906 27.4593 29.5442 24.2541 42.365 22.7109C47.6031 22.0876 54.4883 21.7909 64.0149 21.7909C73.5414 21.7909 80.4266 22.0876 85.6647 22.7109C98.4855 24.2393 104.139 27.4593 108.294 30.7239C112.686 34.4039 115.728 38.4846 117.331 47.284C118.058 51.2608 118.399 56.5731 118.399 64.0223C118.399 71.4714 118.058 76.7837 117.331 80.7605Z"
fill="#2B64F5"
/>
<path
d="M126.783 45.5479C124.721 34.2258 120.269 28.1716 114.333 23.2451C108.249 18.4373 100.786 14.8314 86.7925 13.1695C80.4563 12.4127 72.7401 12.1901 64 12.1901C55.2599 12.1901 47.5437 12.4127 41.2075 13.1695C27.2293 14.8463 19.7505 18.4521 13.6666 23.2451C7.7459 28.1716 3.2794 34.2258 1.2168 45.5479C0.281954 50.6821 1.58835e-05 56.9293 1.52647e-05 64.0074C1.46459e-05 71.0855 0.281952 77.3475 1.2168 82.4669C3.2794 93.789 7.73106 99.8432 13.6666 104.77C19.7505 109.578 27.2145 113.183 41.2075 114.845C47.5437 115.602 55.2599 115.825 64 115.825C72.7401 115.825 80.4563 115.602 86.7925 114.845C100.771 113.169 108.249 109.563 114.333 104.77C120.254 99.8433 124.721 93.789 126.783 82.467C127.718 77.3327 128 71.0856 128 64.0074C128 56.9293 127.718 50.6673 126.783 45.5479ZM117.331 80.7605C115.728 89.5599 112.686 93.6406 108.294 97.3207C104.139 100.585 98.4855 103.79 85.6647 105.334C80.4266 105.957 73.5414 106.254 64.0148 106.254C54.4883 106.254 47.6031 105.957 42.365 105.334C29.5442 103.805 23.8906 100.585 19.7357 97.3206C15.3434 93.6406 12.3014 89.5599 10.6988 80.7605C9.97173 76.7837 9.63044 71.4714 9.63044 64.0222C9.63044 56.5731 9.97173 51.2608 10.6988 47.284C12.3014 38.4846 15.3434 34.4039 19.7357 30.7239C23.8906 27.4593 29.5442 24.2541 42.365 22.7109C47.6031 22.0876 54.4883 21.7909 64.0149 21.7909C73.5414 21.7909 80.4266 22.0876 85.6647 22.7109C98.4855 24.2393 104.139 27.4593 108.294 30.7239C112.686 34.4039 115.728 38.4846 117.331 47.284C118.058 51.2608 118.399 56.5731 118.399 64.0223C118.399 71.4714 118.058 76.7837 117.331 80.7605Z"
fill="url(#qq-channel-glow-teal)"
/>
<path
d="M126.783 45.5479C124.721 34.2258 120.269 28.1716 114.333 23.2451C108.249 18.4373 100.786 14.8314 86.7925 13.1695C80.4563 12.4127 72.7401 12.1901 64 12.1901C55.2599 12.1901 47.5437 12.4127 41.2075 13.1695C27.2293 14.8463 19.7505 18.4521 13.6666 23.2451C7.7459 28.1716 3.2794 34.2258 1.2168 45.5479C0.281954 50.6821 1.58835e-05 56.9293 1.52647e-05 64.0074C1.46459e-05 71.0855 0.281952 77.3475 1.2168 82.4669C3.2794 93.789 7.73106 99.8432 13.6666 104.77C19.7505 109.578 27.2145 113.183 41.2075 114.845C47.5437 115.602 55.2599 115.825 64 115.825C72.7401 115.825 80.4563 115.602 86.7925 114.845C100.771 113.169 108.249 109.563 114.333 104.77C120.254 99.8433 124.721 93.789 126.783 82.467C127.718 77.3327 128 71.0856 128 64.0074C128 56.9293 127.718 50.6673 126.783 45.5479ZM117.331 80.7605C115.728 89.5599 112.686 93.6406 108.294 97.3207C104.139 100.585 98.4855 103.79 85.6647 105.334C80.4266 105.957 73.5414 106.254 64.0148 106.254C54.4883 106.254 47.6031 105.957 42.365 105.334C29.5442 103.805 23.8906 100.585 19.7357 97.3206C15.3434 93.6406 12.3014 89.5599 10.6988 80.7605C9.97173 76.7837 9.63044 71.4714 9.63044 64.0222C9.63044 56.5731 9.97173 51.2608 10.6988 47.284C12.3014 38.4846 15.3434 34.4039 19.7357 30.7239C23.8906 27.4593 29.5442 24.2541 42.365 22.7109C47.6031 22.0876 54.4883 21.7909 64.0149 21.7909C73.5414 21.7909 80.4266 22.0876 85.6647 22.7109C98.4855 24.2393 104.139 27.4593 108.294 30.7239C112.686 34.4039 115.728 38.4846 117.331 47.284C118.058 51.2608 118.399 56.5731 118.399 64.0223C118.399 71.4714 118.058 76.7837 117.331 80.7605Z"
fill="url(#qq-channel-glow-purple)"
/>
<path
d="M126.783 45.5479C124.721 34.2258 120.269 28.1716 114.333 23.2451C108.249 18.4373 100.786 14.8314 86.7925 13.1695C80.4563 12.4127 72.7401 12.1901 64 12.1901C55.2599 12.1901 47.5437 12.4127 41.2075 13.1695C27.2293 14.8463 19.7505 18.4521 13.6666 23.2451C7.7459 28.1716 3.2794 34.2258 1.2168 45.5479C0.281954 50.6821 1.58835e-05 56.9293 1.52647e-05 64.0074C1.46459e-05 71.0855 0.281952 77.3475 1.2168 82.4669C3.2794 93.789 7.73106 99.8432 13.6666 104.77C19.7505 109.578 27.2145 113.183 41.2075 114.845C47.5437 115.602 55.2599 115.825 64 115.825C72.7401 115.825 80.4563 115.602 86.7925 114.845C100.771 113.169 108.249 109.563 114.333 104.77C120.254 99.8433 124.721 93.789 126.783 82.467C127.718 77.3327 128 71.0856 128 64.0074C128 56.9293 127.718 50.6673 126.783 45.5479ZM117.331 80.7605C115.728 89.5599 112.686 93.6406 108.294 97.3207C104.139 100.585 98.4855 103.79 85.6647 105.334C80.4266 105.957 73.5414 106.254 64.0148 106.254C54.4883 106.254 47.6031 105.957 42.365 105.334C29.5442 103.805 23.8906 100.585 19.7357 97.3206C15.3434 93.6406 12.3014 89.5599 10.6988 80.7605C9.97173 76.7837 9.63044 71.4714 9.63044 64.0222C9.63044 56.5731 9.97173 51.2608 10.6988 47.284C12.3014 38.4846 15.3434 34.4039 19.7357 30.7239C23.8906 27.4593 29.5442 24.2541 42.365 22.7109C47.6031 22.0876 54.4883 21.7909 64.0149 21.7909C73.5414 21.7909 80.4266 22.0876 85.6647 22.7109C98.4855 24.2393 104.139 27.4593 108.294 30.7239C112.686 34.4039 115.728 38.4846 117.331 47.284C118.058 51.2608 118.399 56.5731 118.399 64.0223C118.399 71.4714 118.058 76.7837 117.331 80.7605Z"
fill="url(#qq-channel-glow-yellow)"
fill-opacity="0.8"
/>
<defs>
<radialGradient
id="qq-channel-glow-teal"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(95.7003 29.4625) rotate(105.19) scale(76.7612 86.9467)"
>
<stop stop-color="#49E0BC" />
<stop offset="0.71" stop-color="#49E0BC" stop-opacity="0.185" />
<stop offset="1" stop-color="#49E0BC" stop-opacity="0" />
</radialGradient>
<radialGradient
id="qq-channel-glow-purple"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-16.1165 148.834) rotate(-60.1482) scale(97.8038 238.027)"
>
<stop stop-color="#BA76FF" />
<stop offset="1" stop-color="#BA76FF" stop-opacity="0" />
</radialGradient>
<radialGradient
id="qq-channel-glow-yellow"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(103.892 4.32155) rotate(107.684) scale(86.8182 95.2138)"
>
<stop stop-color="#FFF15B" />
<stop offset="0.566778" stop-color="#FFF15B" stop-opacity="0.145" />
<stop offset="1" stop-color="#FFF15B" stop-opacity="0" />
</radialGradient>
</defs>
</svg>
</template>
<style scoped>
/* 官方 SVG 字形默认深色;暗色主题下改为白色,亮色主题保持原色 */
.qq-channel-glyph {
fill: #ffffff;
}
.light-mode .qq-channel-glyph {
fill: #231816;
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<svg
class="w-[2.1rem] h-[2.4rem]"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
fill="currentColor"
aria-hidden="true"
>
<path
d="M433.754 420.445c-11.526 1.393-44.86-52.741-44.86-52.741c0 31.345-16.136 72.247-51.051 101.786c16.842 5.192 54.843 19.167 45.803 34.421c-7.316 12.343-125.51 7.881-159.632 4.037c-34.122 3.844-152.316 8.306-159.632-4.037c-9.045-15.25 28.918-29.214 45.783-34.415c-34.92-29.539-51.059-70.445-51.059-101.792c0 0-33.334 54.134-44.859 52.741c-5.37-.65-12.424-29.644 9.347-99.704c10.261-33.024 21.995-60.478 40.144-105.779C60.683 98.063 108.982.006 224 0c113.737.006 163.156 96.133 160.264 214.963c18.118 45.223 29.912 72.85 40.144 105.778c21.768 70.06 14.716 99.053 9.346 99.704"
/>
</svg>
</template>

View File

@ -0,0 +1,18 @@
<template>
<svg
width="28"
height="28"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 4875 4875"
fill="currentColor"
>
<path d="M0 0h2311v2310H0zm2564 0h2311v2310H2564zM0 2564h2311v2311H0zm2564 0h2311v2311H2564" />
</svg>
</template>
<style scoped>
svg {
width: 1.75rem;
height: 1.75rem;
}
</style>

View File

@ -0,0 +1,162 @@
<script setup lang="ts">
import { defineMessages, useVIntl } from '@modrinth/ui/src/composables/i18n.ts'
const { formatMessage } = useVIntl()
const messages = defineMessages({
features: { id: 'axolotl-site.navigation.features', defaultMessage: 'Features' },
faq: { id: 'axolotl-site.navigation.faq', defaultMessage: 'FAQ' },
downloads: { id: 'axolotl-site.footer.downloads', defaultMessage: 'Downloads' },
changelog: { id: 'axolotl-site.footer.release-history', defaultMessage: 'Changelog' },
terms: { id: 'axolotl-site.navigation.terms', defaultMessage: 'Terms of Service' },
privacy: { id: 'axolotl-site.navigation.privacy', defaultMessage: 'Privacy Policy' },
developer: { id: 'axolotl-site.footer.developer', defaultMessage: 'Axolotl Launcher Team' },
openSourcePrefix: {
id: 'axolotl-site.footer.open-source-prefix',
defaultMessage: 'Axolotl is',
},
openSourceLink: {
id: 'axolotl-site.footer.open-source-link',
defaultMessage: 'open source',
},
disclaimer: {
id: 'axolotl-site.footer.minecraft-disclaimer',
defaultMessage:
'NOT AN OFFICIAL MINECRAFT SERVICE. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT.',
},
})
</script>
<template>
<footer
class="site-footer relative border-t border-divider bg-surface-1 text-[var(--color-base)]"
>
<div class="footer-inner">
<div
class="footer-legal flex flex-wrap gap-x-[1.4rem] gap-y-[0.4rem] text-[0.78rem] text-[var(--color-secondary)]"
>
<p>© {{ new Date().getFullYear() }} {{ formatMessage(messages.developer) }}</p>
<p class="open-source">
<span>
{{ formatMessage(messages.openSourcePrefix) }}
<a href="https://github.com/Mystic-Stars/Axolotl" target="_blank" rel="noopener">
{{ formatMessage(messages.openSourceLink) }} </a
>.
</span>
<a href="https://www.netlify.com" target="_blank" rel="noopener" class="netlify-badge">
<img
src="https://www.netlify.com/assets/badges/netlify-badge-color-accent.svg"
alt="Deploys by Netlify"
/>
</a>
</p>
</div>
<nav
class="footer-links flex flex-wrap justify-end gap-x-[1.3rem] gap-y-[0.35rem]"
aria-label="Footer"
>
<NuxtLink to="/#features">{{ formatMessage(messages.features) }}</NuxtLink>
<NuxtLink to="/#faq">{{ formatMessage(messages.faq) }}</NuxtLink>
<a href="#download">{{ formatMessage(messages.downloads) }}</a>
<NuxtLink to="/changelog">{{ formatMessage(messages.changelog) }}</NuxtLink>
<NuxtLink to="/terms">{{ formatMessage(messages.terms) }}</NuxtLink>
<NuxtLink to="/privacy">{{ formatMessage(messages.privacy) }}</NuxtLink>
</nav>
<p
class="col-span-full m-0 text-center text-[0.64rem] tracking-[0.015em] text-[var(--color-secondary)] opacity-45"
>
{{ formatMessage(messages.disclaimer) }}
</p>
</div>
</footer>
</template>
<style scoped lang="scss">
.site-footer {
&::before {
position: absolute;
top: -1px;
left: 50%;
width: min(76rem, calc(100% - 3rem));
height: 1px;
transform: translateX(-50%);
background: linear-gradient(90deg, transparent, var(--color-brand), transparent);
content: '';
opacity: 0.6;
}
}
.footer-inner {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.8rem 2.5rem;
width: min(76rem, calc(100% - 3rem));
margin: 0 auto;
padding: 1.75rem 0;
}
.footer-legal {
flex-direction: column;
align-items: flex-start;
.open-source {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
p {
margin: 0;
}
a {
color: var(--color-base);
text-underline-offset: 0.15rem;
}
.netlify-badge {
display: inline-flex;
align-items: center;
img {
display: block;
height: 1.5rem;
width: auto;
}
}
}
.footer-links {
a {
color: var(--color-secondary);
font-size: 0.82rem;
font-weight: 500;
text-decoration: none;
transition: color 140ms ease;
&:hover {
color: var(--color-brand);
}
}
}
@media (max-width: 900px) {
.footer-inner {
grid-template-columns: 1fr;
}
.footer-links {
justify-content: flex-start;
}
}
@media (max-width: 560px) {
.footer-inner {
width: calc(100% - 2rem);
}
}
</style>

View File

@ -0,0 +1,281 @@
<script setup lang="ts">
import GithubIcon from '@modrinth/assets/external/github.svg?component'
import DownloadIcon from '@modrinth/assets/icons/download.svg?component'
import HamburgerIcon from '@modrinth/assets/icons/hamburger.svg?component'
import SettingsIcon from '@modrinth/assets/icons/settings.svg?component'
import XIcon from '@modrinth/assets/icons/x.svg?component'
import ButtonStyled from '@modrinth/ui/src/components/base/ButtonStyled.vue'
import { defineMessages, useVIntl } from '@modrinth/ui/src/composables/i18n.ts'
import type { ComponentPublicInstance } from 'vue'
import AxolotlWordmark from '~/components/brand/AxolotlWordmark.vue'
const emit = defineEmits<{
openSettings: []
}>()
const mobileMenuOpen = ref(false)
const mobileMenuRef = ref<HTMLElement | null>(null)
const mobileMenuButtonRef = ref<ComponentPublicInstance | null>(null)
const { formatMessage } = useVIntl()
// 点击菜单与开关之外的区域时收起移动端菜单
function handleOutsideClick(event: MouseEvent) {
if (!mobileMenuOpen.value) return
const target = event.target as Node
const buttonElement = mobileMenuButtonRef.value?.$el
if (!mobileMenuRef.value?.contains(target) && !buttonElement?.contains(target)) {
mobileMenuOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleOutsideClick)
})
onBeforeUnmount(() => {
document.removeEventListener('click', handleOutsideClick)
})
const messages = defineMessages({
home: { id: 'axolotl-site.navigation.home', defaultMessage: 'Axolotl Launcher home' },
primary: { id: 'axolotl-site.navigation.primary', defaultMessage: 'Primary navigation' },
mobile: { id: 'axolotl-site.navigation.mobile', defaultMessage: 'Mobile navigation' },
features: { id: 'axolotl-site.navigation.features', defaultMessage: 'Features' },
faq: { id: 'axolotl-site.navigation.faq', defaultMessage: 'FAQ' },
changelog: { id: 'axolotl-site.navigation.changelog', defaultMessage: 'Changelog' },
terms: { id: 'axolotl-site.navigation.terms', defaultMessage: 'Terms of Service' },
privacy: { id: 'axolotl-site.navigation.privacy', defaultMessage: 'Privacy Policy' },
openSource: { id: 'axolotl-site.navigation.open-source', defaultMessage: 'Open source' },
download: { id: 'axolotl-site.navigation.download', defaultMessage: 'Download' },
openSettings: {
id: 'axolotl-site.navigation.open-settings',
defaultMessage: 'Open display settings',
},
openMenu: { id: 'axolotl-site.navigation.open-menu', defaultMessage: 'Open navigation' },
closeMenu: { id: 'axolotl-site.navigation.close-menu', defaultMessage: 'Close navigation' },
})
function openSettings() {
mobileMenuOpen.value = false
emit('openSettings')
}
</script>
<template>
<header class="absolute top-0 left-0 w-full z-40 pointer-events-none">
<div class="header-inner">
<NuxtLink
to="/"
:aria-label="formatMessage(messages.home)"
class="w-fit no-underline button-animation"
>
<AxolotlWordmark />
</NuxtLink>
<nav
class="desktop-navigation hidden lg:flex items-center gap-1 pointer-events-auto"
:aria-label="formatMessage(messages.primary)"
>
<ButtonStyled type="transparent">
<NuxtLink to="/#features">{{ formatMessage(messages.features) }}</NuxtLink>
</ButtonStyled>
<ButtonStyled type="transparent">
<NuxtLink to="/#faq">{{ formatMessage(messages.faq) }}</NuxtLink>
</ButtonStyled>
<ButtonStyled type="transparent">
<NuxtLink to="/changelog">{{ formatMessage(messages.changelog) }}</NuxtLink>
</ButtonStyled>
<ButtonStyled type="transparent">
<NuxtLink to="/terms">{{ formatMessage(messages.terms) }}</NuxtLink>
</ButtonStyled>
<ButtonStyled type="transparent">
<NuxtLink to="/privacy">{{ formatMessage(messages.privacy) }}</NuxtLink>
</ButtonStyled>
<ButtonStyled type="transparent">
<a href="https://github.com/Mystic-Stars/Axolotl" target="_blank" rel="noopener">
<GithubIcon aria-hidden="true" />
{{ formatMessage(messages.openSource) }}
</a>
</ButtonStyled>
</nav>
<div class="header-actions flex items-center gap-1 pointer-events-auto">
<ButtonStyled class="desktop-download hidden lg:flex" color="brand">
<a href="#download">
<DownloadIcon aria-hidden="true" />
{{ formatMessage(messages.download) }}
</a>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<button :aria-label="formatMessage(messages.openSettings)" @click="openSettings">
<SettingsIcon aria-hidden="true" />
</button>
</ButtonStyled>
<ButtonStyled
ref="mobileMenuButtonRef"
class="hidden max-lg:flex"
circular
type="transparent"
>
<button
:aria-label="formatMessage(mobileMenuOpen ? messages.closeMenu : messages.openMenu)"
:aria-expanded="mobileMenuOpen"
@click="mobileMenuOpen = !mobileMenuOpen"
>
<XIcon v-if="mobileMenuOpen" aria-hidden="true" />
<HamburgerIcon v-else aria-hidden="true" />
</button>
</ButtonStyled>
</div>
</div>
<Transition name="mobile-menu">
<nav
v-if="mobileMenuOpen"
ref="mobileMenuRef"
class="mobile-navigation hidden max-lg:flex"
:aria-label="formatMessage(messages.mobile)"
>
<NuxtLink to="/#features" @click="mobileMenuOpen = false">
{{ formatMessage(messages.features) }}
</NuxtLink>
<NuxtLink to="/#faq" @click="mobileMenuOpen = false">
{{ formatMessage(messages.faq) }}
</NuxtLink>
<NuxtLink to="/changelog" @click="mobileMenuOpen = false">
{{ formatMessage(messages.changelog) }}
</NuxtLink>
<NuxtLink to="/terms" @click="mobileMenuOpen = false">
{{ formatMessage(messages.terms) }}
</NuxtLink>
<NuxtLink to="/privacy" @click="mobileMenuOpen = false">
{{ formatMessage(messages.privacy) }}
</NuxtLink>
<a
href="https://github.com/Mystic-Stars/Axolotl"
target="_blank"
rel="noopener"
@click="mobileMenuOpen = false"
>
{{ formatMessage(messages.openSource) }}
</a>
<a class="mobile-download" href="#download" @click="mobileMenuOpen = false">
<DownloadIcon aria-hidden="true" />
{{ formatMessage(messages.download) }}
</a>
</nav>
</Transition>
</header>
</template>
<style scoped lang="scss">
.header-inner {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 0.5rem;
max-width: 1360px;
margin: 0 auto;
padding: 1.15rem 1.5rem;
pointer-events: auto;
}
.desktop-navigation {
grid-column: 1 / -1;
grid-row: 2;
justify-content: center;
:deep(.button) {
min-height: 2.25rem;
border-radius: 0;
color: var(--color-secondary);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
&:hover {
color: var(--color-contrast);
}
}
}
.header-actions {
grid-column: 2;
grid-row: 1;
justify-content: flex-end;
}
.mobile-navigation {
position: absolute;
top: 100%;
right: 1rem;
width: min(22rem, calc(100% - 2rem));
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem;
border: 1px solid var(--color-divider);
border-radius: 1rem;
background: color-mix(in srgb, var(--color-raised-bg) 92%, transparent);
box-shadow: 0 1.25rem 3rem rgb(0 0 0 / 22%);
backdrop-filter: blur(20px) saturate(150%);
a {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 0.875rem;
border-radius: var(--radius-md);
color: var(--color-base);
font-weight: 600;
text-decoration: none;
&:hover {
background: var(--color-button-bg);
}
}
.mobile-download {
justify-content: center;
margin-top: 0.25rem;
background: var(--color-brand);
color: var(--color-on-brand);
}
}
.mobile-menu-enter-active,
.mobile-menu-leave-active {
transition: 160ms ease;
}
.mobile-menu-enter-from,
.mobile-menu-leave-to {
transform: translateY(-0.5rem) scale(0.98);
opacity: 0;
}
@media (min-width: 1024px) {
.header-inner {
grid-template-columns: auto 1fr auto;
padding-top: 1.25rem;
}
.desktop-navigation {
grid-column: 2;
grid-row: 1;
}
.header-actions {
grid-column: 3;
}
}
@media (max-width: 1023px) {
.header-inner {
grid-template-columns: 1fr auto;
padding: 0.875rem 1rem;
}
}
</style>

View File

@ -0,0 +1,116 @@
<script setup lang="ts">
import { defineMessages, useVIntl } from '@modrinth/ui/src/composables/i18n.ts'
defineProps<{
eyebrow: string
title: string
description: string
updatedAt: string
}>()
const { formatMessage } = useVIntl()
const messages = defineMessages({
updated: {
id: 'axolotl-site.legal.updated',
defaultMessage: 'Last updated: {updatedAt}',
},
})
</script>
<template>
<article class="legal-document">
<header class="legal-header">
<span class="section-eyebrow">{{ eyebrow }}</span>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<p class="updated-at">{{ formatMessage(messages.updated, { updatedAt }) }}</p>
</header>
<div class="legal-content">
<slot />
</div>
</article>
</template>
<style scoped lang="scss">
.legal-document {
width: min(52rem, calc(100% - 2rem));
margin: 0 auto;
padding: 4rem 0 5rem;
}
.legal-header {
max-width: 44rem;
margin-bottom: 2.5rem;
h1 {
margin: 0.5rem 0 0;
color: var(--color-contrast);
font-size: 2.25rem;
line-height: 1.15;
}
p {
margin: 1rem 0 0;
color: var(--color-secondary);
line-height: 1.65;
}
.updated-at {
font-size: 0.875rem;
}
}
.legal-content {
padding: clamp(1.25rem, 3vw, 2.25rem);
border: 1px solid var(--surface-5);
border-radius: 0.75rem;
background: var(--surface-4);
color: var(--color-base);
line-height: 1.8;
:deep(h2) {
margin: 2.25rem 0 0.75rem;
color: var(--color-contrast);
font-size: 1.25rem;
line-height: 1.35;
}
:deep(h2:first-child) {
margin-top: 0;
}
:deep(p) {
margin: 0.75rem 0;
}
:deep(ul) {
margin: 0.75rem 0;
padding-left: 1.5rem;
}
:deep(li + li) {
margin-top: 0.4rem;
}
:deep(a) {
color: var(--color-brand);
text-underline-offset: 0.18rem;
}
:deep(strong) {
color: var(--color-contrast);
}
}
@media (max-width: 600px) {
.legal-document {
padding: 2.5rem 0 3rem;
}
.legal-header h1 {
font-size: 1.875rem;
}
}
</style>

View File

@ -0,0 +1,714 @@
<script setup lang="ts">
import { DownloadIcon, LanguagesIcon, PaintbrushIcon } from '@modrinth/assets'
import SettingsIcon from '@modrinth/assets/icons/settings.svg?component'
import XIcon from '@modrinth/assets/icons/x.svg?component'
import ButtonStyled from '@modrinth/ui/src/components/base/ButtonStyled.vue'
import Combobox from '@modrinth/ui/src/components/base/Combobox.vue'
import Toggle from '@modrinth/ui/src/components/base/Toggle.vue'
import LanguageSelector from '@modrinth/ui/src/components/settings/LanguageSelector.vue'
import ThemeSelector from '@modrinth/ui/src/components/settings/ThemeSelector.vue'
import { defineMessages, LOCALES, useVIntl } from '@modrinth/ui/src/composables/i18n.ts'
import { injectI18n } from '@modrinth/ui/src/providers/i18n.ts'
import { type DownloadSource, useDownloadSource } from '~/composables/use-download-source'
type Theme = 'system' | 'light' | 'dark' | 'oled'
type SettingsTab = 'appearance' | 'downloads' | 'language'
const open = defineModel<boolean>({ required: true })
const { formatMessage } = useVIntl()
const { locale, setLocale } = injectI18n()
const { selectedSource, resolvedSource, setDownloadSource } = useDownloadSource()
const preferredTheme = ref<Theme>('system')
const systemTheme = ref<'light' | 'dark'>('dark')
const advancedRendering = ref(true)
const reduceMotion = ref(false)
const externalLinksNewTab = ref(true)
const themeOptions = ['dark', 'light', 'oled', 'system'] as const
const supportedLocales = LOCALES.filter((item) => ['en-US', 'zh-CN'].includes(item.code))
const changingLocale = ref(false)
const activeTab = ref<SettingsTab>('appearance')
const messages = defineMessages({
title: { id: 'axolotl-settings.title', defaultMessage: 'Display settings' },
description: {
id: 'axolotl-settings.description',
defaultMessage: 'Customize how Axolotl looks and reads on this device.',
},
close: { id: 'axolotl-settings.close', defaultMessage: 'Close settings' },
appearanceTitle: { id: 'axolotl-settings.appearance.title', defaultMessage: 'Appearance' },
languageTitle: { id: 'axolotl-settings.language.title', defaultMessage: 'Language' },
languageDescription: {
id: 'axolotl-settings.language.description',
defaultMessage: 'Choose the language used by this website.',
},
downloadsTitle: { id: 'axolotl-settings.downloads.title', defaultMessage: 'Downloads' },
downloadsDescription: {
id: 'axolotl-settings.downloads.description',
defaultMessage: 'Choose where Axolotl Launcher installers are downloaded from.',
},
downloadSourceTitle: {
id: 'axolotl-settings.download-source.title',
defaultMessage: 'Download source',
},
downloadSourceDescription: {
id: 'axolotl-settings.download-source.description',
defaultMessage:
'Automatic selection uses the Update Server, with GitHub available as a backup.',
},
downloadSourceAuto: {
id: 'axolotl-settings.download-source.auto',
defaultMessage: 'Automatic',
},
downloadSourceAutoDescription: {
id: 'axolotl-settings.download-source.auto.description',
defaultMessage: 'Choose a source from your browser language and timezone.',
},
downloadSourceUpdateServer: {
id: 'axolotl-settings.download-source.update-server',
defaultMessage: 'Update Server',
},
downloadSourceUpdateServerDescription: {
id: 'axolotl-settings.download-source.update-server.description',
defaultMessage: 'Download through the official Axolotl update service.',
},
downloadSourceGithub: {
id: 'axolotl-settings.download-source.github',
defaultMessage: 'GitHub',
},
downloadSourceGithubDescription: {
id: 'axolotl-settings.download-source.github.description',
defaultMessage: 'Download from the official GitHub release.',
},
currentDownloadSource: {
id: 'axolotl-settings.download-source.current',
defaultMessage: 'Current source: {source}',
},
themeTitle: { id: 'axolotl-settings.theme.title', defaultMessage: 'Color theme' },
themeDescription: {
id: 'axolotl-settings.theme.description',
defaultMessage: 'Select your preferred color theme for Axolotl on this device.',
},
interfaceTitle: { id: 'axolotl-settings.interface.title', defaultMessage: 'Interface' },
interfaceDescription: {
id: 'axolotl-settings.interface.description',
defaultMessage: 'Enable or disable visual behavior on this device.',
},
advancedRenderingTitle: {
id: 'axolotl-settings.advanced-rendering.title',
defaultMessage: 'Advanced rendering',
},
advancedRenderingDescription: {
id: 'axolotl-settings.advanced-rendering.description',
defaultMessage: 'Use blur, gradients, and enhanced background effects.',
},
reduceMotionTitle: {
id: 'axolotl-settings.reduce-motion.title',
defaultMessage: 'Reduce motion',
},
reduceMotionDescription: {
id: 'axolotl-settings.reduce-motion.description',
defaultMessage: 'Disable decorative movement and transition effects.',
},
externalLinksTitle: {
id: 'axolotl-settings.external-links.title',
defaultMessage: 'Open external links in new tab',
},
externalLinksDescription: {
id: 'axolotl-settings.external-links.description',
defaultMessage: 'Keep the download page open when visiting another website.',
},
done: { id: 'axolotl-settings.done', defaultMessage: 'Done' },
})
const downloadSourceOptions = computed<
Array<{ value: DownloadSource; label: string; subLabel: string }>
>(() => [
{
value: 'auto',
label: formatMessage(messages.downloadSourceAuto),
subLabel: formatMessage(messages.downloadSourceAutoDescription),
},
{
value: 'update-server',
label: formatMessage(messages.downloadSourceUpdateServer),
subLabel: formatMessage(messages.downloadSourceUpdateServerDescription),
},
{
value: 'github',
label: formatMessage(messages.downloadSourceGithub),
subLabel: formatMessage(messages.downloadSourceGithubDescription),
},
])
const resolvedDownloadSourceLabel = computed(() => {
if (resolvedSource.value === 'update-server')
return formatMessage(messages.downloadSourceUpdateServer)
return formatMessage(messages.downloadSourceGithub)
})
let systemThemeQuery: MediaQueryList | undefined
function applyTheme() {
if (!import.meta.client) return
const resolvedTheme = preferredTheme.value === 'system' ? systemTheme.value : preferredTheme.value
document.documentElement.classList.remove('light-mode', 'dark-mode', 'oled-mode')
document.documentElement.classList.add(`${resolvedTheme}-mode`, 'accent-pink')
document.documentElement.style.colorScheme = resolvedTheme === 'light' ? 'light' : 'dark'
localStorage.setItem('axolotl-theme', preferredTheme.value)
}
function updateColorTheme(theme: Theme) {
preferredTheme.value = theme
applyTheme()
}
async function updateLocale(newLocale: string) {
if (newLocale === locale.value) return
changingLocale.value = true
try {
await setLocale(newLocale)
} finally {
changingLocale.value = false
}
}
function applyRenderingPreferences() {
if (!import.meta.client) return
document.documentElement.classList.toggle('reduced-effects', !advancedRendering.value)
document.documentElement.classList.toggle('reduced-motion', reduceMotion.value)
localStorage.setItem('axolotl-advanced-rendering', String(advancedRendering.value))
localStorage.setItem('axolotl-reduce-motion', String(reduceMotion.value))
localStorage.setItem('axolotl-external-links-new-tab', String(externalLinksNewTab.value))
}
function handleSystemTheme(event: MediaQueryListEvent) {
systemTheme.value = event.matches ? 'dark' : 'light'
if (preferredTheme.value === 'system') applyTheme()
}
const settingsPanel = ref<HTMLElement | null>(null)
let lastFocusedElement: HTMLElement | null = null
function getFocusableElements(container: HTMLElement): HTMLElement[] {
return Array.from(
container.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
),
).filter((element) => !element.hasAttribute('disabled'))
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
open.value = false
return
}
// Tab 焦点圈定在弹窗内,避免焦点逃逸到背景页面
if (event.key !== 'Tab' || !open.value) return
const panel = settingsPanel.value
if (!panel) return
const focusable = getFocusableElements(panel)
if (!focusable.length) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (event.shiftKey && document.activeElement === first) {
event.preventDefault()
last.focus()
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault()
first.focus()
}
}
function handleExternalLink(event: MouseEvent) {
if (!externalLinksNewTab.value || !(event.target instanceof Element)) return
const anchor = event.target.closest<HTMLAnchorElement>('a[href]')
if (!anchor) return
const destination = new URL(anchor.href, window.location.href)
if (destination.origin !== window.location.origin) {
anchor.target = '_blank'
anchor.rel = 'noopener'
}
}
watch(open, (isOpen) => {
if (!import.meta.client) return
document.body.style.overflow = isOpen ? 'hidden' : ''
if (isOpen) {
// 记住打开前的焦点,关闭时还原
lastFocusedElement = document.activeElement as HTMLElement | null
nextTick(() => {
const panel = settingsPanel.value
if (!panel) return
const focusable = getFocusableElements(panel)
;(focusable[0] ?? panel).focus()
})
} else {
lastFocusedElement?.focus()
lastFocusedElement = null
}
})
watch(selectedSource, setDownloadSource)
watch([advancedRendering, reduceMotion, externalLinksNewTab], applyRenderingPreferences)
onMounted(() => {
systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)')
systemTheme.value = systemThemeQuery.matches ? 'dark' : 'light'
preferredTheme.value = (localStorage.getItem('axolotl-theme') as Theme | null) ?? 'system'
advancedRendering.value = localStorage.getItem('axolotl-advanced-rendering') !== 'false'
reduceMotion.value = localStorage.getItem('axolotl-reduce-motion') === 'true'
externalLinksNewTab.value = localStorage.getItem('axolotl-external-links-new-tab') !== 'false'
applyTheme()
applyRenderingPreferences()
systemThemeQuery.addEventListener('change', handleSystemTheme)
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('click', handleExternalLink, true)
})
onBeforeUnmount(() => {
document.body.style.overflow = ''
systemThemeQuery?.removeEventListener('change', handleSystemTheme)
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('click', handleExternalLink, true)
})
</script>
<template>
<Teleport to="body">
<Transition name="settings-modal">
<div v-if="open" class="settings-backdrop" @click.self="open = false">
<section
ref="settingsPanel"
class="settings-panel"
role="dialog"
aria-modal="true"
aria-labelledby="settings-title"
>
<header class="settings-header">
<h2 id="settings-title">
<SettingsIcon aria-hidden="true" />
{{ formatMessage(messages.title) }}
</h2>
<ButtonStyled circular type="transparent">
<button :aria-label="formatMessage(messages.close)" @click="open = false">
<XIcon aria-hidden="true" />
</button>
</ButtonStyled>
</header>
<div class="settings-body">
<aside class="settings-sidebar">
<nav :aria-label="formatMessage(messages.title)">
<button
:class="{ selected: activeTab === 'appearance' }"
@click="activeTab = 'appearance'"
>
<PaintbrushIcon aria-hidden="true" />
{{ formatMessage(messages.appearanceTitle) }}
</button>
<button
:class="{ selected: activeTab === 'downloads' }"
@click="activeTab = 'downloads'"
>
<DownloadIcon aria-hidden="true" />
{{ formatMessage(messages.downloadsTitle) }}
</button>
<button
:class="{ selected: activeTab === 'language' }"
@click="activeTab = 'language'"
>
<LanguagesIcon aria-hidden="true" />
{{ formatMessage(messages.languageTitle) }}
</button>
</nav>
<div
class="settings-brand mt-auto flex items-center gap-3 px-1 pt-4 text-[0.8rem] text-[var(--color-secondary)]"
>
<img src="/axolotl.png" alt="" />
<div>
<strong>Axolotl Launcher</strong>
<span>Website {{ new Date().getFullYear() }}</span>
</div>
</div>
</aside>
<div class="settings-main">
<section v-if="activeTab === 'appearance'" class="settings-pane px-8 pb-12 pl-6">
<div class="settings-section">
<h3>{{ formatMessage(messages.themeTitle) }}</h3>
<p>{{ formatMessage(messages.themeDescription) }}</p>
<ThemeSelector
:update-color-theme="updateColorTheme"
:current-theme="preferredTheme"
:theme-options="themeOptions"
:system-theme-color="systemTheme"
/>
</div>
<div class="settings-section mt-7 border-t border-divider pt-6">
<h3>{{ formatMessage(messages.interfaceTitle) }}</h3>
<p>{{ formatMessage(messages.interfaceDescription) }}</p>
<div class="mt-3 flex flex-col">
<div class="setting-row">
<label for="advanced-rendering">
<strong>{{ formatMessage(messages.advancedRenderingTitle) }}</strong>
<span>{{ formatMessage(messages.advancedRenderingDescription) }}</span>
</label>
<Toggle id="advanced-rendering" v-model="advancedRendering" />
</div>
<div class="setting-row">
<label for="reduce-motion">
<strong>{{ formatMessage(messages.reduceMotionTitle) }}</strong>
<span>{{ formatMessage(messages.reduceMotionDescription) }}</span>
</label>
<Toggle id="reduce-motion" v-model="reduceMotion" />
</div>
<div class="setting-row">
<label for="external-links">
<strong>{{ formatMessage(messages.externalLinksTitle) }}</strong>
<span>{{ formatMessage(messages.externalLinksDescription) }}</span>
</label>
<Toggle id="external-links" v-model="externalLinksNewTab" />
</div>
</div>
</div>
</section>
<section v-else-if="activeTab === 'downloads'" class="settings-pane px-8 pb-12 pl-6">
<div class="settings-section">
<h3>{{ formatMessage(messages.downloadsTitle) }}</h3>
<p>{{ formatMessage(messages.downloadsDescription) }}</p>
<div class="mt-6 max-w-[28rem]">
<h4 class="m-0 text-base font-semibold text-contrast">
{{ formatMessage(messages.downloadSourceTitle) }}
</h4>
<p class="m-0 mt-1 text-sm text-secondary">
{{ formatMessage(messages.downloadSourceDescription) }}
</p>
<div class="mt-3">
<Combobox
id="download-source"
v-model="selectedSource"
:name="formatMessage(messages.downloadSourceTitle)"
:options="downloadSourceOptions"
/>
</div>
<p class="m-0 mt-3 text-sm text-secondary" role="status">
{{
formatMessage(messages.currentDownloadSource, {
source: resolvedDownloadSourceLabel,
})
}}
</p>
</div>
</div>
</section>
<section
v-else-if="activeTab === 'language'"
class="settings-pane language-pane px-8 pb-12 pl-6"
>
<div class="settings-section">
<h3>{{ formatMessage(messages.languageTitle) }}</h3>
<p>{{ formatMessage(messages.languageDescription) }}</p>
<LanguageSelector
:current-locale="locale"
:locales="supportedLocales"
:on-locale-change="updateLocale"
:is-changing="changingLocale"
/>
</div>
</section>
</div>
</div>
</section>
</div>
</Transition>
</Teleport>
</template>
<style scoped lang="scss">
.settings-backdrop {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
background: rgb(10 12 18 / 58%);
backdrop-filter: blur(8px) saturate(90%);
}
.settings-panel {
display: flex;
flex-direction: column;
width: min(60rem, 100%);
// 手机 Firefox/Chrome 的动态工具栏不计入 vh用 dvh 避免面板底部被遮挡
height: min(40rem, calc(100vh - 2rem));
height: min(40rem, calc(100dvh - 2rem));
overflow: hidden;
border: 1px solid var(--color-divider);
border-radius: 1.25rem;
background: var(--color-raised-bg);
box-shadow: 0 2rem 6rem rgb(0 0 0 / 42%);
}
.settings-header {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: space-between;
min-height: 5.25rem;
padding: 0 1.5rem;
border-bottom: 1px solid var(--color-divider);
background: var(--color-raised-bg);
h2 {
display: flex;
align-items: center;
gap: 0.625rem;
margin: 0;
color: var(--color-contrast);
font-size: 1.125rem;
font-weight: 800;
}
svg {
width: 1.125rem;
height: 1.125rem;
}
}
.settings-body {
display: grid;
grid-template-columns: 15.5rem minmax(0, 1fr);
min-height: 0;
flex: 1;
padding: 1.5rem 0 0 1.5rem;
}
.settings-sidebar {
display: flex;
min-height: 0;
flex-direction: column;
padding: 0 1rem 0.75rem 0;
border-right: 1px solid var(--color-divider);
nav {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
button {
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
padding: 0.55rem 1rem;
border: 0;
border-radius: 0.75rem;
background: transparent;
color: var(--color-base);
font: inherit;
font-weight: 650;
text-align: left;
transition: 120ms ease;
&:hover {
background: var(--color-button-bg);
color: var(--color-contrast);
cursor: pointer;
}
&.selected {
background: var(--color-brand);
color: var(--color-brand-inverted);
box-shadow: 0 0.5rem 1.5rem color-mix(in srgb, var(--color-brand) 16%, transparent);
}
svg {
width: 1rem;
height: 1rem;
flex: 0 0 auto;
}
}
}
.settings-brand {
img {
width: 2.25rem;
height: 2.25rem;
object-fit: contain;
}
div {
display: flex;
min-width: 0;
flex-direction: column;
gap: 0.15rem;
}
strong {
color: var(--color-base);
font-size: 0.875rem;
}
span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.settings-main {
position: relative;
min-width: 0;
min-height: 0;
overflow-y: auto;
scrollbar-color: var(--color-scrollbar) transparent;
}
.settings-main::after {
position: sticky;
bottom: 0;
display: block;
height: 2.5rem;
margin-top: -2.5rem;
background: linear-gradient(transparent, var(--color-raised-bg));
content: '';
pointer-events: none;
}
.settings-section {
h3,
p {
margin: 0;
}
h3 {
color: var(--color-contrast);
font-size: 1.125rem;
font-weight: 700;
}
p {
margin-top: 0.25rem;
color: var(--color-base);
}
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
padding: 1rem 0;
border-bottom: 1px solid color-mix(in srgb, var(--color-divider) 68%, transparent);
label {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 0.25rem;
}
strong {
color: var(--color-contrast);
font-size: 0.95rem;
}
span {
color: var(--color-secondary);
font-size: 0.85rem;
line-height: 1.45;
}
}
.language-pane {
:deep(.flex.flex-col.gap-4) {
margin-top: 1rem;
}
}
.settings-modal-enter-active,
.settings-modal-leave-active {
transition: opacity 180ms ease;
.settings-panel {
transition: transform 180ms ease;
}
}
.settings-modal-enter-from,
.settings-modal-leave-to {
opacity: 0;
.settings-panel {
transform: translateY(0.75rem) scale(0.985);
}
}
@media (max-width: 700px) {
.settings-backdrop {
align-items: flex-end;
padding: 0;
}
.settings-panel {
height: 94vh;
height: 94dvh;
border-radius: 1.25rem 1.25rem 0 0;
}
.settings-header {
min-height: 4.5rem;
padding: 0 1rem;
}
.settings-body {
display: flex;
flex-direction: column;
padding: 0;
}
.settings-sidebar {
padding: 0.75rem 1rem;
border-right: 0;
border-bottom: 1px solid var(--color-divider);
nav {
flex-direction: row;
}
button {
justify-content: center;
}
}
.settings-brand {
display: none;
}
.settings-pane {
padding: 1.25rem 1rem 3rem;
}
.setting-row {
align-items: flex-start;
}
}
</style>