feat: new user badges, ui consistency pass (#6262)

* feat: new user badges, ui consistency pass

* prepr

* fix: align with backend

* fix: lint

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Prospector
2026-05-31 08:25:31 -07:00
committed by GitHub
parent cc8d556448
commit 34b87991bc
47 changed files with 947 additions and 142 deletions
@@ -1,5 +1,5 @@
import type { Labrinth } from '@modrinth/api-client'
import { type AuthProvider, provideAuth } from '@modrinth/ui'
import { type AuthProvider, type AuthUser, provideAuth } from '@modrinth/ui'
import { computed, type Ref, ref, watchEffect } from 'vue'
type AppCredentials = {
@@ -12,7 +12,7 @@ export function setupAuthProvider(
requestSignIn: (redirectPath: string) => void | Promise<void>,
) {
const sessionToken = ref<string | null>(null)
const user = ref<Labrinth.Users.v2.User | null>(null)
const user = ref<AuthUser | null>(null)
const isReady = computed(() => credentials.value !== undefined)
const authProvider: AuthProvider = {
+3
View File
@@ -58,6 +58,9 @@ export default defineConfig({
params: {
overrides: {
removeViewBox: false,
cleanupIds: {
minify: false,
},
},
},
},
+3
View File
@@ -104,6 +104,9 @@ export default defineNuxtConfig({
params: {
overrides: {
removeViewBox: false,
cleanupIds: {
minify: false,
},
},
},
},
@@ -1,18 +1,18 @@
/*
Cards and body styling
*/
// CARDS
.base-card {
padding: var(--spacing-card-lg);
padding: 1rem;
position: relative;
min-height: var(--font-size-2xl);
background-color: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
background-color: var(--surface-3);
border-radius: var(--radius-lg);
border: 1px solid var(--surface-4);
margin-bottom: var(--spacing-card-md);
margin-bottom: var(--gap-md);
outline: 2px solid transparent;
outline-offset: -2px;
box-shadow: var(--shadow-card);
.card__overlay {
position: absolute;
@@ -25,6 +25,17 @@
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);
}
@@ -3,7 +3,7 @@
<AutoLink
:to="currentAd.link"
:aria-label="currentAd.description"
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit] bg-bg-raised"
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit] border border-solid border-surface-4 bg-surface-3"
>
<img
:src="currentAd.light"
@@ -19,7 +19,7 @@
/>
</AutoLink>
<div
class="absolute top-0 flex items-center justify-center overflow-hidden rounded-2xl bg-bg-raised"
class="absolute top-0 flex items-center justify-center overflow-hidden rounded-2xl border border-solid border-surface-4 bg-surface-3"
>
<div id="modrinth-rail-1" />
</div>
+2 -1
View File
@@ -1,7 +1,8 @@
<template>
<nav :aria-label="ariaLabel" class="w-full">
<ul
class="card-shadow m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl bg-bg-raised p-4"
class="card-shadow m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4"
:class="{ 'pt-3': filteredItems?.[0]?.type === 'heading' }"
>
<slot v-if="hasSlotContent" />
+2
View File
@@ -75,6 +75,7 @@ export const initAuth = async (oldToken = null) => {
auth.user = await useBaseFetch(
'user',
{
apiVersion: 3,
headers: {
Authorization: auth.token,
},
@@ -105,6 +106,7 @@ export const initAuth = async (oldToken = null) => {
auth.user = await useBaseFetch(
'user',
{
apiVersion: 3,
headers: {
Authorization: auth.token,
},
+2 -1
View File
@@ -793,6 +793,7 @@ import ModrinthFooter from '~/components/ui/ModrinthFooter.vue'
import { getSignInRouteObj } from '~/composables/auth.js'
import { errors as generatedStateErrors } from '~/generated/state.json'
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts'
import { hasActiveMidas } from '~/utils/user-membership.ts'
const generatedState = useGeneratedState()
@@ -1096,7 +1097,7 @@ const userMenuOptions = computed(() => {
id: 'plus',
link: '/plus',
color: 'purple',
shown: !flags.value.hidePlusPromoInUserMenu && !isPermission(user.badges, 1 << 0),
shown: !flags.value.hidePlusPromoInUserMenu && !hasActiveMidas(user),
},
{
id: 'servers',
+5 -3
View File
@@ -2,19 +2,21 @@ import { useAppQueryClient } from '~/composables/query-client'
import { useServerModrinthClient } from '~/server/utils/api-client'
export default defineNuxtRouteMiddleware(async (to) => {
if (!to.path.startsWith('/user/') || !to.params.id) {
const userParam = to.params.user ?? to.params.id
const userId = Array.isArray(userParam) ? userParam[0] : userParam
if (!to.path.startsWith('/user/') || !userId) {
return
}
const queryClient = useAppQueryClient()
const authToken = useCookie('auth-token')
const client = useServerModrinthClient({ authToken: authToken.value || undefined })
const userId = to.params.id as string
try {
const user = await queryClient.fetchQuery({
queryKey: ['user', userId],
queryFn: () => client.labrinth.users_v2.get(userId),
queryFn: () => client.labrinth.users_v3.get(userId),
})
if (!user) return
+2 -6
View File
@@ -38,11 +38,7 @@
{{ calculateSavings(price.prices.intervals.monthly, price.prices.intervals.yearly) }}% with
annual billing!
</p>
<ButtonStyled
v-if="auth.user && isPermission(auth.user.badges, 1 << 0)"
color="purple"
size="large"
>
<ButtonStyled v-if="auth.user && hasActiveMidas(auth.user)" color="purple" size="large">
<nuxt-link to="/settings/billing">
<SettingsIcon aria-hidden="true" />
Manage subscription
@@ -95,8 +91,8 @@ import {
import { calculateSavings, getCurrency } from '@modrinth/utils'
import { useBaseFetch } from '@/composables/fetch.js'
import { isPermission } from '@/utils/permissions.ts'
import { products } from '~/generated/state.json'
import { hasActiveMidas } from '~/utils/user-membership.ts'
const { addNotification } = injectNotificationManager()
const formatPrice = useFormatPrice()
+28 -75
View File
@@ -460,8 +460,11 @@
</div>
</div>
<div class="normal-page__sidebar">
<div v-if="organizations?.length > 0" class="card flex-card">
<h2 class="text-lg text-contrast">
<div
v-if="organizations?.length > 0"
class="mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4 pt-3"
>
<h2 class="m-0 mb-2 text-lg text-contrast">
{{ formatMessage(messages.profileOrganizations) }}
</h2>
<div class="flex flex-wrap gap-2">
@@ -476,24 +479,16 @@
</nuxt-link>
</div>
</div>
<div v-if="badges.length > 0" class="card flex-card">
<h2 class="text-lg text-contrast">
{{ formatMessage(messages.profileBadges) }}
</h2>
<div class="flex flex-wrap gap-2">
<div v-for="badge in badges" :key="badge">
<StaffBadge v-if="badge === 'staff'" class="h-14 w-14" />
<ModBadge v-else-if="badge === 'mod'" class="h-14 w-14" />
<nuxt-link v-else-if="badge === 'plus'" to="/plus">
<PlusBadge class="h-14 w-14" />
</nuxt-link>
<TenMClubBadge v-else-if="badge === '10m-club'" class="h-14 w-14" />
<EarlyAdopterBadge v-else-if="badge === 'early-adopter'" class="h-14 w-14" />
<AlphaTesterBadge v-else-if="badge === 'alpha-tester'" class="h-14 w-14" />
<BetaTesterBadge v-else-if="badge === 'beta-tester'" class="h-14 w-14" />
</div>
</div>
</div>
<UserBadges
:downloads="sumDownloads"
:join-date="joinDate"
:role="user.role"
:badges="user.badges"
:has-midas="hasActiveMidas(user)"
:has-pride="hasPride26Badge(user)"
:earliest-project-by-type="earliestProjectByType"
class="mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4 pt-3"
/>
<AdPlaceholder v-if="!auth.user" />
</div>
</div>
@@ -540,6 +535,7 @@ import {
useCompactNumber,
useFormatDateTime,
useFormatNumber,
UserBadges,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
@@ -547,19 +543,13 @@ import { isAdmin, isStaff, UserBadge } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { onServerPrefetch } from 'vue'
import TenMClubBadge from '~/assets/images/badges/10m-club.svg?component'
import AlphaTesterBadge from '~/assets/images/badges/alpha-tester.svg?component'
import BetaTesterBadge from '~/assets/images/badges/beta-tester.svg?component'
import EarlyAdopterBadge from '~/assets/images/badges/early-adopter.svg?component'
import ModBadge from '~/assets/images/badges/mod.svg?component'
import PlusBadge from '~/assets/images/badges/plus.svg?component'
import StaffBadge from '~/assets/images/badges/staff.svg?component'
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import { getSignInRouteObj } from '~/composables/auth.js'
import { reportUser } from '~/utils/report-helpers.ts'
import { hasActiveMidas, hasPride26Badge } from '~/utils/user-membership.ts'
const data = useNuxtApp()
const route = useNativeRoute()
@@ -743,7 +733,7 @@ const {
suspense: userSuspense,
} = useQuery({
queryKey: computed(() => ['user', userId]),
queryFn: () => client.labrinth.users_v2.get(userId),
queryFn: () => client.labrinth.users_v3.get(userId),
})
watch(
@@ -859,57 +849,21 @@ const sumDownloads = computed(() => {
})
const joinDate = computed(() => new Date(user.value.created))
const MODRINTH_BETA_END_DATE = new Date('2022-02-27T08:00:00.000Z')
const MODRINTH_ALPHA_END_DATE = new Date('2020-11-30T08:00:00.000Z')
const badges = computed(() => {
const badges = []
if (user.value.role === 'admin') {
badges.push('staff')
}
if (user.value.role === 'moderator') {
badges.push('mod')
}
if (isPermission(user.value.badges, 1 << 0)) {
badges.push('plus')
}
if (sumDownloads.value > 10000000) {
badges.push('10m-club')
}
if (
isPermission(user.value.badges, 1 << 1) ||
isPermission(user.value.badges, 1 << 2) ||
isPermission(user.value.badges, 1 << 3)
) {
badges.push('early-adopter')
}
if (isPermission(user.value.badges, 1 << 4) || joinDate.value < MODRINTH_ALPHA_END_DATE) {
badges.push('alpha-tester')
} else if (isPermission(user.value.badges, 1 << 4) || joinDate.value < MODRINTH_BETA_END_DATE) {
badges.push('beta-tester')
}
if (isPermission(user.value.badges, 1 << 5)) {
badges.push('contributor')
}
if (isPermission(user.value.badges, 1 << 6)) {
badges.push('translator')
}
return badges
})
async function copyId() {
await navigator.clipboard.writeText(user.value.id)
}
const earliestProjectByType = computed(() => {
const obj = {}
for (const project of projects.value ?? []) {
obj[project.project_type] = new Date(project.published)
}
return obj
})
async function copyPermalink() {
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`)
}
@@ -1012,7 +966,6 @@ export default defineNuxtComponent({
}
.description {
// Grow to take up remaining space
flex-grow: 1;
color: var(--color-text);
+3 -3
View File
@@ -1,5 +1,5 @@
import type { Labrinth } from '@modrinth/api-client'
import { type AuthProvider, provideAuth } from '@modrinth/ui'
import { type AuthProvider, type AuthUser, provideAuth } from '@modrinth/ui'
import { ref, watchEffect } from 'vue'
import { getSignInRedirectPath } from '~/composables/auth.js'
@@ -8,7 +8,7 @@ export function setupAuthProvider(auth: Awaited<ReturnType<typeof useAuth>>) {
const router = useRouter()
const route = useRoute()
const sessionToken = ref<string | null>(null)
const user = ref<Labrinth.Users.v2.User | null>(null)
const user = ref<AuthUser | null>(null)
const authProvider: AuthProvider = {
session_token: sessionToken,
@@ -26,7 +26,7 @@ export function setupAuthProvider(auth: Awaited<ReturnType<typeof useAuth>>) {
watchEffect(() => {
sessionToken.value = auth.value.token || null
user.value = (auth.value.user as Labrinth.Users.v2.User | null) ?? null
user.value = (auth.value.user as Labrinth.Users.v3.User | null) ?? null
})
provideAuth(authProvider)
@@ -0,0 +1,40 @@
import { UserBadge } from '@modrinth/utils'
const PRIDE_26_MIDAS_DURATION_MS = 30 * 24 * 60 * 60 * 1000
type Pride26Campaign = {
last_donated_at?: string | null
has_badge?: boolean | null
has_midas?: boolean | null
}
type UserWithMembership = {
badges?: number | null
campaigns?: {
pride_26?: Pride26Campaign | null
} | null
}
export function hasPride26Badge(user?: UserWithMembership | null) {
return user?.campaigns?.pride_26?.has_badge === true
}
export function hasActivePride26Midas(user?: UserWithMembership | null, now = Date.now()) {
const pride26Campaign = user?.campaigns?.pride_26
if (!pride26Campaign?.has_midas || !pride26Campaign.last_donated_at) {
return false
}
const lastDonatedAt = Date.parse(pride26Campaign.last_donated_at)
if (!Number.isFinite(lastDonatedAt)) {
return false
}
return lastDonatedAt + PRIDE_26_MIDAS_DURATION_MS > now
}
export function hasActiveMidas(user?: UserWithMembership | null, now = Date.now()) {
return Boolean((user?.badges ?? 0) & UserBadge.MIDAS) || hasActivePride26Midas(user, now)
}
@@ -1298,6 +1298,16 @@ export namespace Labrinth {
export type AuthProvider = Common.AuthProvider
export type UserPayoutData = Common.UserPayoutData
export type Pride26CampaignDonation = {
last_donated_at: string
has_badge: boolean
has_midas: boolean
}
export type UserCampaigns = {
pride_26: Pride26CampaignDonation | null
}
export type User = {
id: string
username: string
@@ -1314,6 +1324,7 @@ export namespace Labrinth {
payout_data?: UserPayoutData
stripe_customer_id?: string
allow_friend_requests?: boolean
campaigns: UserCampaigns
github_id?: number
}
@@ -6,6 +6,25 @@ export class LabrinthUsersV3Module extends AbstractModule {
return 'labrinth_users_v3'
}
/**
* Get a user by ID or username
*
* @param idOrUsername - The user's ID or username
* @returns Promise resolving to the user data
*
* GET /v3/user/{id}
*/
public async get(idOrUsername: string): Promise<Labrinth.Users.v3.User> {
return this.client.request<Labrinth.Users.v3.User>(
`/user/${encodeURIComponent(idOrUsername)}`,
{
api: 'labrinth',
version: 3,
method: 'GET',
},
)
}
/**
* Get all projects the authenticated user can access directly or through
* their organizations.
+55 -6
View File
@@ -121,6 +121,30 @@ function generateIconExports(): {
})
}
// Process badge icons from icons/badges/
const badgesDir = path.join(iconsDir, 'badges')
if (fs.existsSync(badgesDir)) {
const badgeFiles = fs.readdirSync(badgesDir).filter((file) => file.endsWith('.svg'))
badgeFiles.forEach((file) => {
const baseName = path.basename(file, '.svg')
let pascalName = toPascalCase(baseName)
if (pascalName === '') {
pascalName = 'Unknown'
}
if (!pascalName.endsWith('Badge')) {
pascalName += 'Badge'
}
icons.push({
importPath: `./icons/badges/${file}?component`,
pascalName,
privateName: `_${pascalName}`,
})
})
}
// Sort by import path using simple-import-sort's algorithm
icons.sort((a, b) => compareImportSources(a.importPath, b.importPath))
@@ -156,7 +180,7 @@ function generateIconExports(): {
function runTests(): void {
console.log('🧪 Running conversion tests...\n')
const testCases: Array<{ input: string; expected: string }> = [
const testCases: Array<{ input: string; expected: string; suffix?: string }> = [
{ input: 'align-left', expected: 'AlignLeftIcon' },
{ input: 'arrow-big-up-dash', expected: 'ArrowBigUpDashIcon' },
{ input: 'check-check', expected: 'CheckCheckIcon' },
@@ -171,13 +195,17 @@ function runTests(): void {
{ input: 'list_bulleted', expected: 'ListBulletedIcon' },
{ input: 'test.name', expected: 'TestNameIcon' },
{ input: 'test-name_final.icon', expected: 'TestNameFinalIcon' },
{ input: 'downloads-500m', expected: 'Downloads500mBadge', suffix: 'Badge' },
{ input: 'early-modpack', expected: 'EarlyModpackBadge', suffix: 'Badge' },
{ input: 'plus', expected: 'PlusBadge', suffix: 'Badge' },
]
let passed = 0
let failed = 0
testCases.forEach(({ input, expected }) => {
const result = toPascalCase(input) + (toPascalCase(input).endsWith('Icon') ? '' : 'Icon')
testCases.forEach(({ input, expected, suffix = 'Icon' }) => {
const base = toPascalCase(input)
const result = base.endsWith(suffix) ? base : base + suffix
const success = result === expected
if (success) {
@@ -312,6 +340,26 @@ function getExpectedIconExports(iconsDir: string): string[] {
})
}
// Process badge icons from icons/badges/
const badgesDir = path.join(iconsDir, 'badges')
if (fs.existsSync(badgesDir)) {
const badgeFiles = fs.readdirSync(badgesDir).filter((file) => file.endsWith('.svg'))
badgeFiles.forEach((file) => {
const baseName = path.basename(file, '.svg')
let pascalName = toPascalCase(baseName)
if (pascalName === '') {
pascalName = 'Unknown'
}
if (!pascalName.endsWith('Badge')) {
pascalName += 'Badge'
}
exports.push(pascalName)
})
}
return exports.sort()
}
@@ -321,14 +369,15 @@ function getActualIconExports(indexFile: string): string[] {
}
const content = fs.readFileSync(indexFile, 'utf8')
const exportMatches = content.match(/export const (\w+Icon) = _\w+Icon/g) || []
const exportMatches =
content.match(/export const (\w+(?:Icon|Badge)) = _\w+(?:Icon|Badge)/g) || []
return exportMatches
.map((match) => {
const result = match.match(/export const (\w+Icon)/)
const result = match.match(/export const (\w+(?:Icon|Badge))/)
return result ? result[1] : ''
})
.filter((name) => name.endsWith('Icon'))
.filter((name) => name.endsWith('Icon') || name.endsWith('Badge'))
.sort()
}
+40
View File
@@ -23,6 +23,26 @@ import _ArrowUpZAIcon from './icons/arrow-up-z-a.svg?component'
import _AsteriskIcon from './icons/asterisk.svg?component'
import _BadgeCheckIcon from './icons/badge-check.svg?component'
import _BadgeDollarSignIcon from './icons/badge-dollar-sign.svg?component'
import _AlphaBadge from './icons/badges/alpha.svg?component'
import _BetaBadge from './icons/badges/beta.svg?component'
import _Downloads1mBadge from './icons/badges/downloads-1m.svg?component'
import _Downloads10mBadge from './icons/badges/downloads-10m.svg?component'
import _Downloads25mBadge from './icons/badges/downloads-25m.svg?component'
import _Downloads50mBadge from './icons/badges/downloads-50m.svg?component'
import _Downloads100mBadge from './icons/badges/downloads-100m.svg?component'
import _Downloads250mBadge from './icons/badges/downloads-250m.svg?component'
import _Downloads500mBadge from './icons/badges/downloads-500m.svg?component'
import _EarlyDatapackBadge from './icons/badges/early-datapack.svg?component'
import _EarlyHostingBadge from './icons/badges/early-hosting.svg?component'
import _EarlyModpackBadge from './icons/badges/early-modpack.svg?component'
import _EarlyPluginBadge from './icons/badges/early-plugin.svg?component'
import _EarlyResourcepackBadge from './icons/badges/early-resourcepack.svg?component'
import _EarlyServersBadge from './icons/badges/early-servers.svg?component'
import _EarlyShadersBadge from './icons/badges/early-shaders.svg?component'
import _ModeratorBadge from './icons/badges/moderator.svg?component'
import _PlusBadge from './icons/badges/plus.svg?component'
import _PrideBadge from './icons/badges/pride.svg?component'
import _StaffBadge from './icons/badges/staff.svg?component'
import _BanIcon from './icons/ban.svg?component'
import _BellIcon from './icons/bell.svg?component'
import _BellRingIcon from './icons/bell-ring.svg?component'
@@ -423,6 +443,26 @@ export const ArrowUpZAIcon = _ArrowUpZAIcon
export const AsteriskIcon = _AsteriskIcon
export const BadgeCheckIcon = _BadgeCheckIcon
export const BadgeDollarSignIcon = _BadgeDollarSignIcon
export const AlphaBadge = _AlphaBadge
export const BetaBadge = _BetaBadge
export const Downloads1mBadge = _Downloads1mBadge
export const Downloads10mBadge = _Downloads10mBadge
export const Downloads25mBadge = _Downloads25mBadge
export const Downloads50mBadge = _Downloads50mBadge
export const Downloads100mBadge = _Downloads100mBadge
export const Downloads250mBadge = _Downloads250mBadge
export const Downloads500mBadge = _Downloads500mBadge
export const EarlyDatapackBadge = _EarlyDatapackBadge
export const EarlyHostingBadge = _EarlyHostingBadge
export const EarlyModpackBadge = _EarlyModpackBadge
export const EarlyPluginBadge = _EarlyPluginBadge
export const EarlyResourcepackBadge = _EarlyResourcepackBadge
export const EarlyServersBadge = _EarlyServersBadge
export const EarlyShadersBadge = _EarlyShadersBadge
export const ModeratorBadge = _ModeratorBadge
export const PlusBadge = _PlusBadge
export const PrideBadge = _PrideBadge
export const StaffBadge = _StaffBadge
export const BanIcon = _BanIcon
export const BellIcon = _BellIcon
export const BellRingIcon = _BellRingIcon
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="none" viewBox="0 0 90 90"><path fill="#33363d" d="M11.041 30.917a16.666 16.666 0 0 1 19.917-19.875 16.666 16.666 0 0 1 28.083 0 16.666 16.666 0 0 1 19.917 19.917 16.666 16.666 0 0 1 0 28.083 16.666 16.666 0 0 1-19.875 19.917 16.666 16.666 0 0 1-28.125 0A16.666 16.666 0 0 1 11.04 59.084a16.667 16.667 0 0 1 0-28.167"/><path fill="url(#downloads-10m-a)" fill-opacity=".7" d="M11.041 30.917a16.666 16.666 0 0 1 19.917-19.875 16.666 16.666 0 0 1 28.083 0 16.666 16.666 0 0 1 19.917 19.917 16.666 16.666 0 0 1 0 28.083 16.666 16.666 0 0 1-19.875 19.917 16.666 16.666 0 0 1-28.125 0A16.666 16.666 0 0 1 11.04 59.084a16.667 16.667 0 0 1 0-28.167"/><path fill="url(#downloads-10m-b)" fill-opacity=".7" d="M83.647 45a13.67 13.67 0 0 0-6.305-11.514 3 3 0 0 1-1.312-3.185A13.667 13.667 0 0 0 59.7 13.969a3 3 0 0 1-3.186-1.31 13.67 13.67 0 0 0-23.029 0 3 3 0 0 1-3.183 1.31A13.668 13.668 0 0 0 13.97 30.268a3 3 0 0 1-1.325 3.185 13.67 13.67 0 0 0-4.664 18.14 13.67 13.67 0 0 0 4.665 4.957 3 3 0 0 1 1.324 3.184 13.67 13.67 0 0 0 9.54 16.09c2.21.64 4.546.712 6.792.208l.231-.043c1.16-.166 2.32.36 2.956 1.36a13.67 13.67 0 0 0 11.532 6.332v3l-.526-.01a16.7 16.7 0 0 1-7.032-1.802l-.464-.246a16.67 16.67 0 0 1-6.041-5.663 16.67 16.67 0 0 1-15.041-4.1l-.378-.366a16.7 16.7 0 0 1-4.071-6.625l-.155-.503a16.67 16.67 0 0 1-.272-8.281 16.7 16.7 0 0 1-5.426-5.588l-.261-.457a16.67 16.67 0 0 1-.247-15.612l.247-.465a16.67 16.67 0 0 1 5.687-6.045 16.7 16.7 0 0 1 .132-7.775l.14-.507a16.7 16.7 0 0 1 3.86-6.75l.366-.377a16.67 16.67 0 0 1 14.904-4.573l.515.107a16.7 16.7 0 0 1 5.581-5.382l.456-.259a16.667 16.667 0 0 1 21.757 5.204l.289.437a16.667 16.667 0 0 1 19.917 19.917A16.67 16.67 0 0 1 86.647 45l-.01.524a16.7 16.7 0 0 1-2.04 7.48l-.258.456a16.7 16.7 0 0 1-5.381 5.582 16.67 16.67 0 0 1-4.466 15.419l-.377.366a16.7 16.7 0 0 1-6.75 3.86l-.508.14a16.7 16.7 0 0 1-7.774.132 16.67 16.67 0 0 1-6.041 5.663l-.465.246a16.7 16.7 0 0 1-7.556 1.812v-3a13.67 13.67 0 0 0 11.53-6.331l.134-.194a3 3 0 0 1 3.047-1.125 13.67 13.67 0 0 0 16.09-9.54c.64-2.21.712-4.546.208-6.792a3 3 0 0 1 1.312-3.183A13.67 13.67 0 0 0 83.647 45"/><path fill="#000" fill-opacity=".5" fill-rule="evenodd" d="M46.709 6.46a13.66 13.66 0 0 0-8.274 1.573 13.67 13.67 0 0 0-4.95 4.625 3 3 0 0 1-3.183 1.312A13.668 13.668 0 0 0 13.97 30.267a3 3 0 0 1-.489 2.395c-.22.309-.502.579-.835.79a13.68 13.68 0 0 0-6.25 13.267 13.67 13.67 0 0 0 6.25 9.83 3 3 0 0 1 1.307 1.812c.11.44.12.91.017 1.372a13.6 13.6 0 0 0-.155 5.116 13.66 13.66 0 0 0 3.843 7.52 13.67 13.67 0 0 0 12.644 3.662l.231-.043c1.16-.166 2.32.36 2.956 1.36q.116.183.238.36a13.64 13.64 0 0 0 4.715 4.285 13.67 13.67 0 0 0 18.11-4.644q.064-.1.133-.194a3 3 0 0 1 3.047-1.125 13.67 13.67 0 0 0 16.63-13.35c-.001-1-.111-2-.332-2.982a3.01 3.01 0 0 1 1.312-3.183 13.668 13.668 0 0 0 0-23.029 3 3 0 0 1-.54-.441 2.998 2.998 0 0 1-.772-2.744 13.674 13.674 0 0 0-10.785-16.425 13.7 13.7 0 0 0-5.546.093 3 3 0 0 1-3.185-1.31 13.67 13.67 0 0 0-9.805-6.198m-36.1 43.685a10.667 10.667 0 0 1 3.641-14.158 6 6 0 0 0 2.648-6.37 10.666 10.666 0 0 1 12.747-12.72l.232.047a6 6 0 0 0 6.136-2.67 10.668 10.668 0 0 1 17.973 0l.131.197a6 6 0 0 0 6.24 2.424 10.67 10.67 0 0 1 12.746 12.748 6 6 0 0 0 2.623 6.37 10.667 10.667 0 0 1 3.766 13.813l-.157.297a10.7 10.7 0 0 1-3.61 3.864 6 6 0 0 0-2.622 6.368 10.667 10.667 0 0 1-12.721 12.747 6 6 0 0 0-6.095 2.25l-.068.096-.134.194-.065.096a10.67 10.67 0 0 1-8.664 4.937l-.336.005a10.67 10.67 0 0 1-8.815-4.66l-.184-.282a6 6 0 0 0-5.914-2.72l-.122.021-.231.043-.109.022a10.668 10.668 0 0 1-12.747-12.721 6 6 0 0 0-2.45-6.239l-.198-.13a10.7 10.7 0 0 1-3.64-3.87" clip-rule="evenodd"/><path fill="#fff" d="M30.826 51.364V63h-2.46v-9.301h-.068l-2.665 1.67v-2.181l2.88-1.824zm7.328 11.892q-1.465-.006-2.523-.722-1.05-.716-1.619-2.074-.562-1.358-.557-3.267 0-1.903.563-3.244.568-1.341 1.62-2.04 1.056-.705 2.516-.704 1.46 0 2.511.704 1.057.705 1.626 2.046.568 1.336.562 3.238 0 1.916-.568 3.273-.562 1.357-1.614 2.074-1.05.716-2.517.716m0-2.04q1 0 1.597-1.006.596-1.005.59-3.017 0-1.323-.272-2.204-.267-.881-.762-1.324a1.66 1.66 0 0 0-1.153-.443q-.994 0-1.59.994-.598.994-.603 2.977 0 1.34.267 2.239.273.891.767 1.34.495.444 1.16.444m6.521-9.852h3.035l3.204 7.818h.136l3.205-7.818h3.034V63h-2.386v-7.574h-.097l-3.011 7.517H50.17l-3.012-7.545h-.096V63h-2.387zm18.05 9.863v-8.045h2.034v8.045zm-3.006-3.005v-2.035h8.046v2.035z"/><path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M45 35V23m5 7-5 5-5-5m14 5v4a2 2 0 0 1-2 2H38a2 2 0 0 1-2-2v-4"/><defs><linearGradient id="downloads-10m-a" x1="7.5" x2="80" y1="-3" y2="91" gradientUnits="userSpaceOnUse"><stop stop-color="#42ffdf"/><stop offset="1" stop-color="#170fff"/></linearGradient><linearGradient id="downloads-10m-b" x1="7.5" x2="80" y1="-3" y2="91" gradientUnits="userSpaceOnUse"><stop stop-color="#5d46f2"/><stop offset="1" stop-color="#b2ebf8"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="none" viewBox="0 0 90 90"><path fill="#33363d" d="M11.041 30.917a16.666 16.666 0 0 1 19.917-19.875 16.666 16.666 0 0 1 28.083 0 16.666 16.666 0 0 1 19.917 19.917 16.666 16.666 0 0 1 0 28.083 16.666 16.666 0 0 1-19.875 19.917 16.666 16.666 0 0 1-28.125 0A16.666 16.666 0 0 1 11.04 59.084a16.667 16.667 0 0 1 0-28.167"/><path fill="url(#downloads-1m-a)" fill-opacity=".7" d="M11.041 30.917a16.666 16.666 0 0 1 19.917-19.875 16.666 16.666 0 0 1 28.083 0 16.666 16.666 0 0 1 19.917 19.917 16.666 16.666 0 0 1 0 28.083 16.666 16.666 0 0 1-19.875 19.917 16.666 16.666 0 0 1-28.125 0A16.666 16.666 0 0 1 11.04 59.084a16.667 16.667 0 0 1 0-28.167"/><path fill="url(#downloads-1m-b)" fill-opacity=".7" d="M83.647 45a13.67 13.67 0 0 0-6.305-11.514 3 3 0 0 1-1.312-3.185A13.667 13.667 0 0 0 59.7 13.969a3 3 0 0 1-3.186-1.31 13.67 13.67 0 0 0-23.029 0 3 3 0 0 1-3.183 1.31A13.668 13.668 0 0 0 13.97 30.268a3 3 0 0 1-1.325 3.185 13.67 13.67 0 0 0-4.664 18.14 13.67 13.67 0 0 0 4.665 4.957 3 3 0 0 1 1.324 3.184 13.67 13.67 0 0 0 9.54 16.09c2.21.64 4.546.712 6.792.208l.231-.043c1.16-.166 2.32.36 2.956 1.36a13.67 13.67 0 0 0 11.532 6.332v3l-.526-.01a16.7 16.7 0 0 1-7.032-1.802l-.464-.246a16.67 16.67 0 0 1-6.041-5.663 16.67 16.67 0 0 1-15.041-4.1l-.378-.366a16.7 16.7 0 0 1-4.071-6.625l-.155-.503a16.67 16.67 0 0 1-.272-8.281 16.7 16.7 0 0 1-5.426-5.588l-.261-.457a16.67 16.67 0 0 1-.247-15.612l.247-.465a16.67 16.67 0 0 1 5.687-6.045 16.7 16.7 0 0 1 .132-7.775l.14-.507a16.7 16.7 0 0 1 3.86-6.75l.366-.377a16.67 16.67 0 0 1 14.904-4.573l.515.107a16.7 16.7 0 0 1 5.581-5.382l.456-.259a16.667 16.667 0 0 1 21.757 5.204l.289.437a16.667 16.667 0 0 1 19.917 19.917A16.67 16.67 0 0 1 86.647 45l-.01.524a16.7 16.7 0 0 1-2.04 7.48l-.258.456a16.7 16.7 0 0 1-5.381 5.582 16.67 16.67 0 0 1-4.466 15.419l-.377.366a16.7 16.7 0 0 1-6.75 3.86l-.508.14a16.7 16.7 0 0 1-7.774.132 16.67 16.67 0 0 1-6.041 5.663l-.465.246a16.7 16.7 0 0 1-7.556 1.812v-3a13.67 13.67 0 0 0 11.53-6.331l.134-.194a3 3 0 0 1 3.047-1.125 13.67 13.67 0 0 0 16.09-9.54c.64-2.21.712-4.546.208-6.792a3 3 0 0 1 1.312-3.183A13.67 13.67 0 0 0 83.647 45"/><path fill="#000" fill-opacity=".5" fill-rule="evenodd" d="M46.709 6.46a13.66 13.66 0 0 0-8.274 1.573 13.67 13.67 0 0 0-4.95 4.625 3 3 0 0 1-3.183 1.312A13.668 13.668 0 0 0 13.97 30.267a3 3 0 0 1-.489 2.395c-.22.309-.502.579-.835.79a13.68 13.68 0 0 0-6.25 13.267 13.67 13.67 0 0 0 6.25 9.83 3 3 0 0 1 1.307 1.812c.11.44.12.91.017 1.372a13.6 13.6 0 0 0-.155 5.116 13.66 13.66 0 0 0 3.843 7.52 13.67 13.67 0 0 0 12.644 3.662l.231-.043c1.16-.166 2.32.36 2.956 1.36q.116.183.238.36a13.64 13.64 0 0 0 4.715 4.285 13.67 13.67 0 0 0 18.11-4.644q.064-.1.133-.194a3 3 0 0 1 3.047-1.125 13.67 13.67 0 0 0 16.63-13.35c-.001-1-.111-2-.332-2.982a3.01 3.01 0 0 1 1.312-3.183 13.668 13.668 0 0 0 0-23.029 3 3 0 0 1-.54-.441 2.998 2.998 0 0 1-.772-2.744 13.674 13.674 0 0 0-10.785-16.425 13.7 13.7 0 0 0-5.546.093 3 3 0 0 1-3.185-1.31 13.67 13.67 0 0 0-9.805-6.198m-36.1 43.685a10.667 10.667 0 0 1 3.641-14.158 6 6 0 0 0 2.648-6.37 10.666 10.666 0 0 1 12.747-12.72l.232.047a6 6 0 0 0 6.136-2.67 10.668 10.668 0 0 1 17.973 0l.131.197a6 6 0 0 0 6.24 2.424 10.67 10.67 0 0 1 12.746 12.748 6 6 0 0 0 2.623 6.37 10.667 10.667 0 0 1 3.766 13.813l-.157.297a10.7 10.7 0 0 1-3.61 3.864 6 6 0 0 0-2.622 6.368 10.667 10.667 0 0 1-12.721 12.747 6 6 0 0 0-6.095 2.25l-.068.096-.134.194-.065.096a10.67 10.67 0 0 1-8.664 4.937l-.336.005a10.67 10.67 0 0 1-8.815-4.66l-.184-.282a6 6 0 0 0-5.914-2.72l-.122.021-.231.043-.109.022a10.668 10.668 0 0 1-12.747-12.721 6 6 0 0 0-2.45-6.239l-.198-.13a10.7 10.7 0 0 1-3.64-3.87" clip-rule="evenodd"/><path fill="#fff" d="M36.334 51.364V63h-2.46v-9.301h-.069l-2.664 1.67v-2.181l2.88-1.824zm2.834 0h3.034l3.204 7.818h.137l3.204-7.818h3.034V63h-2.386v-7.574h-.097l-3.011 7.517h-1.625l-3.011-7.545h-.097V63h-2.386zm18.05 9.863v-8.045h2.033v8.045zm-3.006-3.005v-2.035h8.045v2.035z"/><path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M45 35V23m5 7-5 5-5-5m14 5v4a2 2 0 0 1-2 2H38a2 2 0 0 1-2-2v-4"/><defs><linearGradient id="downloads-1m-a" x1="7.5" x2="80" y1="-3" y2="91" gradientUnits="userSpaceOnUse"><stop stop-color="#adff42"/><stop offset="1" stop-color="#0fe7ff"/></linearGradient><linearGradient id="downloads-1m-b" x1="7.5" x2="80" y1="-3" y2="91" gradientUnits="userSpaceOnUse"><stop stop-color="#46f291"/><stop offset="1" stop-color="#b2f8c4"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="none" viewBox="0 0 90 90"><path fill="#71b1f5" fill-rule="evenodd" d="M65 10c7.49 0 13.607 5.881 13.982 13.281l.003.075.015.567.001.077v10c0 3.918-1.61 7.459-4.204 10a13.96 13.96 0 0 1 4.188 9.356l.015.567.001.077v10c0 7.732-6.269 14.001-14.001 14.001H24.999c-7.733 0-14-6.27-14-14.001V54c0-3.918 1.61-7.459 4.203-10A13.96 13.96 0 0 1 11 34V24c0-7.732 6.268-14 14-14z" clip-rule="evenodd"/><path fill="url(#early-hosting-a)" fill-rule="evenodd" d="M65 10c7.49 0 13.607 5.881 13.982 13.281l.003.075.015.567.001.077v10c0 3.918-1.61 7.459-4.204 10a13.96 13.96 0 0 1 4.188 9.356l.015.567.001.077v10c0 7.732-6.269 14.001-14.001 14.001H24.999c-7.733 0-14-6.27-14-14.001V54c0-3.918 1.61-7.459 4.203-10A13.96 13.96 0 0 1 11 34V24c0-7.732 6.268-14 14-14z" clip-rule="evenodd"/><path fill="#23258c" fill-rule="evenodd" d="M65 13c5.885 0 10.691 4.621 10.986 10.434l.015.566v10c0 4.438-2.63 8.26-6.416 9.999 3.624 1.664 6.188 5.239 6.401 9.435l.015.566v10c0 6.075-4.926 11.001-11.001 11.001H24.999c-6.075 0-11-4.926-11-11.001V54c0-4.44 2.63-8.262 6.417-10-3.786-1.739-6.417-5.561-6.417-10V24c0-6.075 4.925-11 11-11zM34.496 55a5.98 5.98 0 0 1 1.53 4c0 1.538-.58 2.94-1.53 4.001H64V55zM30 56a3 3 0 1 0 0 6h.026a3 3 0 0 0 0-6zm4.496-31a5.98 5.98 0 0 1 1.53 4c0 1.538-.58 2.94-1.53 4.001H64V25zM30 26a3 3 0 1 0 0 6h.026a3 3 0 0 0 0-6z" clip-rule="evenodd"/><path fill="url(#early-hosting-b)" fill-rule="evenodd" d="M65 13c5.885 0 10.691 4.621 10.986 10.434l.015.566v10c0 4.438-2.63 8.26-6.416 9.999 3.624 1.664 6.188 5.239 6.401 9.435l.015.566v10c0 6.075-4.926 11.001-11.001 11.001H24.999c-6.075 0-11-4.926-11-11.001V54c0-4.44 2.63-8.262 6.417-10-3.786-1.739-6.417-5.561-6.417-10V24c0-6.075 4.925-11 11-11zM34.496 55a5.98 5.98 0 0 1 1.53 4c0 1.538-.58 2.94-1.53 4.001H64V55zM30 56a3 3 0 1 0 0 6h.026a3 3 0 0 0 0-6zm4.496-31a5.98 5.98 0 0 1 1.53 4c0 1.538-.58 2.94-1.53 4.001H64V25zM30 26a3 3 0 1 0 0 6h.026a3 3 0 0 0 0-6z" clip-rule="evenodd"/><path fill="#3b89f4" d="M67.001 24A2 2 0 0 0 65 22H24.999a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2.001H65A2 2 0 0 0 67.001 34zm6 10A8 8 0 0 1 65 42.001H24.999a8 8 0 0 1-8-8.001V24a8 8 0 0 1 8-8H65a8 8 0 0 1 8.001 8z"/><path fill="url(#early-hosting-c)" d="M67.001 24A2 2 0 0 0 65 22H24.999a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2.001H65A2 2 0 0 0 67.001 34zm6 10A8 8 0 0 1 65 42.001H24.999a8 8 0 0 1-8-8.001V24a8 8 0 0 1 8-8H65a8 8 0 0 1 8.001 8z"/><path fill="#3b89f4" d="M67.001 54A2 2 0 0 0 65 52H24.999a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2.001H65A2 2 0 0 0 67.001 64zm6 10A8 8 0 0 1 65 72.001H24.999a8 8 0 0 1-8-8.001V54a8 8 0 0 1 8-8H65a8 8 0 0 1 8.001 8z"/><path fill="url(#early-hosting-d)" d="M67.001 54A2 2 0 0 0 65 52H24.999a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2.001H65A2 2 0 0 0 67.001 64zm6 10A8 8 0 0 1 65 72.001H24.999a8 8 0 0 1-8-8.001V54a8 8 0 0 1 8-8H65a8 8 0 0 1 8.001 8z"/><path fill="#3b89f4" d="M30.026 26a3 3 0 0 1 0 6H30a3 3 0 0 1 0-6z"/><path fill="url(#early-hosting-e)" d="M30.026 26a3 3 0 0 1 0 6H30a3 3 0 0 1 0-6z"/><path fill="#3b89f4" d="M30.026 56a3 3 0 0 1 0 6H30a3 3 0 0 1 0-6z"/><path fill="url(#early-hosting-f)" d="M30.026 56a3 3 0 0 1 0 6H30a3 3 0 0 1 0-6z"/><defs><linearGradient id="early-hosting-a" x1="24.943" x2="79.403" y1="-.014" y2="82.407" gradientUnits="userSpaceOnUse"><stop stop-color="#bddcf3"/><stop offset="1" stop-color="#64aaf4"/></linearGradient><linearGradient id="early-hosting-b" x1="26.712" x2="76.367" y1="3.87" y2="79.018" gradientUnits="userSpaceOnUse"><stop stop-color="#2f137c"/><stop offset="1" stop-color="#19359a"/></linearGradient><linearGradient id="early-hosting-c" x1="28.482" x2="73.332" y1="7.754" y2="75.629" gradientUnits="userSpaceOnUse"><stop stop-color="#2aa3f3"/><stop offset="1" stop-color="#6d3bf7"/></linearGradient><linearGradient id="early-hosting-d" x1="28.482" x2="73.332" y1="7.754" y2="75.629" gradientUnits="userSpaceOnUse"><stop stop-color="#2aa3f3"/><stop offset="1" stop-color="#6d3bf7"/></linearGradient><linearGradient id="early-hosting-e" x1="28.482" x2="73.332" y1="7.754" y2="75.629" gradientUnits="userSpaceOnUse"><stop stop-color="#2aa3f3"/><stop offset="1" stop-color="#6d3bf7"/></linearGradient><linearGradient id="early-hosting-f" x1="28.482" x2="73.332" y1="7.754" y2="75.629" gradientUnits="userSpaceOnUse"><stop stop-color="#2aa3f3"/><stop offset="1" stop-color="#6d3bf7"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="none" viewBox="0 0 90 90"><path fill="url(#early-servers-a)" d="M77.157 44.15a6 6 0 0 0-.625-2.337l-9.195-18.365a11.34 11.34 0 0 0-9.744-6.275l-.056-.002-.29-.005H31.803a11.34 11.34 0 0 0-10.135 6.274l-9.2 18.373a6 6 0 0 0-.635 2.686v16a11.33 11.33 0 0 0 5.514 9.726 11.34 11.34 0 0 0 5.819 1.608h42.667l.563-.014a11.334 11.334 0 0 0 10.77-11.32v-16q0-.175-.01-.349M20.834 47.5v13a2.334 2.334 0 0 0 2.333 2.334h42.667a2.335 2.335 0 0 0 2.333-2.334v-13zM19.395 64.27a5 5 0 0 1-.181-.19zM65.167 50.5v9.334H42.962a5.99 5.99 0 0 0 2.23-4.667c0-1.885-.87-3.567-2.23-4.667zM28.5 58.166a3 3 0 1 1 0-6h.026a3 3 0 0 1 0 6zm10.666 0a3 3 0 0 1 0-6h.026a3 3 0 0 1 0 6zm-7.357-32-.163.006a2.33 2.33 0 0 0-1.925 1.289l-7.03 14.038H66.31l-7.025-14.03-.005-.008a2.33 2.33 0 0 0-1.925-1.29l-.163-.005zm24.971 3 4.673 9.333H27.55l4.673-9.333zm3.53-4.992a5.3 5.3 0 0 1 1.628 1.901l.03.051a5.33 5.33 0 0 0-1.966-2.16zm-31.619-.001a5 5 0 0 0 .308-.205zm2.787-.996q-.102.008-.202.018.08-.009.16-.015zm25.962-.004q-.02-.002-.04-.003l-.065-.001zM22.69 44.499a3 3 0 0 1-2.682-4.343l7.025-14.03-9.2 18.373v3a3 3 0 0 1 3-3zm45.476 0a3 3 0 0 1 3 3v-3l-9.195-18.365 7.02 14.022A3 3 0 0 1 66.31 44.5zm6 16a8.35 8.35 0 0 1-2.44 5.893 8.33 8.33 0 0 1-5.893 2.441H23.167a8.32 8.32 0 0 1-4.958-1.636 8.34 8.34 0 0 1-3.214-5.065 8.3 8.3 0 0 1-.161-1.633v-16a3 3 0 0 1 .317-1.343l9.2-18.373a8.33 8.33 0 0 1 7.455-4.617h25.39l.29.005a8.3 8.3 0 0 1 3.625.974q.24.128.472.272a8.33 8.33 0 0 1 3.071 3.374l9.196 18.365a3 3 0 0 1 .317 1.343zm6 0a14.333 14.333 0 0 1-13.697 14.32l-.563.013-.073.001H23.167A14.335 14.335 0 0 1 8.834 60.499v-16c0-1.398.326-2.778.952-4.028l9.2-18.373a14.33 14.33 0 0 1 5.27-5.78l.044-.027.339-.202q.022-.015.046-.027a14.34 14.34 0 0 1 7.117-1.896h25.495l.29.005.111.004a14.34 14.34 0 0 1 12.321 7.93l9.196 18.366a9 9 0 0 1 .952 4.028z"/><path fill="url(#early-servers-b)" d="M74.167 44.499a3 3 0 0 0-.317-1.343l-9.196-18.365a8.33 8.33 0 0 0-7.168-4.62l-.29-.005h-25.39a8.33 8.33 0 0 0-7.454 4.617l-9.2 18.373a3 3 0 0 0-.318 1.343v16a8.334 8.334 0 0 0 8.333 8.334h42.667a8.334 8.334 0 0 0 8.333-8.334zm-51 18.334a2.334 2.334 0 0 1-2.333-2.334v-13h47.333v13a2.334 2.334 0 0 1-2.333 2.334zM28.5 52.166a3 3 0 0 0 0 6h.026a3 3 0 0 0 0-6zm28.692-26 .163.006a2.33 2.33 0 0 1 1.925 1.289l.005.008 7.025 14.03H22.69l7.03-14.038a2.33 2.33 0 0 1 1.925-1.29l.163-.005zM27.55 38.499h33.904l-4.673-9.333H32.222zm8.617 16.667a3 3 0 0 0 3 3h.026a3 3 0 0 0 0-6h-.026a3 3 0 0 0-3 3m9.026 0c0 1.886-.87 3.567-2.23 4.667h22.205v-9.334H42.962a5.99 5.99 0 0 1 2.23 4.667m31.975 5.333a11.334 11.334 0 0 1-10.77 11.32l-.563.014H23.167a11.335 11.335 0 0 1-11.333-11.334v-16c0-.932.217-1.852.635-2.686l9.2-18.373a11.34 11.34 0 0 1 4.168-4.573l.339-.202a11.34 11.34 0 0 1 5.628-1.499h25.442l.291.005.056.002a11.33 11.33 0 0 1 9.744 6.275l9.195 18.365a6 6 0 0 1 .635 2.686z"/><path fill="url(#early-servers-c)" d="M68.167 47.499H20.834v13a2.334 2.334 0 0 0 2.333 2.334h42.667a2.335 2.335 0 0 0 2.333-2.334zm-39.64 4.667a3 3 0 0 1 0 6H28.5a3 3 0 1 1 0-6zm10.665 0a3 3 0 0 1 0 6h-.026a3 3 0 0 1 0-6zm-7.546-25.994a2.33 2.33 0 0 0-1.925 1.289l-7.03 14.038H66.31l-7.025-14.03-.005-.008a2.33 2.33 0 0 0-1.925-1.29l-.163-.005H31.81zm42.521 34.327a8.334 8.334 0 0 1-8.333 8.334H23.167a8.335 8.335 0 0 1-8.333-8.334v-16a3 3 0 0 1 .317-1.343l9.2-18.373a8.33 8.33 0 0 1 7.455-4.617h25.39l.29.005a8.33 8.33 0 0 1 7.168 4.62l9.196 18.365a3 3 0 0 1 .317 1.343z"/><defs><linearGradient id="early-servers-a" x1="23.461" x2="68.576" y1="5.232" y2="85.514" gradientUnits="userSpaceOnUse"><stop stop-color="#bddcf3"/><stop offset="1" stop-color="#64aaf4"/></linearGradient><linearGradient id="early-servers-b" x1="25.23" x2="65.537" y1="9.116" y2="82.018" gradientUnits="userSpaceOnUse"><stop stop-color="#2f137c"/><stop offset="1" stop-color="#19359a"/></linearGradient><linearGradient id="early-servers-c" x1="27" x2="62.5" y1="12.999" y2="78.499" gradientUnits="userSpaceOnUse"><stop stop-color="#2aa3f3"/><stop offset="1" stop-color="#6d3bf7"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="none" viewBox="0 0 90 90"><path fill="#d9d9d9" d="M78.5 49.252c0 21.25-14.875 31.875-32.555 38.037a4.25 4.25 0 0 1-2.848-.042C25.375 81.127 10.5 70.502 10.5 49.252v-29.75a4.25 4.25 0 0 1 4.25-4.25c8.5 0 19.125-5.1 26.52-11.56a4.97 4.97 0 0 1 6.46 0c7.438 6.502 18.02 11.56 26.52 11.56a4.25 4.25 0 0 1 4.25 4.25z"/><path fill="url(#moderator-a)" d="M78.5 49.252c0 21.25-14.875 31.875-32.555 38.037a4.25 4.25 0 0 1-2.848-.042C25.375 81.127 10.5 70.502 10.5 49.252v-29.75a4.25 4.25 0 0 1 4.25-4.25c8.5 0 19.125-5.1 26.52-11.56a4.97 4.97 0 0 1 6.46 0c7.438 6.502 18.02 11.56 26.52 11.56a4.25 4.25 0 0 1 4.25 4.25z"/><path fill="#000" fill-opacity=".6" d="M78.5 49.252c0 21.25-14.875 31.875-32.555 38.037a4.25 4.25 0 0 1-2.848-.042C25.375 81.127 10.5 70.502 10.5 49.252v-29.75a4.25 4.25 0 0 1 4.25-4.25c8.5 0 19.125-5.1 26.52-11.56a4.97 4.97 0 0 1 6.46 0c7.438 6.502 18.02 11.56 26.52 11.56a4.25 4.25 0 0 1 4.25 4.25z"/><path fill="#000" d="M77 19.502a2.75 2.75 0 0 0-2.75-2.75c-8.98 0-19.891-5.278-27.494-11.92l-.244-.19a3.47 3.47 0 0 0-4.268.19c-7.44 6.495-18.172 11.753-27.071 11.916l-.423.004a2.75 2.75 0 0 0-2.75 2.75v29.75c0 10.229 3.562 17.822 9.226 23.623 5.526 5.66 13.117 9.675 21.544 12.668l.817.286.034.012c.59.22 1.237.23 1.834.03 8.731-3.044 16.624-7.142 22.323-12.99C73.438 67.077 77 59.482 77 49.253zm3 29.75c0 11.021-3.875 19.364-10.073 25.724-6.158 6.317-14.541 10.611-23.489 13.73l-.011.004a5.75 5.75 0 0 1-3.82-.046c-8.97-3.097-17.364-7.38-23.528-13.693C12.875 68.616 9 60.273 9 49.252v-29.75a5.75 5.75 0 0 1 5.75-5.75l.758-.015c7.897-.292 17.77-5.055 24.775-11.174l.013-.011a6.47 6.47 0 0 1 8.185-.183l.223.183.014.01c7.271 6.358 17.516 11.19 25.532 11.19a5.75 5.75 0 0 1 5.75 5.75z"/><path fill="url(#moderator-b)" d="M77 19.502a2.75 2.75 0 0 0-2.75-2.75c-8.98 0-19.891-5.278-27.494-11.92l-.244-.19a3.47 3.47 0 0 0-4.268.19c-7.44 6.495-18.172 11.753-27.071 11.916l-.423.004a2.75 2.75 0 0 0-2.75 2.75v29.75c0 10.229 3.562 17.822 9.226 23.623 5.526 5.66 13.117 9.675 21.544 12.668l.817.286.034.012c.59.22 1.237.23 1.834.03 8.731-3.044 16.624-7.142 22.323-12.99C73.438 67.077 77 59.482 77 49.253zm3 29.75c0 11.021-3.875 19.364-10.073 25.724-6.158 6.317-14.541 10.611-23.489 13.73l-.011.004a5.75 5.75 0 0 1-3.82-.046c-8.97-3.097-17.364-7.38-23.528-13.693C12.875 68.616 9 60.273 9 49.252v-29.75a5.75 5.75 0 0 1 5.75-5.75l.758-.015c7.897-.292 17.77-5.055 24.775-11.174l.013-.011a6.47 6.47 0 0 1 8.185-.183l.223.183.014.01c7.271 6.358 17.516 11.19 25.532 11.19a5.75 5.75 0 0 1 5.75 5.75z"/><path fill="url(#moderator-c)" d="M41.75 19.002a2.75 2.75 0 0 1 5.5 0v3.44a39 39 0 0 0 17.25 4.033H67a2.75 2.75 0 1 1 0 5.5h-1.999l7.081 19.302a2.75 2.75 0 0 1-.909 3.13A15.07 15.07 0 0 1 62 57.528c-3.315 0-6.533-1.1-9.173-3.123a2.75 2.75 0 0 1-.909-3.129l7.201-19.632a44.5 44.5 0 0 1-11.869-3.162v33.769H57a2.75 2.75 0 1 1 0 5.5H32a2.75 2.75 0 0 1 0-5.5h9.75V28.483a44.5 44.5 0 0 1-11.87 3.162l7.202 19.632a2.75 2.75 0 0 1-.909 3.13A15.07 15.07 0 0 1 27 57.528c-3.315 0-6.533-1.1-9.173-3.123a2.75 2.75 0 0 1-.909-3.129L24 31.975H22a2.75 2.75 0 0 1 0-5.5h2.5a39 39 0 0 0 17.25-4.034zm-18.9 32.073a9.5 9.5 0 0 0 4.15.954c1.44 0 2.857-.328 4.15-.954L27 39.765zm35 0a9.5 9.5 0 0 0 4.15.954c1.44 0 2.856-.328 4.15-.954L62 39.765z"/><defs><linearGradient id="moderator-a" x1="32.26" x2="84.247" y1="20.057" y2="133.125" gradientUnits="userSpaceOnUse"><stop stop-color="#ffb66c"/><stop offset=".298" stop-color="#ffa347"/><stop offset="1" stop-color="#ff5100"/></linearGradient><linearGradient id="moderator-b" x1="32.26" x2="84.247" y1="20.057" y2="133.125" gradientUnits="userSpaceOnUse"><stop stop-color="#ffb66c"/><stop offset=".298" stop-color="#ffa347"/><stop offset="1" stop-color="#ff5100"/></linearGradient><linearGradient id="moderator-c" x1="35.5" x2="58" y1="28.502" y2="95.002" gradientUnits="userSpaceOnUse"><stop stop-color="#ffb66c"/><stop offset=".298" stop-color="#ffa347"/><stop offset="1" stop-color="#ff5100"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="none" viewBox="0 0 90 90"><path fill="url(#plus-a)" d="M72 57H58a1 1 0 0 0-1 1v14a7 7 0 0 1-7 7H40a7 7 0 0 1-7-7h3l.005.206A4 4 0 0 0 40 76h10l.206-.005A4 4 0 0 0 54 72V58a4 4 0 0 1 4-4h14l.206-.005A4 4 0 0 0 76 50V40a4 4 0 0 0-3.794-3.995L72 36H58a4 4 0 0 1-4-4V18a4 4 0 0 0-3.794-3.995L50 14H40a4 4 0 0 0-3.995 3.794L36 18h-3a7 7 0 0 1 7-7h10a7 7 0 0 1 7 7v14a1 1 0 0 0 1 1h14a7 7 0 0 1 7 7v10a7 7 0 0 1-7 7M33 18h3v14h-3zm-1 18H18a4 4 0 0 0-3.995 3.794L14 40h-3a7 7 0 0 1 7-7h14a1 1 0 0 0 1-1h3l-.005.206A4 4 0 0 1 32 36m1 22h3v14h-3zm-1-1H18a7 7 0 0 1-7-7h3a4 4 0 0 0 4 4h14l.206.005a4 4 0 0 1 3.79 3.789L36 58h-3a1 1 0 0 0-1-1M11 40h3v10h-3z"/><path fill="#000" fill-opacity=".4" d="M72 57H58a1 1 0 0 0-1 1v14a7 7 0 0 1-7 7H40a7 7 0 0 1-7-7h3l.005.206A4 4 0 0 0 40 76h10l.206-.005A4 4 0 0 0 54 72V58a4 4 0 0 1 4-4h14l.206-.005A4 4 0 0 0 76 50V40a4 4 0 0 0-3.794-3.995L72 36H58a4 4 0 0 1-4-4V18a4 4 0 0 0-3.794-3.995L50 14H40a4 4 0 0 0-3.995 3.794L36 18h-3a7 7 0 0 1 7-7h10a7 7 0 0 1 7 7v14a1 1 0 0 0 1 1h14a7 7 0 0 1 7 7v10a7 7 0 0 1-7 7M33 18h3v14h-3zm-1 18H18a4 4 0 0 0-3.995 3.794L14 40h-3a7 7 0 0 1 7-7h14a1 1 0 0 0 1-1h3l-.005.206A4 4 0 0 1 32 36m1 22h3v14h-3zm-1-1H18a7 7 0 0 1-7-7h3a4 4 0 0 0 4 4h14l.206.005a4 4 0 0 1 3.79 3.789L36 58h-3a1 1 0 0 0-1-1M11 40h3v10h-3z"/><path fill="url(#plus-b)" d="M58 54q-.105 0-.206.005A4 4 0 0 0 54 58h-3a7 7 0 0 1 6.64-6.991L58 51h13.97l.096-.002A1 1 0 0 0 73 50V40a1 1 0 0 0-.934-.997L71.97 39H58a7 7 0 0 1-7-7h3q0 .105.005.206A4 4 0 0 0 58 36h14l.206.005A4 4 0 0 1 76 40v10a4 4 0 0 1-3.794 3.995L72 54zM40 76a4 4 0 0 1-3.995-3.794L36 72h-3a7 7 0 0 0 7 7h10a7 7 0 0 0 7-7V58a1 1 0 0 1 1-1h14a7 7 0 0 0 7-7V40a7 7 0 0 0-7-7H58a1 1 0 0 1-1-1V18a7 7 0 0 0-7-7H40a7 7 0 0 0-7 7h3l.005-.206A4 4 0 0 1 40 14h10l.206.005A4 4 0 0 1 54 18v14h-3V18a1 1 0 0 0-.934-.997L49.97 17H40a1 1 0 0 0-.998.934L39 18.03v14.005l-.001.036-.006.246-.002.041A7 7 0 0 1 32 39H18a1 1 0 0 0-.998.934L17 40.03V50a1 1 0 0 0 .898.995L18 51h14.035l.036.001.246.006.041.002a7 7 0 0 1 6.633 6.633l.002.04.006.247.001.036V71.97l.002.097a1 1 0 0 0 .9.928L40 73h9.97l.096-.002A1 1 0 0 0 51 72V58h3v14a4 4 0 0 1-3.794 3.995L50 76zm-8-40a4 4 0 0 0 3.995-3.794L36 32V18h-3v14a1 1 0 0 1-1 1H18a7 7 0 0 0-7 7h3l.005-.206A4 4 0 0 1 18 36zm-2-6V18c0-5.35 4.202-9.72 9.485-9.987L40 8h10c5.523 0 10 4.477 10 10v12h12c5.523 0 10 4.477 10 10v10c0 5.523-4.477 10-10 10H60v12c0 5.523-4.477 10-10 10H40c-5.523 0-10-4.477-10-10h6V58h-3v14h-3V60H18c-5.523 0-10-4.477-10-10h3q0 .18.009.36A7 7 0 0 0 18 57h14a1 1 0 0 1 1 1h3l-.005-.206a4 4 0 0 0-3.789-3.79L32 54H18q-.105 0-.206-.005A4 4 0 0 1 14 50V40h-3v10H8V40l.013-.515C8.28 34.202 12.65 30 18 30z"/><path fill="#fff" fill-opacity=".4" d="M58 54q-.105 0-.206.005A4 4 0 0 0 54 58h-3a7 7 0 0 1 6.64-6.991L58 51h13.97l.096-.002A1 1 0 0 0 73 50V40a1 1 0 0 0-.934-.997L71.97 39H58a7 7 0 0 1-7-7h3q0 .105.005.206A4 4 0 0 0 58 36h14l.206.005A4 4 0 0 1 76 40v10a4 4 0 0 1-3.794 3.995L72 54zM40 76a4 4 0 0 1-3.995-3.794L36 72h-3a7 7 0 0 0 7 7h10a7 7 0 0 0 7-7V58a1 1 0 0 1 1-1h14a7 7 0 0 0 7-7V40a7 7 0 0 0-7-7H58a1 1 0 0 1-1-1V18a7 7 0 0 0-7-7H40a7 7 0 0 0-7 7h3l.005-.206A4 4 0 0 1 40 14h10l.206.005A4 4 0 0 1 54 18v14h-3V18a1 1 0 0 0-.934-.997L49.97 17H40a1 1 0 0 0-.998.934L39 18.03v14.005l-.001.036-.006.246-.002.041A7 7 0 0 1 32 39H18a1 1 0 0 0-.998.934L17 40.03V50a1 1 0 0 0 .898.995L18 51h14.035l.036.001.246.006.041.002a7 7 0 0 1 6.633 6.633l.002.04.006.247.001.036V71.97l.002.097a1 1 0 0 0 .9.928L40 73h9.97l.096-.002A1 1 0 0 0 51 72V58h3v14a4 4 0 0 1-3.794 3.995L50 76zm-8-40a4 4 0 0 0 3.995-3.794L36 32V18h-3v14a1 1 0 0 1-1 1H18a7 7 0 0 0-7 7h3l.005-.206A4 4 0 0 1 18 36zm-2-6V18c0-5.35 4.202-9.72 9.485-9.987L40 8h10c5.523 0 10 4.477 10 10v12h12c5.523 0 10 4.477 10 10v10c0 5.523-4.477 10-10 10H60v12c0 5.523-4.477 10-10 10H40c-5.523 0-10-4.477-10-10h6V58h-3v14h-3V60H18c-5.523 0-10-4.477-10-10h3q0 .18.009.36A7 7 0 0 0 18 57h14a1 1 0 0 1 1 1h3l-.005-.206a4 4 0 0 0-3.789-3.79L32 54H18q-.105 0-.206-.005A4 4 0 0 1 14 50V40h-3v10H8V40l.013-.515C8.28 34.202 12.65 30 18 30z"/><path fill="url(#plus-c)" d="M32 36a4 4 0 0 0 4-4V18a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4H58a4 4 0 0 0-4 4v14a4 4 0 0 1-4 4H40a4 4 0 0 1-4-4V58a4 4 0 0 0-4-4H18a4 4 0 0 1-4-4V40a4 4 0 0 1 4-4z"/><defs><linearGradient id="plus-a" x1="29" x2="55.5" y1="4.5" y2="85" gradientUnits="userSpaceOnUse"><stop stop-color="#d000ff"/><stop offset="1" stop-color="#7b00ff"/></linearGradient><linearGradient id="plus-b" x1="29" x2="55.5" y1="4.5" y2="85" gradientUnits="userSpaceOnUse"><stop stop-color="#d000ff"/><stop offset="1" stop-color="#2f00ff"/></linearGradient><linearGradient id="plus-c" x1="29" x2="55.5" y1="4.5" y2="85" gradientUnits="userSpaceOnUse"><stop stop-color="#da5fff"/><stop offset="1" stop-color="#7b00ff"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="none" viewBox="0 0 90 90"><path fill="url(#staff-a)" d="M85 45c0 22.091-17.909 40-40 40S5 67.091 5 45 22.909 5 45 5s40 17.909 40 40"/><path fill="#000" fill-opacity=".8" d="M85 45c0 22.091-17.909 40-40 40S5 67.091 5 45 22.909 5 45 5s40 17.909 40 40"/><path fill="url(#staff-b)" d="M85 45C85 22.909 67.091 5 45 5S5 22.909 5 45s17.909 40 40 40 40-17.909 40-40m3 0c0 23.748-19.252 43-43 43S2 68.748 2 45 21.252 2 45 2s43 19.252 43 43"/><path fill="url(#staff-c)" fill-rule="evenodd" d="M77.835 53.867A34.042 34.042 0 1 0 11.1 41.5h5.758a28.206 28.206 0 0 1 54.336-6.522l-5.595 1.497A22.33 22.33 0 0 0 51.682 23.75l-1.031 5.821a16.464 16.464 0 0 1 1.329 30.357l1.53 5.71a22.324 22.324 0 0 0 13.595-23.514l5.576-1.487c.59 3.72.416 7.52-.508 11.171z" clip-rule="evenodd"/><path fill="url(#staff-d)" d="M53.77 77.865A34.036 34.036 0 0 1 11 47.489h5.759A28.6 28.6 0 0 0 19.355 57l5.125-3.076a22.352 22.352 0 0 1 21.46-31.191l-1.042 5.82a16.5 16.5 0 0 0-15.87 20.736c.15.542.308 1.08.514 1.56l6.555-3.93-1.962-5.222 6.19-6.358 7.827-1.684 2.25 2.797-3.608 3.652-3.143.989-2.251 2.312 1.104 3.062s2.226 2.37 2.231 2.37l3.153-.834 2.24-2.462 4.895-1.555 1.44 3.282-5.029 6.186-8.465 2.673-3.8-4.228-6.618 3.978a16.53 16.53 0 0 0 13.782 5.562l1.53 5.725a22.36 22.36 0 0 1-20.384-8.24l-5.106 3.062a28.22 28.22 0 0 0 47.795-4.688l5.663 2.058a33.96 33.96 0 0 1-22.06 18.509"/><defs><linearGradient id="staff-a" x1="17.289" x2="58.84" y1="-6.043" y2="119.285" gradientUnits="userSpaceOnUse"><stop offset=".103" stop-color="#1bd96a"/><stop offset=".321" stop-color="#4ef092"/><stop offset=".66" stop-color="#1bd96a"/><stop offset="1" stop-color="#0e7338"/></linearGradient><linearGradient id="staff-b" x1="10.553" x2="72.296" y1="-6.667" y2="93.318" gradientUnits="userSpaceOnUse"><stop stop-color="#1bd9aa"/><stop offset=".5" stop-color="#1bd96a"/><stop offset="1" stop-color="#1bd951"/></linearGradient><linearGradient id="staff-c" x1="15.813" x2="53.404" y1="3.032" y2="78.68" gradientUnits="userSpaceOnUse"><stop stop-color="#1bd9aa"/><stop offset=".5" stop-color="#1bd96a"/><stop offset="1" stop-color="#1bd951"/></linearGradient><linearGradient id="staff-d" x1="15.5" x2="56" y1="14.5" y2="90" gradientUnits="userSpaceOnUse"><stop stop-color="#1bd9aa"/><stop offset=".5" stop-color="#1bd96a"/><stop offset="1" stop-color="#1bd951"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+22 -31
View File
@@ -495,12 +495,13 @@ a:not(.no-click-animation),
// CARDS
.base-card {
padding: var(--gap-xl);
padding: 1rem;
position: relative;
min-height: var(--font-size-2xl);
background-color: var(--color-raised-bg);
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;
@@ -526,19 +527,6 @@ a:not(.no-click-animation),
text-decoration: underline;
}
}
// TODO: Add back later
//&.warning {
// border-left: 0.5rem solid var(--color-warning-banner-side);
// background-color: var(--color-warning-banner-bg);
// color: var(--color-warning-banner-text);
//}
//
//&.information {
// border-left: 0.5rem solid var(--color-info-banner-side);
// background-color: var(--color-info-banner-bg);
// color: var(--color-info-banner-text);
//}
}
.card {
@@ -654,20 +642,20 @@ a:not(.no-click-animation),
.v-popper--theme-dropdown,
.v-popper--theme-dropdown.v-popper--theme-ribbit-popout {
.v-popper__inner {
border: 1px solid var(--color-divider) !important;
border: 1px solid var(--surface-5) !important;
padding: var(--gap-sm) !important;
width: fit-content !important;
border-radius: var(--radius-md) !important;
background-color: var(--color-raised-bg) !important;
box-shadow: var(--shadow-floating) !important;
border-radius: 12px !important;
background-color: var(--surface-3) !important;
box-shadow: 3px 3px 0.8rem rgba(0, 0, 0, 0.3) !important;
}
.v-popper__arrow-outer {
border-color: var(--color-divider) !important;
border-color: var(--surface-5) !important;
}
.v-popper__arrow-inner {
border-color: var(--color-raised-bg) !important;
border-color: var(--surface-3) !important;
}
}
@@ -711,22 +699,25 @@ a:not(.no-click-animation),
}
.v-popper--theme-tooltip {
pointer-events: none;
.v-popper__inner {
background: var(--color-tooltip-bg) !important;
color: var(--color-tooltip-text) !important;
padding: 0.5rem 0.5rem !important;
border-radius: var(--radius-sm) !important;
filter: drop-shadow(5px 5px 0.8rem rgba(0, 0, 0, 0.35));
background: var(--surface-3) !important;
color: var(--color-contrast) !important;
padding: 0.625rem 0.75rem !important;
border-radius: 12px !important;
filter: drop-shadow(2px 2px 0.4rem rgba(0, 0, 0, 0.5));
font-size: 0.9rem;
font-weight: bold;
font-weight: 500;
line-height: 1;
border: 1px solid var(--surface-5);
}
.v-popper__arrow-outer {
border-color: var(--surface-5) !important;
}
.v-popper__arrow-outer,
.v-popper__arrow-inner {
border-color: var(--color-tooltip-bg);
visibility: visible;
border-color: var(--surface-3) !important;
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
v-if="filteredLinks.length > 1"
ref="scrollContainer"
class="relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
:class="{ 'drop-shadow-xl': mode === 'navigation' }"
:class="{ 'drop-shadow-xl border border-solid border-surface-4': mode === 'navigation' }"
>
<template v-if="mode === 'navigation'">
<RouterLink
+1
View File
@@ -14,4 +14,5 @@ export * from './search'
export * from './servers'
export * from './settings'
export * from './skin'
export * from './user'
export * from './version'
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { ExternalIcon } from '@modrinth/assets'
import { Tooltip } from 'floating-vue'
import { type Component, useId } from 'vue'
import { type MessageDescriptor, useVIntl } from '#ui/composables/i18n.ts'
import AutoLink from '../base/AutoLink.vue'
const { formatMessage } = useVIntl()
defineProps<{
icon: Component
name: MessageDescriptor
about: MessageDescriptor[]
values?: Record<string, unknown>
link?: {
href: string
message: MessageDescriptor
}
}>()
const baseId = useId()
</script>
<template>
<Tooltip theme="tooltip" :triggers="['hover', 'focus']" :aria-id="`${baseId}-${name.id}`">
<AutoLink
:to="link?.href"
class="rounded-2xl flex"
:class="{
'hover:bg-surface-4 focus:bg-surface-4': !!link,
}"
target="_blank"
tabindex="0"
>
<component :is="icon" class="size-full p-0.5" />
</AutoLink>
<template #popper>
<div class="flex flex-col max-w-[22rem] leading-tight gap-0.5">
<span class="text-contrast mb-1">{{ formatMessage(name, values) }}</span>
<span v-for="message of about" :key="message.id" class="text-primary">
{{ formatMessage(message, values) }}
</span>
<span v-if="link" class="text-secondary text-xs opacity-80">
{{ formatMessage(link.message, values) }} <ExternalIcon />
</span>
</div>
</template>
</Tooltip>
</template>
@@ -0,0 +1,507 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
AlphaBadge,
BetaBadge,
Downloads1mBadge,
Downloads10mBadge,
Downloads25mBadge,
Downloads50mBadge,
Downloads100mBadge,
Downloads250mBadge,
Downloads500mBadge,
EarlyDatapackBadge,
EarlyHostingBadge,
EarlyModpackBadge,
EarlyPluginBadge,
EarlyResourcepackBadge,
EarlyServersBadge,
EarlyShadersBadge,
ModeratorBadge,
PlusBadge,
PrideBadge,
StaffBadge,
} from '@modrinth/assets'
import {
defineMessage,
defineMessages,
type MessageDescriptor,
useFormatNumber,
useVIntl,
} from '@modrinth/ui'
import { UserBadge as BadgeBitflag } from '@modrinth/utils'
import { type Component, computed } from 'vue'
import UserBadge from './UserBadge.vue'
const { formatMessage } = useVIntl()
const formatNumber = useFormatNumber()
type EarlyAdopterProjectTypes =
| 'modpack'
| 'resourcepack'
| 'plugin'
| 'datapack'
| 'shader'
| 'server'
type BadgeCriterion =
| {
type: 'earliest_project_date'
project_type: EarlyAdopterProjectTypes
cutoff: Date
}
| {
type: 'join_date'
cutoff: Date
}
| {
type: 'badge'
bitflag: number
}
| {
type: 'midas'
}
| {
type: 'pride'
}
| {
type: 'role'
role: Labrinth.Users.v3.Role
}
type Badge = {
icon: Component
name: MessageDescriptor
about: MessageDescriptor[]
criteria: BadgeCriterion[] // if any criterion matches, the badge will apply (OR logic)
link?: {
href: string
message: MessageDescriptor
}
}
const BADGES = [
{
icon: StaffBadge,
name: defineMessage({
id: 'user.profile.badge.staff.name',
defaultMessage: 'Modrinth Team',
}),
about: [
defineMessage({
id: 'user.profile.badge.staff.about.1',
defaultMessage: `This user works for Modrinth.`,
}),
],
criteria: [
{
type: 'role',
role: 'admin',
},
{
type: 'role',
role: 'moderator',
},
],
},
{
icon: ModeratorBadge,
name: defineMessage({
id: 'user.profile.badge.moderator.name',
defaultMessage: 'Content Moderator',
}),
about: [
defineMessage({
id: 'user.profile.badge.moderator.about.1',
defaultMessage: `This user works for Modrinth as a Content Moderator.`,
}),
defineMessage({
id: 'user.profile.badge.moderator.about.2',
defaultMessage: `Content Moderators on Modrinth review projects, handle reports, and help keep Modrinth safe.`,
}),
],
criteria: [
{
type: 'role',
role: 'moderator',
},
],
},
{
icon: AlphaBadge,
name: defineMessage({
id: 'user.profile.badge.alpha.name',
defaultMessage: 'Alpha Tester',
}),
about: [
defineMessage({
id: 'user.profile.badge.alpha.about.1',
defaultMessage: `This user has been around since Modrinth Alpha, which ended in November 2020`,
}),
],
criteria: [
{
type: 'badge',
bitflag: BadgeBitflag.ALPHA_TESTER,
},
{
type: 'join_date',
cutoff: new Date('2020-11-30T08:00:00.000Z'),
},
],
},
{
icon: BetaBadge,
name: defineMessage({
id: 'user.profile.badge.beta.name',
defaultMessage: 'Beta Tester',
}),
about: [
defineMessage({
id: 'user.profile.badge.beta.about.1',
defaultMessage: `This user has been around since Modrinth Beta, which ended in February 2022.`,
}),
],
criteria: [
{
type: 'join_date',
cutoff: new Date('2022-02-27T08:00:00.000Z'),
},
],
link: {
href: 'https://modrinth.com/news/article/modrinth-beta/',
message: defineMessage({
id: 'user.profile.badge.beta.link',
defaultMessage: `Click to read about the launch of Modrinth Beta.`,
}),
},
},
{
icon: PlusBadge,
name: defineMessage({
id: 'user.profile.badge.plus.name',
defaultMessage: 'Modrinth+ Member',
}),
about: [
defineMessage({
id: 'user.profile.badge.plus.about.1',
defaultMessage: `This user is going the extra mile to support Modrinth and the creators on the platform.`,
}),
],
criteria: [
{
type: 'badge',
bitflag: BadgeBitflag.MIDAS,
},
{
type: 'midas',
},
],
link: {
href: 'https://modrinth.com/plus',
message: defineMessage({
id: 'user.profile.badge.plus.link',
defaultMessage: `Click to learn more about how you can become a member.`,
}),
},
},
{
icon: PrideBadge,
name: defineMessage({
id: 'user.profile.badge.pride.name',
defaultMessage: 'Pride Fundraiser Supporter',
}),
about: [
defineMessage({
id: 'user.profile.badge.pride.about.1',
defaultMessage: `This user participated in at least one of Modrinth's Pride fundraisers for the LGBTQ+ community.`,
}),
],
criteria: [
{
type: 'pride',
},
],
link: {
href: 'https://modrinth.com/pride',
message: defineMessage({
id: 'user.profile.badge.pride.link',
defaultMessage: `Click to visit our latest Pride fundraiser.`,
}),
},
},
{
icon: EarlyModpackBadge,
name: defineMessage({
id: 'user.profile.badge.early-modpack-adopter.name',
defaultMessage: 'Early Modpack Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-modpack-adopter.about.1',
defaultMessage: `This user helped us test Modpack projects on Modrinth before we launched them in May 2022.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'modpack',
cutoff: new Date('2022-05-23T00:57:00.000Z'),
},
{
type: 'badge',
bitflag: BadgeBitflag.EARLY_MODPACK_ADOPTER,
},
],
},
{
icon: EarlyResourcepackBadge,
name: defineMessage({
id: 'user.profile.badge.early-resourcepack-adopter.name',
defaultMessage: 'Early Resource Pack Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-resourcepack-adopter.about.1',
defaultMessage: `This user helped us test Resource Pack projects on Modrinth before we launched them in August 2022.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'resourcepack',
cutoff: new Date('2022-08-27T23:03:00.000Z'),
},
{
type: 'badge',
bitflag: BadgeBitflag.EARLY_RESPACK_ADOPTER,
},
],
},
{
icon: EarlyPluginBadge,
name: defineMessage({
id: 'user.profile.badge.early-plugin-adopter.name',
defaultMessage: 'Early Plugin Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-plugin-adopter.about.1',
defaultMessage: `This user helped us test Plugin projects on Modrinth before we launched them in August 2022.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'plugin',
cutoff: new Date('2022-08-27T23:03:00.000Z'),
},
{
type: 'badge',
bitflag: BadgeBitflag.EARLY_PLUGIN_ADOPTER,
},
],
},
{
icon: EarlyDatapackBadge,
name: defineMessage({
id: 'user.profile.badge.early-datapack-adopter.name',
defaultMessage: 'Early Data Pack Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-datapack-adopter.about.1',
defaultMessage: `This user helped us test Data Pack projects on Modrinth before we launched them in January 2023.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'datapack',
cutoff: new Date('2023-01-08T02:00:00.000Z'),
},
],
},
{
icon: EarlyShadersBadge,
name: defineMessage({
id: 'user.profile.badge.early-shader-adopter.name',
defaultMessage: 'Early Shader Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-shader-adopter.about.1',
defaultMessage: `This user helped us test Shader projects on Modrinth before we launched them in January 2023.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'shader',
cutoff: new Date('2023-01-08T02:00:00.000Z'),
},
],
},
{
icon: EarlyServersBadge,
name: defineMessage({
id: 'user.profile.badge.early-server-adopter.name',
defaultMessage: 'Early Server Adopter',
}),
about: [
defineMessage({
id: 'user.profile.badge.early-server-adopter.about.1',
defaultMessage: `This user helped us test Server projects on Modrinth before we launched them in March 2026.`,
}),
],
criteria: [
{
type: 'earliest_project_date',
project_type: 'server',
cutoff: new Date('2026-03-04T01:33:00.000Z'),
},
],
},
{
icon: EarlyHostingBadge,
name: defineMessage({
id: 'user.profile.badge.hosting-alpha.name',
defaultMessage: 'Modrinth Hosting Alpha Tester',
}),
about: [
defineMessage({
id: 'user.profile.badge.hosting-alpha.about.1',
defaultMessage: `This user participated in a closed alpha test of Modrinth Hosting before we launched Modrinth Hosting Beta in November 2024`,
}),
],
criteria: [], // TODO: Add badge on backend for Hosting Alpha Tester
},
] satisfies Badge[]
const DOWNLOAD_BADGES = [
{
icon: Downloads1mBadge,
threshold: 1_000_000,
},
{
icon: Downloads10mBadge,
threshold: 10_000_000,
},
{
icon: Downloads25mBadge,
threshold: 25_000_000,
},
{
icon: Downloads50mBadge,
threshold: 50_000_000,
},
{
icon: Downloads100mBadge,
threshold: 100_000_000,
},
{
icon: Downloads250mBadge,
threshold: 250_000_000,
},
{
icon: Downloads500mBadge,
threshold: 500_000_000,
},
].sort((a, b) => b.threshold - a.threshold)
const props = defineProps<{
role: Labrinth.Users.v2.Role
badges: number
hasMidas?: boolean
hasPride?: boolean
downloads: number
joinDate: Date
earliestProjectByType: Record<EarlyAdopterProjectTypes, Date>
}>()
const downloadsBadge = computed(() => {
return DOWNLOAD_BADGES.find((badge) => props.downloads >= badge.threshold)
})
const messages = defineMessages({
title: {
id: 'profile.label.badges',
defaultMessage: 'Badges',
},
downloadsBadgeName: {
id: 'user.profile.badge.downloads.name',
defaultMessage: '{download_sum} Downloads',
},
downloadsBadgeAbout1: {
id: 'user.profile.badge.downloads.about.1',
defaultMessage: `This user's projects have collectively achieved {download_sum} downloads.`,
},
})
function passesCriterion(criterion: BadgeCriterion) {
switch (criterion.type) {
case 'role': {
return props.role === criterion.role
}
case 'badge': {
return props.badges & criterion.bitflag
}
case 'midas': {
return props.hasMidas === true
}
case 'pride': {
return props.hasPride === true
}
case 'join_date': {
return props.joinDate < criterion.cutoff
}
case 'earliest_project_date': {
const date = props.earliestProjectByType[criterion.project_type]
return date && date < criterion.cutoff
}
default: {
return false
}
}
}
const earnedBadges = computed(() => {
const badges: Badge[] = []
loopingBadges: for (const badge of BADGES) {
for (const criterion of badge.criteria) {
if (passesCriterion(criterion)) {
badges.push(badge)
continue loopingBadges
}
}
}
return badges
})
</script>
<template>
<div v-if="earnedBadges.length > 0 || !!downloadsBadge" class="flex flex-col">
<h2 class="text-lg text-contrast m-0 mb-2">
{{ formatMessage(messages.title) }}
</h2>
<div class="grid grid-cols-[repeat(auto-fill,minmax(64px,1fr))] gap-2">
<UserBadge
v-for="badge in earnedBadges"
:key="badge.name.id"
:name="badge.name"
:icon="badge.icon"
:about="badge.about"
:link="badge.link"
/>
<UserBadge
v-if="downloadsBadge"
:name="messages.downloadsBadgeName"
:icon="downloadsBadge.icon"
:about="[messages.downloadsBadgeAbout1]"
:values="{ download_sum: formatNumber(downloadsBadge.threshold) }"
/>
</div>
</div>
</template>
+1
View File
@@ -0,0 +1 @@
export { default as UserBadges } from './UserBadges.vue'
@@ -30,14 +30,14 @@ const filterClass = computed(() => {
if (ctx.filtersMenuOpen?.value) {
return 'border-0 border-b-[1px] border-solid border-divider last:border-b-0'
}
return 'card-shadow rounded-2xl bg-bg-raised'
return 'card-shadow rounded-2xl bg-surface-3 border border-solid border-surface-4'
})
const buttonClass = computed(() => {
if (isApp.value) {
return 'button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg'
return 'button-animation flex flex-col gap-1 px-3 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg'
}
return 'button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none'
return 'button-animation flex flex-col gap-1 px-6 py-3 w-full bg-transparent cursor-pointer border-none'
})
const contentClass = computed(() => (isApp.value ? 'mt-2 mb-3' : 'mb-4 mx-3'))
+99
View File
@@ -2423,6 +2423,9 @@
"payment-method.visa": {
"defaultMessage": "Visa"
},
"profile.label.badges": {
"defaultMessage": "Badges"
},
"project-card.date.published.tooltip": {
"defaultMessage": "Published {date}"
},
@@ -4483,5 +4486,101 @@
},
"ui.stacked-admonitions.dismiss-all": {
"defaultMessage": "Dismiss all"
},
"user.profile.badge.alpha.about.1": {
"defaultMessage": "This user has been around since Modrinth Alpha, which ended in November 2020"
},
"user.profile.badge.alpha.name": {
"defaultMessage": "Alpha Tester"
},
"user.profile.badge.beta.about.1": {
"defaultMessage": "This user has been around since Modrinth Beta, which ended in February 2022."
},
"user.profile.badge.beta.link": {
"defaultMessage": "Click to read about the launch of Modrinth Beta."
},
"user.profile.badge.beta.name": {
"defaultMessage": "Beta Tester"
},
"user.profile.badge.downloads.about.1": {
"defaultMessage": "This user's projects have collectively achieved {download_sum} downloads."
},
"user.profile.badge.downloads.name": {
"defaultMessage": "{download_sum} Downloads"
},
"user.profile.badge.early-datapack-adopter.about.1": {
"defaultMessage": "This user helped us test Data Pack projects on Modrinth before we launched them in January 2023."
},
"user.profile.badge.early-datapack-adopter.name": {
"defaultMessage": "Early Data Pack Adopter"
},
"user.profile.badge.early-modpack-adopter.about.1": {
"defaultMessage": "This user helped us test Modpack projects on Modrinth before we launched them in May 2022."
},
"user.profile.badge.early-modpack-adopter.name": {
"defaultMessage": "Early Modpack Adopter"
},
"user.profile.badge.early-plugin-adopter.about.1": {
"defaultMessage": "This user helped us test Plugin projects on Modrinth before we launched them in August 2022."
},
"user.profile.badge.early-plugin-adopter.name": {
"defaultMessage": "Early Plugin Adopter"
},
"user.profile.badge.early-resourcepack-adopter.about.1": {
"defaultMessage": "This user helped us test Resource Pack projects on Modrinth before we launched them in August 2022."
},
"user.profile.badge.early-resourcepack-adopter.name": {
"defaultMessage": "Early Resource Pack Adopter"
},
"user.profile.badge.early-server-adopter.about.1": {
"defaultMessage": "This user helped us test Server projects on Modrinth before we launched them in March 2026."
},
"user.profile.badge.early-server-adopter.name": {
"defaultMessage": "Early Server Adopter"
},
"user.profile.badge.early-shader-adopter.about.1": {
"defaultMessage": "This user helped us test Shader projects on Modrinth before we launched them in January 2023."
},
"user.profile.badge.early-shader-adopter.name": {
"defaultMessage": "Early Shader Adopter"
},
"user.profile.badge.hosting-alpha.about.1": {
"defaultMessage": "This user participated in a closed alpha test of Modrinth Hosting before we launched Modrinth Hosting Beta in November 2024"
},
"user.profile.badge.hosting-alpha.name": {
"defaultMessage": "Modrinth Hosting Alpha Tester"
},
"user.profile.badge.moderator.about.1": {
"defaultMessage": "This user works for Modrinth as a Content Moderator."
},
"user.profile.badge.moderator.about.2": {
"defaultMessage": "Content Moderators on Modrinth review projects, handle reports, and help keep Modrinth safe."
},
"user.profile.badge.moderator.name": {
"defaultMessage": "Content Moderator"
},
"user.profile.badge.plus.about.1": {
"defaultMessage": "This user is going the extra mile to support Modrinth and the creators on the platform."
},
"user.profile.badge.plus.link": {
"defaultMessage": "Click to learn more about how you can become a member."
},
"user.profile.badge.plus.name": {
"defaultMessage": "Modrinth+ Member"
},
"user.profile.badge.pride.about.1": {
"defaultMessage": "This user participated in at least one of Modrinth's Pride fundraisers for the LGBTQ+ community."
},
"user.profile.badge.pride.link": {
"defaultMessage": "Click to visit our latest Pride fundraiser."
},
"user.profile.badge.pride.name": {
"defaultMessage": "Pride Fundraiser Supporter"
},
"user.profile.badge.staff.about.1": {
"defaultMessage": "This user works for Modrinth."
},
"user.profile.badge.staff.name": {
"defaultMessage": "Modrinth Team"
}
}
+3 -1
View File
@@ -3,9 +3,11 @@ import type { Ref } from 'vue'
import { createContext } from './create-context'
export type AuthUser = Labrinth.Users.v2.User | Labrinth.Users.v3.User
export interface AuthProvider {
session_token: Ref<string | null>
user: Ref<Labrinth.Users.v2.User | null>
user: Ref<AuthUser | null>
/** True once the initial auth check has completed (regardless of result). */
isReady?: Ref<boolean>
requestSignIn: (redirectPath: string) => void | Promise<void>
+3
View File
@@ -15,6 +15,9 @@ export default defineConfig({
params: {
overrides: {
removeViewBox: false,
cleanupIds: {
minify: false,
},
},
},
},