feat:移除了弹窗,服务器添加sls
This commit is contained in:
commit
6a295f9a7a
4082 files changed
+1322534
No files matched your search
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<svg
|
||||
class="dark-icon"
|
||||
width="45"
|
||||
height="46"
|
||||
viewBox="0 0 45 46"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
id="Subtract"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M23.5477 0H23.5461C23.2284 0 22.9077 0.0163711 22.5876 0.0415902C20.4638 0.208942 18.8991 0.910133 17.7996 2.0082C16.7124 3.09404 16.1751 4.47323 15.9172 5.82868C15.6602 7.18017 15.6685 8.57463 15.7244 9.76049C15.7464 10.2267 15.7737 10.6424 15.7981 11.0129L15.7981 11.0129C15.8393 11.6392 15.8719 12.1361 15.856 12.5288C15.7269 14.3647 15.3652 15.7513 14.1358 17.5116C12.4894 19.4715 10.1353 22.6851 9.00147 26.0938C8.50987 27.5652 8.23247 29.1259 8.37804 30.6595C8.14194 30.9205 7.94151 31.1909 7.77396 31.4209L7.69935 31.5235C7.53577 31.7486 7.41036 31.9212 7.28343 32.0672C7.22885 32.1165 7.13829 32.1747 6.95723 32.2529C6.85646 32.2965 6.74665 32.3386 6.60714 32.3918L6.60229 32.3937L6.60063 32.3943C6.4689 32.4446 6.31402 32.5038 6.15349 32.5722L6.14713 32.575C6.11461 32.5891 6.08029 32.6038 6.04439 32.6191L6.04194 32.6201C5.49857 32.8521 4.5956 33.2374 4.06765 34.2907L4.06313 34.2998L4.05878 34.3089C3.83111 34.787 3.71086 35.3126 3.71965 35.8518C3.72044 36.2784 3.77526 36.6973 3.83488 36.9965L3.8573 37.1494C3.90153 37.4502 3.93607 37.685 3.9521 37.8975C3.96952 38.1284 3.9557 38.2416 3.94333 38.2911C3.47501 39.5811 3.29341 40.7747 3.80981 41.7808L3.81261 41.7862C4.34311 42.8045 5.41437 43.1509 6.09597 43.3713L6.14873 43.3884L6.1831 43.3968C6.76559 43.5407 7.45296 43.6219 8.08651 43.6967L8.08657 43.6967C8.33286 43.7258 8.57102 43.7539 8.79172 43.7844C9.65223 43.9032 10.4057 44.0681 11.0346 44.4066L11.0467 44.4131L11.0589 44.4193C12.8487 45.3199 14.8309 45.8073 16.5413 45.3594C17.5947 45.1214 18.5098 44.4944 19.1114 43.6161C19.5314 43.5681 19.9501 43.481 20.3513 43.3976L20.4887 43.3691C21.1456 43.2333 21.8552 43.0939 22.7255 43.039L22.7353 43.0384L22.7452 43.0376C23.2171 42.9984 23.7676 43.0823 24.5478 43.2012L24.6654 43.2192C25.2572 43.3092 25.9398 43.4072 26.6797 43.4223L26.7366 43.5729L26.7865 43.6228C27.7132 45.3272 29.3865 46.1223 31.1344 45.9841C32.8861 45.8473 34.6161 44.7911 35.9501 43.2465L35.9575 43.2379L35.9647 43.2291C36.4178 42.6799 37.0511 42.2572 37.7983 41.8603C38.1016 41.6992 38.4052 41.5515 38.7175 41.3996L38.7179 41.3994L38.936 41.2932C39.3111 41.1099 39.7022 40.9143 40.0596 40.6996C40.783 40.2838 41.6765 39.5491 41.7413 38.3282C41.8014 37.2663 41.2531 36.3256 40.4338 35.3719V35.1588L40.1744 34.8993C40.1374 34.8477 40.0795 34.7411 40.0113 34.5353C39.934 34.302 39.8672 34.0188 39.7856 33.6562C39.6368 32.9545 39.4127 31.9102 38.5493 31.186L38.3995 31.0604C38.9576 28.7316 38.5908 26.4633 37.8259 24.46C36.7898 21.7201 34.9984 19.3692 33.6785 17.7973L33.6778 17.7965C32.1849 15.9116 31.019 14.4397 31.0434 12.2669C31.0453 12.1131 31.0478 11.9522 31.0504 11.7849C31.0801 9.87812 31.1226 7.15021 30.3612 4.83914C29.9387 3.55665 29.2488 2.33019 28.115 1.42642C26.9706 0.514298 25.4646 0.00304389 23.5477 0ZM22.666 1.0385C22.9685 1.01467 23.2618 1 23.546 1C30.2102 1.01058 30.1098 7.76032 30.0499 11.7866C30.0475 11.9472 30.0452 12.1035 30.0433 12.2548C30.0146 14.7849 31.382 16.51 32.8074 18.3084C32.8393 18.3487 32.8713 18.3891 32.9033 18.4295C34.205 19.9787 35.9137 22.23 36.8908 24.815C37.692 26.9123 37.9982 29.2443 37.208 31.5873C37.3329 31.5924 37.4538 31.6331 37.5563 31.7047C37.6223 31.7737 37.6877 31.8118 37.7513 31.8488C37.8026 31.8787 37.8527 31.908 37.901 31.9522H37.9065C38.4748 32.4288 38.6527 33.1347 38.8085 33.8698C38.9698 34.5867 39.1165 35.2008 39.4282 35.5675L39.4337 35.573V35.7508C40.376 36.7867 40.7848 37.5402 40.7427 38.2735C40.706 38.9775 40.1908 39.4725 39.5528 39.8373C39.1757 40.0647 38.7412 40.276 38.2863 40.4973C37.2045 41.0234 36.0076 41.6055 35.1932 42.5928C33.974 44.0045 32.4688 44.8772 31.0553 44.9872C29.6418 45.099 28.3182 44.45 27.6013 43.0237L27.5958 43.0182C27.5697 42.949 27.5454 42.89 27.523 42.8356C27.4653 42.6951 27.4199 42.5847 27.3868 42.4077C26.4008 42.4735 25.4823 42.3328 24.6448 42.2044C23.9204 42.0934 23.2567 41.9917 22.6623 42.041C21.6653 42.1039 20.8661 42.2696 20.1753 42.4127C19.5578 42.5407 19.027 42.6507 18.519 42.6533C18.0827 43.537 17.2687 44.175 16.3043 44.3877C14.9293 44.7543 13.206 44.3803 11.5083 43.526C10.4773 42.9711 9.26303 42.8279 8.13135 42.6944C7.52434 42.6228 6.9411 42.554 6.42267 42.426C5.68017 42.1858 5.01833 41.9365 4.69933 41.3242C4.38033 40.7027 4.43716 39.8502 4.89366 38.6035C5.02207 38.1946 4.94184 37.6503 4.84524 36.995C4.83702 36.9392 4.82868 36.8826 4.82033 36.8252C4.769 36.5758 4.7195 36.2073 4.7195 35.8425C4.71216 35.4612 4.7965 35.0853 4.9615 34.7388C5.32126 34.021 5.91228 33.7658 6.46354 33.5276C6.49097 33.5158 6.5183 33.504 6.5455 33.4922C6.68913 33.4309 6.82976 33.3772 6.96561 33.3253C7.37077 33.1706 7.73344 33.0321 8.00667 32.7588C8.17742 32.5672 8.34012 32.343 8.50901 32.1103C8.72634 31.8108 8.95394 31.4971 9.22217 31.2207C9.27154 31.1251 9.34021 31.0408 9.42383 30.9732C9.19833 29.4973 9.44033 27.9353 9.95 26.41C11.0298 23.1632 13.3068 20.0483 14.9293 18.1215C16.3043 16.1653 16.715 14.5868 16.8543 12.5848C16.8738 12.1381 16.8352 11.5454 16.7908 10.8636C16.5636 7.37332 16.1844 1.54924 22.666 1.0385ZM24.5397 7.2425H24.5158C24.1226 7Line truncated
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg class="light-icon" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8.097 8Line truncated
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user