feat:移除了弹窗,服务器添加sls
1
apps/website/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.netlify
|
||||
2
apps/website/.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
10
apps/website/.prettierignore
Normal file
@ -0,0 +1,10 @@
|
||||
**/.nuxt
|
||||
**/dist
|
||||
**/.output
|
||||
**/.data
|
||||
**/.wrangler
|
||||
src/generated/**
|
||||
src/locales/**
|
||||
src/public/news/feed
|
||||
src/assets/**/*.svg
|
||||
**/.wrangler
|
||||
1
apps/website/AGENTS.md
Normal file
@ -0,0 +1 @@
|
||||
CLAUDE.md
|
||||
15
apps/website/CLAUDE.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Axolotl Website
|
||||
|
||||
`apps/website` is the official Axolotl Launcher website. It is a Nuxt 3 static site using Vue 3, Tailwind CSS, and the shared UI and asset packages.
|
||||
|
||||
## Development
|
||||
|
||||
Run `pnpm website:dev` from the repository root for local development and `pnpm website:build` for the production build.
|
||||
|
||||
## Structure
|
||||
|
||||
- `src/pages/` contains file-based routes.
|
||||
- `src/components/` contains website-specific components.
|
||||
- `src/layouts/` contains page layouts.
|
||||
- `src/locales/` contains website messages.
|
||||
- Shared components and styles belong in `packages/ui` and `packages/assets` when both the website and desktop application can use them.
|
||||
25
apps/website/COPYING.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Copying
|
||||
|
||||
The source code of Modrinth's web frontend is licensed under the GNU Affero General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
|
||||
|
||||
## Modrinth logo
|
||||
|
||||
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements. The assets associated with our blog articles are also subject to the same restrictions.
|
||||
|
||||
> All rights reserved. © 2020-2025 Rinth, Inc.
|
||||
|
||||
This includes, but may not be limited to, the following files:
|
||||
|
||||
- src/assets/images/404.svg
|
||||
- src/assets/images/logo.svg
|
||||
- src/components/brand/\*
|
||||
- src/public/favicon.ico
|
||||
- src/public/favicon-light.ico
|
||||
- src/public/news/\*
|
||||
|
||||
## External logos
|
||||
|
||||
The following files are owned by their respective copyright holders and must be used within each of their Brand Guidelines:
|
||||
|
||||
- src/assets/icons/auth/sso-\*
|
||||
- src/assets/images/external/\*
|
||||
661
apps/website/LICENSE
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
14
apps/website/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Axolotl Website
|
||||
|
||||
The official website for Axolotl Launcher.
|
||||
|
||||
## Development
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm website:dev
|
||||
```
|
||||
|
||||
Create the static production output with `pnpm website:build`.
|
||||
10
apps/website/eslint.config.mjs
Normal file
@ -0,0 +1,10 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config.append([
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
])
|
||||
93
apps/website/nuxt.config.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
|
||||
const SITE_URL = 'https://axlmc.org'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
srcDir: 'src/',
|
||||
app: {
|
||||
head: {
|
||||
htmlAttrs: {
|
||||
class: 'accent-pink dark-mode',
|
||||
lang: 'zh-CN',
|
||||
},
|
||||
title: 'Axolotl Launcher - 免费开源的 Minecraft 启动器',
|
||||
// 在 body 渲染前同步应用已保存的主题偏好,避免浅色用户首屏闪深色
|
||||
script: [
|
||||
{
|
||||
key: 'theme-init',
|
||||
innerHTML: `(function(){try{var t=localStorage.getItem('axolotl-theme');var r=t;if(!r||r==='system'){r=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: light)').matches)?'light':'dark'}var d=document.documentElement;d.classList.remove('light-mode','dark-mode','oled-mode');d.classList.add(r==='light'?'light-mode':r==='oled'?'oled-mode':'dark-mode');d.style.colorScheme=r==='light'?'light':'dark'}catch(e){}})()`,
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/png', href: '/axolotl.png' },
|
||||
{ rel: 'apple-touch-icon', type: 'image/png', href: '/axolotl.png' },
|
||||
],
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
siteUrl: SITE_URL,
|
||||
},
|
||||
},
|
||||
experimental: {
|
||||
// Keep the app manifest disabled for the static website. Nuxt's runtime
|
||||
// composable still contains a server-side `#app-manifest` import, which
|
||||
// Vite tries to resolve during dev even when the feature is disabled.
|
||||
appManifest: false,
|
||||
},
|
||||
vite: {
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
silenceDeprecations: ['import'],
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
dedupe: ['vue'],
|
||||
},
|
||||
plugins: [
|
||||
svgLoader({
|
||||
svgoConfig: {
|
||||
plugins: [
|
||||
{
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
removeViewBox: false,
|
||||
cleanupIds: { minify: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
css: ['~/assets/styles/tailwind.css'],
|
||||
postcss: {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
},
|
||||
nitro: {
|
||||
prerender: {
|
||||
crawlLinks: false,
|
||||
routes: ['/', '/changelog', '/terms', '/privacy'],
|
||||
},
|
||||
},
|
||||
routeRules: {
|
||||
'/': { static: true },
|
||||
'/changelog': { static: true },
|
||||
'/terms': { static: true },
|
||||
'/privacy': { static: true },
|
||||
},
|
||||
typescript: {
|
||||
shim: false,
|
||||
strict: true,
|
||||
typeCheck: false,
|
||||
},
|
||||
compatibilityDate: '2025-01-01',
|
||||
telemetry: false,
|
||||
})
|
||||
38
apps/website/package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@axolotl/website",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxi generate",
|
||||
"dev": "nuxi dev",
|
||||
"generate": "nuxi generate",
|
||||
"preview": "nuxi preview",
|
||||
"postinstall": "nuxi prepare",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"prepr": "pnpm run lint && pnpm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modrinth/tooling-config": "workspace:*",
|
||||
"@types/node": "^24",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"nuxt": "=3.20.2",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"sass": "^1.58.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.4.5",
|
||||
"vite-svg-loader": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"howxm-js": "0.0.10",
|
||||
"intl-messageformat": "^10.7.7",
|
||||
"lru-cache": "^11.2.4",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "*"
|
||||
},
|
||||
"web-types": "../../web-types.json",
|
||||
"prettier": "@modrinth/tooling-config/frontend.prettier.config.cjs"
|
||||
}
|
||||
2854
apps/website/releases/catalog.json
Normal file
34
apps/website/releases/latest.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"tag_name": "v1.9.5",
|
||||
"assets": [
|
||||
"Axolotl.Launcher-1.9.5-1.aarch64.rpm",
|
||||
"Axolotl.Launcher-1.9.5-1.aarch64.rpm.sig",
|
||||
"Axolotl.Launcher-1.9.5-1.x86_64.rpm",
|
||||
"Axolotl.Launcher-1.9.5-1.x86_64.rpm.sig",
|
||||
"Axolotl.Launcher_1.9.5_aarch64.AppImage",
|
||||
"Axolotl.Launcher_1.9.5_aarch64.AppImage.sig",
|
||||
"Axolotl.Launcher_1.9.5_aarch64.AppImage.tar.gz",
|
||||
"Axolotl.Launcher_1.9.5_aarch64.AppImage.tar.gz.sig",
|
||||
"Axolotl.Launcher_1.9.5_amd64.AppImage",
|
||||
"Axolotl.Launcher_1.9.5_amd64.AppImage.sig",
|
||||
"Axolotl.Launcher_1.9.5_amd64.AppImage.tar.gz",
|
||||
"Axolotl.Launcher_1.9.5_amd64.AppImage.tar.gz.sig",
|
||||
"Axolotl.Launcher_1.9.5_amd64.deb",
|
||||
"Axolotl.Launcher_1.9.5_amd64.deb.sig",
|
||||
"Axolotl.Launcher_1.9.5_arm64.deb",
|
||||
"Axolotl.Launcher_1.9.5_arm64.deb.sig",
|
||||
"Axolotl.Launcher_1.9.5_universal.dmg",
|
||||
"Axolotl.Launcher_1.9.5_x64-setup.exe",
|
||||
"Axolotl.Launcher_1.9.5_x64-setup.exe.sig",
|
||||
"Axolotl.Launcher_1.9.5_x64-setup.nsis.zip",
|
||||
"Axolotl.Launcher_1.9.5_x64-setup.nsis.zip.sig",
|
||||
"Axolotl.Launcher_universal.app.tar.gz",
|
||||
"Axolotl.Launcher_universal.app.tar.gz.sig",
|
||||
"Axolotl_Launcher_1.9.5_x64_modern-setup.exe",
|
||||
"Axolotl_Launcher_1.9.5_x64_modern-setup.exe.sig",
|
||||
"Axolotl_Launcher_1.9.5_x64_nsis-setup.exe",
|
||||
"Axolotl_Launcher_1.9.5_x64_nsis-setup.exe.sig",
|
||||
"Axolotl_Launcher_1.9.5_x64_portable.zip",
|
||||
"latest.json"
|
||||
]
|
||||
}
|
||||
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>
|
||||
19
apps/website/tailwind.config.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import preset from '@modrinth/tooling-config/tailwind/tailwind-preset.ts'
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./src/components/**/*.{js,vue,ts}',
|
||||
'./src/layouts/**/*.vue',
|
||||
'./src/pages/**/*.vue',
|
||||
'./src/plugins/**/*.{js,ts}',
|
||||
'./src/app.vue',
|
||||
'./src/error.vue',
|
||||
// monorepo - TODO: migrate this to its own package
|
||||
'../../packages/**/*.{js,vue,ts}',
|
||||
'!../../packages/**/node_modules/**',
|
||||
],
|
||||
presets: [preset],
|
||||
}
|
||||
|
||||
export default config
|
||||
3
apps/website/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
5
apps/website/vercel.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"buildCommand": "pnpm build",
|
||||
"framework": "nuxtjs",
|
||||
"outputDirectory": ".output/public"
|
||||
}
|
||||