refactor: Button components (#6929)

* feat: refactor button components

* fix: qa

* fix: storybook

* a11y: pass

* fix: build

* fix: rename default ->md

* fix: lint

* docs: buttons.md

* refactor part 1

* refactor part 2

* fix: undo refactor for fresh restart

* refactor

* Revert "refactor"

This reverts commit 96d65902d7.

* refactor: part 1

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: remove text-contrast

* fix: qa

* fix: v-tooltip

* fix: lint

* fix: split broken

* fix: passkey qa

* fix: qa

* fix: qa

* fix: prepr

* fix: splitbutton

* improve button group seam

---------

Signed-off-by: Calum H. <calum@modrinth.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-08-04 13:54:04 -07:00
committed by GitHub
co-authored by Prospector
parent 6cfe999cb3
commit bb2193b6f5
398 changed files with 13688 additions and 13133 deletions
@@ -15,12 +15,10 @@
</div>
</template>
<template #actions>
<ButtonStyled color="brand" size="large">
<button type="button" @click="requestSignIn">
<LogInIcon aria-hidden="true" />
{{ formatMessage(commonMessages.signInButton) }}
</button>
</ButtonStyled>
<Button type="colored" color="brand" size="xl" native-type="button" @click="requestSignIn">
<LogInIcon aria-hidden="true" />
{{ formatMessage(commonMessages.signInButton) }}
</Button>
</template>
</EmptyState>
@@ -56,30 +54,33 @@
<div class="flex items-center gap-4">
<Avatar :src="displayedAvatarUrl" size="md" circle :alt="auth.user.value.username" />
<div class="flex flex-col gap-2">
<ButtonStyled>
<FileInput
:max-size="262144"
:show-icon="true"
class="button-like !shadow-none"
:prompt="formatMessage(commonMessages.uploadImageButton)"
accept="image/png,image/jpeg,image/gif,image/webp"
@change="showPreviewImage"
>
<UploadIcon aria-hidden="true" />
</FileInput>
</ButtonStyled>
<ButtonStyled v-if="avatarUrl && !pendingAvatarDeletion">
<button type="button" class="!shadow-none" @click="removePreviewImage">
<TrashIcon aria-hidden="true" />
{{ formatMessage(commonMessages.removeImageButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-if="avatarFile || pendingAvatarDeletion">
<button type="button" class="!shadow-none" @click="resetAvatar">
<UndoIcon aria-hidden="true" />
{{ formatMessage(commonMessages.resetButton) }}
</button>
</ButtonStyled>
<FileButton
:max-size="262144"
:prompt="formatMessage(commonMessages.uploadImageButton)"
accept="image/png,image/jpeg,image/gif,image/webp"
size="md"
@change="showPreviewImage"
>
<UploadIcon aria-hidden="true" />
</FileButton>
<Button
v-if="avatarUrl && !pendingAvatarDeletion"
native-type="button"
size="md"
@click="removePreviewImage"
>
<TrashIcon aria-hidden="true" />
{{ formatMessage(commonMessages.removeImageButton) }}
</Button>
<Button
v-if="avatarFile || pendingAvatarDeletion"
native-type="button"
size="md"
@click="resetAvatar"
>
<UndoIcon aria-hidden="true" />
{{ formatMessage(commonMessages.resetButton) }}
</Button>
</div>
</div>
</div>
@@ -135,9 +136,8 @@ import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import { RouterLink } from 'vue-router'
import Avatar from '#ui/components/base/Avatar.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button, FileButton } from '#ui/components/base/buttons'
import EmptyState from '#ui/components/base/EmptyState.vue'
import FileInput from '#ui/components/base/FileInput.vue'
import IntlFormatted from '#ui/components/base/IntlFormatted.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import { defineMessages, useVIntl } from '#ui/composables'
@@ -15,12 +15,10 @@
</div>
</template>
<template #actions>
<ButtonStyled color="brand" size="large">
<button type="button" @click="requestSignIn">
<LogInIcon aria-hidden="true" />
{{ formatMessage(commonMessages.signInButton) }}
</button>
</ButtonStyled>
<Button type="colored" color="brand" size="xl" native-type="button" @click="requestSignIn">
<LogInIcon aria-hidden="true" />
{{ formatMessage(commonMessages.signInButton) }}
</Button>
</template>
</EmptyState>
@@ -112,11 +110,9 @@
</div>
<div v-else-if="loadError" class="flex flex-col items-center gap-3">
<span>{{ formatMessage(messages.loadError) }}</span>
<ButtonStyled type="outlined">
<button type="button" @click="retry">
{{ formatMessage(commonMessages.retryButton) }}
</button>
</ButtonStyled>
<Button type="outlined" native-type="button" @click="retry">
{{ formatMessage(commonMessages.retryButton) }}
</Button>
</div>
<span v-else>{{ formatMessage(messages.noBlockedUsers) }}</span>
</div>
@@ -145,25 +141,24 @@
<template #cell-actions="{ row }">
<div class="flex justify-end">
<ButtonStyled type="outlined">
<button
type="button"
:disabled="unblockingUserId !== null"
:aria-label="
formatMessage(messages.unblockUserAriaLabel, {
username: row.username,
})
"
@click="unblock(row)"
>
<SpinnerIcon
v-if="unblockingUserId === row.id"
class="animate-spin"
aria-hidden="true"
/>
{{ formatMessage(messages.unblockButton) }}
</button>
</ButtonStyled>
<Button
type="outlined"
native-type="button"
:disabled="unblockingUserId !== null"
:aria-label="
formatMessage(messages.unblockUserAriaLabel, {
username: row.username,
})
"
@click="unblock(row)"
>
<SpinnerIcon
v-if="unblockingUserId === row.id"
class="animate-spin"
aria-hidden="true"
/>
{{ formatMessage(messages.unblockButton) }}
</Button>
</div>
</template>
</Table>
@@ -195,7 +190,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import Avatar from '#ui/components/base/Avatar.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button } from '#ui/components/base/buttons'
import Chips from '#ui/components/base/Chips.vue'
import EmptyState from '#ui/components/base/EmptyState.vue'
import Table, { type TableColumn } from '#ui/components/base/Table.vue'
@@ -14,23 +14,25 @@
v-model="roleFilter"
:options="roleFilterOptions"
:display-value="selectedRoleFilterLabel"
trigger-class="min-w-[225px] !h-10 !min-h-10 !py-0"
trigger-size="lg"
trigger-class="min-w-[225px]"
>
<template #prefix>
<FilterIcon class="size-5 text-secondary" aria-hidden="true" />
</template>
</Combobox>
<ButtonStyled color="brand">
<button
v-tooltip="manageUsersActionTooltip"
class="!h-10 w-full md:w-fit"
:disabled="!canManageUsers"
@click="grantAccessModal?.show($event)"
>
<UserPlusIcon aria-hidden="true" />
{{ formatMessage(messages.inviteFriends) }}
</button>
</ButtonStyled>
<Button
v-tooltip="manageUsersActionTooltip"
type="colored"
color="brand"
size="lg"
class="w-full md:w-fit"
:disabled="!canManageUsers"
@click="grantAccessModal?.show($event)"
>
<UserPlusIcon aria-hidden="true" />
{{ formatMessage(messages.inviteFriends) }}
</Button>
</div>
</div>
@@ -113,7 +115,7 @@ import { FilterIcon, SearchIcon, UserPlusIcon } from '@modrinth/assets'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button } from '#ui/components/base/buttons'
import Combobox, { type ComboboxOption } from '#ui/components/base/Combobox.vue'
import DropdownFilterBar from '#ui/components/base/DropdownFilterBar.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
@@ -42,22 +42,22 @@
</div>
<div class="w-full">
<ButtonStyled v-if="uploading" size="large">
<button class="ml-auto" disabled>
<SpinnerIcon class="animate-spin" />
{{ formatMessage(messages.uploadingProgress, { percent: uploadPercent }) }}
</button>
</ButtonStyled>
<ButtonStyled v-else color="brand" size="large">
<button
v-tooltip="!canSetup ? permissionDeniedMessage : undefined"
class="ml-auto"
:disabled="!canSetup"
@click="openModal"
>
{{ formatMessage(messages.setupServerButton) }} <RightArrowIcon />
</button>
</ButtonStyled>
<Button v-if="uploading" size="xl" class="ml-auto" disabled>
<SpinnerIcon class="animate-spin" />
{{ formatMessage(messages.uploadingProgress, { percent: uploadPercent }) }}
</Button>
<Button
v-else
v-tooltip="!canSetup ? permissionDeniedMessage : undefined"
type="colored"
color="brand"
size="xl"
class="ml-auto"
:disabled="!canSetup"
@click="openModal"
>
{{ formatMessage(messages.setupServerButton) }} <RightArrowIcon />
</Button>
</div>
<CreationFlowModal
@@ -80,7 +80,6 @@
import type { Archon } from '@modrinth/api-client'
import { GlobeIcon, PackageIcon, RightArrowIcon, SpinnerIcon, UsersIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
injectModrinthClient,
injectNotificationManager,
@@ -93,6 +92,7 @@ import { useRoute, useRouter } from 'vue-router'
import type { CreationFlowContextValue } from '#ui/components'
import { CreationFlowModal } from '#ui/components'
import { Button } from '#ui/components/base/buttons'
import { injectModrinthServerContext } from '#ui/providers'
const client = injectModrinthClient()
@@ -19,9 +19,9 @@
<p>
<span class="break-all font-mono">{{ error.message }}</span>
</p>
<ButtonStyled size="large" color="brand" @click="refetch">
<button class="mt-6 !w-full">Retry</button>
</ButtonStyled>
<Button type="colored" color="brand" size="xl" class="mt-6 !w-full" @click="refetch"
>Retry</Button
>
</div>
</div>
</div>
@@ -71,16 +71,16 @@
<template #all>{{ formatMessage(commonMessages.allProjectType) }}</template>
</FilterPills>
</div>
<ButtonStyled color="brand">
<button
v-tooltip="backupCreationDisabled"
:disabled="!!backupCreationDisabled"
@click="showCreateModel"
>
<PlusIcon class="size-5" />
{{ formatMessage(messages.createBackup) }}
</button>
</ButtonStyled>
<Button
v-tooltip="backupCreationDisabled"
type="colored"
color="brand"
:disabled="!!backupCreationDisabled"
@click="showCreateModel"
>
<PlusIcon class="size-5" />
{{ formatMessage(messages.createBackup) }}
</Button>
</div>
<div class="flex w-full flex-col gap-1.5">
@@ -95,17 +95,17 @@
:description="formatMessage(messages.emptyDescription)"
>
<template #actions>
<ButtonStyled color="brand">
<button
v-tooltip="backupCreationDisabled"
:disabled="!!backupCreationDisabled"
class="mx-auto w-min"
@click="showCreateModel"
>
<PlusIcon class="size-5" />
{{ formatMessage(messages.createBackup) }}
</button>
</ButtonStyled>
<Button
v-tooltip="backupCreationDisabled"
type="colored"
color="brand"
:disabled="!!backupCreationDisabled"
class="mx-auto w-min"
@click="showCreateModel"
>
<PlusIcon class="size-5" />
{{ formatMessage(messages.createBackup) }}
</Button>
</template>
</EmptyState>
<EmptyState
@@ -115,11 +115,9 @@
:description="formatMessage(messages.filteredEmptyDescription)"
>
<template #actions>
<ButtonStyled type="outlined">
<button @click="clearBackupFilters">
{{ formatMessage(messages.clearFilters) }}
</button>
</ButtonStyled>
<Button type="outlined" @click="clearBackupFilters">
{{ formatMessage(messages.clearFilters) }}
</Button>
</template>
</EmptyState>
</div>
@@ -198,30 +196,30 @@
}}
</span>
<div class="mx-1 h-6 w-px bg-surface-5" />
<ButtonStyled type="transparent">
<button
type="button"
:disabled="isBulkOperating"
:class="{ 'pointer-events-none opacity-60': isBulkOperating }"
@click="deselectAll"
>
{{ formatMessage(commonMessages.clearButton) }}
</button>
</ButtonStyled>
<Button
type="quiet"
native-type="button"
:disabled="isBulkOperating"
:class="{ 'pointer-events-none opacity-60': isBulkOperating }"
@click="deselectAll"
>
{{ formatMessage(commonMessages.clearButton) }}
</Button>
</div>
<div v-if="!isBulkOperating" class="ml-auto flex items-center gap-0.5">
<ButtonStyled type="transparent" color="red" hover-color-fill="background">
<button
v-tooltip="!canManageBackups ? permissionDeniedMessage : undefined"
type="button"
:disabled="!canManageBackups"
@click="confirmBulkDelete"
>
<TrashIcon />
<span class="bar-label">{{ formatMessage(commonMessages.deleteLabel) }}</span>
</button>
</ButtonStyled>
<Button
v-tooltip="!canManageBackups ? permissionDeniedMessage : undefined"
type="quiet"
color="red"
native-type="button"
:disabled="!canManageBackups"
class="hover:!bg-red focus-visible:!bg-red hover:!text-[var(--color-accent-contrast)] focus-visible:!text-[var(--color-accent-contrast)]"
@click="confirmBulkDelete"
>
<TrashIcon />
<span class="bar-label">{{ formatMessage(commonMessages.deleteLabel) }}</span>
</Button>
</div>
<div v-else class="ml-auto flex items-center" aria-live="polite">
@@ -273,7 +271,7 @@ import type { Component } from 'vue'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button } from '#ui/components/base/buttons'
import Checkbox from '#ui/components/base/Checkbox.vue'
import EmptyState from '#ui/components/base/EmptyState.vue'
import FilterPills, { type FilterPillOption } from '#ui/components/base/FilterPills.vue'
@@ -75,14 +75,17 @@
</li>
</ul>
</div>
<ButtonStyled size="large" type="standard" color="brand">
<AutoLink class="mt-6 !w-full" to="https://support.modrinth.com">{{
formatMessage(messages.contactSupportButton)
}}</AutoLink>
</ButtonStyled>
<ButtonStyled size="large" @click="() => router.go(0)">
<button class="mt-3 !w-full">{{ formatMessage(messages.reloadButton) }}</button>
</ButtonStyled>
<ButtonLink
type="colored"
color="brand"
size="xl"
class="mt-6 !w-full"
to="https://support.modrinth.com"
>{{ formatMessage(messages.contactSupportButton) }}</ButtonLink
>
<Button size="xl" class="mt-3 !w-full" @click="() => router.go(0)">{{
formatMessage(messages.reloadButton)
}}</Button>
</div>
</div>
@@ -106,12 +109,10 @@
:placeholder="formatMessage(messages.searchPlaceholder, { count: filteredData.length })"
wrapper-class="w-full md:w-72"
/>
<ButtonStyled type="standard" color="brand">
<button @click="openPurchaseModal">
<PlusIcon />
{{ formatMessage(messages.newServerButton) }}
</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openPurchaseModal">
<PlusIcon />
{{ formatMessage(messages.newServerButton) }}
</Button>
</div>
</div>
@@ -231,8 +232,6 @@
import type { Archon, Labrinth } from '@modrinth/api-client'
import { HammerIcon, LoaderCircleIcon, PlusIcon, SearchIcon } from '@modrinth/assets'
import {
AutoLink,
ButtonStyled,
CopyCode,
defineMessages,
injectAuth,
@@ -255,6 +254,7 @@ import type Stripe from 'stripe'
import { type ComponentPublicInstance, computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Button, ButtonLink } from '#ui/components/base/buttons'
import ServersUpgradeModalWrapper from '#ui/components/billing/ServersUpgradeModalWrapper.vue'
import type { ServerListingOwner } from '#ui/components/servers/access'
import MedalServerListing from '#ui/components/servers/marketing/MedalServerListing.vue'
@@ -181,31 +181,30 @@
:auto-hide="false"
placement="bottom-end"
>
<ButtonStyled circular size="large">
<button
v-tooltip="showSettingsHint ? undefined : 'Server settings'"
type="button"
aria-label="Server settings"
@click="handleOpenServerSettings"
>
<SettingsIcon />
</button>
</ButtonStyled>
<IconButton
v-tooltip="showSettingsHint ? undefined : 'Server settings'"
size="xl"
label="Server settings"
native-type="button"
@click="handleOpenServerSettings"
>
<SettingsIcon />
</IconButton>
<template #popper>
<div class="grid grid-cols-[min-content] gap-1">
<div class="flex min-w-48 items-center justify-between gap-8">
<h3 class="m-0 whitespace-nowrap text-base font-bold text-contrast">
{{ formatMessage(settingsHintMessages.title) }}
</h3>
<ButtonStyled size="small" circular>
<button
type="button"
:aria-label="formatMessage(settingsHintMessages.dismiss)"
@click="dismissSettingsHint"
>
<XIcon aria-hidden="true" />
</button>
</ButtonStyled>
<IconButton
class="!size-6"
size="xs"
:label="formatMessage(settingsHintMessages.dismiss)"
native-type="button"
@click="dismissSettingsHint"
>
<XIcon aria-hidden="true" />
</IconButton>
</div>
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
{{ formatMessage(settingsHintMessages.description) }}
@@ -213,15 +212,15 @@
</div>
</template>
</Tooltip>
<ButtonStyled circular type="transparent" size="large">
<TeleportOverflowMenu
:options="serverMenuOptions"
:disabled="!!installError"
aria-label="More server options"
>
<MoreVerticalIcon aria-hidden="true" />
</TeleportOverflowMenu>
</ButtonStyled>
<TeleportOverflowMenu
type="quiet"
size="xl"
label="More server options"
:options="serverMenuOptions"
:disabled="!!installError"
>
<MoreVerticalIcon aria-hidden="true" />
</TeleportOverflowMenu>
</PageHeaderActions>
</template>
</PageHeader>
@@ -284,13 +283,11 @@
If you're stuck, please contact Modrinth Support with the information below:
</li>
</ul>
<ButtonStyled>
<button class="mt-2" @click="copyServerDebugInfo">
<CopyIcon v-if="!copied" />
<CheckIcon v-else />
Copy Debug Info
</button>
</ButtonStyled>
<Button class="mt-2" @click="copyServerDebugInfo">
<CopyIcon v-if="!copied" />
<CheckIcon v-else />
Copy Debug Info
</Button>
</div>
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
An internal error occurred while installing your server. Don't fret try
@@ -310,25 +307,23 @@
v-if="errorTitle === 'Installation error'"
class="mt-2 flex flex-col gap-4 sm:flex-row"
>
<ButtonStyled v-if="errorLog">
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
</ButtonStyled>
<ButtonStyled>
<button @click="copyServerDebugInfo">
<CopyIcon v-if="!copied" />
<CheckIcon v-else />
Copy Debug Info
</button>
</ButtonStyled>
<ButtonStyled color="red" type="standard">
<button
class="whitespace-pre"
@click="openServerSettingsModal('installation')"
>
<RightArrowIcon />
Change Loader
</button>
</ButtonStyled>
<Button v-if="errorLog" @click="openInstallLog"
><FileIcon />Open Installation Log</Button
>
<Button @click="copyServerDebugInfo">
<CopyIcon v-if="!copied" />
<CheckIcon v-else />
Copy Debug Info
</Button>
<Button
type="colored"
color="red"
class="whitespace-pre"
@click="openServerSettingsModal('installation')"
>
<RightArrowIcon />
Change Loader
</Button>
</div>
</div>
</div>
@@ -430,7 +425,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import Avatar from '#ui/components/base/Avatar.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button, IconButton, TeleportOverflowMenu } from '#ui/components/base/buttons'
import ErrorInformationCard from '#ui/components/base/ErrorInformationCard.vue'
import NavTabs from '#ui/components/base/NavTabs.vue'
import PageHeader from '#ui/components/base/page-header/index.vue'
@@ -438,7 +433,6 @@ import PageHeaderMetadata from '#ui/components/base/page-header/metadata/index.v
import PageHeaderMetadataItem from '#ui/components/base/page-header/metadata/page-header-metadata-item.vue'
import PageHeaderActions from '#ui/components/base/page-header/page-header-actions.vue'
import ServerNotice from '#ui/components/base/ServerNotice.vue'
import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue'
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
import ServerPanelAdmonitions from '#ui/components/servers/admonitions/ServerPanelAdmonitions.vue'
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'