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
@@ -27,18 +27,17 @@
</div>
</div>
<template v-if="contentError" #top-right-actions>
<ButtonStyled color="red" type="outlined">
<button
v-tooltip="retryDisabled ? retryDisabledTooltip : undefined"
class="!border"
type="button"
:disabled="retryDisabled"
@click="emit('retry')"
>
<RotateCounterClockwiseIcon class="size-5" />
{{ formatMessage(commonMessages.retryButton) }}
</button>
</ButtonStyled>
<Button
v-tooltip="retryDisabled ? retryDisabledTooltip : undefined"
type="outlined"
class="!border !text-red [&>svg]:!text-red !shadow-[inset_0_0_0_1px_var(--color-red)]"
native-type="button"
:disabled="retryDisabled"
@click="emit('retry')"
>
<RotateCounterClockwiseIcon class="size-5" />
{{ formatMessage(commonMessages.retryButton) }}
</Button>
</template>
</Admonition>
</template>
@@ -47,11 +46,11 @@
import { RotateCounterClockwiseIcon } from '@modrinth/assets'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages'
import Admonition from '../base/Admonition.vue'
import ButtonStyled from '../base/ButtonStyled.vue'
export interface SyncProgress {
phase: 'Analyzing' | 'InstallingPack' | 'InstallingLoader' | 'Addons'
@@ -3,22 +3,29 @@
<FloatingActionBar :shown="props.isVisible">
<p class="m-0 font-semibold text-sm md:text-base">You have unsaved changes.</p>
<div class="ml-auto flex gap-2">
<ButtonStyled type="transparent">
<button :disabled="props.isUpdating" @click="props.reset"><HistoryIcon /> Reset</button>
</ButtonStyled>
<ButtonStyled :color="props.restart ? 'standard' : 'brand'">
<button :disabled="props.isUpdating" @click="props.save">
<SpinnerIcon v-if="props.isUpdating" class="animate-spin" />
<SaveIcon v-else />
{{ props.isUpdating ? 'Saving...' : 'Save' }}
</button>
</ButtonStyled>
<ButtonStyled v-if="props.restart" color="brand">
<button :disabled="props.isUpdating || isTransitioning" @click="saveAndPower">
<SpinnerIcon v-if="props.isUpdating || isTransitioning" class="animate-spin" />
{{ powerButtonLabel }}
</button>
</ButtonStyled>
<Button type="quiet" :disabled="props.isUpdating" @click="props.reset"
><HistoryIcon /> Reset</Button
>
<Button
:type="props.restart ? 'base' : 'colored'"
:color="props.restart ? undefined : 'brand'"
:disabled="props.isUpdating"
@click="props.save"
>
<SpinnerIcon v-if="props.isUpdating" class="animate-spin" />
<SaveIcon v-else />
{{ props.isUpdating ? 'Saving...' : 'Save' }}
</Button>
<Button
v-if="props.restart"
type="colored"
color="brand"
:disabled="props.isUpdating || isTransitioning"
@click="saveAndPower"
>
<SpinnerIcon v-if="props.isUpdating || isTransitioning" class="animate-spin" />
{{ powerButtonLabel }}
</Button>
</div>
</FloatingActionBar>
</Teleport>
@@ -28,7 +35,7 @@
import { HistoryIcon, SaveIcon, SpinnerIcon } from '@modrinth/assets'
import { computed } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button } from '#ui/components/base/buttons'
import FloatingActionBar from '#ui/components/base/FloatingActionBar.vue'
import { injectModrinthClient, injectModrinthServerContext } from '#ui/providers'
@@ -183,46 +183,53 @@
</div>
<div v-if="noticeButtons" class="flex gap-2">
<ButtonStyled
<IconButton
v-if="noticeButtons.downloadBackup && onDownloadBackup && isBackupDownloadEnabled"
v-tooltip="formatMessage(messages.downloadLatestBackupTooltip)"
type="outlined"
circular
:label="formatMessage(messages.downloadLatestBackupTooltip)"
data-server-listing-button
@click="onDownloadBackup"
>
<button
v-tooltip="formatMessage(messages.downloadLatestBackupTooltip)"
data-server-listing-button
@click="onDownloadBackup"
>
<DownloadIcon />
</button>
</ButtonStyled>
<ButtonStyled v-if="noticeButtons.copyId" type="outlined">
<button
v-tooltip="formatMessage(messages.copyCodeToClipboardTooltip)"
data-server-listing-button
@click="copyToClipboard(server_id)"
>
<template v-if="copied">
{{ formatMessage(messages.copiedLabel) }} <CheckIcon class="text-green" />
</template>
<template v-else> {{ formatMessage(messages.copyIdLabel) }} <CopyIcon /> </template>
</button>
</ButtonStyled>
<ButtonStyled v-if="noticeButtons.support">
<a href="https://support.modrinth.com/en/" target="_blank" data-server-listing-button
><MessagesSquareIcon /> {{ formatMessage(messages.supportLabel) }}
</a>
</ButtonStyled>
<ButtonStyled v-if="noticeButtons.manageBilling" color="brand">
<AutoLink :to="`/settings/billing#server-${server_id}`" data-server-listing-button>
<CardIcon /> {{ formatMessage(messages.manageBillingLabel) }}
</AutoLink>
</ButtonStyled>
<ButtonStyled v-if="noticeButtons.resubscribe && onResubscribe" color="brand">
<button data-server-listing-button @click="onResubscribe">
<RotateCounterClockwiseIcon /> {{ formatMessage(messages.resubscribeLabel) }}
</button>
</ButtonStyled>
<DownloadIcon />
</IconButton>
<Button
v-if="noticeButtons.copyId"
v-tooltip="formatMessage(messages.copyCodeToClipboardTooltip)"
type="outlined"
data-server-listing-button
@click="copyToClipboard(server_id)"
>
<template v-if="copied">
{{ formatMessage(messages.copiedLabel) }} <CheckIcon class="text-green" />
</template>
<template v-else> {{ formatMessage(messages.copyIdLabel) }} <CopyIcon /> </template>
</Button>
<ButtonLink
v-if="noticeButtons.support"
href="https://support.modrinth.com/en/"
target="_blank"
data-server-listing-button
><MessagesSquareIcon /> {{ formatMessage(messages.supportLabel) }}
</ButtonLink>
<ButtonLink
v-if="noticeButtons.manageBilling"
type="colored"
color="brand"
:to="`/settings/billing#server-${server_id}`"
data-server-listing-button
>
<CardIcon /> {{ formatMessage(messages.manageBillingLabel) }}
</ButtonLink>
<Button
v-if="noticeButtons.resubscribe && onResubscribe"
type="colored"
color="brand"
data-server-listing-button
@click="onResubscribe"
>
<RotateCounterClockwiseIcon /> {{ formatMessage(messages.resubscribeLabel) }}
</Button>
</div>
</div>
@@ -261,11 +268,12 @@ import {
SparklesIcon,
SpinnerIcon,
} from '@modrinth/assets'
import { AutoLink, ButtonStyled } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button, ButtonLink, IconButton } from '#ui/components/base/buttons'
import {
CardIcon,
CheckIcon,
@@ -1,8 +1,7 @@
<script setup lang="ts">
import { ModrinthIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
import AutoLink from '../base/AutoLink.vue'
import ButtonStyled from '../base/ButtonStyled.vue'
import { ButtonLink, IconButton } from '#ui/components/base/buttons'
const emit = defineEmits<{
(e: 'close'): void
@@ -36,16 +35,21 @@ withDefaults(
<span class="text-sm font-medium">Create a server with Modrinth in just a few clicks.</span>
</div>
<div class="flex flex-col items-end justify-end z-10">
<ButtonStyled color="brand">
<AutoLink :to="link"> View plans <RightArrowIcon /> </AutoLink>
</ButtonStyled>
<ButtonLink type="colored" color="brand" :to="link">
View plans <RightArrowIcon />
</ButtonLink>
</div>
<div class="absolute top-2 right-2 z-10">
<ButtonStyled v-if="closable" size="small" circular>
<button v-tooltip="`Don't show again`" @click="emit('close')">
<XIcon aria-hidden="true" />
</button>
</ButtonStyled>
<IconButton
v-if="closable"
v-tooltip="`Don't show again`"
class="!size-6"
size="xs"
:label="`Don't show again`"
@click="emit('close')"
>
<XIcon aria-hidden="true" />
</IconButton>
</div>
</div>
</template>
@@ -74,29 +74,28 @@
<template #cell-actions="{ row: member }">
<div v-if="!member.isOwner" class="flex items-center justify-end gap-1">
<ButtonStyled v-if="member.pending" circular type="transparent">
<button
v-tooltip="resendInviteTooltip(member)"
:aria-label="resendInviteLabel(member)"
:disabled="resendInviteDisabled(member)"
class="text-secondary hover:!filter-none hover:text-contrast focus-visible:!filter-none active:!scale-100 active:!filter-none disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:text-secondary"
@click="handleResendInvite(member)"
>
<SendIcon aria-hidden="true" />
</button>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<button
v-tooltip="memberAccessActionTooltip(member)"
:aria-label="memberAccessActionLabel(member)"
:disabled="!canManageUsers"
class="text-secondary hover:!filter-none hover:text-red focus-visible:!filter-none active:!scale-100 active:!filter-none disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:text-secondary"
@click="member.pending ? handleCancelInvite(member) : handleRemoveMember(member)"
>
<XIcon v-if="member.pending" aria-hidden="true" />
<UserXIcon v-else aria-hidden="true" />
</button>
</ButtonStyled>
<IconButton
v-if="member.pending"
v-tooltip="resendInviteTooltip(member)"
type="quiet"
:label="resendInviteLabel(member)"
:disabled="resendInviteDisabled(member)"
class="text-secondary hover:!filter-none hover:text-contrast focus-visible:!filter-none active:!scale-100 active:!filter-none disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:text-secondary"
@click="handleResendInvite(member)"
>
<SendIcon aria-hidden="true" />
</IconButton>
<IconButton
v-tooltip="memberAccessActionTooltip(member)"
type="quiet"
:label="memberAccessActionLabel(member)"
:disabled="!canManageUsers"
class="text-secondary hover:!filter-none hover:text-red focus-visible:!filter-none active:!scale-100 active:!filter-none disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:text-secondary"
@click="member.pending ? handleCancelInvite(member) : handleRemoveMember(member)"
>
<XIcon v-if="member.pending" aria-hidden="true" />
<UserXIcon v-else aria-hidden="true" />
</IconButton>
</div>
</template>
</Table>
@@ -223,29 +222,30 @@
<span v-else>{{ formatMessage(messages.unknownJoinedDate) }}</span>
</div>
<div class="flex min-w-0 items-center justify-end pr-4">
<ButtonStyled v-if="!member.isOwner" circular type="transparent">
<TeleportOverflowMenu
:options="memberActionOptions(member)"
btn-class="hover:!filter-none focus-visible:!filter-none active:!scale-100 active:!filter-none"
>
<MoreVerticalIcon aria-hidden="true" class="size-5" />
<span class="sr-only">
{{ formatMessage(messages.memberActionsLabel, { username: member.user.username }) }}
</span>
<template #resend-invite>
<SendIcon aria-hidden="true" />
{{ resendInviteLabel(member) }}
</template>
<template #cancel-invite>
<XIcon aria-hidden="true" />
{{ formatMessage(messages.cancelInvite) }}
</template>
<template #remove-user>
<UserXIcon aria-hidden="true" />
{{ formatMessage(messages.removeUser) }}
</template>
</TeleportOverflowMenu>
</ButtonStyled>
<TeleportOverflowMenu
v-if="!member.isOwner"
type="quiet"
:label="formatMessage(messages.memberActionsLabel, { username: member.user.username })"
:options="memberActionOptions(member)"
class="hover:!filter-none focus-visible:!filter-none active:!scale-100 active:!filter-none"
>
<MoreVerticalIcon aria-hidden="true" class="size-5" />
<span class="sr-only">
{{ formatMessage(messages.memberActionsLabel, { username: member.user.username }) }}
</span>
<template #resend-invite>
<SendIcon aria-hidden="true" />
{{ resendInviteLabel(member) }}
</template>
<template #cancel-invite>
<XIcon aria-hidden="true" />
{{ formatMessage(messages.cancelInvite) }}
</template>
<template #remove-user>
<UserXIcon aria-hidden="true" />
{{ formatMessage(messages.removeUser) }}
</template>
</TeleportOverflowMenu>
</div>
</div>
</div>
@@ -284,17 +284,21 @@ import {
UserXIcon,
XIcon,
} from '@modrinth/assets'
import { type Component, computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import {
IconButton,
type OverflowMenuOption,
TeleportOverflowMenu,
} from '#ui/components/base/buttons'
import { useFormatDateTime, useRelativeTime } from '../../../composables'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import { commonMessages } from '../../../utils/common-messages'
import AutoLink from '../../base/AutoLink.vue'
import Avatar from '../../base/Avatar.vue'
import ButtonStyled from '../../base/ButtonStyled.vue'
import Combobox, { type ComboboxOption } from '../../base/Combobox.vue'
import Table, { type SortDirection, type TableColumn } from '../../base/Table.vue'
import TeleportOverflowMenu from '../../base/TeleportOverflowMenu.vue'
import type {
ServerAccessMember,
ServerAccessRole,
@@ -396,16 +400,6 @@ const messages = defineMessages({
type AccessTableColumn = 'user' | 'role' | 'joined' | 'actions'
type AccessTableSortableColumn = Exclude<AccessTableColumn, 'actions'>
type AccessTableRow = ServerAccessMember & Record<string, unknown>
type OverflowMenuOption = {
id: string
icon?: Component
action: () => void
shown?: boolean
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
disabled?: boolean
tooltip?: string
}
const columns = computed<TableColumn<AccessTableColumn>[]>(() => [
{ key: 'user', label: formatMessage(messages.userColumn), width: '32%', enableSorting: true },
{ key: 'role', label: formatMessage(messages.roleColumn), width: '28%', enableSorting: true },
@@ -609,6 +603,7 @@ function memberActionOptions(member: ServerAccessMember): OverflowMenuOption[] {
return [
{
id: 'resend-invite',
label: resendInviteLabel(member),
icon: SendIcon,
action: () => handleResendInvite(member),
shown: member.pending,
@@ -617,18 +612,20 @@ function memberActionOptions(member: ServerAccessMember): OverflowMenuOption[] {
},
{
id: 'cancel-invite',
label: formatMessage(messages.cancelInvite),
icon: XIcon,
action: () => handleCancelInvite(member),
color: 'red',
tone: 'red',
shown: member.pending,
disabled: !canManageUsers.value,
tooltip: memberAccessActionTooltip(member),
},
{
id: 'remove-user',
label: formatMessage(messages.removeUser),
icon: UserXIcon,
action: () => handleRemoveMember(member),
color: 'red',
tone: 'red',
shown: !member.pending,
disabled: !canManageUsers.value,
tooltip: memberAccessActionTooltip(member),
@@ -9,6 +9,7 @@
v-model:custom-start-date="timeframeCustomStartDate"
v-model:custom-end-date="timeframeCustomEndDate"
:class="timeframePickerClass"
trigger-size="lg"
:trigger-class="timeframePickerTriggerClass"
/>
<template v-if="slots.filters">
@@ -363,7 +364,7 @@ const timeframePickerClass = computed(() =>
slots.filters ? '!w-full @[640px]:!w-[225px] shrink-0' : '!w-full @[640px]:!w-[225px]',
)
const timeframePickerTriggerClass =
'!h-10 !min-h-10 !w-full !rounded-[14px] !bg-surface-4 !py-2.5 !pl-4 !pr-3 !text-base shadow-[0px_1px_1px_rgba(0,0,0,0.3),0px_1px_1.5px_rgba(0,0,0,0.15)]'
'!w-full !rounded-[14px] !bg-surface-4 !py-2.5 !pl-4 !pr-3 !text-base shadow-[0px_1px_1px_rgba(0,0,0,0.3),0px_1px_1.5px_rgba(0,0,0,0.15)]'
onMounted(() => {
updateLoadMoreObserver()
@@ -126,18 +126,20 @@
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button @click="hide">
<XIcon aria-hidden="true" />
{{ formatMessage(messages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button v-tooltip="grantPermissionTooltip" :disabled="!canSubmit" @click="submit">
<UserPlusIcon aria-hidden="true" />
{{ formatMessage(messages.inviteButton) }}
</button>
</ButtonStyled>
<Button type="outlined" @click="hide">
<XIcon aria-hidden="true" />
{{ formatMessage(messages.cancelButton) }}
</Button>
<Button
v-tooltip="grantPermissionTooltip"
type="colored"
color="brand"
:disabled="!canSubmit"
@click="submit"
>
<UserPlusIcon aria-hidden="true" />
{{ formatMessage(messages.inviteButton) }}
</Button>
</div>
</template>
</NewModal>
@@ -148,10 +150,11 @@ import { EyeIcon, PencilIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
import { useDebounceFn } from '@vueuse/core'
import { computed, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import { commonMessages } from '../../../utils/common-messages'
import Avatar from '../../base/Avatar.vue'
import ButtonStyled from '../../base/ButtonStyled.vue'
import Checkbox from '../../base/Checkbox.vue'
import Combobox, { type ComboboxOption } from '../../base/Combobox.vue'
import IntlFormatted from '../../base/IntlFormatted.vue'
@@ -62,21 +62,23 @@
</div>
<div class="flex justify-end gap-2 pt-1">
<ButtonStyled type="outlined">
<button class="!border !border-surface-5" @click="hide">
<XIcon aria-hidden="true" />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="orange">
<button v-tooltip="removePermissionTooltip" :disabled="!canRemove" @click="confirm">
<TrashIcon v-if="modalState.shouldCancel" aria-hidden="true" />
<UserXIcon v-else aria-hidden="true" />
{{
formatMessage(modalState.shouldCancel ? messages.cancelButton : messages.removeButton)
}}
</button>
</ButtonStyled>
<Button type="outlined" class="!border !border-surface-5" @click="hide">
<XIcon aria-hidden="true" />
{{ formatMessage(commonMessages.cancelButton) }}
</Button>
<Button
v-tooltip="removePermissionTooltip"
type="colored"
color="orange"
:disabled="!canRemove"
@click="confirm"
>
<TrashIcon v-if="modalState.shouldCancel" aria-hidden="true" />
<UserXIcon v-else aria-hidden="true" />
{{
formatMessage(modalState.shouldCancel ? messages.cancelButton : messages.removeButton)
}}
</Button>
</div>
</div>
</NewModal>
@@ -86,12 +88,13 @@
import { TrashIcon, UserXIcon, XIcon } from '@modrinth/assets'
import { computed, ref, watch } from 'vue'
import { Button } from '#ui/components/base/buttons'
import { useRelativeTime } from '../../../composables'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import { commonMessages } from '../../../utils/common-messages'
import Admonition from '../../base/Admonition.vue'
import Avatar from '../../base/Avatar.vue'
import ButtonStyled from '../../base/ButtonStyled.vue'
import NewModal from '../../modal/NewModal.vue'
import type { ServerAccessRole } from './types'
@@ -8,7 +8,7 @@ import {
} from '@modrinth/assets'
import Admonition from '#ui/components/base/Admonition.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button } from '#ui/components/base/buttons'
import type { MessageDescriptor } from '#ui/composables/i18n'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages'
@@ -279,29 +279,29 @@ function getDescription(item: BackupAdmonitionEntry): string {
</span>
</div>
<template #top-right-actions>
<ButtonStyled v-if="canCancel(item)" type="outlined" color="blue">
<button
v-tooltip="canManageBackups === false ? permissionDeniedMessage : undefined"
class="!border"
type="button"
:disabled="cancelling || canManageBackups === false"
@click="$emit('cancel')"
>
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-if="canRetry(item)" color="red" type="outlined">
<button
v-tooltip="canManageBackups === false ? permissionDeniedMessage : undefined"
class="!border"
type="button"
:disabled="canManageBackups === false"
@click="$emit('retry')"
>
<RotateCounterClockwiseIcon class="size-5" />
{{ formatMessage(commonMessages.retryButton) }}
</button>
</ButtonStyled>
<Button
v-if="canCancel(item)"
v-tooltip="canManageBackups === false ? permissionDeniedMessage : undefined"
type="outlined"
class="!border !text-blue [&>svg]:!text-blue !shadow-[inset_0_0_0_1px_var(--color-blue)]"
native-type="button"
:disabled="cancelling || canManageBackups === false"
@click="$emit('cancel')"
>
{{ formatMessage(commonMessages.cancelButton) }}
</Button>
<Button
v-if="canRetry(item)"
v-tooltip="canManageBackups === false ? permissionDeniedMessage : undefined"
type="outlined"
class="!border !text-red [&>svg]:!text-red !shadow-[inset_0_0_0_1px_var(--color-red)]"
native-type="button"
:disabled="canManageBackups === false"
@click="$emit('retry')"
>
<RotateCounterClockwiseIcon class="size-5" />
{{ formatMessage(commonMessages.retryButton) }}
</Button>
</template>
</Admonition>
</template>
@@ -24,17 +24,17 @@
</span>
</span>
<template v-if="op.id" #top-right-actions>
<ButtonStyled v-if="!isTerminal" type="outlined" color="blue">
<button
v-tooltip="!canWriteFiles ? permissionDeniedMessage : undefined"
class="!border"
type="button"
:disabled="!canWriteFiles"
@click="cancelOperation"
>
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<Button
v-if="!isTerminal"
v-tooltip="!canWriteFiles ? permissionDeniedMessage : undefined"
type="outlined"
class="!border !text-blue [&>svg]:!text-blue !shadow-[inset_0_0_0_1px_var(--color-blue)]"
native-type="button"
:disabled="!canWriteFiles"
@click="cancelOperation"
>
{{ formatMessage(commonMessages.cancelButton) }}
</Button>
</template>
</Admonition>
</template>
@@ -44,7 +44,7 @@ import { PackageOpenIcon } from '@modrinth/assets'
import { computed } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button } from '#ui/components/base/buttons'
import { useFormatBytes } from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { useServerPermissions } from '#ui/composables/server-permissions'
@@ -16,11 +16,15 @@
}}%)
</span>
<template v-if="cancelable" #top-right-actions>
<ButtonStyled type="outlined" color="blue">
<button class="!border" type="button" :disabled="cancelling" @click="$emit('cancel')">
Cancel
</button>
</ButtonStyled>
<Button
type="outlined"
class="!border !text-blue [&>svg]:!text-blue !shadow-[inset_0_0_0_1px_var(--color-blue)]"
native-type="button"
:disabled="cancelling"
@click="$emit('cancel')"
>
Cancel
</Button>
</template>
</Admonition>
</template>
@@ -30,7 +34,7 @@ import { UploadIcon } from '@modrinth/assets'
import { computed } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button } from '#ui/components/base/buttons'
import { useFormatBytes } from '#ui/composables'
import { injectModrinthServerContext } from '#ui/providers'
@@ -46,22 +46,20 @@
</div>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled type="outlined">
<button @click="hideModal">
<XIcon />
Cancel
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button
v-tooltip="createDisabledTooltip"
:disabled="createDisabled"
@click="createBackup"
>
<PlusIcon />
Create backup
</button>
</ButtonStyled>
<Button type="outlined" @click="hideModal">
<XIcon />
Cancel
</Button>
<Button
v-tooltip="createDisabledTooltip"
type="colored"
color="brand"
:disabled="createDisabled"
@click="createBackup"
>
<PlusIcon />
Create backup
</Button>
</div>
</template>
</NewModal>
@@ -73,6 +71,8 @@ import { IssuesIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { computed, nextTick, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import { useVIntl } from '../../../composables/i18n'
import {
injectModrinthClient,
@@ -80,7 +80,6 @@ import {
injectNotificationManager,
} from '../../../providers'
import { commonMessages } from '../../../utils'
import ButtonStyled from '../../base/ButtonStyled.vue'
import StyledInput from '../../base/StyledInput.vue'
import NewModal from '../../modal/NewModal.vue'
@@ -60,22 +60,20 @@
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button @click="modal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button
v-tooltip="deleteDisabledTooltip"
:disabled="!props.canDelete"
@click="confirmDelete"
>
<TrashIcon />
{{ formatMessage(messages.confirm, { count }) }}
</button>
</ButtonStyled>
<Button type="outlined" @click="modal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</Button>
<Button
v-tooltip="deleteDisabledTooltip"
type="colored"
color="red"
:disabled="!props.canDelete"
@click="confirmDelete"
>
<TrashIcon />
{{ formatMessage(messages.confirm, { count }) }}
</Button>
</div>
</template>
</NewModal>
@@ -86,11 +84,12 @@ import type { Archon } from '@modrinth/api-client'
import { TrashIcon, XIcon } from '@modrinth/assets'
import { computed, nextTick, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import { useScrollIndicator } from '../../../composables/scroll-indicator'
import { commonMessages } from '../../../utils'
import Admonition from '../../base/Admonition.vue'
import ButtonStyled from '../../base/ButtonStyled.vue'
import NewModal from '../../modal/NewModal.vue'
import BackupItem from './BackupItem.vue'
@@ -13,13 +13,14 @@ import {
} from '@modrinth/assets'
import { computed, ref } from 'vue'
import type { OverflowMenuOption } from '#ui/components/base/buttons'
import { Button, TeleportOverflowMenu } from '#ui/components/base/buttons'
import { useFormatDateTime } from '../../../composables'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import { commonMessages, truncatedTooltip } from '../../../utils'
import AutoLink from '../../base/AutoLink.vue'
import Avatar from '../../base/Avatar.vue'
import ButtonStyled from '../../base/ButtonStyled.vue'
import OverflowMenu, { type Option as OverflowOption } from '../../base/OverflowMenu.vue'
const { formatMessage } = useVIntl()
const formatDateTime = useFormatDateTime({
@@ -93,38 +94,41 @@ const itemBorderClass = computed(() => {
return 'border-transparent'
})
const overflowMenuOptions = computed<OverflowOption[]>(() => {
const options: OverflowOption[] = []
const overflowMenuOptions = computed<OverflowMenuOption[]>(() => {
const options: OverflowMenuOption[] = []
if (props.showCopyIdAction) {
options.push({
id: 'copy-id',
label: formatMessage(commonMessages.copyIdButton),
action: () => copyId(),
})
}
if (options.length > 0) {
options.push({ divider: true })
options.push({ type: 'divider' })
}
options.push({
id: 'download',
label: formatMessage(commonMessages.downloadButton),
action: () => emit('download'),
link: `https://${props.kyrosUrl}/modrinth/v0/backups/${props.backup.id}/download?auth=${props.jwt}`,
disabled: !props.kyrosUrl || !props.jwt,
})
options.push({
id: 'rename',
label: formatMessage(messages.rename),
action: () => emit('rename'),
disabled: props.writeDisabled,
tooltip: props.writeDisabled ? props.writeDisabledTooltip : undefined,
})
options.push({ divider: true })
options.push({ type: 'divider' })
options.push({
id: 'delete',
color: 'red',
label: formatMessage(commonMessages.deleteLabel),
tone: 'red',
action: () => emit('delete'),
disabled: props.writeDisabled,
tooltip: props.writeDisabled ? props.writeDisabledTooltip : undefined,
@@ -265,35 +269,32 @@ const creatorAvatarSrc = computed(() =>
<!-- Right side actions -->
<div v-if="!preview" class="flex min-w-0 flex-1 items-center justify-end gap-2">
<ButtonStyled color="brand" type="outlined">
<button
v-tooltip="props.restoreDisabled"
class="!border"
:disabled="!!props.restoreDisabled"
@click="() => emit('restore')"
>
<RotateCounterClockwiseIcon class="size-5" />
{{ formatMessage(messages.restore) }}
</button>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<OverflowMenu :options="overflowMenuOptions">
<MoreVerticalIcon class="size-5" />
<template #copy-id>
<ClipboardCopyIcon class="size-5" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
<template #download>
<DownloadIcon class="size-5" /> {{ formatMessage(commonMessages.downloadButton) }}
</template>
<template #rename>
<EditIcon class="size-5" /> {{ formatMessage(messages.rename) }}
</template>
<template #delete>
<TrashIcon class="size-5" /> {{ formatMessage(commonMessages.deleteLabel) }}
</template>
</OverflowMenu>
</ButtonStyled>
<Button
v-tooltip="props.restoreDisabled"
type="outlined"
class="!border !text-brand [&>svg]:!text-brand !shadow-[inset_0_0_0_1px_var(--color-brand)]"
:disabled="!!props.restoreDisabled"
@click="() => emit('restore')"
>
<RotateCounterClockwiseIcon class="size-5" />
{{ formatMessage(messages.restore) }}
</Button>
<TeleportOverflowMenu type="quiet" label="More options" :options="overflowMenuOptions">
<MoreVerticalIcon class="size-5" />
<template #copy-id>
<ClipboardCopyIcon class="size-5" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
<template #download>
<DownloadIcon class="size-5" /> {{ formatMessage(commonMessages.downloadButton) }}
</template>
<template #rename>
<EditIcon class="size-5" /> {{ formatMessage(messages.rename) }}
</template>
<template #delete>
<TrashIcon class="size-5" /> {{ formatMessage(commonMessages.deleteLabel) }}
</template>
</TeleportOverflowMenu>
</div>
<pre v-if="!preview && showDebugInfo" class="w-full rounded-xl bg-surface-4 p-2 text-xs">{{
@@ -22,28 +22,26 @@
</div>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled type="outlined">
<button @click="hide">
<XIcon />
Cancel
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button
v-tooltip="renameDisabledTooltip"
:disabled="renameDisabled"
@click="renameBackup"
>
<template v-if="renameMutation.isPending.value">
<SpinnerIcon class="animate-spin" />
Renaming...
</template>
<template v-else>
<SaveIcon />
Save changes
</template>
</button>
</ButtonStyled>
<Button type="outlined" @click="hide">
<XIcon />
Cancel
</Button>
<Button
v-tooltip="renameDisabledTooltip"
type="colored"
color="brand"
:disabled="renameDisabled"
@click="renameBackup"
>
<template v-if="renameMutation.isPending.value">
<SpinnerIcon class="animate-spin" />
Renaming...
</template>
<template v-else>
<SaveIcon />
Save changes
</template>
</Button>
</div>
</template>
</NewModal>
@@ -55,6 +53,8 @@ import { IssuesIcon, SaveIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { computed, nextTick, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import { useVIntl } from '../../../composables/i18n'
import {
injectModrinthClient,
@@ -62,7 +62,6 @@ import {
injectNotificationManager,
} from '../../../providers'
import { commonMessages } from '../../../utils'
import ButtonStyled from '../../base/ButtonStyled.vue'
import StyledInput from '../../base/StyledInput.vue'
import NewModal from '../../modal/NewModal.vue'
@@ -17,23 +17,21 @@
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled type="outlined">
<button @click="modal?.hide()">
<XIcon />
Cancel
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button
v-tooltip="restoreDisabledTooltip"
:disabled="restoreDisabled"
@click="restoreBackup"
>
<SpinnerIcon v-if="isRestoring" class="animate-spin" />
<RotateCounterClockwiseIcon v-else />
{{ isRestoring ? 'Restoring...' : 'Restore backup' }}
</button>
</ButtonStyled>
<Button type="outlined" @click="modal?.hide()">
<XIcon />
Cancel
</Button>
<Button
v-tooltip="restoreDisabledTooltip"
type="colored"
color="red"
:disabled="restoreDisabled"
@click="restoreBackup"
>
<SpinnerIcon v-if="isRestoring" class="animate-spin" />
<RotateCounterClockwiseIcon v-else />
{{ isRestoring ? 'Restoring...' : 'Restore backup' }}
</Button>
</div>
</template>
</NewModal>
@@ -45,6 +43,8 @@ import { RotateCounterClockwiseIcon, SpinnerIcon, XIcon } from '@modrinth/assets
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import { useVIntl } from '../../../composables/i18n'
import {
injectModrinthClient,
@@ -53,7 +53,6 @@ import {
} from '../../../providers'
import { commonMessages } from '../../../utils'
import Admonition from '../../base/Admonition.vue'
import ButtonStyled from '../../base/ButtonStyled.vue'
import NewModal from '../../modal/NewModal.vue'
import BackupItem from './BackupItem.vue'
@@ -2,19 +2,25 @@
<div class="flex flex-col gap-2.5">
<span class="text-lg font-semibold text-contrast">Icon</span>
<div class="group relative w-fit">
<OverflowMenu
v-tooltip="editIconTooltip"
class="m-0 cursor-pointer appearance-none border-none bg-transparent p-0 transition-transform group-active:scale-95"
<TeleportOverflowMenu
:label="editIconTooltip"
:tooltip="editIconTooltip"
:icon-only="false"
type="quiet"
interaction="none"
class="m-0 !h-auto cursor-pointer appearance-none border-none bg-transparent !p-0 transition-transform group-active:scale-95"
:disabled="isIconActionDisabled"
:options="[
{
id: 'upload',
label: 'Upload icon',
action: () => triggerFileInput(),
disabled: !props.canEdit,
tooltip: !props.canEdit ? editIconTooltip : undefined,
},
{
id: 'sync',
label: 'Sync icon',
action: () => resetIcon(),
disabled: !props.canEdit,
tooltip: !props.canEdit ? editIconTooltip : undefined,
@@ -39,7 +45,7 @@
</div>
<template #upload> <UploadIcon /> Upload icon </template>
<template #sync> <TransferIcon /> Sync icon </template>
</OverflowMenu>
</TeleportOverflowMenu>
</div>
</div>
</template>
@@ -49,7 +55,7 @@ import { EditIcon, SpinnerIcon, TransferIcon, UploadIcon } from '@modrinth/asset
import { useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { OverflowMenu, ServerIcon } from '#ui/components'
import { ServerIcon, TeleportOverflowMenu } from '#ui/components'
import { useServerImage } from '#ui/composables'
import { useVIntl } from '#ui/composables/i18n'
import {
@@ -30,9 +30,10 @@
</Transition>
</div>
<ButtonStyled color="medal-promo" type="outlined" size="large">
<button class="z-10 my-auto" @click="openUpgradeModal"><RocketIcon /> Upgrade</button>
</ButtonStyled>
<Button type="outlined" size="xl" class="z-10 my-auto" @click="openUpgradeModal">
<RocketIcon aria-hidden="true" />
Upgrade
</Button>
</div>
<ServersUpgradeModalWrapper
ref="upgradeModal"
@@ -50,7 +51,7 @@ import dayjs from 'dayjs'
import dayjsDuration from 'dayjs/plugin/duration'
import { type ComponentPublicInstance, computed, onMounted, onUnmounted, ref } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button } from '#ui/components/base/buttons'
import ServersUpgradeModalWrapper from '#ui/components/billing/ServersUpgradeModalWrapper.vue'
import { injectModrinthClient } from '#ui/providers'
@@ -120,11 +120,17 @@
</div>
<div class="z-10 ml-auto">
<ButtonStyled color="medal-promo" type="outlined" size="large">
<button class="my-auto" data-server-listing-button @click="handleUpgrade">
<RocketIcon /> {{ formatMessage(messages.upgradeButton) }}
</button>
</ButtonStyled>
<Button
type="outlined"
color="medal_promotion"
size="xl"
class="my-auto"
data-server-listing-button
@click="handleUpgrade"
>
<RocketIcon aria-hidden="true" />
{{ formatMessage(messages.upgradeButton) }}
</Button>
</div>
</div>
@@ -168,10 +174,11 @@ import dayjsDuration from 'dayjs/plugin/duration'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { Button } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import { injectModrinthClient } from '../../../providers/api-client'
import Avatar from '../../base/Avatar.vue'
import ButtonStyled from '../../base/ButtonStyled.vue'
import CopyCode from '../../base/CopyCode.vue'
import IntlFormatted from '../../base/IntlFormatted.vue'
import type { ServerListingOwner } from '../access/types'
@@ -1,60 +1,75 @@
<template>
<div class="contents">
<div class="flex flex-row items-center gap-2 rounded-lg">
<ButtonStyled v-if="isInstalling" type="standard" color="brand" size="large">
<button disabled class="flex-shrink-0">
<LoaderCircleIcon class="size-5 animate-spin" /> Installing...
</button>
</ButtonStyled>
<Button
v-if="isInstalling"
type="colored"
color="brand"
size="xl"
disabled
class="flex-shrink-0"
>
<LoaderCircleIcon class="size-5 animate-spin" /> Installing...
</Button>
<template v-else-if="showRestartButton">
<ButtonStyled type="standard" color="orange" size="large">
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
<UpdatedIcon />
<span>{{ primaryActionText }}</span>
</button>
</ButtonStyled>
<JoinedButtons
color="red"
size="large"
:actions="stopSplitActions"
:primary-disabled="!canTakeAction"
:dropdown-disabled="!canKill"
:primary-tooltip="busyTooltip"
:dropdown-tooltip="busyTooltip"
<Button
v-tooltip="busyTooltip"
type="colored"
color="orange"
size="xl"
:disabled="!canTakeAction"
@click="handlePrimaryAction"
>
<template #kill_server>
<SlashIcon class="h-5 w-5" />
Kill server
</template>
</JoinedButtons>
<UpdatedIcon />
<span>{{ primaryActionText }}</span>
</Button>
<SplitButton
v-tooltip="busyTooltip"
type="colored"
color="red"
size="xl"
:options="stopMenuOptions"
menu-label="Force stop options"
:primary-disabled="!canTakeAction"
:menu-disabled="!canKill"
@click="initiateAction('Stop')"
>
<StopCircleIcon />
{{ isStopping ? 'Stopping' : 'Stop' }}
</SplitButton>
</template>
<template v-else-if="isStopping">
<JoinedButtons
<SplitButton
v-tooltip="busyTooltip"
type="colored"
color="red"
size="large"
:actions="stopSplitActions"
size="xl"
:options="stopMenuOptions"
menu-label="Force stop options"
:primary-disabled="true"
:dropdown-disabled="!canKill"
:primary-muted="true"
:dropdown-tooltip="busyTooltip"
:menu-disabled="!canKill"
class="opacity-60"
>
<template #kill_server>
<SlashIcon class="h-5 w-5" />
Kill server
</template>
</JoinedButtons>
<StopCircleIcon />
Stopping
</SplitButton>
</template>
<template v-else>
<ButtonStyled type="standard" color="brand" size="large">
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
<PlayIcon />
<span>{{ primaryActionText }}</span>
</button>
</ButtonStyled>
<Button
v-tooltip="busyTooltip"
type="colored"
color="brand"
size="xl"
:disabled="!canTakeAction"
@click="handlePrimaryAction"
>
<PlayIcon />
<span>{{ primaryActionText }}</span>
</Button>
</template>
</div>
</div>
@@ -70,7 +85,8 @@ import {
} from '@modrinth/assets'
import { computed } from 'vue'
import { ButtonStyled, type JoinedButtonAction, JoinedButtons } from '#ui/components'
import type { OverflowMenuOption } from '#ui/components/base/buttons'
import { Button, SplitButton } from '#ui/components/base/buttons'
import { useServerPowerAction } from './use-server-power-action'
@@ -97,13 +113,7 @@ const {
disabled: computed(() => props.disabled),
})
const stopSplitActions = computed<JoinedButtonAction[]>(() => [
{
id: 'stop',
label: isStopping.value ? 'Stopping' : 'Stop',
icon: StopCircleIcon,
action: () => initiateAction('Stop'),
},
const stopMenuOptions = computed<OverflowMenuOption[]>(() => [
{
id: 'kill_server',
label: 'Kill server',
@@ -66,12 +66,10 @@
<!-- CTA section -->
<div class="flex flex-col gap-6 items-start">
<div class="flex flex-col gap-3 items-start">
<ButtonStyled color="brand">
<button @click="onClickNewServer?.()">
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.newServerButton) }}
</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="onClickNewServer?.()">
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.newServerButton) }}
</Button>
<AutoLink
to="https://modrinth.com/hosting"
@@ -90,12 +88,10 @@
<p class="text-base font-normal text-primary">
{{ formatMessage(messages.alreadyHaveServerLabel) }}
</p>
<ButtonStyled>
<button @click="onClickSignIn?.()">
<LogInIcon aria-hidden="true" />
{{ formatMessage(messages.signInButton) }}
</button>
</ButtonStyled>
<Button @click="onClickSignIn?.()">
<LogInIcon aria-hidden="true" />
{{ formatMessage(messages.signInButton) }}
</Button>
</div>
</template>
</div>
@@ -153,7 +149,7 @@ import {
} from '@modrinth/assets'
import { AutoLink } from '@modrinth/ui'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import { Button } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import imgAircraft from './grid-images/aircraft.png'