fix: remove legacy button classes (#7011)

* fix: remove legacy button-group class for tailwind (conflicts)

* fix: delete legacy button classes from scss

* fix: fmt
This commit is contained in:
Calum H.
2026-08-05 16:34:00 +02:00
committed by GitHub
parent c0e6f094d9
commit 0038ea8b15
23 changed files with 162 additions and 564 deletions
+1 -182
View File
@@ -93,7 +93,7 @@
} }
} }
:where(button:not([data-button]), .button, .iconified-button) { :where(button:not([data-button])) {
width: fit-content; width: fit-content;
} }
@@ -118,7 +118,6 @@
gap: var(--spacing-card-sm); gap: var(--spacing-card-sm);
margin-bottom: calc(var(--spacing-card-sm) + var(--spacing-card-md)); margin-bottom: calc(var(--spacing-card-sm) + var(--spacing-card-md));
.iconified-button,
.input-group { .input-group {
flex-shrink: 0; flex-shrink: 0;
} }
@@ -250,185 +249,6 @@
} }
} }
.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);
}
.button-group {
display: flex;
grid-gap: var(--spacing-card-sm);
flex-wrap: wrap;
margin-top: var(--spacing-card-md);
justify-content: right;
}
.error { .error {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -501,7 +321,6 @@ textarea.known-error {
} }
@media (prefers-reduced-motion) { @media (prefers-reduced-motion) {
.button-animation,
button { button {
transform: none !important; transform: none !important;
} }
@@ -475,18 +475,6 @@ input {
outline: 2px solid transparent !important; 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 { .qc-cmp2-close-tooltip {
background-color: transparent; background-color: transparent;
color: hsl(145, 78%, 28%); color: hsl(145, 78%, 28%);
-103
View File
@@ -1,103 +0,0 @@
<template>
<div class="chips">
<button
v-for="item in items"
:key="item"
class="iconified-button"
:class="{ selected: selected === item, capitalize: capitalize }"
@click="toggleItem(item)"
>
<CheckIcon v-if="selected === item" />
<span>{{ formatLabel(item) }}</span>
</button>
</div>
</template>
<script>
import { CheckIcon } from '@modrinth/assets'
export default {
components: {
CheckIcon,
},
props: {
modelValue: {
required: true,
type: String,
},
items: {
required: true,
type: Array,
},
neverEmpty: {
default: true,
type: Boolean,
},
formatLabel: {
default: (x) => x,
type: Function,
},
capitalize: {
type: Boolean,
default: true,
},
},
emits: ['update:modelValue'],
computed: {
selected: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
},
},
},
created() {
if (this.items.length > 0 && this.neverEmpty) {
this.selected = this.items[0]
}
},
methods: {
toggleItem(item) {
if (this.selected === item && !this.neverEmpty) {
this.selected = null
} else {
this.selected = item
}
},
},
}
</script>
<style lang="scss" scoped>
.chips {
display: flex;
grid-gap: 0.5rem;
flex-wrap: wrap;
.iconified-button {
&.capitalize {
text-transform: capitalize;
}
svg {
width: 1em;
height: 1em;
}
&:focus-visible {
outline: 0.25rem solid var(--color-focus-ring);
border-radius: 0.25rem;
}
}
.selected {
color: var(--color-button-text-active);
background-color: var(--color-brand-highlight);
box-shadow:
inset 0 0 0 transparent,
0 0 0 2px var(--color-brand);
}
}
</style>
@@ -7,7 +7,7 @@ import {
ToggleRightIcon, ToggleRightIcon,
TwitterIcon, TwitterIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { ButtonLink } from '@modrinth/ui' import { Button, ButtonLink } from '@modrinth/ui'
import { import {
AutoLink, AutoLink,
defineMessage, defineMessage,
@@ -266,11 +266,17 @@ function developerModeIncrement() {
:aria-label="formatMessage(messages.modrinthInformation)" :aria-label="formatMessage(messages.modrinthInformation)"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<TextLogo <Button
aria-hidden="true" type="quiet"
class="text-logo button-base h-6 w-auto text-contrast lg:h-8" interaction="none"
aria-label="Modrinth"
class="!h-auto !p-0"
@click="developerModeIncrement()" @click="developerModeIncrement()"
/> >
<span class="inline-flex">
<TextLogo aria-hidden="true" class="text-logo h-6 w-auto text-contrast lg:h-8" />
</span>
</Button>
<ButtonLink <ButtonLink
v-if="flags.developerMode" v-if="flags.developerMode"
v-tooltip="formatMessage(commonSettingsMessages.featureFlags)" v-tooltip="formatMessage(commonSettingsMessages.featureFlags)"
+12 -7
View File
@@ -17,10 +17,12 @@
{{ item.label }} {{ item.label }}
</div> </div>
<NuxtLink <ButtonLink
v-else-if="item.link ?? item.to" v-else-if="item.link ?? item.to"
type="quiet"
interaction="surface"
:to="(item.link ?? item.to) as string" :to="(item.link ?? item.to) as string"
class="nav-item inline-flex w-full cursor-pointer items-center gap-2 rounded-xl border-none bg-transparent px-4 py-2.5 text-left text-base font-semibold leading-tight text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97]" class="nav-item !h-auto !w-full !justify-start !rounded-xl !px-4 !py-2.5 text-left leading-tight"
:class="{ 'is-active': isActive(item as NavStackLinkItem) }" :class="{ 'is-active': isActive(item as NavStackLinkItem) }"
> >
<component <component
@@ -37,12 +39,14 @@
{{ String(item.badge) }} {{ String(item.badge) }}
</span> </span>
<span v-if="item.chevron" class="ml-auto"><ChevronRightIcon /></span> <span v-if="item.chevron" class="ml-auto"><ChevronRightIcon /></span>
</NuxtLink> </ButtonLink>
<button <Button
v-else-if="item.action" v-else-if="item.action"
class="nav-item inline-flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2.5 text-left text-base font-semibold leading-tight text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97]" type="quiet"
:class="{ 'danger-button': item.danger }" interaction="surface"
:color="item.danger ? 'red' : undefined"
class="nav-item !h-auto !w-full !justify-start !rounded-xl !px-4 !py-2.5 text-left leading-tight"
@click="item.action" @click="item.action"
> >
<component <component
@@ -58,7 +62,7 @@
> >
{{ String(item.badge) }} {{ String(item.badge) }}
</span> </span>
</button> </Button>
<span v-else>You frog. 🐸</span> <span v-else>You frog. 🐸</span>
</li> </li>
@@ -69,6 +73,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChevronRightIcon } from '@modrinth/assets' import { ChevronRightIcon } from '@modrinth/assets'
import { Button, ButtonLink } from '@modrinth/ui'
import { type Component, computed, useSlots } from 'vue' import { type Component, computed, useSlots } from 'vue'
type NavStackBaseItem = { type NavStackBaseItem = {
@@ -1,128 +0,0 @@
<template>
<nav
ref="scrollContainer"
class="card-shadow relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
>
<button
v-for="(option, index) in options"
:key="`option-group-${index}`"
ref="optionButtons"
class="button-animation z-[1] flex flex-row items-center gap-2 rounded-full bg-transparent px-4 py-2 font-semibold"
:class="{
'text-button-textSelected': modelValue === option,
'text-primary': modelValue !== option,
}"
@click="setOption(option)"
>
<slot :option="option" :selected="modelValue === option" />
</button>
<div
class="navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full bg-button-bgSelected p-1"
:style="{
left: sliderLeftPx,
top: sliderTopPx,
right: sliderRightPx,
bottom: sliderBottomPx,
opacity: initialized ? 1 : 0,
}"
aria-hidden="true"
></div>
</nav>
</template>
<script setup lang="ts" generic="T">
import { computed, onMounted, ref } from 'vue'
const modelValue = defineModel<T>({ required: true })
const props = defineProps<{
options: T[]
}>()
const scrollContainer = ref<HTMLElement | null>(null)
const sliderLeft = ref(4)
const sliderTop = ref(4)
const sliderRight = ref(4)
const sliderBottom = ref(4)
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
const sliderTopPx = computed(() => `${sliderTop.value}px`)
const sliderRightPx = computed(() => `${sliderRight.value}px`)
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
const optionButtons = ref()
const initialized = ref(false)
function setOption(option: T) {
modelValue.value = option
}
watch(modelValue, () => {
startAnimation(props.options.indexOf(modelValue.value))
})
function startAnimation(index: number) {
const el = optionButtons.value[index]
if (!el || !el.offsetParent) return
const newValues = {
left: el.offsetLeft,
top: el.offsetTop,
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
}
if (sliderLeft.value === 4 && sliderRight.value === 4) {
sliderLeft.value = newValues.left
sliderRight.value = newValues.right
sliderTop.value = newValues.top
sliderBottom.value = newValues.bottom
} else {
const delay = 200
if (newValues.left < sliderLeft.value) {
sliderLeft.value = newValues.left
setTimeout(() => {
sliderRight.value = newValues.right
}, delay)
} else {
sliderRight.value = newValues.right
setTimeout(() => {
sliderLeft.value = newValues.left
}, delay)
}
if (newValues.top < sliderTop.value) {
sliderTop.value = newValues.top
setTimeout(() => {
sliderBottom.value = newValues.bottom
}, delay)
} else {
sliderBottom.value = newValues.bottom
setTimeout(() => {
sliderTop.value = newValues.top
}, delay)
}
}
initialized.value = true
}
onMounted(() => {
startAnimation(props.options.indexOf(modelValue.value))
})
</script>
<style scoped>
.navtabs-transition {
transition:
all 150ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
}
.card-shadow {
box-shadow: var(--shadow-card);
}
</style>
@@ -6,7 +6,7 @@
</p> </p>
<p v-else>{{ getFormattedMessage(messages.invitationNoRole) }}</p> <p v-else>{{ getFormattedMessage(messages.invitationNoRole) }}</p>
<div class="input-group"> <div class="input-group">
<Button type="colored" color="brand" class="brand-button" @click="acceptInvite()"> <Button type="colored" color="brand" @click="acceptInvite()">
<CheckIcon /> <CheckIcon />
{{ getFormattedMessage(commonMessages.acceptButton) }} {{ getFormattedMessage(commonMessages.acceptButton) }}
</Button> </Button>
@@ -275,7 +275,6 @@
{{ formatMessage(messages.actionApprove) }} {{ formatMessage(messages.actionApprove) }}
</Button> </Button>
<SplitButton <SplitButton
class="!m-0 !inline-flex !flex-nowrap !justify-start !gap-0"
type="colored" type="colored"
color="red" color="red"
:menu-label="formatMessage(commonMessages.moreOptionsButton)" :menu-label="formatMessage(commonMessages.moreOptionsButton)"
+66 -40
View File
@@ -629,14 +629,14 @@
@focusout="isBrowseMenuOpen = false" @focusout="isBrowseMenuOpen = false"
> >
<div class="links cascade-links"> <div class="links cascade-links">
<NuxtLink <ButtonLink
v-for="navRoute in navRoutes" v-for="navRoute in navRoutes"
:key="navRoute.href" :key="navRoute.href"
:to="navRoute.href" :to="navRoute.href"
class="iconified-button" class="!h-auto !whitespace-normal"
> >
{{ navRoute.label }} {{ navRoute.label }}
</NuxtLink> </ButtonLink>
</div> </div>
</div> </div>
<div <div
@@ -646,10 +646,10 @@
@focusout="isMobileMenuOpen = false" @focusout="isMobileMenuOpen = false"
> >
<div class="account-container"> <div class="account-container">
<NuxtLink <ButtonLink
v-if="auth.user" v-if="auth.user"
:to="`/user/${auth.user.username}`" :to="`/user/${auth.user.username}`"
class="iconified-button account-button" class="account-button !h-auto !whitespace-normal"
> >
<Avatar <Avatar
:src="auth.user.avatar_url" :src="auth.user.avatar_url"
@@ -662,66 +662,85 @@
<div>@{{ auth.user.username }}</div> <div>@{{ auth.user.username }}</div>
<div>{{ formatMessage(commonMessages.visitYourProfile) }}</div> <div>{{ formatMessage(commonMessages.visitYourProfile) }}</div>
</div> </div>
</NuxtLink> </ButtonLink>
<nuxt-link v-else class="iconified-button brand-button" :to="signInRouteObj"> <ButtonLink
v-else
type="colored"
color="brand"
:to="signInRouteObj"
class="!h-auto !whitespace-normal"
>
<LogInIcon aria-hidden="true" /> {{ formatMessage(commonMessages.signInButton) }} <LogInIcon aria-hidden="true" /> {{ formatMessage(commonMessages.signInButton) }}
</nuxt-link> </ButtonLink>
</div> </div>
<div class="links"> <div class="links">
<template v-if="auth.user"> <template v-if="auth.user">
<button class="iconified-button danger-button" @click="logoutUser()"> <Button
type="colored"
color="red"
class="!h-auto !whitespace-normal"
@click="logoutUser()"
>
<LogOutIcon aria-hidden="true" /> <LogOutIcon aria-hidden="true" />
{{ formatMessage(commonMessages.signOutButton) }} {{ formatMessage(commonMessages.signOutButton) }}
</button> </Button>
<button class="iconified-button" @click="$refs.modal_creation.show()"> <Button class="!h-auto !whitespace-normal" @click="$refs.modal_creation.show()">
<PlusIcon aria-hidden="true" /> <PlusIcon aria-hidden="true" />
{{ formatMessage(commonMessages.createAProjectButton) }} {{ formatMessage(commonMessages.createAProjectButton) }}
</button> </Button>
<NuxtLink class="iconified-button" to="/dashboard/collections"> <ButtonLink class="!h-auto !whitespace-normal" to="/dashboard/collections">
<LibraryIcon class="icon" /> <LibraryIcon class="icon" />
{{ formatMessage(commonMessages.collectionsLabel) }} {{ formatMessage(commonMessages.collectionsLabel) }}
</NuxtLink> </ButtonLink>
<NuxtLink class="iconified-button" to="/hosting/manage"> <ButtonLink class="!h-auto !whitespace-normal" to="/hosting/manage">
<ServerIcon class="icon" /> <ServerIcon class="icon" />
{{ formatMessage(commonMessages.serversLabel) }} {{ formatMessage(commonMessages.serversLabel) }}
</NuxtLink> </ButtonLink>
<NuxtLink <ButtonLink
v-if="auth.user.role === 'moderator' || auth.user.role === 'admin'" v-if="auth.user.role === 'moderator' || auth.user.role === 'admin'"
class="iconified-button" class="!h-auto !whitespace-normal"
to="/moderation" to="/moderation"
> >
<ScaleIcon aria-hidden="true" /> <ScaleIcon aria-hidden="true" />
{{ formatMessage(commonMessages.moderationLabel) }} {{ formatMessage(commonMessages.moderationLabel) }}
</NuxtLink> </ButtonLink>
<NuxtLink v-if="flags.developerMode" class="iconified-button" to="/settings/flags"> <ButtonLink
v-if="flags.developerMode"
class="!h-auto !whitespace-normal"
to="/settings/flags"
>
<ToggleRightIcon aria-hidden="true" /> <ToggleRightIcon aria-hidden="true" />
{{ formatMessage(commonSettingsMessages.featureFlags) }} {{ formatMessage(commonSettingsMessages.featureFlags) }}
</NuxtLink> </ButtonLink>
</template> </template>
<NuxtLink class="iconified-button" to="/settings"> <ButtonLink class="!h-auto !whitespace-normal" to="/settings">
<SettingsIcon aria-hidden="true" /> <SettingsIcon aria-hidden="true" />
{{ formatMessage(commonMessages.settingsLabel) }} {{ formatMessage(commonMessages.settingsLabel) }}
</NuxtLink> </ButtonLink>
<button class="iconified-button" @click="changeTheme"> <Button class="!h-auto !whitespace-normal" @click="changeTheme">
<MoonIcon v-if="$theme.active === 'light'" class="icon" /> <MoonIcon v-if="$theme.active === 'light'" class="icon" />
<SunIcon v-else class="icon" /> <SunIcon v-else class="icon" />
<span class="dropdown-item__text"> <span class="dropdown-item__text">
{{ formatMessage(messages.changeTheme) }} {{ formatMessage(messages.changeTheme) }}
</span> </span>
</button> </Button>
</div> </div>
</div> </div>
<div class="mobile-navbar" :class="{ expanded: isBrowseMenuOpen || isMobileMenuOpen }"> <div class="mobile-navbar" :class="{ expanded: isBrowseMenuOpen || isMobileMenuOpen }">
<NuxtLink <ButtonLink
to="/" to="/"
class="tab button-animation" type="quiet"
interaction="none"
class="tab !h-auto !rounded-none !px-0"
:title="formatMessage(navMenuMessages.home)" :title="formatMessage(navMenuMessages.home)"
:aria-label="formatMessage(navMenuMessages.home)" :aria-label="formatMessage(navMenuMessages.home)"
> >
<HomeIcon aria-hidden="true" /> <HomeIcon aria-hidden="true" />
</NuxtLink> </ButtonLink>
<button <Button
class="tab button-animation" type="quiet"
interaction="none"
class="tab !h-auto !rounded-none !px-0"
:class="{ 'router-link-exact-active': isBrowseMenuOpen }" :class="{ 'router-link-exact-active': isBrowseMenuOpen }"
:title="formatMessage(navMenuMessages.search)" :title="formatMessage(navMenuMessages.search)"
:aria-label="formatMessage(navMenuMessages.search)" :aria-label="formatMessage(navMenuMessages.search)"
@@ -734,11 +753,13 @@
<SearchIcon aria-hidden="true" class="smaller" /> <SearchIcon aria-hidden="true" class="smaller" />
{{ formatMessage(navMenuMessages.search) }} {{ formatMessage(navMenuMessages.search) }}
</template> </template>
</button> </Button>
<template v-if="auth.user"> <template v-if="auth.user">
<NuxtLink <ButtonLink
to="/dashboard/notifications" to="/dashboard/notifications"
class="tab button-animation" type="quiet"
interaction="none"
class="tab !h-auto !rounded-none !px-0"
:aria-label="formatMessage(commonMessages.notificationsLabel)" :aria-label="formatMessage(commonMessages.notificationsLabel)"
:class="{ :class="{
'no-active': isMobileMenuOpen || isBrowseMenuOpen, 'no-active': isMobileMenuOpen || isBrowseMenuOpen,
@@ -752,18 +773,22 @@
" "
> >
<BellIcon aria-hidden="true" /> <BellIcon aria-hidden="true" />
</NuxtLink> </ButtonLink>
<NuxtLink <ButtonLink
to="/dashboard" to="/dashboard"
class="tab button-animation" type="quiet"
interaction="none"
class="tab !h-auto !rounded-none !px-0"
:aria-label="formatMessage(commonMessages.dashboardLabel)" :aria-label="formatMessage(commonMessages.dashboardLabel)"
:title="formatMessage(commonMessages.dashboardLabel)" :title="formatMessage(commonMessages.dashboardLabel)"
> >
<ChartIcon aria-hidden="true" /> <ChartIcon aria-hidden="true" />
</NuxtLink> </ButtonLink>
</template> </template>
<button <Button
class="tab button-animation" type="quiet"
interaction="none"
class="tab !h-auto !rounded-none !px-0"
:title="formatMessage(messages.toggleMenu)" :title="formatMessage(messages.toggleMenu)"
:aria-label=" :aria-label="
isMobileMenuOpen ? formatMessage(messages.closeMenu) : formatMessage(messages.openMenu) isMobileMenuOpen ? formatMessage(messages.closeMenu) : formatMessage(messages.openMenu)
@@ -784,7 +809,7 @@
circle circle
/> />
</template> </template>
</button> </Button>
</div> </div>
</header> </header>
<main class="min-h-[calc(100vh-4.5rem-310.59px)]"> <main class="min-h-[calc(100vh-4.5rem-310.59px)]">
@@ -842,6 +867,7 @@ import {
} from '@modrinth/assets' } from '@modrinth/assets'
import { import {
Avatar, Avatar,
Button,
ButtonLink, ButtonLink,
commonMessages, commonMessages,
commonProjectTypeCategoryMessages, commonProjectTypeCategoryMessages,
@@ -368,8 +368,4 @@ function getVersionLoaders(version) {
} }
} }
} }
.brand-button {
color: var(--color-accent-contrast);
}
</style> </style>
@@ -86,7 +86,7 @@
<StarIcon fill="currentColor" aria-hidden="true" /> <StarIcon fill="currentColor" aria-hidden="true" />
Unset as banner Unset as banner
</Button> </Button>
<div class="button-group"> <div class="mt-3 flex flex-wrap justify-end gap-2">
<Button type="outlined" @click="modalEditItem?.hide()"> <Button type="outlined" @click="modalEditItem?.hide()">
<XIcon aria-hidden="true" /> <XIcon aria-hidden="true" />
Cancel Cancel
@@ -86,7 +86,7 @@
<StarIcon fill="currentColor" aria-hidden="true" /> <StarIcon fill="currentColor" aria-hidden="true" />
Unset as banner Unset as banner
</Button> </Button>
<div class="button-group"> <div class="mt-3 flex flex-wrap justify-end gap-2">
<Button type="outlined" @click="modal_edit_item.hide()"> <Button type="outlined" @click="modal_edit_item.hide()">
<XIcon aria-hidden="true" /> <XIcon aria-hidden="true" />
Cancel Cancel
@@ -740,8 +740,4 @@ svg {
color: var(--color-orange); color: var(--color-orange);
} }
} }
.button-group {
justify-content: flex-start;
}
</style> </style>
@@ -104,7 +104,7 @@
:disabled="!hasPermission" :disabled="!hasPermission"
/> />
</div> </div>
<div class="button-group"> <div class="mt-3 flex flex-wrap justify-start gap-2">
<Button <Button
type="colored" type="colored"
color="brand" color="brand"
@@ -272,7 +272,7 @@
@update:model-value="updateDonationLinks" @update:model-value="updateDonationLinks"
/> />
</div> </div>
<div class="button-group"> <div class="mt-3 flex flex-wrap justify-start gap-2">
<Button type="colored" color="brand" :disabled="!hasChanges" @click="saveChanges()"> <Button type="colored" color="brand" :disabled="!hasChanges" @click="saveChanges()">
<SaveIcon /> <SaveIcon />
Save changes Save changes
@@ -560,8 +560,4 @@ function checkDifference(newLink, existingLink) {
max-width: 26rem; max-width: 26rem;
} }
} }
.button-group {
justify-content: flex-start;
}
</style> </style>
@@ -270,9 +270,11 @@
</nuxt-link> </nuxt-link>
. You may override them below. . You may override them below.
</p> </p>
<nuxt-link <ButtonLink
type="quiet"
interaction="none"
:to="`/organization/${organization.slug}`" :to="`/organization/${organization.slug}`"
class="universal-card button-base recessed org" class="universal-card recessed org !h-auto !w-full !shrink !items-stretch !justify-start !whitespace-normal !p-[var(--spacing-card-bg)]"
> >
<Avatar :src="organization.icon_url" :alt="organization.name" size="md" /> <Avatar :src="organization.icon_url" :alt="organization.name" size="md" />
<div class="details"> <div class="details">
@@ -294,7 +296,7 @@
</div> </div>
</span> </span>
</div> </div>
</nuxt-link> </ButtonLink>
</div> </div>
<p v-else> <p v-else>
This project is not managed by an organization. If you are the member of any organizations, This project is not managed by an organization. If you are the member of any organizations,
@@ -562,6 +564,7 @@ import {
Avatar, Avatar,
Badge, Badge,
Button, Button,
ButtonLink,
Card, Card,
Checkbox, Checkbox,
Combobox, Combobox,
@@ -434,11 +434,6 @@ const toggleFeaturedCategory = (category: Category) => {
vertical-align: top; vertical-align: top;
} }
} }
.button-group {
justify-content: flex-start;
}
.category-list { .category-list {
column-count: 4; column-count: 4;
column-gap: var(--spacing-card-lg); column-gap: var(--spacing-card-lg);
@@ -166,7 +166,7 @@
v-if=" v-if="
subscription.metadata?.type === 'pyro' || subscription.metadata?.type === 'medal' subscription.metadata?.type === 'pyro' || subscription.metadata?.type === 'medal'
" "
:to="`/hosting/manage/${subscription.metadata.id}`" :href="`/hosting/manage/${subscription.metadata.id}`"
target="_blank" target="_blank"
class="w-fit" class="w-fit"
> >
+7 -11
View File
@@ -18,6 +18,7 @@ import {
Avatar, Avatar,
Badge, Badge,
Button, Button,
ButtonLink,
Checkbox, Checkbox,
commonMessages, commonMessages,
defineMessages, defineMessages,
@@ -546,7 +547,7 @@ useSeoMeta({
<h2 class="main-subheader"> <h2 class="main-subheader">
{{ formatMessage(messages.description) }} {{ formatMessage(messages.description) }}
</h2> </h2>
<div class="button-group"> <div class="mx-auto flex w-fit flex-wrap justify-center gap-2 [mask-image:none]">
<Button v-if="os" type="colored" color="brand" size="xl" @click="handleDownload"> <Button v-if="os" type="colored" color="brand" size="xl" @click="handleDownload">
<LinuxIcon v-if="os === 'Linux'" /> <LinuxIcon v-if="os === 'Linux'" />
<WindowsIcon v-else-if="os === 'Windows'" /> <WindowsIcon v-else-if="os === 'Windows'" />
@@ -745,10 +746,12 @@ useSeoMeta({
<div class="projects-showcase"> <div class="projects-showcase">
<div v-for="(row, index) in rows" :key="index" class="row"> <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="n in 2" :key="n" class="row__content" :class="{ offset: index % 2 }">
<nuxt-link <ButtonLink
v-for="project in row" v-for="project in row"
:key="project.id" :key="project.id"
class="project button-animation gradient-border" type="quiet"
interaction="none"
class="project gradient-border !h-auto !shrink !items-stretch !justify-start !whitespace-normal !p-4 !font-normal !leading-normal"
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}`" :to="`/${project.project_type}/${project.slug ? project.slug : project.id}`"
> >
<Avatar :src="project.icon_url!" alt="" size="sm" /> <Avatar :src="project.icon_url!" alt="" size="sm" />
@@ -760,7 +763,7 @@ useSeoMeta({
{{ project.description }} {{ project.description }}
</span> </span>
</div> </div>
</nuxt-link> </ButtonLink>
</div> </div>
</div> </div>
</div> </div>
@@ -1228,13 +1231,6 @@ useSeoMeta({
mask-image: none; mask-image: none;
} }
.button-group {
width: fit-content;
margin: 0 auto;
justify-content: center;
mask-image: none;
}
img { img {
width: 100%; width: 100%;
max-width: 65rem; max-width: 65rem;
@@ -13,11 +13,13 @@
</div> </div>
<template v-if="orgs?.length > 0"> <template v-if="orgs?.length > 0">
<div class="orgs-grid"> <div class="orgs-grid">
<nuxt-link <ButtonLink
v-for="org in sortedOrgs" v-for="org in sortedOrgs"
:key="org.id" :key="org.id"
type="quiet"
interaction="none"
:to="`/organization/${org.slug}`" :to="`/organization/${org.slug}`"
class="universal-card button-base recessed org" class="universal-card recessed org !h-auto !w-full !shrink !items-stretch !justify-start !whitespace-normal !p-[var(--spacing-card-bg)]"
:class="{ 'is-disabled': onlyAcceptedMembers(org.members).length === 0 }" :class="{ 'is-disabled': onlyAcceptedMembers(org.members).length === 0 }"
> >
<Avatar :src="org.icon_url" :alt="org.name" class="icon" /> <Avatar :src="org.icon_url" :alt="org.name" class="icon" />
@@ -41,7 +43,7 @@
</div> </div>
</span> </span>
</div> </div>
</nuxt-link> </ButtonLink>
</div> </div>
</template> </template>
<template v-else> {{ formatMessage(messages.makeOrganization) }} </template> <template v-else> {{ formatMessage(messages.makeOrganization) }} </template>
@@ -51,7 +53,14 @@
<script setup> <script setup>
import { PlusIcon, UsersIcon } from '@modrinth/assets' import { PlusIcon, UsersIcon } from '@modrinth/assets'
import { Avatar, Button, defineMessages, injectModrinthClient, useVIntl } from '@modrinth/ui' import {
Avatar,
Button,
ButtonLink,
defineMessages,
injectModrinthClient,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query' import { useQuery } from '@tanstack/vue-query'
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue' import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
+1 -1
View File
@@ -643,6 +643,7 @@ import {
IntlFormatted, IntlFormatted,
LoaderIcon, LoaderIcon,
ModrinthServersPurchaseModal, ModrinthServersPurchaseModal,
OptionGroup,
useFormatPrice, useFormatPrice,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
@@ -650,7 +651,6 @@ import { monthsInInterval } from '@modrinth/ui/src/utils/billing.ts'
import { useQuery } from '@tanstack/vue-query' import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue' import { computed } from 'vue'
import OptionGroup from '~/components/ui/OptionGroup.vue'
import MedalPlanPromotion from '~/components/ui/servers/marketing/MedalPlanPromotion.vue' import MedalPlanPromotion from '~/components/ui/servers/marketing/MedalPlanPromotion.vue'
import ServerPlanSelector from '~/components/ui/servers/marketing/ServerPlanSelector.vue' import ServerPlanSelector from '~/components/ui/servers/marketing/ServerPlanSelector.vue'
import { products } from '~/generated/state.json' import { products } from '~/generated/state.json'
+6 -11
View File
@@ -25,7 +25,7 @@
<h2> <h2>
{{ formatMessage(messages.discoverHeading) }} {{ formatMessage(messages.discoverHeading) }}
</h2> </h2>
<div class="button-group"> <div class="mx-auto mb-20 flex w-fit flex-wrap justify-center gap-5">
<ButtonLink type="colored" color="brand" size="xl" to="/discover/mods"> <ButtonLink type="colored" color="brand" size="xl" to="/discover/mods">
<CompassIcon aria-hidden="true" /> <CompassIcon aria-hidden="true" />
{{ formatMessage(messages.discoverMods) }} {{ formatMessage(messages.discoverMods) }}
@@ -50,10 +50,12 @@
<div v-if="rows" class="projects-showcase"> <div v-if="rows" class="projects-showcase">
<div v-for="(row, index) in rows" :key="index" class="row"> <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="n in 2" :key="n" class="row__content" :class="{ offset: index % 2 }">
<nuxt-link <ButtonLink
v-for="project in row" v-for="project in row"
:key="project.id" :key="project.id"
class="project button-animation" type="quiet"
interaction="none"
class="project !h-auto !shrink !items-stretch !justify-start !whitespace-normal !p-4 !font-normal !leading-normal"
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}`" :to="`/${project.project_type}/${project.slug ? project.slug : project.id}`"
> >
<Avatar :src="project.icon_url" :alt="project.title" size="sm" loading="lazy" /> <Avatar :src="project.icon_url" :alt="project.title" size="sm" loading="lazy" />
@@ -65,7 +67,7 @@
{{ project.description }} {{ project.description }}
</span> </span>
</div> </div>
</nuxt-link> </ButtonLink>
</div> </div>
</div> </div>
</div> </div>
@@ -738,13 +740,6 @@ const creatorFeatureMessages = defineMessages({
color: var(--landing-color-subheading); color: var(--landing-color-subheading);
max-width: 50rem; max-width: 50rem;
} }
.button-group {
width: fit-content;
gap: 1.25rem;
margin: 0 auto 5rem;
justify-content: center;
}
} }
.users-section-outer { .users-section-outer {
+23 -27
View File
@@ -43,13 +43,17 @@
</div> </div>
<div class="project-list-layouts"> <div class="project-list-layouts">
<button <button
class="preview-radio button-base" type="button"
class="flex !w-full cursor-pointer flex-col overflow-hidden rounded-[var(--radius-md)] border border-solid border-divider bg-button-bg p-0 text-left text-primary outline-2 outline-transparent transition-[filter,transform] hover:brightness-[0.85] focus-visible:ring-4 focus-visible:ring-brand-shadow active:scale-[0.97] active:brightness-[0.8] [&_.example-card]:m-0 [&_.example-card]:min-h-0 [&_.example-card]:border-2 [&_.example-card]:border-solid [&_.example-card]:border-transparent [&_.example-card]:p-4 [&_.example-card]:outline-2 [&_.example-card]:outline-transparent"
:class="{ :class="{
selected: cosmetics.searchDisplayMode[projectType.id] === 'list', '!text-contrast [&_.example-card]:!border-brand [&_.example-card]:!bg-brand-highlight [&_.radio]:text-brand':
cosmetics.searchDisplayMode[projectType.id] === 'list',
}" }"
@click="() => (cosmetics.searchDisplayMode[projectType.id] = 'list')" @click="() => (cosmetics.searchDisplayMode[projectType.id] = 'list')"
> >
<div class="preview"> <div
class="preview flex w-full items-center justify-center bg-bg p-6 outline-2 outline-transparent"
>
<div class="layout-list-mode"> <div class="layout-list-mode">
<div class="example-card card"></div> <div class="example-card card"></div>
<div class="example-card card"></div> <div class="example-card card"></div>
@@ -57,25 +61,30 @@
<div class="example-card card"></div> <div class="example-card card"></div>
</div> </div>
</div> </div>
<div class="label"> <div
class="label flex grow items-center px-[var(--gap-lg)] py-[var(--gap-md)] text-left"
>
<RadioButtonCheckedIcon <RadioButtonCheckedIcon
v-if="cosmetics.searchDisplayMode[projectType.id] === 'list'" v-if="cosmetics.searchDisplayMode[projectType.id] === 'list'"
class="radio shrink-0" class="radio mr-2 shrink-0"
/> />
<RadioButtonIcon v-else class="radio shrink-0" /> <RadioButtonIcon v-else class="radio mr-2 shrink-0" />
{{ formatMessage(layoutMode.rows) }} {{ formatMessage(layoutMode.rows) }}
</div> </div>
</button> </button>
<button <button
class="preview-radio button-base" type="button"
class="flex !w-full cursor-pointer flex-col overflow-hidden rounded-[var(--radius-md)] border border-solid border-divider bg-button-bg p-0 text-left text-primary outline-2 outline-transparent transition-[filter,transform] hover:brightness-[0.85] focus-visible:ring-4 focus-visible:ring-brand-shadow active:scale-[0.97] active:brightness-[0.8] [&_.example-card]:m-0 [&_.example-card]:min-h-0 [&_.example-card]:border-2 [&_.example-card]:border-solid [&_.example-card]:border-transparent [&_.example-card]:p-4 [&_.example-card]:outline-2 [&_.example-card]:outline-transparent"
:class="{ :class="{
selected: '!text-contrast [&_.example-card]:!border-brand [&_.example-card]:!bg-brand-highlight [&_.radio]:text-brand':
cosmetics.searchDisplayMode[projectType.id] === 'gallery' || cosmetics.searchDisplayMode[projectType.id] === 'gallery' ||
cosmetics.searchDisplayMode[projectType.id] === 'grid', cosmetics.searchDisplayMode[projectType.id] === 'grid',
}" }"
@click="() => (cosmetics.searchDisplayMode[projectType.id] = 'grid')" @click="() => (cosmetics.searchDisplayMode[projectType.id] = 'grid')"
> >
<div class="preview"> <div
class="preview flex w-full items-center justify-center bg-bg p-6 outline-2 outline-transparent"
>
<div class="layout-gallery-mode"> <div class="layout-gallery-mode">
<div class="example-card card"></div> <div class="example-card card"></div>
<div class="example-card card"></div> <div class="example-card card"></div>
@@ -83,15 +92,17 @@
<div class="example-card card"></div> <div class="example-card card"></div>
</div> </div>
</div> </div>
<div class="label"> <div
class="label flex grow items-center px-[var(--gap-lg)] py-[var(--gap-md)] text-left"
>
<RadioButtonCheckedIcon <RadioButtonCheckedIcon
v-if=" v-if="
cosmetics.searchDisplayMode[projectType.id] === 'gallery' || cosmetics.searchDisplayMode[projectType.id] === 'gallery' ||
cosmetics.searchDisplayMode[projectType.id] === 'grid' cosmetics.searchDisplayMode[projectType.id] === 'grid'
" "
class="radio shrink-0" class="radio mr-2 shrink-0"
/> />
<RadioButtonIcon v-else class="radio shrink-0" /> <RadioButtonIcon v-else class="radio mr-2 shrink-0" />
{{ formatMessage(layoutMode.grid) }} {{ formatMessage(layoutMode.grid) }}
</div> </div>
</button> </button>
@@ -487,20 +498,5 @@ const listTypes = computed(() => {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
gap: var(--gap-lg); gap: var(--gap-lg);
.preview-radio .example-card {
border: 2px solid transparent;
}
.preview-radio.selected .example-card {
border-color: var(--color-brand);
background-color: var(--color-brand-highlight);
}
.preview {
display: flex;
align-items: center;
justify-content: center;
}
} }
</style> </style>
@@ -3,19 +3,21 @@
ref="scrollContainer" ref="scrollContainer"
class="card-shadow relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold" class="card-shadow relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
> >
<button <Button
v-for="(option, index) in options" v-for="(option, index) in options"
:key="`option-group-${index}`" :key="`option-group-${index}`"
ref="optionButtons" ref="optionButtons"
class="button-animation z-[1] flex flex-row items-center gap-2 rounded-full bg-transparent px-4 py-2 font-semibold" type="quiet"
interaction="none"
class="z-[1] !h-auto !gap-2 !rounded-full !px-4 !py-2"
:class="{ :class="{
'text-button-textSelected': modelValue === option, '!text-brand': modelValue === option,
'text-primary': modelValue !== option, '!text-primary': modelValue !== option,
}" }"
@click="setOption(option)" @click="setOption(option)"
> >
<slot :option="option" :selected="modelValue === option" /> <slot :option="option" :selected="modelValue === option" />
</button> </Button>
<div <div
class="navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full bg-button-bgSelected p-1" class="navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full bg-button-bgSelected p-1"
:style="{ :style="{
@@ -33,6 +35,8 @@
<script setup lang="ts" generic="T"> <script setup lang="ts" generic="T">
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import Button from './buttons/Button.vue'
const modelValue = defineModel<T>({ required: true }) const modelValue = defineModel<T>({ required: true })
const props = defineProps<{ const props = defineProps<{
@@ -64,7 +68,7 @@ watch(modelValue, () => {
}) })
function startAnimation(index: number) { function startAnimation(index: number) {
const el = optionButtons.value[index] const el = optionButtons.value[index]?.element
if (!el || !el.offsetParent) return if (!el || !el.offsetParent) return