feat:移除了弹窗,服务器添加sls
7
apps/website/src/app.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
<div id="teleports"></div>
|
||||
</template>
|
||||
1
apps/website/src/assets/images/external/atlauncher.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 2084 2084" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><g fill-rule="nonzero"><path d="M1041.67 81.38l272.437 159.032-825.246 478.685-272.438-157.971L1041.67 81.38zm87.28 371.074l274.024-159.032 463.937 271.945-276.14 153.73-461.821-266.643z" fill="#3b3b3b"/><path d="M216.42 561.126v961.081l825.247 479.746V1684.95l-551.222-321.774-1.587-644.079L216.42 561.126z" fill="#2e2e2e"/><path d="M1866.91 1517.97l-825.246 483.986v-317.003l550.164-320.714-1.058-645.139 276.14-153.73v952.6z" fill="#333"/><path d="M1590.77 719.097l-549.106 310.112v165.393l214.246-122.984v488.757l138.599-81.106V989.451l196.261-115.563V719.097z" fill="#89c236"/><path d="M488.858 719.097l1.587 644.079 152.353 90.118v-198.79l230.645 132.527v199.319l168.753 98.6v-655.741L488.858 719.097zm383.527 531.166l-227.471-131.466v-150.02l227.471 127.225v154.261z" fill="#7baf31"/></g></svg>
|
||||
|
After Width: | Height: | Size: 952 B |
3
apps/website/src/assets/images/external/curseforge.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="40" height="26" viewBox="0 0 40 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M30.5432 8.2353C30.5432 8.2353 38.7102 6.9135 40 3.0587H27.4891V0H0L3.38636 4.0327V8.1646C3.38636 8.1646 11.9309 7.7086 15.236 10.2808C19.7602 14.5846 10.1476 20.4021 10.1476 20.4021L8.49916 26C11.0768 23.4811 15.9894 20.2226 24.9967 20.3797C21.5689 21.4915 18.1223 23.228 15.4392 26H33.6473L31.9326 20.4021C31.9326 20.4021 18.7359 12.4154 30.5432 8.2353Z" fill="#F16436"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 486 B |
BIN
apps/website/src/assets/images/external/gdlauncher.png
vendored
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
apps/website/src/assets/images/external/multimc.webp
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
80
apps/website/src/assets/images/external/prism.svg
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
<svg
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title id="title261">Prism Launcher Logo</title>
|
||||
<defs id="defs3603" />
|
||||
<g id="layer1">
|
||||
<g
|
||||
id="g531"
|
||||
transform="matrix(0.1353646,0,0,0.1353646,15.301582,0.52916663)" />
|
||||
<g
|
||||
id="g397">
|
||||
<path
|
||||
style="fill:#99cd61;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 6.3500002,6.350001 Z"
|
||||
id="path7899" />
|
||||
<path
|
||||
id="path3228"
|
||||
style="fill:#df6277;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 6.35 0.52916667 L 3.8292236 4.8947917 L 6.35 6.35 L 8.8702596 4.8947917 L 8.9798136 1.7952393 C 7.828708 1.1306481 6.6410414 0.52916667 6.35 0.52916667 z " />
|
||||
<path
|
||||
id="path2659"
|
||||
style="fill:#fb9168;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 8.9798136 1.7952393 L 6.35 6.35 L 8.8702596 7.8052083 L 11.391036 3.4395833 C 11.245515 3.1875341 10.130919 2.4598305 8.9798136 1.7952393 z " />
|
||||
<path
|
||||
id="path2708"
|
||||
style="fill:#f3db6c;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 11.391036 3.4395833 L 6.35 6.35 L 8.8702596 7.8052083 L 11.609111 6.35 C 11.609111 5.0208177 11.536557 3.6916326 11.391036 3.4395833 z " />
|
||||
<path
|
||||
id="path1737"
|
||||
style="fill:#7ab392;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 6.35 6.35 L 6.35 9.2604167 L 11.391036 9.2604167 C 11.536557 9.0083674 11.60911 7.6791823 11.609111 6.35 L 6.35 6.35 z " />
|
||||
<path
|
||||
id="path2937"
|
||||
style="fill:#4b7cbc;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 6.35 6.35 L 6.35 9.2604167 L 8.9798136 10.904761 C 10.130919 10.24017 11.245515 9.5124659 11.391036 9.2604167 L 6.35 6.35 z " />
|
||||
<path
|
||||
id="path3117"
|
||||
style="fill:#6f488c;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 6.35 6.35 L 3.8292236 7.8052083 L 6.35 12.170833 C 6.6410414 12.170833 7.8287079 11.569352 8.9798136 10.904761 L 6.35 6.35 z " />
|
||||
<path
|
||||
id="path2010"
|
||||
style="fill:#4d3f33;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 3.8292236 4.8947917 L 1.308964 9.2604167 C 1.6000054 9.7645152 5.7679172 12.170833 6.35 12.170833 L 6.35 6.35 L 3.8292236 4.8947917 z " />
|
||||
<path
|
||||
id="path1744"
|
||||
style="fill:#7a573b;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 1.308964 3.4395833 C 1.0179226 3.9436818 1.0179227 8.7563182 1.308964 9.2604167 L 6.35 6.35 L 6.35 3.4395833 L 1.308964 3.4395833 z " />
|
||||
<path
|
||||
id="path1739"
|
||||
style="fill:#99cd61;fill-opacity:1;stroke-width:0.264583"
|
||||
d="M 6.35 0.52916667 C 5.7679172 0.52916665 1.6000054 2.9354849 1.308964 3.4395833 L 6.35 6.35 L 6.35 0.52916667 z " />
|
||||
<g
|
||||
id="g379">
|
||||
<g
|
||||
id="g1657"
|
||||
transform="matrix(0.87999988,0,0,0.87999988,-10.906495,-1.242093)">
|
||||
<g
|
||||
id="g7651"
|
||||
transform="translate(13.259961,2.2775894)">
|
||||
<path
|
||||
id="path6659"
|
||||
style="fill:#ffffff;stroke-width:0.264583"
|
||||
d="m 6.3498163,2.9393223 c -0.3410461,0 -2.782726,1.4098777 -2.9532491,1.7052323 L 6.3498163,9.7602513 9.3035983,4.6445546 C 9.1330753,4.3492 6.6908624,2.9393223 6.3498163,2.9393223 Z"
|
||||
transform="matrix(0.96974817,0,0,0.96974817,0.19209885,0.19209792)" />
|
||||
</g>
|
||||
<path
|
||||
id="path461"
|
||||
style="fill:#dfdfdf;fill-opacity:1;stroke-width:0.264583"
|
||||
d="m 16.745875,6.9737355 2.863908,4.9609385 c 0.330729,0 2.69906,-1.367226 2.864424,-1.653646 0.165365,-0.2864204 0.165365,-3.0208729 0,-3.3072925 l -2.864424,1.6536459 z" />
|
||||
</g>
|
||||
<path
|
||||
id="path5065"
|
||||
style="fill:#d6d2d2;fill-opacity:1;stroke-width:0.264583"
|
||||
d="m 3.8298625,4.8947933 c -0.1455111,0.2520549 -0.1455304,2.6583729 0,2.9104166 0.1455304,0.2520438 2.2292181,1.4552195 2.5202596,1.4552084 V 6.3500016 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
788
apps/website/src/assets/styles/components.scss
Normal file
@ -0,0 +1,788 @@
|
||||
/*
|
||||
Cards and body styling
|
||||
*/
|
||||
// CARDS
|
||||
.base-card {
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
min-height: var(--font-size-2xl);
|
||||
|
||||
background-color: var(--surface-3);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--surface-4);
|
||||
|
||||
margin-bottom: var(--gap-md);
|
||||
outline: 2px solid transparent;
|
||||
|
||||
.card__overlay {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
grid-gap: 0.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:where(&.warning, &.information) {
|
||||
padding: 1.5rem;
|
||||
line-height: 1.5;
|
||||
min-height: 0;
|
||||
|
||||
a {
|
||||
color: var(--color-blue);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&.moderation-card {
|
||||
background-color: var(--color-warning-banner-bg);
|
||||
}
|
||||
|
||||
&.recessed {
|
||||
background-color: var(--color-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.universal-labels {
|
||||
label,
|
||||
.label {
|
||||
:where(.label__title) {
|
||||
display: block;
|
||||
margin-block: var(--spacing-card-md) var(--spacing-card-sm);
|
||||
|
||||
// Same styling as h3
|
||||
color: var(--color-text-primary);
|
||||
font-size: 1.17rem;
|
||||
font-weight: bold;
|
||||
|
||||
.required {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
&.size-card-header {
|
||||
font-size: var(--font-size-xl);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
:where(.label__description) {
|
||||
display: block;
|
||||
margin-block-end: var(--spacing-card-sm);
|
||||
|
||||
.label__subdescription {
|
||||
display: block;
|
||||
margin-block-start: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
|
||||
:where(h1, h2, h3, h4) {
|
||||
margin-block: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.universal-body {
|
||||
@extend .universal-labels;
|
||||
|
||||
> :where(input + *, .input-group + *, .chips + *, .input-div + *) {
|
||||
&:not(:empty) {
|
||||
margin-block-start: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
|
||||
:where(button, .button, .iconified-button) {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
input {
|
||||
width: auto;
|
||||
flex-basis: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:where(input) {
|
||||
box-sizing: border-box;
|
||||
max-height: 40px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:where(.adjacent-input, &.adjacent-input) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-card-sm);
|
||||
margin-bottom: calc(var(--spacing-card-sm) + var(--spacing-card-md));
|
||||
|
||||
.iconified-button,
|
||||
.input-group {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
flex-shrink: 2;
|
||||
flex-grow: 1;
|
||||
flex-basis: min-content;
|
||||
}
|
||||
|
||||
label,
|
||||
.label {
|
||||
.label__title {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.label__description {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.label__description:not(:first-child) {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
&:not(&.small) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
:first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
:where(.header__row) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-card-sm);
|
||||
|
||||
* {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header__title {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.universal-card {
|
||||
@extend .base-card;
|
||||
@extend .universal-body;
|
||||
}
|
||||
|
||||
.universal-modal {
|
||||
@extend .universal-body;
|
||||
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
.adjacent-input,
|
||||
&.adjacent-input &:not(&.small) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: calc(600px + 2rem)) {
|
||||
.adjacent-input,
|
||||
&.adjacent-input &:not(&.small) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Other
|
||||
*/
|
||||
|
||||
.text-link {
|
||||
color: var(--color-link);
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
color: var(--color-link-hover);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-link-active);
|
||||
}
|
||||
}
|
||||
|
||||
.button-base {
|
||||
@extend .button-animation;
|
||||
font-weight: 500;
|
||||
outline: 2px solid transparent;
|
||||
|
||||
&:focus-visible:not(&:disabled),
|
||||
&:hover:not(&:disabled) {
|
||||
cursor: pointer;
|
||||
filter: brightness(0.85);
|
||||
}
|
||||
|
||||
&:active:not(&:disabled) {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
:not(tr).button-transparent {
|
||||
@extend .button-base;
|
||||
background-color: transparent;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
|
||||
&:focus-visible:not(&:disabled),
|
||||
&:hover:not(&:disabled),
|
||||
&:active:not(&:disabled) {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
&.brand-button {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
&.danger-button {
|
||||
color: var(--color-red);
|
||||
}
|
||||
}
|
||||
|
||||
tr.button-transparent {
|
||||
@extend .button-animation;
|
||||
background-color: transparent;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
|
||||
&:focus-visible:not(&:disabled) > *,
|
||||
&:hover:not(&:disabled) > * {
|
||||
cursor: pointer;
|
||||
filter: brightness(0.85);
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
&:active:not(&:disabled) > * {
|
||||
filter: brightness(0.8);
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
&:disabled > *,
|
||||
&[disabled='true'] > * {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.button-color-base {
|
||||
box-sizing: border-box;
|
||||
--text-color: var(--color-button-text);
|
||||
--background-color: var(--color-button-bg);
|
||||
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
}
|
||||
|
||||
.iconified-button {
|
||||
@extend .button-base;
|
||||
@extend .button-color-base;
|
||||
|
||||
display: flex;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-bg);
|
||||
margin: 0;
|
||||
font-size: var(--font-size-nm);
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
transition:
|
||||
opacity 0.5s ease-in-out,
|
||||
filter 0.2s ease-in-out,
|
||||
scale 0.05s ease-in-out,
|
||||
outline 0.2s ease-in-out;
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
svg {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
&.icon-only {
|
||||
padding: 0 0.5rem;
|
||||
|
||||
svg {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.square-button {
|
||||
@extend .button-base;
|
||||
|
||||
--text-color: var(--color-button-text);
|
||||
--background-color: var(--color-button-bg);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
|
||||
svg {
|
||||
min-width: 1.25rem;
|
||||
max-width: 1.25rem;
|
||||
min-height: 1.25rem;
|
||||
max-height: 1.25rem;
|
||||
}
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.raised-button {
|
||||
--background-color: var(--color-raised-bg);
|
||||
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
--background-color: var(--color-red);
|
||||
--text-color: var(--color-brand-inverted);
|
||||
}
|
||||
|
||||
.moderation-button {
|
||||
--background-color: var(--color-orange);
|
||||
--text-color: var(--color-brand-inverted);
|
||||
}
|
||||
|
||||
.brand-button {
|
||||
--background-color: var(--color-brand);
|
||||
--text-color: var(--color-brand-inverted);
|
||||
}
|
||||
|
||||
.error {
|
||||
.icon {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.text-input-wrapper.known-error,
|
||||
input.known-error,
|
||||
textarea.known-error {
|
||||
outline: 2px solid var(--color-red);
|
||||
background-color: var(--color-warning-bg) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-warning-text);
|
||||
}
|
||||
}
|
||||
|
||||
.known-errors {
|
||||
min-height: 0;
|
||||
color: var(--color-red);
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.goto-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.goto-link:hover,
|
||||
.goto-link:focus-visible {
|
||||
color: var(--color-link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.goto-link:active {
|
||||
color: var(--color-link-active);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.button-animation,
|
||||
button {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.push-right:not(.input-group),
|
||||
.push-right.input-group > :first-child {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.full-width-inputs {
|
||||
input,
|
||||
.iconified-input {
|
||||
width: 100%;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
&:disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
grid-gap: var(--spacing-card-sm);
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
flex-shrink: 2;
|
||||
}
|
||||
|
||||
&.shrink-first {
|
||||
:first-child {
|
||||
flex-shrink: 2;
|
||||
flex-grow: 1;
|
||||
flex-basis: min-content;
|
||||
}
|
||||
|
||||
:not(:first-child) {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.right-aligned {
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
.text-input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
background: var(--color-button-bg);
|
||||
width: fit-content;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
outline: 2px solid transparent;
|
||||
|
||||
.text-input-wrapper__before {
|
||||
display: flex;
|
||||
color: var(--color-text);
|
||||
padding: 0.5rem 0 0.5rem 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
box-shadow: unset !important;
|
||||
padding-left: 0;
|
||||
flex-grow: 1;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:focus-within {
|
||||
box-shadow:
|
||||
inset 0 0 0 transparent,
|
||||
0 0 0 0.25rem var(--color-brand-shadow);
|
||||
color: var(--color-button-text-active);
|
||||
}
|
||||
}
|
||||
|
||||
.project-list {
|
||||
width: 100%;
|
||||
gap: var(--spacing-card-md);
|
||||
overflow: hidden;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
&:not(:empty) {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
}
|
||||
|
||||
.project-list.display-mode--list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project-list.display-mode--gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.project-list.display-mode--grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
|
||||
@media screen and (max-width: 80rem) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media screen and (max-width: 860px) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.wrap-as-needed {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.backed-svg {
|
||||
--size: 2.5rem;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
background-color: var(--color-button-bg);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
width: calc(0.6 * var(--size));
|
||||
height: calc(0.6 * var(--size));
|
||||
}
|
||||
|
||||
&.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.raised {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
}
|
||||
|
||||
a.iconified-link,
|
||||
a.iconified-stacked-link {
|
||||
display: contents;
|
||||
|
||||
.space {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stacked {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&:focus-visible .title,
|
||||
&:hover .title {
|
||||
text-decoration: underline;
|
||||
filter: var(--hover-filter);
|
||||
}
|
||||
|
||||
&:active .title {
|
||||
filter: var(--active-filter);
|
||||
}
|
||||
}
|
||||
|
||||
a.iconified-link {
|
||||
&:focus-visible span,
|
||||
&:hover span {
|
||||
text-decoration: underline;
|
||||
filter: var(--hover-filter);
|
||||
}
|
||||
|
||||
&:active span {
|
||||
filter: var(--active-filter);
|
||||
}
|
||||
}
|
||||
|
||||
.inline-svg svg,
|
||||
svg.inline-svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// START STUFF FOR OMORPHIA
|
||||
.flex-card {
|
||||
h2 {
|
||||
@apply font-semibold;
|
||||
font-size: var(--text-18);
|
||||
color: var(--color-contrast);
|
||||
line-height: initial;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply font-semibold;
|
||||
font-size: var(--text-16);
|
||||
color: var(--color-base);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-8);
|
||||
}
|
||||
}
|
||||
|
||||
.list-style {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-12);
|
||||
font-weight: var(--weight-bold);
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
border-color: var(--color-button-border);
|
||||
margin-block: var(--gap-2);
|
||||
}
|
||||
}
|
||||
|
||||
.iconified-list-item {
|
||||
display: flex;
|
||||
gap: var(--gap-8);
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
|
||||
svg {
|
||||
width: var(--icon-16);
|
||||
height: var(--icon-16);
|
||||
}
|
||||
|
||||
> svg:first-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.details-list {
|
||||
@extend .list-style;
|
||||
}
|
||||
|
||||
.details-list__item {
|
||||
@apply font-normal;
|
||||
@extend .iconified-list-item;
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
506
apps/website/src/assets/styles/global.scss
Normal file
@ -0,0 +1,506 @@
|
||||
@import '@modrinth/assets/styles/reset.scss';
|
||||
|
||||
// 主题化图标:图标组件同时渲染暗色/亮色两版,由主题类切换显示
|
||||
.dark-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.light-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.light-mode .dark-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.light-mode .light-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html {
|
||||
--dark-color-text: #b0bac5;
|
||||
--dark-color-text-dark: #ecf9fb;
|
||||
--color-text-secondary: var(--color-icon);
|
||||
}
|
||||
|
||||
// TO BE MOVED TO OMORPHIA
|
||||
:root {
|
||||
--gap-2: 0.125rem;
|
||||
--gap-4: calc(2 * var(--gap-2));
|
||||
--gap-6: calc(3 * var(--gap-2));
|
||||
--gap-8: calc(2 * var(--gap-4));
|
||||
--gap-12: calc(3 * var(--gap-4));
|
||||
--gap-16: calc(2 * var(--gap-8));
|
||||
--gap-24: calc(3 * var(--gap-8));
|
||||
|
||||
--radius-card: 1rem;
|
||||
|
||||
--radius-btn: 0.75rem;
|
||||
--radius-btn-large: 1rem;
|
||||
--radius-btn-circle: var(--radius-max);
|
||||
|
||||
--text-14: 0.875rem;
|
||||
--text-16: 1rem;
|
||||
--text-18: 1.125rem;
|
||||
--text-24: 1.5rem;
|
||||
--text-32: 2rem;
|
||||
|
||||
--weight-normal: 500; // used for general body text
|
||||
--weight-bold: 600; // used for text needing extra emphasis
|
||||
--weight-extrabold: 800; // used for main titles and headings
|
||||
|
||||
--icon-14: 0.875rem; // used for icons inside a small container alongside a label
|
||||
--icon-16: 1rem; // used for normal-sized icons alongside a label
|
||||
--icon-20: 1.25rem; // used for icons in normal sized buttons
|
||||
--icon-24: 1.5rem; // used for icons that are used as a primary label or in large buttons
|
||||
--icon-32: 2rem;
|
||||
|
||||
interpolate-size: allow-keywords;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.light-mode {
|
||||
--color-icon: var(--color-secondary);
|
||||
--color-text: hsl(221, 39%, 11%);
|
||||
--color-text-inactive: hsl(215, 14%, 34%);
|
||||
--color-text-dark: #1a202c;
|
||||
--color-heading: #2c313d;
|
||||
|
||||
--color-text-inverted: var(--color-bg);
|
||||
--color-bg-inverted: var(--color-text);
|
||||
|
||||
--color-brand-inverted: #ffffff;
|
||||
|
||||
--tab-underline-hovered: #e2e8f0;
|
||||
|
||||
--color-button-text: var(--color-text-dark);
|
||||
--color-button-bg-hover: #d9dce0;
|
||||
--color-button-text-hover: #1b1e24;
|
||||
--color-button-bg-active: #c3c6cb;
|
||||
--color-button-text-active: var(--color-button-text-hover);
|
||||
|
||||
--color-dropdown-bg: var(--color-button-bg);
|
||||
--color-dropdown-text: var(--color-button-text);
|
||||
|
||||
--color-code-bg: var(--color-bg);
|
||||
--color-code-text: var(--color-text-dark);
|
||||
|
||||
--color-kbd-shadow: rgba(0, 0, 0, 0.25);
|
||||
|
||||
--color-grey-link: var(--color-text);
|
||||
--color-grey-link-hover: var(--color-heading);
|
||||
--color-grey-link-active: var(--color-text-dark);
|
||||
|
||||
--color-warning-bg: hsl(355, 70%, 88%);
|
||||
--color-warning-text: hsl(342, 70%, 35%);
|
||||
|
||||
--color-warning-banner-text: hsl(0, 11%, 16%);
|
||||
--color-warning-banner-bg: hsl(0, 100%, 95%);
|
||||
--color-warning-banner-side: hsl(357, 78%, 40%);
|
||||
|
||||
--color-info-banner-text: var(--color-text);
|
||||
--color-info-banner-bg: var(--color-ad);
|
||||
--color-info-banner-side: var(--color-blue);
|
||||
|
||||
--color-block-quote: var(--color-tooltip-bg);
|
||||
--color-header-underline: var(--color-divider-dark);
|
||||
--color-hr: var(--color-text);
|
||||
|
||||
--color-table-border: #dfe2e5;
|
||||
--color-table-alternate-row: #f0f1f2;
|
||||
|
||||
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing-light.webp');
|
||||
--landing-maze-gradient-bg: url('https://cdn.modrinth.com/landing-new/landing-lower-light.webp');
|
||||
--landing-maze-outer-bg: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
|
||||
|
||||
--landing-color-heading: #000;
|
||||
--landing-color-subheading: #3a3f45;
|
||||
|
||||
--landing-transition-gradient-start: rgba(255, 255, 255, 0);
|
||||
--landing-transition-gradient-end: #ffffff;
|
||||
--landing-hover-card-gradient: radial-gradient(
|
||||
50% 50% at 50% 50%,
|
||||
#fff 0%,
|
||||
rgba(204, 204, 204, 0.77) 100%
|
||||
);
|
||||
--landing-border-gradient: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(129, 137, 175, 0.75) 0%,
|
||||
rgba(66, 71, 97, 0.34) 100%
|
||||
);
|
||||
--landing-border-color: rgba(129, 137, 175, 0.55);
|
||||
--landing-creator-gradient: linear-gradient(180deg, #f8f8f8 0%, #f8f8f8 63.19%);
|
||||
|
||||
--landing-blob-gradient: radial-gradient(
|
||||
50% 50% at 50% 50%,
|
||||
rgba(255, 255, 255, 0.35) 0%,
|
||||
rgba(255, 255, 255, 0.2695) 100%
|
||||
);
|
||||
--landing-blob-shadow:
|
||||
2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(255, 255, 255, 0.45);
|
||||
|
||||
--landing-card-bg: rgba(255, 255, 255, 0.8);
|
||||
--landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16);
|
||||
|
||||
--landing-blue-label: #0098ba;
|
||||
--landing-blue-label-bg: rgba(0, 177, 216, 0.15);
|
||||
--landing-green-label: #00a936;
|
||||
--landing-green-label-bg: rgba(0, 216, 69, 0.15);
|
||||
|
||||
--landing-raw-bg: #fff;
|
||||
|
||||
--medal-promotion-bg: #fff;
|
||||
--medal-promotion-bg-orange: #48aaff;
|
||||
--medal-promotion-text-orange: #156db8;
|
||||
--medal-promotion-bg-gradient: linear-gradient(
|
||||
90deg,
|
||||
rgba(152, 207, 255, 0.2) 20%,
|
||||
rgba(152, 207, 255, 0.1) 100%
|
||||
);
|
||||
|
||||
--banner-error-bg: #fee2e2;
|
||||
--banner-error-text: #991b1b;
|
||||
--banner-error-border: #ef4444;
|
||||
|
||||
--banner-warning-bg: #ffedd5;
|
||||
--banner-warning-text: #713f12;
|
||||
--banner-warning-border: #f97316;
|
||||
|
||||
--banner-info-bg: #dbeafe;
|
||||
--banner-info-text: #1e3a8a;
|
||||
--banner-info-border: #3b82f6;
|
||||
}
|
||||
|
||||
.dark,
|
||||
.dark-mode,
|
||||
.oled-mode,
|
||||
.retro-mode {
|
||||
--color-icon: var(--color-secondary);
|
||||
--color-text: var(--dark-color-text);
|
||||
--color-text-inactive: #929aa3;
|
||||
--color-text-dark: var(--dark-color-text-dark);
|
||||
--color-heading: #c4cfdd;
|
||||
|
||||
--color-text-inverted: var(--color-bg);
|
||||
--color-bg-inverted: var(--color-text);
|
||||
|
||||
--color-brand-inverted: #000;
|
||||
|
||||
--tab-underline-hovered: #414146;
|
||||
|
||||
--color-button-text: var(--color-text);
|
||||
--color-button-bg-hover: #494f58;
|
||||
--color-button-text-hover: #ffffff;
|
||||
--color-button-bg-active: #616570;
|
||||
--color-button-text-active: var(--color-button-text-hover);
|
||||
|
||||
--color-dropdown-bg: var(--color-button-bg);
|
||||
--color-dropdown-text: var(--color-button-text);
|
||||
|
||||
--color-code-bg: var(--color-button-bg);
|
||||
--color-code-text: var(--color-text-dark);
|
||||
|
||||
--color-kbd-shadow: rgba(0, 0, 0, 0.35);
|
||||
|
||||
--color-warning-bg: hsl(355, 70%, 20%);
|
||||
--color-warning-text: hsl(342, 70%, 75%);
|
||||
|
||||
--color-warning-banner-text: hsl(0, 100%, 96%);
|
||||
--color-warning-banner-bg: hsl(356, 18%, 18%);
|
||||
--color-warning-banner-side: hsl(357, 78%, 40%);
|
||||
|
||||
--color-info-banner-text: var(--color-text);
|
||||
--color-info-banner-bg: var(--color-ad);
|
||||
--color-info-banner-side: var(--color-blue);
|
||||
|
||||
--color-block-quote: var(--color-code-bg);
|
||||
--color-header-underline: var(--color-divider-dark);
|
||||
--color-hr: var(--color-text);
|
||||
|
||||
--color-table-border: #4f5864;
|
||||
--color-table-alternate-row: #202228;
|
||||
|
||||
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing.webp');
|
||||
--landing-maze-gradient-bg:
|
||||
linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%),
|
||||
url('https://cdn.modrinth.com/landing-new/landing-lower.webp');
|
||||
--landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%);
|
||||
|
||||
--landing-color-heading: #fff;
|
||||
--landing-color-subheading: #afb6be;
|
||||
|
||||
--landing-transition-gradient-start: rgba(14, 16, 32, 0);
|
||||
--landing-transition-gradient-end: #060a1c;
|
||||
--landing-hover-card-gradient: radial-gradient(
|
||||
50% 50% at 50% 50%,
|
||||
#2c304f 0%,
|
||||
rgba(32, 35, 50, 0.77) 100%
|
||||
);
|
||||
--landing-border-gradient: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(168, 177, 221, 0.75) 0%,
|
||||
rgba(168, 177, 221, 0.18) 100%
|
||||
);
|
||||
--landing-border-color: rgba(168, 177, 221, 0.55);
|
||||
--landing-creator-gradient: linear-gradient(180deg, #000000 0%, #0e101d 100%);
|
||||
|
||||
--landing-blob-gradient: radial-gradient(
|
||||
50% 50% at 50% 50%,
|
||||
rgba(44, 48, 79, 0.35) 0%,
|
||||
rgba(32, 35, 50, 0.2695) 100%
|
||||
);
|
||||
--landing-blob-shadow:
|
||||
2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(57, 61, 94, 0.45);
|
||||
|
||||
--landing-card-bg: rgba(59, 63, 85, 0.15);
|
||||
--landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16);
|
||||
|
||||
--landing-blue-label: #10c0e7;
|
||||
--landing-blue-label-bg: rgba(0, 177, 216, 0.15);
|
||||
--landing-green-label: #00d845;
|
||||
--landing-green-label-bg: rgba(0, 216, 69, 0.15);
|
||||
|
||||
--landing-raw-bg: #000;
|
||||
|
||||
--medal-promotion-bg: #000;
|
||||
--medal-promotion-bg-orange: rgba(208, 246, 255, 0.25);
|
||||
--medal-promotion-text-orange: #42abff;
|
||||
--medal-promotion-bg-gradient: linear-gradient(
|
||||
90deg,
|
||||
rgba(66, 170, 255, 0.15),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
|
||||
--hover-filter: brightness(120%);
|
||||
--active-filter: brightness(140%);
|
||||
|
||||
--banner-error-bg: #45222c;
|
||||
--banner-error-text: #fee2e2;
|
||||
--banner-error-border: var(--color-red);
|
||||
|
||||
--banner-warning-bg: #453425;
|
||||
--banner-warning-text: #e4d9ca;
|
||||
--banner-warning-border: var(--color-orange);
|
||||
|
||||
--banner-info-bg: #28374b;
|
||||
--banner-info-text: #dbeafe;
|
||||
--banner-info-border: var(--color-blue);
|
||||
}
|
||||
|
||||
.oled-mode {
|
||||
--color-button-bg-hover: #2d2d32;
|
||||
--color-button-bg-active: #3c3c40;
|
||||
--color-table-alternate-row: #19191c;
|
||||
--color-divider-dark: #2c3134;
|
||||
|
||||
--color-warning-banner-bg: hsl(0, 45%, 11%);
|
||||
}
|
||||
|
||||
.retro-mode {
|
||||
--color-bg: #191917;
|
||||
--color-raised-bg: #1d1e1b;
|
||||
--color-button-bg: #3a3b38;
|
||||
--color-base: #c3c4b3;
|
||||
--color-secondary: #777a74;
|
||||
--color-contrast: #e6e2d1;
|
||||
|
||||
--color-brand: #4d9227;
|
||||
--color-brand-highlight: #25421e;
|
||||
--color-ad: var(--color-brand-highlight);
|
||||
--color-ad-raised: var(--color-brand);
|
||||
--color-ad-contrast: black;
|
||||
--color-ad-highlight: var(--color-brand);
|
||||
|
||||
--color-red: rgb(232, 32, 13);
|
||||
--color-orange: rgb(232, 141, 13);
|
||||
--color-green: rgb(60, 219, 54);
|
||||
--color-blue: rgb(9, 159, 239);
|
||||
--color-purple: rgb(139, 129, 230);
|
||||
--color-gray: #718096;
|
||||
|
||||
--color-red-highlight: rgba(232, 32, 13, 0.25);
|
||||
--color-orange-highlight: rgba(232, 141, 13, 0.25);
|
||||
--color-green-highlight: rgba(60, 219, 54, 0.25);
|
||||
--color-blue-highlight: rgba(9, 159, 239, 0.25);
|
||||
--color-purple-highlight: rgba(139, 129, 230, 0.25);
|
||||
--color-gray-highlight: rgba(113, 128, 150, 0.25);
|
||||
}
|
||||
|
||||
body {
|
||||
// Defaults
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
--font-standard:
|
||||
Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell,
|
||||
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
font-family: var(--font-standard);
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
// Rounding sizes
|
||||
--size-rounded-xs: 0.5rem;
|
||||
--size-rounded-sm: 0.75rem;
|
||||
--size-rounded-md: 1rem;
|
||||
--size-rounded-lg: 1.25rem;
|
||||
|
||||
--size-rounded-max: 999999999px;
|
||||
--size-rounded-card: 1rem;
|
||||
--size-rounded-icon: 1rem;
|
||||
--size-rounded-control: 0.25rem;
|
||||
--size-rounded-tooltip: 0.25rem;
|
||||
|
||||
--size-navbar-height: 3.5rem;
|
||||
--size-mobile-navbar-height: 3.5rem;
|
||||
--size-mobile-navbar-height-expanded: 26.5rem;
|
||||
|
||||
--spacing-card-lg: 1.5rem;
|
||||
--spacing-card-bg: 1rem;
|
||||
--spacing-card-md: 0.75rem;
|
||||
--spacing-card-sm: 0.5rem;
|
||||
--spacing-card-xs: 0.25rem;
|
||||
|
||||
// Font Sizes
|
||||
--font-size-xxs: 0.625rem; //10px
|
||||
--font-size-xs: 0.75rem; //12px
|
||||
--font-size-sm: 0.875rem; //14px
|
||||
--font-size-nm: 1rem; //16px
|
||||
--font-size-md: 1.125rem; //18px
|
||||
--font-size-lg: 1.25rem; //20px
|
||||
--font-size-xl: 1.5rem; //24px
|
||||
--font-size-2xl: 2rem; //32px
|
||||
--font-size-3xl: 3rem; //48px
|
||||
|
||||
// Font Weights
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extrabold: 800;
|
||||
|
||||
--font-weight-text: var(--font-weight-medium);
|
||||
--font-weight-heading: var(--font-weight-extrabold);
|
||||
--font-weight-title: var(--font-weight-extrabold);
|
||||
|
||||
@media screen and (min-width: 354px) {
|
||||
--size-mobile-navbar-height-expanded: 15.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-block: var(--spacing-card-md) var(--spacing-card-sm);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: var(--size-rounded-sm);
|
||||
box-sizing: border-box;
|
||||
border: 2px solid transparent;
|
||||
// safari iOS rounds inputs by default
|
||||
// set the appearance to none to prevent this
|
||||
appearance: none !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-text);
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
border: none;
|
||||
outline: 2px solid transparent;
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
min-height: 36px;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
box-shadow:
|
||||
inset 0 0 0 transparent,
|
||||
0 0 0 0.25rem var(--color-brand-shadow);
|
||||
color: var(--color-button-text-active);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled='true'] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus::placeholder {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-button-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: var(--color-code-bg);
|
||||
color: var(--color-code-text);
|
||||
box-shadow: 0 2px 1px var(--color-kbd-shadow);
|
||||
padding: 0.2em 0.5em 0.1em;
|
||||
border-radius: 3px;
|
||||
line-height: 1;
|
||||
font-size: 0.85em !important;
|
||||
}
|
||||
|
||||
@import './layout.scss';
|
||||
@import './utils.scss';
|
||||
@import './components.scss';
|
||||
|
||||
// OMORPHIA FIXES
|
||||
.card {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
input {
|
||||
outline: 2px solid transparent !important;
|
||||
}
|
||||
|
||||
.button-animation {
|
||||
transition:
|
||||
opacity 0.5s ease-in-out,
|
||||
filter 0.2s ease-in-out,
|
||||
transform 0.05s ease-in-out,
|
||||
outline-width 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.button-transparent {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.qc-cmp2-close-tooltip {
|
||||
background-color: transparent;
|
||||
color: hsl(145, 78%, 28%);
|
||||
}
|
||||
161
apps/website/src/assets/styles/layout.scss
Normal file
@ -0,0 +1,161 @@
|
||||
.page-container {
|
||||
margin: var(--spacing-card-md);
|
||||
margin-top: 0;
|
||||
|
||||
.page-contents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.content {
|
||||
width: 100%;
|
||||
}
|
||||
@media screen and (min-width: 1024px) {
|
||||
flex-direction: row;
|
||||
//max-width: 1280px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.normal-page {
|
||||
display: grid;
|
||||
padding: 0 1.5rem;
|
||||
|
||||
grid-template:
|
||||
'header'
|
||||
'sidebar'
|
||||
'content'
|
||||
'info'
|
||||
/ 100%;
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
margin-top: 1.5rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.normal-page__sidebar {
|
||||
grid-area: sidebar;
|
||||
}
|
||||
|
||||
.normal-page__info {
|
||||
grid-area: info;
|
||||
}
|
||||
|
||||
.normal-page__content {
|
||||
grid-area: content;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.normal-page__header {
|
||||
grid-area: header;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.full-page {
|
||||
margin: 0 auto;
|
||||
max-width: min(1280px, 100vw);
|
||||
width: 80rem;
|
||||
}
|
||||
|
||||
.normal-page {
|
||||
margin: 0 auto;
|
||||
max-width: 80rem;
|
||||
column-gap: 0.75rem;
|
||||
|
||||
grid-template:
|
||||
'sidebar content' auto
|
||||
'info content' auto
|
||||
'dummy content' 1fr
|
||||
/ 18.75rem 1fr;
|
||||
|
||||
&.alt-layout {
|
||||
grid-template:
|
||||
'content sidebar' auto
|
||||
'content info' auto
|
||||
'content dummy' 1fr
|
||||
/ 1fr 18.75rem;
|
||||
}
|
||||
|
||||
&.no-sidebar {
|
||||
grid-template:
|
||||
'header header' auto
|
||||
'content content' auto
|
||||
'info info' auto
|
||||
'dummy dummy' 1fr
|
||||
/ 1fr 1fr;
|
||||
|
||||
.normal-page__content {
|
||||
grid-area: content;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.normal-page__sidebar {
|
||||
min-width: 18.75rem;
|
||||
width: 18.75rem;
|
||||
}
|
||||
|
||||
.normal-page__content {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: calc(80rem - 18.75rem - 1.5rem);
|
||||
//overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.new-page {
|
||||
display: grid;
|
||||
margin: 0 auto;
|
||||
max-width: 80rem;
|
||||
column-gap: 0.75rem;
|
||||
padding: 0 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
|
||||
grid-template:
|
||||
'header'
|
||||
'content'
|
||||
'sidebar'
|
||||
/ 100%;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
&.sidebar {
|
||||
grid-template:
|
||||
'header header' auto
|
||||
'content sidebar' auto
|
||||
'content dummy' 1fr
|
||||
/ 1fr 18.75rem;
|
||||
|
||||
&.alt-layout {
|
||||
grid-template:
|
||||
'header header' auto
|
||||
'sidebar content' auto
|
||||
'dummy content' 1fr
|
||||
/ 18.75rem 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.normal-page__sidebar {
|
||||
min-width: 18.75rem;
|
||||
width: 18.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.normal-page__sidebar {
|
||||
grid-area: sidebar;
|
||||
}
|
||||
|
||||
.normal-page__content {
|
||||
grid-area: content;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: calc(80rem - 18.75rem - 1.5rem);
|
||||
//overflow-x: hidden;
|
||||
}
|
||||
|
||||
.normal-page__header {
|
||||
grid-area: header;
|
||||
}
|
||||
}
|
||||
11
apps/website/src/assets/styles/tailwind.css
Normal file
@ -0,0 +1,11 @@
|
||||
@import '@modrinth/ui/src/styles/tailwind-utilities.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
@apply font-bold;
|
||||
}
|
||||
27
apps/website/src/assets/styles/utils.scss
Normal file
@ -0,0 +1,27 @@
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.text-container p {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
// From the Bootstrap project
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2011-2023 The Bootstrap Authors
|
||||
// https://github.com/twbs/bootstrap/blob/2f617215755b066904248525a8c56ea425dde871/scss/mixins/_visually-hidden.scss#L8
|
||||
.visually-hidden {
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
|
||||
&:not(caption) {
|
||||
position: absolute !important;
|
||||
}
|
||||
}
|
||||
15
apps/website/src/components/brand/AxolotlWordmark.vue
Normal 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>
|
||||
25
apps/website/src/components/brand/LogoAnimated.vue
Normal 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>
|
||||
15
apps/website/src/components/landing/AppleLogo.vue
Normal 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>
|
||||
245
apps/website/src/components/landing/CommunitySection.vue
Normal 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>
|
||||
61
apps/website/src/components/landing/ImportGradientIcon.vue
Normal 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>
|
||||
30
apps/website/src/components/landing/LinuxLogo.vue
Normal file
63
apps/website/src/components/landing/MultiplayerIcon.vue
Normal 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>
|
||||
102
apps/website/src/components/landing/OfflineModeIcon.vue
Normal 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>
|
||||
336
apps/website/src/components/landing/ProjectsShowcase.vue
Normal 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>
|
||||
80
apps/website/src/components/landing/QqChannelIcon.vue
Normal 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>
|
||||
13
apps/website/src/components/landing/QqIcon.vue
Normal 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>
|
||||
18
apps/website/src/components/landing/WindowsLogo.vue
Normal 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>
|
||||
162
apps/website/src/components/ui/AxolotlFooter.vue
Normal 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>
|
||||
281
apps/website/src/components/ui/AxolotlHeader.vue
Normal 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>
|
||||
116
apps/website/src/components/ui/LegalDocument.vue
Normal 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>
|
||||
714
apps/website/src/components/ui/SiteSettingsModal.vue
Normal 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>
|
||||
36
apps/website/src/composables/use-download-source.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export type DownloadSource = 'auto' | 'update-server' | 'github'
|
||||
export type ResolvedDownloadSource = Exclude<DownloadSource, 'auto'>
|
||||
|
||||
const DOWNLOAD_SOURCE_STORAGE_KEY = 'axolotl-download-source'
|
||||
function isDownloadSource(value: string | null): value is DownloadSource {
|
||||
return value === 'auto' || value === 'update-server' || value === 'github'
|
||||
}
|
||||
|
||||
export function useDownloadSource() {
|
||||
const selectedSource = useState<DownloadSource>('axolotl-download-source', () => 'auto')
|
||||
|
||||
const resolvedSource = computed<ResolvedDownloadSource>(() => {
|
||||
if (selectedSource.value !== 'auto') return selectedSource.value
|
||||
return 'update-server'
|
||||
})
|
||||
|
||||
function setDownloadSource(source: DownloadSource) {
|
||||
selectedSource.value = source
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(DOWNLOAD_SOURCE_STORAGE_KEY, source)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const savedSource = localStorage.getItem(DOWNLOAD_SOURCE_STORAGE_KEY)
|
||||
if (isDownloadSource(savedSource)) {
|
||||
selectedSource.value = savedSource
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
selectedSource,
|
||||
resolvedSource,
|
||||
setDownloadSource,
|
||||
}
|
||||
}
|
||||
105
apps/website/src/layouts/default.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import AxolotlFooter from '~/components/ui/AxolotlFooter.vue'
|
||||
import AxolotlHeader from '~/components/ui/AxolotlHeader.vue'
|
||||
import SiteSettingsModal from '~/components/ui/SiteSettingsModal.vue'
|
||||
|
||||
const settingsOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="site-layout">
|
||||
<AxolotlHeader @open-settings="settingsOpen = true" />
|
||||
<main id="main">
|
||||
<slot />
|
||||
</main>
|
||||
<AxolotlFooter />
|
||||
<SiteSettingsModal v-model="settingsOpen" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~/assets/styles/global.scss';
|
||||
|
||||
html.accent-pink.light-mode {
|
||||
--surface-1: #f8f4f7;
|
||||
--surface-2: #fcf8fb;
|
||||
--surface-3: #fffafd;
|
||||
--surface-4: #ffffff;
|
||||
--surface-5: #eadde5;
|
||||
--color-text-primary: #2e2029;
|
||||
--color-text-default: #493944;
|
||||
--color-text-tertiary: #75616e;
|
||||
--color-pink: #c72f6c;
|
||||
--color-pink-highlight: rgb(199 47 108 / 14%);
|
||||
--color-brand-shadow: rgb(199 47 108 / 30%);
|
||||
--landing-color-heading: #281b24;
|
||||
--landing-color-subheading: #65515e;
|
||||
--landing-transition-gradient-end: #f8f4f7;
|
||||
--landing-border-color: rgb(166 107 138 / 32%);
|
||||
}
|
||||
|
||||
html.accent-pink.dark-mode {
|
||||
--surface-1: #101018;
|
||||
--surface-2: #15151f;
|
||||
--surface-3: #1b1b27;
|
||||
--surface-4: #242432;
|
||||
--surface-5: #343446;
|
||||
--color-text-primary: #fff7fb;
|
||||
--color-text-default: #ddd3da;
|
||||
--color-text-tertiary: #aa9ca6;
|
||||
--color-pink: #ff82b2;
|
||||
--color-pink-highlight: rgb(255 130 178 / 18%);
|
||||
--color-brand-shadow: rgb(255 130 178 / 30%);
|
||||
--landing-color-heading: #fff8fc;
|
||||
--landing-color-subheading: #bdb0ba;
|
||||
--landing-transition-gradient-end: #101018;
|
||||
--landing-border-color: rgb(230 170 204 / 24%);
|
||||
}
|
||||
|
||||
html.accent-pink.oled-mode {
|
||||
--color-pink: #ff82b2;
|
||||
--color-pink-highlight: rgb(255 130 178 / 18%);
|
||||
--color-brand-shadow: rgb(255 130 178 / 30%);
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 5rem;
|
||||
}
|
||||
|
||||
.site-layout {
|
||||
// svh 以小视口(工具栏展开时)为基准,避免移动端 Firefox/Chrome 动态工具栏导致的高度偏差
|
||||
min-height: 100vh;
|
||||
min-height: 100svh;
|
||||
background: linear-gradient(rgb(255 255 255 / 1%) 1px, transparent 1px), var(--color-bg);
|
||||
background-size: 100% 8rem;
|
||||
}
|
||||
|
||||
.site-layout > main {
|
||||
min-height: calc(100vh - 4.5rem);
|
||||
min-height: calc(100svh - 4.5rem);
|
||||
}
|
||||
|
||||
.reduced-effects * {
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
.reduced-motion,
|
||||
.reduced-motion * {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
223
apps/website/src/locales/site.ts
Normal file
@ -0,0 +1,223 @@
|
||||
export const SUPPORTED_SITE_LOCALES = ['en-US', 'zh-CN'] as const
|
||||
|
||||
export const siteLocaleMessages: Record<string, Record<string, string>> = {
|
||||
'zh-CN': {
|
||||
'axolotl-site.seo.title':
|
||||
'Axolotl Launcher - 支持 Modrinth 与 CurseForge 的开源 Minecraft 启动器',
|
||||
'axolotl-site.seo.description':
|
||||
'下载 Axolotl Launcher:适用于 Windows、macOS 和 Linux 的免费开源 Tauri v2 Minecraft(我的世界)启动器,支持 Modrinth 与 CurseForge 内容管理、主题、账户及更多功能。',
|
||||
'axolotl-site.seo.social-image-alt':
|
||||
'Axolotl Launcher 正在展示一个 Minecraft 实例及其已安装内容。',
|
||||
'axolotl-site.faq.eyebrow': '常见问题',
|
||||
'axolotl-site.faq.title': '关于 Axolotl Launcher,你需要了解的一切',
|
||||
'axolotl-site.faq.description': '了解支持平台、账户类型、内容来源与官方下载方式。',
|
||||
'axolotl-site.faq.platforms.question': 'Axolotl Launcher 支持哪些操作系统?',
|
||||
'axolotl-site.faq.platforms.answer':
|
||||
'Axolotl Launcher 支持 Windows 10/11 x64、搭载 Intel 或 Apple 芯片的 macOS,以及通过 AppImage、DEB 和 RPM 软件包安装的 Linux x64。',
|
||||
'axolotl-site.faq.free.question': 'Axolotl Launcher 免费且开源吗?',
|
||||
'axolotl-site.faq.free.answer':
|
||||
'是。Axolotl Launcher 是基于 GPL-3.0 发布的自由软件,源代码和版本历史均在 GitHub 上公开。',
|
||||
'axolotl-site.faq.accounts.question': '可以使用 Microsoft 正版账户和离线账户吗?',
|
||||
'axolotl-site.faq.accounts.answer':
|
||||
'可以。Axolotl 支持 Microsoft Minecraft 正版账户、本地离线账户与第三方 Yggdrasil 认证,包括 LittleSkin 预设和自定义服务器。',
|
||||
'axolotl-site.faq.content.question': 'Axolotl 从哪里获取模组和其他 Minecraft 内容?',
|
||||
'axolotl-site.faq.content.answer':
|
||||
'Axolotl 可以搜索、查看、选择版本、安装、更新和管理来自 Modrinth 与 CurseForge 的内容。受 CurseForge 分发限制的文件可能需要手动下载。',
|
||||
'axolotl-site.faq.download.question': '应该从哪里下载 Axolotl Launcher?',
|
||||
'axolotl-site.faq.download.answer':
|
||||
'请使用本官方网站的下载区域。自动模式使用 Update Server,GitHub 作为备用来源;你也可以在网站设置中手动更换下载源。',
|
||||
'axolotl-marketing.hero.download': 'Axolotl Launcher',
|
||||
'axolotl-marketing.hero.download-for-os': '适用于 {os} 的 Axolotl Launcher',
|
||||
'app-marketing.hero.description':
|
||||
'Axolotl Launcher 是一款免费、开源、无广告的跨平台 Minecraft Java 版启动器,可在同一个客户端中搜索、安装和更新来自 Modrinth 与 CurseForge 的模组、整合包、资源包和光影,并内置 Axolotl 实验室。',
|
||||
'axolotl-marketing.hero.screenshot-alt': 'Axolotl Launcher 主界面。',
|
||||
'axolotl-marketing.highlights.eyebrow': '一个启动器,两个来源',
|
||||
'axolotl-marketing.highlights.title': '管理 Minecraft 内容',
|
||||
'axolotl-marketing.highlights.title-second': '不再绕路',
|
||||
'axolotl-marketing.highlights.description':
|
||||
'搜索 Modrinth 与 CurseForge,查看项目详情、选择版本并安装内容,由启动器处理依赖和更新。',
|
||||
'axolotl-marketing.highlights.ad-free.title': '免费、开源且独立',
|
||||
'axolotl-marketing.highlights.ad-free.description':
|
||||
'基于 GPL-3.0 发布,免费使用且无广告。Axolotl 不是 Modrinth 官方客户端。',
|
||||
'axolotl-marketing.highlights.localized.title': '井然有序的内容管理',
|
||||
'axolotl-marketing.highlights.localized.description':
|
||||
'同时安装和管理整合包与单个项目。部分 CurseForge 文件受分发限制,可能需要手动下载。',
|
||||
'axolotl-marketing.showcase.offline.title': '由你选择账户方式',
|
||||
'axolotl-marketing.showcase.offline.description':
|
||||
'使用 Microsoft 正版账户、本地离线身份,或 Yggdrasil 认证;支持 LittleSkin 预设和自定义服务器。',
|
||||
'axolotl-marketing.showcase.offline.label': '账户',
|
||||
'axolotl-marketing.showcase.themes.title': '每套设备,都有适合的色彩主题',
|
||||
'axolotl-marketing.showcase.themes.description':
|
||||
'在浅色、深色、OLED 和系统模式之间切换,并设置适合自己的强调色、背景和透明效果。',
|
||||
'axolotl-marketing.showcase.themes.label': '个性化',
|
||||
'axolotl-marketing.showcase.translation.title': '启动器里的 Axolotl 实验室',
|
||||
'axolotl-marketing.showcase.translation.description':
|
||||
'直接在 Axolotl 中使用渐变文字生成器、Java 版种子地图和 3D 投影工坊,而不是跳转到外部网页。',
|
||||
'axolotl-marketing.showcase.translation.label': '实验室',
|
||||
'axolotl-marketing.showcase.offline.alt': 'Axolotl Launcher 的添加账户界面。',
|
||||
'axolotl-marketing.showcase.themes.alt': 'Axolotl Launcher 的强调色设置。',
|
||||
'axolotl-marketing.showcase.translation.alt': 'Axolotl Launcher 实验室。',
|
||||
'axolotl-marketing.hero.download-button': '下载 Axolotl',
|
||||
'axolotl-marketing.download.fetching-links': '正在获取下载链接…',
|
||||
'axolotl-marketing.download.links-failed': '暂时无法获取最新的下载链接。',
|
||||
'axolotl-marketing.download.manual-fallback': '前往 GitHub Releases 手动下载',
|
||||
'app-marketing.hero.more-download-options': '更多下载选项',
|
||||
'app-marketing.features.unlike-any-launcher': '与众不同的启动器',
|
||||
'app-marketing.features.youve-used-before': '带来前所未有的体验',
|
||||
'app-marketing.features.mod-management.installed-mods': '已安装模组',
|
||||
'app-marketing.features.mod-management.search-mods': '搜索模组',
|
||||
'app-marketing.features.mod-management.name': '名称',
|
||||
'app-marketing.features.mod-management.version': '版本',
|
||||
'app-marketing.features.mod-management.actions': '操作',
|
||||
'app-marketing.features.mod-management.byAuthor': '作者:{author}',
|
||||
'app-marketing.features.mod-management.title': '高效实例管理',
|
||||
'app-marketing.features.mod-management.description':
|
||||
'创建、导入和批量管理实例;集中管理模组、资源包、光影、文件、世界、截图和日志,以及更新、启动设置与整合包导出。',
|
||||
'app-marketing.features.performance.title': '高性能',
|
||||
'app-marketing.features.performance.description':
|
||||
'Axolotl 以灵敏的界面与轻量的桌面核心保持安静、高效,不打扰你的游戏。',
|
||||
'app-marketing.features.website.title': '从发现到安装',
|
||||
'app-marketing.features.website.description':
|
||||
'从 Modrinth 或 CurseForge 发现内容后,通过项目详情和版本选择直接安装到实例中,并在原处处理依赖和更新。',
|
||||
'app-marketing.features.importing.title': '导入配置',
|
||||
'app-marketing.features.importing.description':
|
||||
'从 PCL2, HMCL 或任意你喜欢的启动器一键导入现有配置,无需手动重建即可继续游玩。',
|
||||
'app-marketing.features.offline.title': '每个世界都用得上的能力',
|
||||
'app-marketing.features.offline.description':
|
||||
'中文搜索和项目翻译、拖拽导入、Java 管理、离线模式与皮肤管理,随时为你准备好。',
|
||||
'app-marketing.features.follow.title': '联机能力',
|
||||
'app-marketing.features.follow.description':
|
||||
'由陶瓦联机强力驱动,一键体验启动器联机功能,方便快捷。',
|
||||
'app-marketing.download.options-title': '下载选项',
|
||||
'axolotl-marketing.download.title': '下载 Axolotl Launcher',
|
||||
'app-marketing.download.description': '桌面应用支持多个平台,请选择适合你的版本。',
|
||||
'app-marketing.download.channel-label': '版本通道',
|
||||
'app-marketing.download.channel-description':
|
||||
'选择稳定版或抢先体验版,切换后会自动更新下载链接。',
|
||||
'app-marketing.download.channel-latest-version': '最新版本:{version}',
|
||||
'app-marketing.download.channel-fetching-version': '正在获取最新版本…',
|
||||
'app-marketing.download.channel-version-unavailable': '暂时无法获取版本信息。',
|
||||
'app-marketing.download.channel-release': 'Release(稳定版)',
|
||||
'app-marketing.download.channel-beta': 'Beta(测试版)',
|
||||
'app-marketing.download.windows': 'Windows',
|
||||
'app-marketing.download.mac': 'macOS',
|
||||
'app-marketing.download.linux': 'Linux',
|
||||
'axolotl-marketing.download.installer': '下载安装程序',
|
||||
'axolotl-marketing.download.appimage': '下载 AppImage',
|
||||
'app-marketing.show-other-packages': '显示其他软件包',
|
||||
'app-marketing.hide-other-packages': '隐藏其他软件包',
|
||||
'app-marketing.not-recommended': '请选择与你的 Linux 发行版匹配的软件包格式。',
|
||||
'app-marketing.download.download-deb': '下载 DEB',
|
||||
'app-marketing.download.download-rpm': '下载 RPM',
|
||||
'app-marketing.download.terms':
|
||||
'Axolotl 是基于 <terms-link>GPL-3.0</terms-link> 发布的自由软件。安装前请阅读<privacy-link>隐私政策</privacy-link>。',
|
||||
'app-marketing.download.linux-disclaimer':
|
||||
'每个版本都会发布 Linux 软件包。请在<issues-link>发布页面</issues-link>查看架构详情;如果你的发行版需要额外配置,也可以<prism-link>报告问题</prism-link>。',
|
||||
'app-marketing.hero.app-screenshot-alt': 'Axolotl Launcher 实例内容预览。',
|
||||
'axolotl-marketing.hero.open-source': 'Tauri v2 · Rust · Vue 3',
|
||||
'axolotl-marketing.demo.one-launcher': '一个启动器,承载每一个世界。',
|
||||
'axolotl-marketing.demo.everything-together': '配置、模组、存档和设置始终井然有序。',
|
||||
'axolotl-marketing.demo.included-mods': '包含的模组',
|
||||
'axolotl-site.navigation.home': 'Axolotl Launcher 首页',
|
||||
'axolotl-site.navigation.primary': '主导航',
|
||||
'axolotl-site.navigation.mobile': '移动端导航',
|
||||
'axolotl-site.navigation.features': '功能',
|
||||
'axolotl-site.navigation.faq': '常见问题',
|
||||
'axolotl-site.navigation.changelog': '更新日志',
|
||||
'axolotl-site.navigation.open-source': '开放源代码',
|
||||
'axolotl-site.navigation.download': '下载',
|
||||
'axolotl-site.navigation.open-settings': '打开显示设置',
|
||||
'axolotl-site.navigation.open-menu': '打开导航',
|
||||
'axolotl-site.navigation.close-menu': '关闭导航',
|
||||
'axolotl-site.navigation.terms': '服务条款',
|
||||
'axolotl-site.navigation.privacy': '隐私政策',
|
||||
'axolotl-site.legal.updated': '最后更新:{updatedAt}',
|
||||
'axolotl-site.footer.developer': 'Axolotl Launcher 开发组',
|
||||
'axolotl-site.community.eyebrow': '社区',
|
||||
'axolotl-site.community.title': '加入我们的社区',
|
||||
'axolotl-site.community.description':
|
||||
'加入官方 QQ 群与 QQ 频道,获取帮助、参与讨论,第一时间了解新版本。',
|
||||
'axolotl-site.community.qq-group': '官方 QQ 群',
|
||||
'axolotl-site.community.qq-group-description': '群号 {number},与其他玩家交流反馈、获取帮助。',
|
||||
'axolotl-site.community.copy-qq-group': '复制群号',
|
||||
'axolotl-site.community.copied-qq-group': '已复制',
|
||||
'axolotl-site.community.qq-channel': 'QQ 频道',
|
||||
'axolotl-site.community.qq-channel-description': '第一时间获取官方公告、版本动态与频道活动。',
|
||||
'axolotl-site.community.qq-channel-join': '加入频道',
|
||||
'axolotl-site.footer.axolotl': 'Axolotl',
|
||||
'axolotl-site.footer.downloads': '下载',
|
||||
'axolotl-site.footer.release-history': '版本历史',
|
||||
'axolotl-site.changelog.seo.title': '更新日志 - Axolotl Launcher',
|
||||
'axolotl-site.changelog.seo.description': '查看 Axolotl Launcher 已公开发布版本的更新内容。',
|
||||
'axolotl-site.changelog.eyebrow': '版本历史',
|
||||
'axolotl-site.changelog.title': '更新日志',
|
||||
'axolotl-site.changelog.description': '查看每个已公开发布版本的功能改进、变更与修复。',
|
||||
'axolotl-site.changelog.loading': '正在核对已发布版本…',
|
||||
'axolotl-site.changelog.error.title': '暂时无法加载更新日志',
|
||||
'axolotl-site.changelog.error.description':
|
||||
'未能获取更新日志。可能是网络连接不畅,或数据源暂时不可达。',
|
||||
'axolotl-site.changelog.retry': '重试',
|
||||
'axolotl-site.changelog.empty': '尚无已公开发布的更新日志。',
|
||||
'axolotl-site.changelog.no-release-notes': '该版本未提供更新说明。',
|
||||
'axolotl-site.changelog.category.added': '新增',
|
||||
'axolotl-site.changelog.category.changed': '变更',
|
||||
'axolotl-site.changelog.category.deprecated': '弃用',
|
||||
'axolotl-site.changelog.category.removed': '移除',
|
||||
'axolotl-site.changelog.category.fixed': '修复',
|
||||
'axolotl-site.changelog.category.security': '安全',
|
||||
'axolotl-site.footer.resources': '资源',
|
||||
'axolotl-site.footer.documentation': '文档',
|
||||
'axolotl-site.footer.report-issue': '报告问题',
|
||||
'axolotl-site.footer.legal': '法律信息',
|
||||
'axolotl-site.footer.copying-guidelines': '复制指南',
|
||||
'axolotl-site.footer.open-source-prefix': 'Axolotl 是',
|
||||
'axolotl-site.footer.open-source-link': '开源软件',
|
||||
'axolotl-site.footer.minecraft-disclaimer':
|
||||
'非 Minecraft 官方服务。未经 Mojang 或 Microsoft 批准,亦与其无关联。',
|
||||
'axolotl-site.footer.cta-eyebrow': '免费、开源,为 Minecraft 而生。',
|
||||
'axolotl-site.footer.cta-title': '你的下一个世界,从这里开始。',
|
||||
'axolotl-site.footer.cta-description':
|
||||
'安装 Axolotl,在一个清爽有序的空间中管理实例、模组与存档。',
|
||||
'axolotl-site.footer.download-now': '立即下载',
|
||||
'axolotl-site.footer.view-github': '前往 GitHub',
|
||||
'axolotl-settings.title': '显示设置',
|
||||
'axolotl-settings.description': '自定义 Axolotl 在此设备上的显示方式与语言。',
|
||||
'axolotl-settings.downloads.title': '下载',
|
||||
'axolotl-settings.downloads.description': '选择 Axolotl Launcher 安装程序的下载来源。',
|
||||
'axolotl-settings.download-source.title': '下载源',
|
||||
'axolotl-settings.download-source.description':
|
||||
'自动模式使用 Update Server,GitHub 作为备用来源。',
|
||||
'axolotl-settings.download-source.auto': '自动选择',
|
||||
'axolotl-settings.download-source.auto.description': '根据浏览器语言和时区选择下载源。',
|
||||
'axolotl-settings.download-source.update-server': 'Update Server',
|
||||
'axolotl-settings.download-source.update-server.description': '通过 Axolotl 官方更新服务下载。',
|
||||
'axolotl-settings.download-source.github': 'GitHub',
|
||||
'axolotl-settings.download-source.github.description': '从官方 GitHub 版本下载。',
|
||||
'axolotl-settings.download-source.current': '当前下载源:{source}',
|
||||
'axolotl-settings.close': '关闭设置',
|
||||
'axolotl-settings.appearance.title': '外观',
|
||||
'axolotl-settings.language.title': '语言',
|
||||
'axolotl-settings.language.description': '选择此网站使用的界面语言。',
|
||||
'axolotl-settings.theme.title': '颜色主题',
|
||||
'axolotl-settings.theme.description': '选择此设备上的 Axolotl 颜色主题。',
|
||||
'axolotl-settings.interface.title': '界面',
|
||||
'axolotl-settings.interface.description': '启用或停用此设备上的视觉效果。',
|
||||
'axolotl-settings.advanced-rendering.title': '高级渲染',
|
||||
'axolotl-settings.advanced-rendering.description': '使用模糊、渐变和增强背景效果。',
|
||||
'axolotl-settings.reduce-motion.title': '减少动态效果',
|
||||
'axolotl-settings.reduce-motion.description': '停用装饰动画和过渡效果。',
|
||||
'axolotl-settings.external-links.title': '在新标签页打开外部链接',
|
||||
'axolotl-settings.external-links.description': '访问其他网站时保留当前下载页面。',
|
||||
'axolotl-settings.storage-note': '偏好设置将保存在此浏览器中。',
|
||||
'axolotl-settings.done': '完成',
|
||||
'axolotl-site.faq.lab.question': '什么是 Axolotl 实验室?',
|
||||
'axolotl-site.faq.lab.answer':
|
||||
'Axolotl 实验室是一个内置的网页工具集合,提供渐变文字生成器、Java 版种子地图、3D 投影工坊、配方生成器、Minecraft 模组翻译工具等功能。内容持续更新中。',
|
||||
'axolotl-site.seo.keywords':
|
||||
'Axolotl Launcher, 美西螈启动器, Minecraft 启动器, Modrinth, CurseForge, Minecraft Java 版, 模组, 整合包, 资源包, 光影, Axolotl 实验室, Axolotl Launcher 下载, Axolotl Launcher 安装, Axolotl Launcher 中文, Axolotl Launcher 开源, Axolotl Launcher 免费, Axolotl Launcher 跨平台, Axolotl Launcher Windows, Axolotl Launcher macOS, Axolotl Launcher Linux',
|
||||
'axolotl-site.faq.project-disclaimer.question':
|
||||
'Axolotl Launcher 和 Axolotl Client 是同一个项目吗?',
|
||||
'axolotl-site.faq.project-disclaimer.answer':
|
||||
'不是。Axolotl Launcher 和 Axolotl Client 是两个独立的项目。Axolotl Launcher 是基于 Modrinth 单体仓库的独立非官方下游启动器,与其他名为 Axolotl Client 的 Minecraft 项目无关联。',
|
||||
},
|
||||
}
|
||||
389
apps/website/src/pages/changelog.vue
Normal file
@ -0,0 +1,389 @@
|
||||
<script setup lang="ts">
|
||||
import { CalendarIcon, HistoryIcon } from '@modrinth/assets'
|
||||
import Accordion from '@modrinth/ui/src/components/base/Accordion.vue'
|
||||
import ButtonStyled from '@modrinth/ui/src/components/base/ButtonStyled.vue'
|
||||
import TagItem from '@modrinth/ui/src/components/base/TagItem.vue'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui/src/composables/i18n.ts'
|
||||
|
||||
type AnnouncementLocale = 'en-US' | 'zh-CN'
|
||||
type AnnouncementChangeType = 'added' | 'changed' | 'deprecated' | 'removed' | 'fixed' | 'security'
|
||||
type LocalizedAnnouncementText = Record<AnnouncementLocale, string>
|
||||
|
||||
type LauncherAnnouncement = {
|
||||
id: string
|
||||
version: string
|
||||
publishedAt: string
|
||||
title: LocalizedAnnouncementText
|
||||
changes: Partial<Record<AnnouncementChangeType, LocalizedAnnouncementText[]>>
|
||||
notes?: LocalizedAnnouncementText
|
||||
externalUrl?: string
|
||||
}
|
||||
|
||||
type AnnouncementCatalog = {
|
||||
updated_at: string
|
||||
announcements: LauncherAnnouncement[]
|
||||
}
|
||||
|
||||
// 每次用户访问时从客户端实时拉取。数据来自 app 前端的公告 catalog
|
||||
// (apps/app-frontend/src/announcements/catalog.ts),由发布 CI 导出并经
|
||||
// Release metadata is served by the website endpoint and does not require a
|
||||
// direct browser request to the GitHub API.
|
||||
const CATALOG_URL = '/api/releases/catalog'
|
||||
const CHANGE_TYPES: readonly AnnouncementChangeType[] = [
|
||||
'added',
|
||||
'changed',
|
||||
'deprecated',
|
||||
'removed',
|
||||
'fixed',
|
||||
'security',
|
||||
]
|
||||
|
||||
const { formatMessage, locale } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
seoTitle: {
|
||||
id: 'axolotl-site.changelog.seo.title',
|
||||
defaultMessage: 'Changelog - Axolotl Launcher',
|
||||
},
|
||||
seoDescription: {
|
||||
id: 'axolotl-site.changelog.seo.description',
|
||||
defaultMessage: 'See what changed in each public Axolotl Launcher release.',
|
||||
},
|
||||
eyebrow: { id: 'axolotl-site.changelog.eyebrow', defaultMessage: 'Release history' },
|
||||
title: { id: 'axolotl-site.changelog.title', defaultMessage: 'Changelog' },
|
||||
description: {
|
||||
id: 'axolotl-site.changelog.description',
|
||||
defaultMessage: 'Browse features, changes, and fixes in every public release.',
|
||||
},
|
||||
loading: {
|
||||
id: 'axolotl-site.changelog.loading',
|
||||
defaultMessage: 'Checking published releases…',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'axolotl-site.changelog.error.title',
|
||||
defaultMessage: 'Changelog is temporarily unavailable',
|
||||
},
|
||||
errorDescription: {
|
||||
id: 'axolotl-site.changelog.error.description',
|
||||
defaultMessage:
|
||||
'We could not fetch the release history. Your network may be unavailable, or the data source is temporarily unreachable.',
|
||||
},
|
||||
retry: { id: 'axolotl-site.changelog.retry', defaultMessage: 'Retry' },
|
||||
empty: {
|
||||
id: 'axolotl-site.changelog.empty',
|
||||
defaultMessage: 'No public release notes are available yet.',
|
||||
},
|
||||
noReleaseNotes: {
|
||||
id: 'axolotl-site.changelog.no-release-notes',
|
||||
defaultMessage: 'No release notes were provided for this version.',
|
||||
},
|
||||
added: { id: 'axolotl-site.changelog.category.added', defaultMessage: 'Added' },
|
||||
changed: { id: 'axolotl-site.changelog.category.changed', defaultMessage: 'Changed' },
|
||||
deprecated: {
|
||||
id: 'axolotl-site.changelog.category.deprecated',
|
||||
defaultMessage: 'Deprecated',
|
||||
},
|
||||
removed: { id: 'axolotl-site.changelog.category.removed', defaultMessage: 'Removed' },
|
||||
fixed: { id: 'axolotl-site.changelog.category.fixed', defaultMessage: 'Fixed' },
|
||||
security: { id: 'axolotl-site.changelog.category.security', defaultMessage: 'Security' },
|
||||
})
|
||||
|
||||
const categoryClasses: Record<AnnouncementChangeType, string> = {
|
||||
added: 'bg-brand-green',
|
||||
changed: 'bg-brand-blue',
|
||||
deprecated: 'bg-brand-orange',
|
||||
removed: 'bg-brand-red',
|
||||
fixed: 'bg-brand-purple',
|
||||
security: 'bg-brand-orange',
|
||||
}
|
||||
|
||||
function getLocalizedText(text: LocalizedAnnouncementText): string {
|
||||
return text[locale.value === 'zh-CN' ? 'zh-CN' : 'en-US']
|
||||
}
|
||||
|
||||
function getAnnouncementChangeTypes(announcement: LauncherAnnouncement): AnnouncementChangeType[] {
|
||||
return CHANGE_TYPES.filter((type) => announcement.changes?.[type]?.length)
|
||||
}
|
||||
|
||||
const {
|
||||
data: announcements,
|
||||
error,
|
||||
status,
|
||||
refresh,
|
||||
} = await useAsyncData(
|
||||
'axolotl-release-catalog',
|
||||
async () => {
|
||||
const catalog = await $fetch<AnnouncementCatalog>(CATALOG_URL, { timeout: 8000 })
|
||||
return catalog.announcements
|
||||
},
|
||||
{ server: false },
|
||||
)
|
||||
|
||||
const isLoading = computed(() => status.value === 'idle' || status.value === 'pending')
|
||||
const seoTitle = computed(() => formatMessage(messages.seoTitle))
|
||||
const seoDescription = computed(() => formatMessage(messages.seoDescription))
|
||||
|
||||
useSeoMeta({
|
||||
title: () => seoTitle.value,
|
||||
description: () => seoDescription.value,
|
||||
ogTitle: () => seoTitle.value,
|
||||
ogDescription: () => seoDescription.value,
|
||||
ogType: 'website',
|
||||
ogUrl: 'https://axlmc.org/changelog',
|
||||
robots: 'index, follow',
|
||||
})
|
||||
|
||||
useHead({
|
||||
link: [{ rel: 'canonical', href: 'https://axlmc.org/changelog' }],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="changelog-page">
|
||||
<header class="changelog-header">
|
||||
<span class="section-eyebrow">{{ formatMessage(messages.eyebrow) }}</span>
|
||||
<h1>{{ formatMessage(messages.title) }}</h1>
|
||||
<p>{{ formatMessage(messages.description) }}</p>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="status-panel m-0 flex items-center justify-center gap-3 rounded-lg border border-surface-5 bg-surface-4 p-8 text-center text-[var(--color-secondary)]"
|
||||
role="status"
|
||||
>
|
||||
<div class="loading-indicator" aria-hidden="true" />
|
||||
{{ formatMessage(messages.loading) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="status-panel error-panel m-0 flex items-center justify-center justify-between gap-3 rounded-lg border border-surface-5 bg-surface-4 p-8 text-left text-center text-[var(--color-secondary)]"
|
||||
role="alert"
|
||||
>
|
||||
<div>
|
||||
<h2>{{ formatMessage(messages.errorTitle) }}</h2>
|
||||
<p>{{ formatMessage(messages.errorDescription) }}</p>
|
||||
</div>
|
||||
<ButtonStyled color="brand" type="outlined">
|
||||
<button type="button" @click="refresh()">{{ formatMessage(messages.retry) }}</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-else-if="!announcements?.length"
|
||||
class="status-panel m-0 flex items-center justify-center gap-3 rounded-lg border border-surface-5 bg-surface-4 p-8 text-center text-[var(--color-secondary)]"
|
||||
>
|
||||
{{ formatMessage(messages.empty) }}
|
||||
</p>
|
||||
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<Accordion
|
||||
v-for="(announcement, index) in announcements"
|
||||
:key="announcement.id"
|
||||
:open-by-default="index === 0"
|
||||
class="overflow-hidden rounded-lg border border-surface-5 bg-surface-4"
|
||||
button-class="group flex w-full cursor-pointer items-center gap-4 border-0 bg-transparent px-5 py-4 text-left"
|
||||
>
|
||||
<template #title>
|
||||
<div class="announcement-heading flex min-w-0 flex-1 items-center justify-between gap-4">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<h2>{{ getLocalizedText(announcement.title) }}</h2>
|
||||
<TagItem>{{ announcement.version }}</TagItem>
|
||||
</div>
|
||||
<div
|
||||
class="flex shrink-0 items-center gap-[0.35rem] text-[0.8125rem] text-[var(--color-secondary)]"
|
||||
>
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<time :datetime="announcement.publishedAt">
|
||||
{{ announcement.publishedAt }}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="border-t border-surface-5 bg-surface-3 px-[1.25rem] pb-2">
|
||||
<p
|
||||
v-if="
|
||||
!announcement.changes ||
|
||||
CHANGE_TYPES.every((type) => !announcement.changes?.[type]?.length)
|
||||
"
|
||||
class="m-0 pb-2 pt-4 leading-[1.6] text-[var(--color-secondary)]"
|
||||
>
|
||||
{{ formatMessage(messages.noReleaseNotes) }}
|
||||
</p>
|
||||
<section
|
||||
v-for="(type, typeIndex) in getAnnouncementChangeTypes(announcement)"
|
||||
:key="type"
|
||||
class="change-group"
|
||||
:class="{ 'first-change-group': typeIndex === 0 }"
|
||||
>
|
||||
<h3>
|
||||
<span :class="categoryClasses[type]" aria-hidden="true" />
|
||||
{{ formatMessage(messages[type]) }}
|
||||
</h3>
|
||||
<ul>
|
||||
<li v-for="change in announcement.changes[type]" :key="change">
|
||||
{{ getLocalizedText(change) }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex items-center justify-center gap-2 text-sm text-[var(--color-secondary)]">
|
||||
<HistoryIcon aria-hidden="true" />
|
||||
<a href="https://github.com/Mystic-Stars/Axolotl/releases" target="_blank" rel="noopener">
|
||||
GitHub Releases
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.changelog-page {
|
||||
width: min(52rem, calc(100% - 2rem));
|
||||
margin: 0 auto;
|
||||
padding: 4rem 0 5rem;
|
||||
}
|
||||
|
||||
.changelog-header {
|
||||
max-width: 40rem;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-title-row {
|
||||
h2 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: var(--color-contrast);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-date {
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.change-group {
|
||||
display: grid;
|
||||
grid-template-columns: 7rem minmax(0, 1fr);
|
||||
gap: 1.25rem;
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid var(--surface-5);
|
||||
|
||||
&.first-change-group {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
color: var(--color-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
|
||||
span {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
color: var(--color-base);
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
.error-panel {
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--color-contrast);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid var(--surface-5);
|
||||
border-top-color: var(--color-brand);
|
||||
border-radius: 50%;
|
||||
animation: spin 700ms linear infinite;
|
||||
}
|
||||
|
||||
.changelog-footer {
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.changelog-page {
|
||||
padding: 2.5rem 0 3rem;
|
||||
}
|
||||
|
||||
.changelog-header h1 {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.announcement-heading,
|
||||
.error-panel,
|
||||
.change-group {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.change-group {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
2229
apps/website/src/pages/index.vue
Normal file
288
apps/website/src/pages/privacy.vue
Normal file
@ -0,0 +1,288 @@
|
||||
<script setup lang="ts">
|
||||
import { useVIntl } from '@modrinth/ui/src/composables/i18n.ts'
|
||||
|
||||
import LegalDocument from '~/components/ui/LegalDocument.vue'
|
||||
|
||||
const { locale } = useVIntl()
|
||||
const isChinese = computed(() => locale.value === 'zh-CN')
|
||||
|
||||
const seoTitle = computed(() =>
|
||||
isChinese.value ? '隐私政策 - Axolotl Launcher' : 'Privacy Policy - Axolotl Launcher',
|
||||
)
|
||||
const seoDescription = computed(() =>
|
||||
isChinese.value
|
||||
? '了解 Axolotl Launcher 官方网站和桌面应用如何处理数据。'
|
||||
: 'Learn how the Axolotl Launcher official website and desktop application handle data.',
|
||||
)
|
||||
|
||||
useSeoMeta({
|
||||
title: () => seoTitle.value,
|
||||
description: () => seoDescription.value,
|
||||
ogTitle: () => seoTitle.value,
|
||||
ogDescription: () => seoDescription.value,
|
||||
ogType: 'website',
|
||||
ogUrl: 'https://axlmc.org/privacy',
|
||||
robots: 'index, follow',
|
||||
})
|
||||
|
||||
useHead({
|
||||
link: [{ rel: 'canonical', href: 'https://axlmc.org/privacy' }],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LegalDocument
|
||||
v-if="isChinese"
|
||||
:key="`zh-${locale}`"
|
||||
eyebrow="隐私政策"
|
||||
title="隐私政策"
|
||||
description="本政策说明 Axolotl Launcher 官方网站和桌面应用处理数据的方式,以及第三方服务可能接收的信息。"
|
||||
updated-at="2026 年 8 月 14 日"
|
||||
>
|
||||
<h2>1. 适用范围</h2>
|
||||
<p>
|
||||
本政策适用于 www.axlmc.org 官方网站和 Axolotl Launcher
|
||||
桌面应用。通过启动器访问的第三方网站、API、认证服务、服务器及内容下载地址,适用对应第三方自己的隐私政策。
|
||||
</p>
|
||||
|
||||
<h2>2. 官方网站处理的数据</h2>
|
||||
<p>网站无需注册账户。为保存你的显示和下载偏好,浏览器会在本地保存:</p>
|
||||
<ul>
|
||||
<li>语言偏好 Cookie(<code>locale</code>);</li>
|
||||
<li>主题、高级渲染、减少动态效果和外部链接打开方式;</li>
|
||||
<li>下载源选择(自动、Update Server 或 GitHub)。</li>
|
||||
</ul>
|
||||
<p>
|
||||
这些偏好主要保存在 Cookie 或浏览器 localStorage 中,你可通过浏览器设置清除。网站不使用广告
|
||||
Cookie。网站接入<a href="https://howxm.com" target="_blank" rel="noopener">浩客(Howxm)</a>
|
||||
作为第三方用户体验分析和反馈收集服务;其可根据服务配置处理与网站访问、交互、设备或浏览器及网络请求相关的信息,以展示问卷、收集评价和改进网站体验。
|
||||
</p>
|
||||
<p>
|
||||
网站托管和网络基础设施提供者可能为保障访问、安全和故障排查而处理常规请求信息,例如 IP
|
||||
地址、访问时间、请求路径、User-Agent 和错误日志。此类处理受对应提供者政策约束。
|
||||
</p>
|
||||
|
||||
<h2>3. 桌面应用处理的数据</h2>
|
||||
<p>启动器会在你的设备本地处理并保存运行所需的数据,包括:</p>
|
||||
<ul>
|
||||
<li>应用设置、界面偏好和下载配置;</li>
|
||||
<li>Minecraft 实例、游戏版本、模组及其他内容的元数据;</li>
|
||||
<li>账户资料、登录会话、离线账户信息和自定义认证服务地址;</li>
|
||||
<li>游戏日志、崩溃信息、缓存、下载记录及更新状态。</li>
|
||||
</ul>
|
||||
<p>
|
||||
这些数据用于登录、启动游戏、管理内容、诊断故障和提供你主动使用的功能。启动器会在首次运行或功能升级后询问是否允许匿名遥测;未确认或关闭时不会发送遥测。允许后,启动器每天最多发送一次匿名活跃信号,用于统计选择加入遥测的匿名安装量、DAU、WAU、MAU。不会自动上传完整的启动器日志或 Minecraft
|
||||
令牌、账户凭据或原始随机安装标识。
|
||||
</p>
|
||||
<p>
|
||||
遥测使用本地生成的随机安装标识;服务端仅保存经密钥 HMAC-SHA256
|
||||
处理后的不可逆标识。你可随时在“隐私与安全”中关闭遥测;关闭后会停止后续采集并清空本地待发送队列,但已经上传的数据仍按下述保留周期清理。Discord
|
||||
Rich Presence 是独立的本地开关,不属于遥测。
|
||||
</p>
|
||||
|
||||
<h2>4. 与第三方共享和传输</h2>
|
||||
<p>
|
||||
当你使用登录、搜索、下载、更新、皮肤或联机等功能时,启动器会按你的操作直接连接第三方服务。请求可能包含服务完成操作所需的账户令牌、用户标识、项目或文件标识、IP
|
||||
地址、设备和网络请求信息。可能涉及的服务包括:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Microsoft 和 Minecraft 的登录、资料及游戏服务;</li>
|
||||
<li>你选择的第三方 Yggdrasil 认证服务;</li>
|
||||
<li>Modrinth、CurseForge 及内容作者提供的 API 和下载地址;</li>
|
||||
<li>Update Server、GitHub 及其他用于版本检查、更新或文件分发的服务;</li>
|
||||
<li>用于官网用户体验分析和反馈收集的浩客(Howxm);</li>
|
||||
<li>用于接收和保存选择加入的匿名遥测数据的 Cloudflare Workers 和 D1;</li>
|
||||
<li>你主动连接的 Minecraft 服务器或其他外部链接。</li>
|
||||
</ul>
|
||||
<p>项目维护者不会出售你的个人信息。第三方如何保存和使用请求数据,由其各自的隐私政策决定。</p>
|
||||
|
||||
<h2>5. 数据保存与删除</h2>
|
||||
<p>
|
||||
网站偏好保存在你的浏览器中;启动器数据主要保存在你的设备中。你可以清除浏览器站点数据,或在启动器内移除账户、实例和缓存。卸载应用不一定自动删除全部数据目录,请在确认备份需求后手动清理残留文件。
|
||||
</p>
|
||||
<p>
|
||||
遥测中的匿名日活记录保留 35 天;每日汇总和匿名安装哈希会长期保留,用于历史趋势和累计安装统计。项目不会在遥测数据库中保存请求
|
||||
IP。Cloudflare
|
||||
作为基础设施提供者仍可能按其自身政策处理网络请求所必需的信息。
|
||||
</p>
|
||||
<p>
|
||||
第三方服务保存的数据须通过对应服务的账户设置或隐私渠道管理。项目维护者无法代你访问、导出或删除第三方持有的数据。
|
||||
</p>
|
||||
|
||||
<h2>6. 数据安全</h2>
|
||||
<p>
|
||||
我们通过开源审查、系统权限和安全更新等方式降低风险,但无法保证绝对安全。请从官方渠道下载软件、及时更新、保护系统账户,不要公开登录令牌、完整日志或包含个人信息的截图。
|
||||
</p>
|
||||
|
||||
<h2>7. 未成年人</h2>
|
||||
<p>
|
||||
未成年人应在监护人指导下使用本项目,并遵守所在地区的年龄要求及 Minecraft、Microsoft
|
||||
和其他相关服务的规则。请勿通过公开反馈渠道提交不必要的个人信息。
|
||||
</p>
|
||||
|
||||
<h2>8. 政策变更</h2>
|
||||
<p>
|
||||
本政策可能因功能、第三方服务或法律要求变化而更新。新版本将在本页面发布,并标明最后更新日期。重大变化会尽量通过官方网站、GitHub
|
||||
仓库或版本说明告知。
|
||||
</p>
|
||||
|
||||
<h2>9. 联系方式</h2>
|
||||
<p>
|
||||
如有隐私问题或发现安全风险,请通过
|
||||
<a href="https://github.com/Mystic-Stars/Axolotl/issues" target="_blank" rel="noopener"
|
||||
>GitHub Issues</a
|
||||
>
|
||||
联系项目维护者。请勿在公开 Issue 中提交账户令牌、密码、完整日志或其他敏感信息。也可加入官方 QQ
|
||||
群 737601250 咨询一般问题。
|
||||
</p>
|
||||
</LegalDocument>
|
||||
<LegalDocument
|
||||
v-else
|
||||
:key="`en-${locale}`"
|
||||
eyebrow="Privacy Policy"
|
||||
title="Privacy Policy"
|
||||
description="This policy explains how the Axolotl Launcher official website and desktop application handle data, and what information third-party services may receive."
|
||||
updated-at="August 14, 2026"
|
||||
>
|
||||
<h2>1. Scope</h2>
|
||||
<p>
|
||||
This policy applies to the www.axlmc.org official website and the Axolotl Launcher desktop
|
||||
application. Third-party websites, APIs, authentication services, servers, and content
|
||||
download addresses accessed through the launcher are governed by their own privacy policies.
|
||||
</p>
|
||||
|
||||
<h2>2. Data Processed by the Official Website</h2>
|
||||
<p>
|
||||
The website does not require an account. To remember your display and download preferences,
|
||||
your browser stores locally:
|
||||
</p>
|
||||
<ul>
|
||||
<li>A language preference cookie (<code>locale</code>);</li>
|
||||
<li>Theme, advanced rendering, reduced motion, and external-link behavior;</li>
|
||||
<li>Your download-source selection (automatic, Update Server, or GitHub).</li>
|
||||
</ul>
|
||||
<p>
|
||||
These preferences are mainly stored in cookies or browser localStorage and can be cleared
|
||||
through your browser settings. The website uses no advertising cookies. It integrates
|
||||
<a href="https://howxm.com" target="_blank" rel="noopener">Howxm</a> as a third-party
|
||||
user-experience analytics and feedback collection service. Depending on its service
|
||||
configuration, Howxm may process information related to website visits, interactions, devices
|
||||
or browsers, and network requests to display surveys, collect ratings, and improve the website
|
||||
experience.
|
||||
</p>
|
||||
<p>
|
||||
Website hosting and network infrastructure providers may process routine request information
|
||||
such as IP addresses, access times, request paths, User-Agent strings, and error logs to
|
||||
ensure availability, security, and troubleshooting. Such processing is subject to the
|
||||
corresponding providers' policies.
|
||||
</p>
|
||||
|
||||
<h2>3. Data Processed by the Desktop Application</h2>
|
||||
<p>
|
||||
The launcher processes and stores the data needed to run locally on your device, including:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Application settings, interface preferences, and download configuration;</li>
|
||||
<li>Metadata for Minecraft instances, game versions, mods, and other content;</li>
|
||||
<li>
|
||||
Account profiles, login sessions, offline account information, and custom authentication
|
||||
service addresses;
|
||||
</li>
|
||||
<li>Game logs, crash information, caches, download records, and update status.</li>
|
||||
</ul>
|
||||
<p>
|
||||
This data is used for signing in, launching the game, managing content, diagnosing faults, and
|
||||
providing features you actively use. The launcher asks whether to allow anonymous telemetry on
|
||||
first use or after a relevant consent update. No telemetry is sent before confirmation or
|
||||
while it is disabled. When enabled, the launcher sends at most one anonymous daily activity
|
||||
signal. This is used to count opted-in anonymous installations, DAU, WAU, and MAU. Full
|
||||
launcher or Minecraft logs, Minecraft tokens, account credentials, and the original random
|
||||
installation identifier are not uploaded automatically.
|
||||
</p>
|
||||
<p>
|
||||
Telemetry uses a random installation identifier generated locally; the service stores only an
|
||||
irreversible HMAC-SHA256 value produced with a server secret. You can disable telemetry at any time
|
||||
under Privacy & security. Disabling it stops future collection and clears the local pending
|
||||
queue, while previously uploaded records are deleted under the retention periods below.
|
||||
Discord Rich Presence is an independent local setting and is not telemetry.
|
||||
</p>
|
||||
|
||||
<h2>4. Sharing and Transfers with Third Parties</h2>
|
||||
<p>
|
||||
When you use features such as sign-in, search, download, update, skins, or multiplayer, the
|
||||
launcher connects directly to third-party services based on your actions. Requests may include
|
||||
the account tokens, user identifiers, project or file identifiers, IP addresses, device, and
|
||||
network request information needed to complete the operation. Services that may be involved
|
||||
include:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Microsoft and Minecraft sign-in, profile, and game services;</li>
|
||||
<li>Third-party Yggdrasil authentication services you choose;</li>
|
||||
<li>APIs and download addresses provided by Modrinth, CurseForge, and content authors;</li>
|
||||
<li>
|
||||
Update Server, GitHub, and other services used for version checks, updates, or file
|
||||
distribution;
|
||||
</li>
|
||||
<li>Howxm for official-website user-experience analytics and feedback collection;</li>
|
||||
<li>
|
||||
Cloudflare Workers and D1 for opted-in anonymous telemetry ingestion and storage;
|
||||
</li>
|
||||
<li>Minecraft servers or other external links you connect to.</li>
|
||||
</ul>
|
||||
<p>
|
||||
The project maintainers do not sell your personal information. How third parties store and use
|
||||
request data is determined by their respective privacy policies.
|
||||
</p>
|
||||
|
||||
<h2>5. Data Retention and Deletion</h2>
|
||||
<p>
|
||||
Website preferences are stored in your browser; launcher data is mainly stored on your device.
|
||||
You can clear browser site data or remove accounts, instances, and caches inside the launcher.
|
||||
Uninstalling the application does not necessarily delete all data directories automatically;
|
||||
after confirming your backup needs, manually clean up any leftover files.
|
||||
</p>
|
||||
<p>
|
||||
Anonymous daily-active records are retained for 35 days; daily totals and anonymous installation hashes are retained long-term for
|
||||
historical trends and cumulative installation counts. The project does not store request IP
|
||||
addresses in the telemetry database. Cloudflare, as the infrastructure provider, may still process information
|
||||
required to deliver network requests under its own policies.
|
||||
</p>
|
||||
<p>
|
||||
Data stored by third-party services must be managed through the corresponding service's
|
||||
account settings or privacy channels. The project maintainers cannot access, export, or delete
|
||||
data held by third parties on your behalf.
|
||||
</p>
|
||||
|
||||
<h2>6. Data Security</h2>
|
||||
<p>
|
||||
We reduce risk through open-source review, system permissions, and security updates, but
|
||||
cannot guarantee absolute security. Download software from official channels, update
|
||||
regularly, protect your system accounts, and do not publicly share login tokens, full logs, or
|
||||
screenshots containing personal information.
|
||||
</p>
|
||||
|
||||
<h2>7. Minors</h2>
|
||||
<p>
|
||||
Minors should use this project under the guidance of a guardian and comply with the age
|
||||
requirements of their region as well as the rules of Minecraft, Microsoft, and other related
|
||||
services. Do not submit unnecessary personal information through public feedback channels.
|
||||
</p>
|
||||
|
||||
<h2>8. Changes to This Policy</h2>
|
||||
<p>
|
||||
This policy may be updated as features, third-party services, or legal requirements change.
|
||||
New versions will be published on this page with a last-updated date. Significant changes will
|
||||
be announced through the official website, the GitHub repository, or release notes when
|
||||
possible.
|
||||
</p>
|
||||
|
||||
<h2>9. Contact</h2>
|
||||
<p>
|
||||
For privacy questions or security concerns, contact the project maintainers via
|
||||
<a href="https://github.com/Mystic-Stars/Axolotl/issues" target="_blank" rel="noopener"
|
||||
>GitHub Issues</a
|
||||
>. Do not submit account tokens, passwords, full logs, or other sensitive information in
|
||||
public issues. You may also join the official QQ group 737601250 for general questions.
|
||||
</p>
|
||||
</LegalDocument>
|
||||
</template>
|
||||
323
apps/website/src/pages/terms.vue
Normal file
@ -0,0 +1,323 @@
|
||||
<script setup lang="ts">
|
||||
import { useVIntl } from '@modrinth/ui/src/composables/i18n.ts'
|
||||
|
||||
import LegalDocument from '~/components/ui/LegalDocument.vue'
|
||||
|
||||
const { locale } = useVIntl()
|
||||
const isChinese = computed(() => locale.value === 'zh-CN')
|
||||
|
||||
const seoTitle = computed(() =>
|
||||
isChinese.value ? '服务条款 - Axolotl Launcher' : 'Terms of Service - Axolotl Launcher',
|
||||
)
|
||||
const seoDescription = computed(() =>
|
||||
isChinese.value
|
||||
? '使用 Axolotl Launcher 官方网站及桌面应用前,请阅读本服务条款。'
|
||||
: 'Read these terms of service before using the Axolotl Launcher website and desktop application.',
|
||||
)
|
||||
|
||||
useSeoMeta({
|
||||
title: () => seoTitle.value,
|
||||
description: () => seoDescription.value,
|
||||
ogTitle: () => seoTitle.value,
|
||||
ogDescription: () => seoDescription.value,
|
||||
ogType: 'website',
|
||||
ogUrl: 'https://axlmc.org/terms',
|
||||
robots: 'index, follow',
|
||||
})
|
||||
|
||||
useHead({
|
||||
link: [{ rel: 'canonical', href: 'https://axlmc.org/terms' }],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LegalDocument
|
||||
v-if="isChinese"
|
||||
:key="`zh-${locale}`"
|
||||
eyebrow="服务条款"
|
||||
title="服务条款"
|
||||
description="本条款说明 Axolotl Launcher 各部分适用的开源许可证、官方服务规则及第三方权利边界。"
|
||||
updated-at="2026 年 8 月 2 日"
|
||||
>
|
||||
<h2>1. 适用范围与文件效力</h2>
|
||||
<p>
|
||||
本条款适用于 Axolotl Launcher
|
||||
官方网站、官方发布的桌面应用及项目维护者提供的相关服务。软件的复制、修改、分发和网络部署,以仓库中对应文件的许可证与复制声明为准;本页面只是中文说明,不替代、修改或限制原始许可证。
|
||||
</p>
|
||||
<p>
|
||||
根据 GPL-3.0 和
|
||||
AGPL-3.0,仅接收或运行未经修改的软件副本不要求你接受许可证。只有在复制、修改、传播、分发软件,或以适用
|
||||
AGPL 的修改版本向网络用户提供服务时,许可证中的对应条件才适用。
|
||||
</p>
|
||||
|
||||
<h2>2. 仓库采用的许可证</h2>
|
||||
<p>本仓库没有一份覆盖全部文件的统一许可证。不同部分采用不同许可:</p>
|
||||
<ul>
|
||||
<li>
|
||||
官方网站源代码采用
|
||||
<a
|
||||
href="https://github.com/Mystic-Stars/Axolotl/blob/main/apps/website/LICENSE"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>GNU Affero General Public License Version 3 only(AGPL-3.0-only)</a
|
||||
>;
|
||||
</li>
|
||||
<li>
|
||||
桌面应用相关 package 采用
|
||||
<a
|
||||
href="https://github.com/Mystic-Stars/Axolotl/blob/main/apps/app-frontend/LICENSE"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>GNU General Public License Version 3 only(GPL-3.0-only)</a
|
||||
>;
|
||||
</li>
|
||||
<li>
|
||||
其他 package 保留各自的上游许可证,应查看对应目录中的 <code>LICENSE</code>、<code
|
||||
>COPYING.md</code
|
||||
>
|
||||
和源码声明;
|
||||
</li>
|
||||
<li>
|
||||
第三方代码、数据、图标、图片、商标及其他素材可能采用单独许可,不因被收录在本仓库中而自动改用
|
||||
GPL 或 AGPL。
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. 你的开源软件权利</h2>
|
||||
<p>
|
||||
在遵守适用许可证的前提下,你可以运行、研究、复制、修改和再分发受 GPL-3.0-only 或 AGPL-3.0-only
|
||||
保护的代码,也可以收费分发。项目维护者不会通过本服务条款对这些许可证已经授予的权利增加限制。
|
||||
</p>
|
||||
<p>
|
||||
复制或分发时通常需要保留版权、许可证和无担保声明;分发修改版本时需要明确标注修改及日期,并按对应许可证向接收者提供完整许可权利与相应源代码。分发目标代码时,应按许可证规定提供可机器读取的对应源代码。不得对接收者行使许可证权利增加额外限制。
|
||||
</p>
|
||||
<p>
|
||||
若你修改采用 AGPL-3.0-only 的网站代码,并让用户通过网络与修改版本交互,应按 AGPL 第 13
|
||||
条向这些用户显著提供免费获取该版本对应源代码的机会。具体义务以许可证原文为准。
|
||||
</p>
|
||||
|
||||
<h2>4. 品牌、商标与受限素材</h2>
|
||||
<p>
|
||||
开源软件许可证不自动授予商标权。Axolotl Launcher 是独立的非官方项目,与
|
||||
Mojang、Microsoft、Rinth, Inc.、Modrinth、CurseForge 或 MinecraftSearch
|
||||
不存在隶属、认可或背书关系。相关名称和商标归各自权利人所有。
|
||||
</p>
|
||||
<p>
|
||||
Modrinth 商标、徽标、封面和其他受限品牌素材不属于 Axolotl Launcher 品牌。网站复制声明列出的
|
||||
Modrinth 品牌元素未经 Rinth, Inc.
|
||||
明确书面许可不得使用。外部徽标须遵守各权利人的品牌规范。使用或再分发其他第三方图片、图标、数据和素材前,也应核对对应声明。
|
||||
</p>
|
||||
|
||||
<h2>5. 特别归属与第三方许可</h2>
|
||||
<p>
|
||||
桌面启动器的中文内容搜索包含来自 Plain Craft Launcher 的未修改
|
||||
<code>WikiEntries.txt</code> 快照,该文件采用 PCL Limited Distribution License,并包含由 MC
|
||||
百科贡献的中文项目名称信息。种子地图功能还包含 MIT 许可的 cubiomes,以及权利归
|
||||
MinecraftSearch、Mojang 或各素材创作者所有的图像。它们不适用 Axolotl 桌面代码的 GPL-3.0-only
|
||||
授权,具体范围以仓库复制声明为准。
|
||||
</p>
|
||||
|
||||
<h2>6. 官方网站与服务的使用</h2>
|
||||
<p>
|
||||
使用项目维护者运营的官方网站、下载源、反馈渠道或其他基础设施时,你应遵守适用法律及对应第三方平台规则,不得攻击、干扰、滥用基础设施,不得实施欺诈或侵害他人权利。这些服务规则只约束官方服务的使用,不限制
|
||||
GPL 或 AGPL 授予你对软件副本的权利。
|
||||
</p>
|
||||
<p>
|
||||
启动器会按你的操作连接 Microsoft、Minecraft、Modrinth、CurseForge、GitHub、Update
|
||||
Server、自定义 Yggdrasil
|
||||
服务及内容作者提供的下载地址。第三方服务、游戏内容和文件受其各自条款、隐私政策、许可证、可用性及地区限制约束;项目维护者不替第三方提供担保。
|
||||
</p>
|
||||
|
||||
<h2>7. 无担保</h2>
|
||||
<p>
|
||||
按照 GPL-3.0 和 AGPL-3.0 第 15
|
||||
条,在适用法律允许的范围内,软件按“现状”提供,不附带任何明示或默示担保,包括适销性和特定用途适用性的默示担保。软件质量与性能风险由使用者承担;软件存在缺陷时,必要的维护、修复或更正费用由使用者承担,除非另有书面约定。
|
||||
</p>
|
||||
|
||||
<h2>8. 责任限制</h2>
|
||||
<p>
|
||||
按照 GPL-3.0 和 AGPL-3.0 第 16
|
||||
条,除适用法律要求或另有书面约定外,版权人以及依许可证修改或分发软件的其他主体,不对因使用或无法使用软件产生的一般、特殊、附带或后果性损害负责,包括数据丢失、数据不准确、第三方损失或软件无法与其他程序协同工作。若当地法律不允许完整排除责任,则按许可证第
|
||||
17 条及当地法律处理。
|
||||
</p>
|
||||
|
||||
<h2>9. 条款与许可证更新</h2>
|
||||
<p>
|
||||
本服务条款可随官方服务、第三方平台或法律要求更新。更新不会追溯撤销已经依适用开源许可证授予的权利,也不会把标注为“Version
|
||||
3
|
||||
only”的代码自动改为后续许可证版本。任何代码许可变更必须由相应权利人依法作出,并体现在仓库文件中。
|
||||
</p>
|
||||
|
||||
<h2>10. 联系与完整文本</h2>
|
||||
<p>
|
||||
许可范围存在疑问时,请先查看仓库根目录及对应 package 的 <code>COPYING.md</code>、<code
|
||||
>LICENSE</code
|
||||
>
|
||||
和源码声明。如需进一步确认,请通过
|
||||
<a href="https://github.com/Mystic-Stars/Axolotl/issues" target="_blank" rel="noopener"
|
||||
>GitHub Issues</a
|
||||
>
|
||||
联系项目维护者,或加入官方 QQ 群 737601250。本页面不是法律意见。
|
||||
</p>
|
||||
</LegalDocument>
|
||||
<LegalDocument
|
||||
v-else
|
||||
:key="`en-${locale}`"
|
||||
eyebrow="Terms of Service"
|
||||
title="Terms of Service"
|
||||
description="These terms explain the open-source licenses that apply to each part of Axolotl Launcher, the rules for official services, and the boundaries of third-party rights."
|
||||
updated-at="August 2, 2026"
|
||||
>
|
||||
<h2>1. Scope and Effect of This Document</h2>
|
||||
<p>
|
||||
These terms apply to the official Axolotl Launcher website, the officially published desktop
|
||||
application, and related services operated by the project maintainers. Copying, modification,
|
||||
distribution, and network deployment of the software are governed by the licenses and copying
|
||||
notices in the corresponding files of the repository; this page is a plain-language
|
||||
explanation and does not replace, modify, or restrict the original licenses.
|
||||
</p>
|
||||
<p>
|
||||
Under GPL-3.0 and AGPL-3.0, merely receiving or running an unmodified copy of the software
|
||||
does not require you to accept the license. The relevant conditions of the license apply only
|
||||
when you copy, modify, propagate, or distribute the software, or when you provide a modified
|
||||
version governed by the AGPL to users over a network.
|
||||
</p>
|
||||
|
||||
<h2>2. Licenses Used in This Repository</h2>
|
||||
<p>
|
||||
This repository has no single license covering all files. Different parts use different
|
||||
licenses:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
The official website source code is licensed under the
|
||||
<a
|
||||
href="https://github.com/Mystic-Stars/Axolotl/blob/main/apps/website/LICENSE"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>GNU Affero General Public License Version 3 only (AGPL-3.0-only)</a
|
||||
>;
|
||||
</li>
|
||||
<li>
|
||||
The desktop application packages are licensed under the
|
||||
<a
|
||||
href="https://github.com/Mystic-Stars/Axolotl/blob/main/apps/app-frontend/LICENSE"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>GNU General Public License Version 3 only (GPL-3.0-only)</a
|
||||
>;
|
||||
</li>
|
||||
<li>
|
||||
Other packages retain their upstream licenses; check the <code>LICENSE</code>,
|
||||
<code>COPYING.md</code>, and source-code notices in the corresponding directories;
|
||||
</li>
|
||||
<li>
|
||||
Third-party code, data, icons, images, trademarks, and other assets may use separate
|
||||
licenses and do not automatically become GPL or AGPL by being included in this repository.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Your Rights Under the Open-Source Licenses</h2>
|
||||
<p>
|
||||
Subject to the applicable license, you may run, study, copy, modify, and redistribute code
|
||||
protected by GPL-3.0-only or AGPL-3.0-only, including for a fee. These terms do not add
|
||||
restrictions to the rights already granted by those licenses.
|
||||
</p>
|
||||
<p>
|
||||
When copying or distributing, you usually need to retain copyright, license, and no-warranty
|
||||
notices; when distributing modified versions, you must clearly mark the modifications and
|
||||
their dates and provide recipients with the full license rights and corresponding source code
|
||||
under the applicable license. When distributing object code, provide the corresponding
|
||||
machine-readable source code as required by the license. You may not impose additional
|
||||
restrictions on recipients' exercise of their license rights.
|
||||
</p>
|
||||
<p>
|
||||
If you modify the AGPL-3.0-only website code and let users interact with the modified version
|
||||
over a network, you must offer those users prominent access to the corresponding source code
|
||||
of that version free of charge, as required by Section 13 of the AGPL. The license text takes
|
||||
precedence over this summary.
|
||||
</p>
|
||||
|
||||
<h2>4. Branding, Trademarks, and Restricted Assets</h2>
|
||||
<p>
|
||||
Open-source licenses do not automatically grant trademark rights. Axolotl Launcher is an
|
||||
independent, unofficial project with no affiliation, endorsement, or sponsorship from Mojang,
|
||||
Microsoft, Rinth, Inc., Modrinth, CurseForge, or MinecraftSearch. The related names and
|
||||
trademarks belong to their respective owners.
|
||||
</p>
|
||||
<p>
|
||||
Modrinth trademarks, logos, covers, and other restricted brand assets are not part of the
|
||||
Axolotl Launcher brand. Modrinth brand elements listed in the repository's copying notices
|
||||
must not be used without the explicit written permission of Rinth, Inc. External logos must
|
||||
follow each owner's brand guidelines. Before using or redistributing other third-party images,
|
||||
icons, data, and assets, check the corresponding notices.
|
||||
</p>
|
||||
|
||||
<h2>5. Specific Attributions and Third-Party Licenses</h2>
|
||||
<p>
|
||||
The desktop launcher's Chinese content search includes an unmodified
|
||||
<code>WikiEntries.txt</code> snapshot from Plain Craft Launcher, distributed under the PCL
|
||||
Limited Distribution License and containing Chinese project-name information contributed by MC
|
||||
Wiki. The seed-map feature also includes MIT-licensed cubiomes and images owned by
|
||||
MinecraftSearch, Mojang, or the respective asset creators. These are not covered by the
|
||||
GPL-3.0-only license of Axolotl's desktop code; the exact scope is defined by the repository's
|
||||
copying notices.
|
||||
</p>
|
||||
|
||||
<h2>6. Use of the Official Website and Services</h2>
|
||||
<p>
|
||||
When using the official website, download sources, feedback channels, or other infrastructure
|
||||
operated by the project maintainers, you must comply with applicable law and the rules of the
|
||||
corresponding third-party platforms, and must not attack, interfere with, or abuse the
|
||||
infrastructure, commit fraud, or infringe the rights of others. These service rules only
|
||||
govern the use of official services and do not limit the rights granted to you under GPL or
|
||||
AGPL.
|
||||
</p>
|
||||
<p>
|
||||
The launcher connects to Microsoft, Minecraft, Modrinth, CurseForge, GitHub, the Update
|
||||
Server, custom Yggdrasil services, and download addresses provided by content authors based on
|
||||
your actions. Third-party services, game content, and files are subject to their own terms,
|
||||
privacy policies, licenses, availability, and regional restrictions; the project maintainers
|
||||
do not warrant third parties.
|
||||
</p>
|
||||
|
||||
<h2>7. No Warranty</h2>
|
||||
<p>
|
||||
Under Section 15 of GPL-3.0 and AGPL-3.0, to the extent permitted by applicable law, the
|
||||
software is provided "as is" without any express or implied warranty, including implied
|
||||
warranties of merchantability and fitness for a particular purpose. You assume the risk of the
|
||||
software's quality and performance; if the software proves defective, you assume the cost of
|
||||
all necessary servicing, repair, or correction, unless otherwise agreed in writing.
|
||||
</p>
|
||||
|
||||
<h2>8. Limitation of Liability</h2>
|
||||
<p>
|
||||
Under Section 16 of GPL-3.0 and AGPL-3.0, except as required by applicable law or agreed in
|
||||
writing, copyright holders and other parties who modify or distribute the software under the
|
||||
license are not liable for general, special, incidental, or consequential damages arising from
|
||||
the use or inability to use the software, including data loss, inaccurate data, third-party
|
||||
losses, or the software failing to interoperate with other programs. If local law does not
|
||||
allow the complete exclusion of liability, it is handled under Section 17 of the license and
|
||||
local law.
|
||||
</p>
|
||||
|
||||
<h2>9. Updates to These Terms and Licenses</h2>
|
||||
<p>
|
||||
These terms may be updated as official services, third-party platforms, or legal requirements
|
||||
change. Updates do not retroactively revoke rights already granted under the applicable
|
||||
open-source licenses, and do not automatically upgrade code marked "Version 3 only" to later
|
||||
license versions. Any license change must be made lawfully by the appropriate rights holder
|
||||
and reflected in the repository files.
|
||||
</p>
|
||||
|
||||
<h2>10. Contact and Full Texts</h2>
|
||||
<p>
|
||||
If you have questions about license scope, first check the <code>COPYING.md</code>,
|
||||
<code>LICENSE</code>, and source-code notices in the repository root and the corresponding
|
||||
packages. For further confirmation, contact the project maintainers via
|
||||
<a href="https://github.com/Mystic-Stars/Axolotl/issues" target="_blank" rel="noopener"
|
||||
>GitHub Issues</a
|
||||
>, or join the official QQ group 737601250. This page is not legal advice.
|
||||
</p>
|
||||
</LegalDocument>
|
||||
</template>
|
||||
9
apps/website/src/plugins/howxm.client.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { initHowxm } from 'howxm-js'
|
||||
|
||||
const HOWXM_APP_ID = '0b1c924f-5bca-4865-a52f-e3416fd26e46'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook('app:mounted', () => {
|
||||
initHowxm(HOWXM_APP_ID)
|
||||
})
|
||||
})
|
||||
236
apps/website/src/plugins/i18n.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import { useDebugLogger } from '@modrinth/ui/src/composables/debug-logger.ts'
|
||||
import {
|
||||
type CrowdinMessages,
|
||||
LOCALES,
|
||||
transformCrowdinMessages,
|
||||
} from '@modrinth/ui/src/composables/i18n.ts'
|
||||
import { uiLocaleModules } from '@modrinth/ui/src/locales.ts'
|
||||
import { I18N_INJECTION_KEY, type I18nContext } from '@modrinth/ui/src/providers/i18n.ts'
|
||||
import IntlMessageFormat from 'intl-messageformat'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
|
||||
import { siteLocaleMessages, SUPPORTED_SITE_LOCALES } from '~/locales/site'
|
||||
|
||||
const debug = useDebugLogger('i18n')
|
||||
const DEFAULT_LOCALE = 'zh-CN'
|
||||
const supportedLocales = LOCALES.filter((locale) =>
|
||||
SUPPORTED_SITE_LOCALES.includes(locale.code as (typeof SUPPORTED_SITE_LOCALES)[number]),
|
||||
)
|
||||
|
||||
function isSupportedLocale(locale: string): boolean {
|
||||
return supportedLocales.some((item) => item.code === locale)
|
||||
}
|
||||
|
||||
// 解析 Accept-Language 头:取第一个可支持的语言,zh* 归一为 zh-CN,en* 归一为 en-US
|
||||
function parseAcceptLanguage(header: string): string {
|
||||
for (const part of header.split(',')) {
|
||||
const [code] = part.trim().split(';')
|
||||
if (!code) continue
|
||||
if (isSupportedLocale(code)) return code
|
||||
|
||||
const language = code.toLowerCase()
|
||||
if (language.startsWith('zh')) return 'zh-CN'
|
||||
if (language.startsWith('en')) return 'en-US'
|
||||
}
|
||||
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
function getBrowserLocale(): string {
|
||||
if (!import.meta.client) return DEFAULT_LOCALE
|
||||
|
||||
const browserLocales = navigator.languages?.length ? navigator.languages : [navigator.language]
|
||||
|
||||
for (const browserLocale of browserLocales) {
|
||||
if (isSupportedLocale(browserLocale)) return browserLocale
|
||||
|
||||
const language = browserLocale.toLowerCase()
|
||||
if (language.startsWith('zh')) return 'zh-CN'
|
||||
if (language.startsWith('en')) return DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
const messageCache = new LRUCache<string, Record<string, string>>({ max: 10 })
|
||||
const formatterCache = new LRUCache<string, IntlMessageFormat>({ max: 1000 })
|
||||
const loadingPromises = new Map<string, Promise<void>>() // Dedupe concurrent loads
|
||||
|
||||
type LocaleModules = Record<string, () => Promise<{ default: CrowdinMessages }>>
|
||||
|
||||
// Find the loader for a locale code in a glob result (paths end with /{code}/index.json)
|
||||
function findLocaleLoader(modules: LocaleModules, code: string) {
|
||||
for (const [path, loader] of Object.entries(modules)) {
|
||||
if (path.endsWith(`/${code}/index.json`)) {
|
||||
return loader
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function formatIcuMessage(msg: string, locale: string, values: Record<string, unknown>) {
|
||||
const cacheKey = `${locale}:${msg}`
|
||||
let formatter = formatterCache.get(cacheKey)
|
||||
|
||||
try {
|
||||
if (!formatter) {
|
||||
formatter = new IntlMessageFormat(msg, locale)
|
||||
formatterCache.set(cacheKey, formatter)
|
||||
}
|
||||
const result = formatter.format(values)
|
||||
if (import.meta.dev && typeof result !== 'string') {
|
||||
debug('formatIcuMessage: format returned non-string', typeof result)
|
||||
}
|
||||
return result as string
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLocale(code: string): Promise<void> {
|
||||
if (messageCache.has(code)) {
|
||||
debug('loadLocale: already cached', code)
|
||||
return
|
||||
}
|
||||
|
||||
// Dedupe concurrent requests for the same locale
|
||||
const existing = loadingPromises.get(code)
|
||||
if (existing) {
|
||||
debug('loadLocale: already loading', code)
|
||||
return existing
|
||||
}
|
||||
|
||||
debug('loadLocale: starting', code)
|
||||
|
||||
const promise = (async () => {
|
||||
const uiLoader = findLocaleLoader(uiLocaleModules, code)
|
||||
|
||||
debug('loadLocale: loaders found', {
|
||||
code,
|
||||
ui: !!uiLoader,
|
||||
})
|
||||
|
||||
const uiData = await uiLoader?.().catch((e) => {
|
||||
debug('loadLocale: ui loader failed', code, e)
|
||||
return null
|
||||
})
|
||||
|
||||
debug('loadLocale: data loaded', {
|
||||
code,
|
||||
uiKeys: uiData ? Object.keys(uiData.default).length : 0,
|
||||
})
|
||||
|
||||
const mergedMessages: Record<string, string> = {}
|
||||
if (uiData) Object.assign(mergedMessages, transformCrowdinMessages(uiData.default))
|
||||
Object.assign(mergedMessages, siteLocaleMessages[code])
|
||||
|
||||
debug('loadLocale: merged', code, 'total keys:', Object.keys(mergedMessages).length)
|
||||
|
||||
if (Object.keys(mergedMessages).length > 0) {
|
||||
messageCache.set(code, mergedMessages)
|
||||
}
|
||||
})()
|
||||
|
||||
loadingPromises.set(code, promise)
|
||||
try {
|
||||
await promise
|
||||
} finally {
|
||||
loadingPromises.delete(code)
|
||||
}
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'i18n',
|
||||
enforce: 'pre',
|
||||
async setup(nuxtApp) {
|
||||
const locale = ref(DEFAULT_LOCALE)
|
||||
const savedLocale = useCookie<string | null>('locale').value
|
||||
|
||||
// 首屏语言优先级:cookie > Accept-Language(SSR)> 默认中文。
|
||||
// SSR 阶段就决定语言,避免客户端挂载后整页跳变。
|
||||
let initialLocale = DEFAULT_LOCALE
|
||||
if (savedLocale && isSupportedLocale(savedLocale)) {
|
||||
initialLocale = savedLocale
|
||||
} else if (import.meta.server) {
|
||||
const acceptLanguage = useRequestHeaders(['accept-language'])['accept-language']
|
||||
if (acceptLanguage) initialLocale = parseAcceptLanguage(acceptLanguage)
|
||||
}
|
||||
|
||||
function t(key: string, values?: Record<string, unknown>): string {
|
||||
const currentLocale = locale.value
|
||||
const localeMessages = messageCache.get(currentLocale)
|
||||
// 只有默认语言才回退到默认语言消息;其他语言缺失时返回 key,
|
||||
// 由 formatMessage 使用英文 defaultMessage
|
||||
const fallbackMessages =
|
||||
currentLocale === DEFAULT_LOCALE ? messageCache.get(DEFAULT_LOCALE) : undefined
|
||||
const msg = localeMessages?.[key] ?? fallbackMessages?.[key]
|
||||
|
||||
if (!msg) {
|
||||
debug('t: key not found', {
|
||||
key,
|
||||
locale: currentLocale,
|
||||
hasLocaleMessages: !!localeMessages,
|
||||
hasFallbackMessages: !!fallbackMessages,
|
||||
})
|
||||
return key
|
||||
}
|
||||
|
||||
if (!values || Object.keys(values).length === 0) return msg
|
||||
|
||||
const formatted = formatIcuMessage(msg, currentLocale, values)
|
||||
if (formatted !== null) return formatted
|
||||
|
||||
const fallbackMsg = fallbackMessages?.[key]
|
||||
if (fallbackMsg && fallbackMsg !== msg) {
|
||||
return formatIcuMessage(fallbackMsg, DEFAULT_LOCALE, values) ?? fallbackMsg
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
async function setLocale(newLocale: string): Promise<void> {
|
||||
debug('setLocale: called', { newLocale, currentLocale: locale.value })
|
||||
|
||||
if (!isSupportedLocale(newLocale)) {
|
||||
debug('setLocale: invalid locale', newLocale)
|
||||
return
|
||||
}
|
||||
|
||||
await loadLocale(newLocale)
|
||||
|
||||
debug('setLocale: loaded', {
|
||||
newLocale,
|
||||
cacheHas: messageCache.has(newLocale),
|
||||
cacheKeys: messageCache.get(newLocale)
|
||||
? Object.keys(messageCache.get(newLocale)!).length
|
||||
: 0,
|
||||
})
|
||||
|
||||
locale.value = newLocale
|
||||
useCookie('locale', { maxAge: 31536000, path: '/' }).value = newLocale
|
||||
if (import.meta.client) document.documentElement.lang = newLocale
|
||||
}
|
||||
|
||||
await loadLocale(DEFAULT_LOCALE)
|
||||
if (initialLocale !== DEFAULT_LOCALE) await loadLocale(initialLocale)
|
||||
locale.value = initialLocale
|
||||
if (import.meta.client) document.documentElement.lang = initialLocale
|
||||
|
||||
debug('init: complete', { locale: locale.value })
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: () => locale.value,
|
||||
},
|
||||
})
|
||||
|
||||
const context: I18nContext = { locale, t, setLocale }
|
||||
nuxtApp.vueApp.provide(I18N_INJECTION_KEY, context)
|
||||
|
||||
nuxtApp.hook('app:mounted', async () => {
|
||||
const preferredLocale =
|
||||
savedLocale && isSupportedLocale(savedLocale) ? savedLocale : getBrowserLocale()
|
||||
if (preferredLocale !== locale.value) await setLocale(preferredLocale)
|
||||
})
|
||||
},
|
||||
})
|
||||
4
apps/website/src/public/_redirects
Normal file
@ -0,0 +1,4 @@
|
||||
/api/releases/latest /.netlify/functions/releases-latest 200
|
||||
/api/releases/catalog /.netlify/functions/releases-catalog 200
|
||||
/api/releases/miawa /.netlify/functions/releases-miawa 200
|
||||
/api/downloads/prepare /.netlify/functions/downloads-prepare 200
|
||||
BIN
apps/website/src/public/axolotl.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/website/src/public/favicon-light.ico
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/website/src/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 12 KiB |
25
apps/website/src/public/llms.txt
Normal file
@ -0,0 +1,25 @@
|
||||
# Axolotl Launcher
|
||||
|
||||
Axolotl Launcher is a free, open-source, ad-free, cross-platform Minecraft Java Edition launcher which supports Windows, macOS, and Linux.
|
||||
|
||||
It supports searching, installing, and updating mods, modpacks, resource packs, and shaders from both Modrinth and CurseForge.
|
||||
|
||||
Axolotl Launcher also provides instance management, Microsoft and offline/Yggdrasil accounts, Java management, skin customization, and Axolotl Labs.
|
||||
|
||||
Axolotl Labs includes:
|
||||
- Gradient text generator
|
||||
- Minecraft Java Edition seed map
|
||||
- 3D schematic workshop
|
||||
- Recipe generator
|
||||
- AI Powered Minecraft mod translation tool
|
||||
|
||||
Axolotl Launcher is a fork of Modrinth App.
|
||||
|
||||
Official website:
|
||||
https://axlmc.org/
|
||||
|
||||
Source code:
|
||||
https://github.com/Mystic-Stars/Axolotl
|
||||
|
||||
Changelog:
|
||||
https://axlmc.org/changelog
|
||||
4
apps/website/src/public/robots.txt
Normal file
@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://axlmc.org/sitemap.xml
|
||||
BIN
apps/website/src/public/showcase/account-login.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
apps/website/src/public/showcase/axolotl-lab.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
apps/website/src/public/showcase/launcher-home.webp
Normal file
|
After Width: | Height: | Size: 269 KiB |
BIN
apps/website/src/public/showcase/theme-accent.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
19
apps/website/src/public/sitemap.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://axlmc.org/</loc>
|
||||
<lastmod>2026-08-10</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://axlmc.org/changelog</loc>
|
||||
<lastmod>2026-08-10</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://axlmc.org/terms</loc>
|
||||
<lastmod>2026-08-02</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://axlmc.org/privacy</loc>
|
||||
<lastmod>2026-08-02</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||