import type { Archon } from '@modrinth/api-client'
-import { CalendarIcon, DownloadIcon, IssuesIcon, PlusIcon } from '@modrinth/assets'
-import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
+import { CalendarIcon, DownloadIcon, IssuesIcon, PlusIcon, TrashIcon } from '@modrinth/assets'
+import { useMutation, useQueryClient } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import type { Component } from 'vue'
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
+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'
+import FloatingActionBar from '#ui/components/base/FloatingActionBar.vue'
import ReadyTransition from '#ui/components/base/ReadyTransition.vue'
import BackupCreateModal from '#ui/components/servers/backups/BackupCreateModal.vue'
import BackupDeleteModal from '#ui/components/servers/backups/BackupDeleteModal.vue'
import BackupItem from '#ui/components/servers/backups/BackupItem.vue'
import BackupRenameModal from '#ui/components/servers/backups/BackupRenameModal.vue'
import BackupRestoreModal from '#ui/components/servers/backups/BackupRestoreModal.vue'
-import { useReadyState } from '#ui/composables'
-import { useVIntl } from '#ui/composables/i18n'
+import { defineMessages, useVIntl } from '#ui/composables/i18n'
+import { useServerBackupsQueue } from '#ui/composables/server-backups-queue'
+import { useBulkOperation } from '#ui/layouts/shared/content-tab/composables/bulk-operations'
import {
injectModrinthClient,
injectModrinthServerContext,
injectNotificationManager,
} from '#ui/providers'
+import { commonMessages } from '#ui/utils/common-messages'
+
+import { useBackupsSelection } from './backups-selection'
+
+const messages = defineMessages({
+ selectAll: {
+ id: 'servers.backups.toolbar.select-all',
+ defaultMessage: 'Select all',
+ },
+ selectBackupAria: {
+ id: 'servers.backups.select-backup-aria',
+ defaultMessage: 'Select backup {name}',
+ },
+ filterManual: {
+ id: 'servers.backups.toolbar.filter-manual',
+ defaultMessage: 'Manual',
+ },
+ filterAuto: {
+ id: 'servers.backups.toolbar.filter-auto',
+ defaultMessage: 'Auto',
+ },
+ selectedCount: {
+ id: 'servers.backups.bulk-bar.selected-count',
+ defaultMessage: '{count, plural, one {# backup selected} other {# backups selected}}',
+ },
+ bulkBarAriaLabel: {
+ id: 'servers.backups.bulk-bar.aria-label',
+ defaultMessage:
+ '{count, plural, one {Bulk actions for one selected backup} other {Bulk actions for # selected backups}}',
+ },
+ createBackup: {
+ id: 'servers.backups.toolbar.create-backup',
+ defaultMessage: 'Create backup',
+ },
+ emptyHeading: {
+ id: 'servers.backups.empty.heading',
+ defaultMessage: 'No backups yet',
+ },
+ emptyDescription: {
+ id: 'servers.backups.empty.description',
+ defaultMessage: 'Create your first backup',
+ },
+ filteredEmptyHeading: {
+ id: 'servers.backups.filtered-empty.heading',
+ defaultMessage: 'No backups match',
+ },
+ filteredEmptyDescription: {
+ id: 'servers.backups.filtered-empty.description',
+ defaultMessage: 'Try a different filter or clear filters to see all backups.',
+ },
+ clearFilters: {
+ id: 'servers.backups.filtered-empty.clear-filters',
+ defaultMessage: 'Clear filters',
+ },
+ bulkDeleting: {
+ id: 'servers.backups.bulk-bar.deleting',
+ defaultMessage: 'Deleting {total, plural, one {# backup} other {# backups}}...',
+ },
+})
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
+
+const filterPillOptions = computed
(() => [
+ { id: 'manual', label: formatMessage(messages.filterManual) },
+ { id: 'auto', label: formatMessage(messages.filterAuto) },
+])
const client = injectModrinthClient()
const queryClient = useQueryClient()
-const { server, worldId, backupsState, markBackupCancelled, busyReasons } =
- injectModrinthServerContext()
+const { server, worldId, busyReasons } = injectModrinthServerContext()
const props = defineProps<{
isServerRunning: boolean
@@ -183,81 +347,82 @@ const serverId = route.params.id as string
defineEmits(['onDownload'])
-const backupsQueryKey = ['backups', 'list', serverId]
-const {
- data: backupsData,
- isLoading,
- error,
- refetch,
-} = useQuery({
- queryKey: backupsQueryKey,
- queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!),
- enabled: computed(() => worldId.value !== null),
+const { backups, invalidate, hasActiveCreate, hasActiveRestore, query } = useServerBackupsQueue(
+ computed(() => serverId),
+ worldId,
+)
+
+const error = computed(() => {
+ const err = query.error.value
+ return err instanceof Error ? err : err ? new Error(String(err)) : null
})
+const refetch = () => query.refetch()
+
+/** Until world exists we cannot fetch; `isLoading` is false while the query is disabled, which would flash empty state. */
+const backupsReadyPending = computed(
+ () => !worldId.value || (query.data.value === undefined && !query.error.value),
+)
+
+const selectedFilters = ref([])
-const backupsReadyPending = useReadyState({ isLoading, data: backupsData })
+const completedBackups = computed(() => backups.value.filter((backup) => backup.status === 'done'))
+
+const filteredBackups = computed(() => {
+ const f = selectedFilters.value
+ if (f.length === 0 || f.length === 2) {
+ return completedBackups.value
+ }
+ const wantAuto = f.includes('auto')
+ return completedBackups.value.filter((b) => b.automated === wantAuto)
+})
-const deleteMutation = useMutation({
+/** Completed backups with a snapshot: queue API schedules deletion. */
+const deleteQueueMutation = useMutation({
mutationFn: (backupId: string) =>
- client.archon.backups_v1.delete(serverId, worldId.value!, backupId),
- onSuccess: (_data, backupId) => {
- markBackupCancelled(backupId)
- backupsState.delete(backupId)
- queryClient.invalidateQueries({ queryKey: backupsQueryKey })
- queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] })
+ client.archon.backups_queue_v1.delete(serverId, worldId.value!, backupId),
+ onSuccess: async () => {
+ await invalidate()
+ await queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] })
},
})
-const retryMutation = useMutation({
+/** In-progress / incomplete backups: legacy cancel + delete path. */
+const deleteLegacyMutation = useMutation({
mutationFn: (backupId: string) =>
- client.archon.backups_v1.retry(serverId, worldId.value!, backupId),
- onSuccess: () => queryClient.invalidateQueries({ queryKey: backupsQueryKey }),
+ client.archon.backups_v1.delete(serverId, worldId.value!, backupId),
+ onSuccess: async () => {
+ await invalidate()
+ await queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] })
+ },
})
-const backups = computed(() => {
- if (!backupsData.value) return []
-
- const merged = backupsData.value.map((backup) => {
- const progressState = backupsState.get(backup.id)
- if (progressState) {
- const hasOngoingTask = Object.values(progressState).some((task) => task?.state === 'ongoing')
- const hasCompletedTask = Object.values(progressState).some((task) => task?.state === 'done')
-
- return {
- ...backup,
- task: {
- ...backup.task,
- ...progressState,
- },
- status: hasOngoingTask
- ? ('in_progress' as const)
- : hasCompletedTask
- ? ('done' as const)
- : backup.status,
- ongoing: hasOngoingTask || (backup.ongoing && !hasCompletedTask),
- }
- }
- return backup
- })
-
- return merged.sort((a, b) => {
- return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
- })
+/** Bulk delete via queue API — handles both completed and in-progress backups (cancels the latter). */
+const deleteManyMutation = useMutation({
+ mutationFn: (backupIds: string[]) =>
+ client.archon.backups_queue_v1.deleteMany(serverId, worldId.value!, backupIds),
+ onSuccess: async () => {
+ await invalidate()
+ await queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] })
+ },
})
type BackupGroup = {
label: string
icon: Component | null
- backups: Archon.Backups.v1.Backup[]
+ backups: Archon.BackupsQueue.v1.BackupQueueBackup[]
}
const groupedBackups = computed((): BackupGroup[] => {
- if (!backups.value.length) return []
+ if (!filteredBackups.value.length) return []
const now = dayjs()
const groups: BackupGroup[] = []
- const addToGroup = (label: string, icon: Component | null, backup: Archon.Backups.v1.Backup) => {
+ const addToGroup = (
+ label: string,
+ icon: Component | null,
+ backup: Archon.BackupsQueue.v1.BackupQueueBackup,
+ ) => {
let group = groups.find((g) => g.label === label)
if (!group) {
group = { label, icon, backups: [] }
@@ -266,7 +431,7 @@ const groupedBackups = computed((): BackupGroup[] => {
group.backups.push(backup)
}
- for (const backup of backups.value) {
+ for (const backup of filteredBackups.value) {
const created = dayjs(backup.created_at)
const diffMinutes = now.diff(created, 'minute')
const isToday = created.isSame(now, 'day')
@@ -289,6 +454,20 @@ const groupedBackups = computed((): BackupGroup[] => {
return groups
})
+const displayOrderedBackups = computed(() => groupedBackups.value.flatMap((g) => g.backups))
+
+const {
+ selectedIds,
+ toggleSelection,
+ deselectAll,
+ toggleSelectAll,
+ allSelected,
+ someSelected,
+ selectedBackups,
+} = useBackupsSelection(filteredBackups, displayOrderedBackups)
+
+const { isBulkOperating, bulkTotal } = useBulkOperation()
+
const overTheTopDownloadAnimation = ref()
const createBackupModal = ref>()
const renameBackupModal = ref>()
@@ -302,13 +481,16 @@ const backupRestoreDisabled = computed(() => {
if (busyReasons.value.length > 0) {
return formatMessage(busyReasons.value[0].reason)
}
+ if (hasActiveCreate.value || hasActiveRestore.value) {
+ return 'A backup operation is already queued or in progress'
+ }
return undefined
})
const backupCreationDisabled = computed(() => {
const quota = server.value.backup_quota
if (quota !== undefined) {
- const usedCount = backupsData.value?.length ?? server.value.used_backup_quota ?? 0
+ const usedCount = backups.value.length ?? server.value.used_backup_quota ?? 0
if (usedCount >= quota) {
return `All ${quota} of your backup slots are in use`
}
@@ -316,9 +498,8 @@ const backupCreationDisabled = computed(() => {
if (busyReasons.value.length > 0) {
return formatMessage(busyReasons.value[0].reason)
}
- // also check for active backups, combining REST data with WS overlay
- if (backups.value.some((b) => b.status === 'in_progress' || b.status === 'pending')) {
- return 'A backup is already in progress'
+ if (hasActiveCreate.value) {
+ return 'A backup is already queued or in progress'
}
return undefined
})
@@ -327,20 +508,46 @@ const showCreateModel = () => {
createBackupModal.value?.show()
}
+function clearBackupFilters() {
+ selectedFilters.value = []
+}
+
+function confirmBulkDelete() {
+ if (!selectedBackups.value.length) return
+ deleteBackupModal.value?.showBulk(selectedBackups.value)
+}
+
+async function bulkDelete(toRemove: Archon.BackupsQueue.v1.BackupQueueBackup[]) {
+ if (!toRemove.length) return
+
+ isBulkOperating.value = true
+ bulkTotal.value = toRemove.length
+
+ try {
+ await deleteManyMutation.mutateAsync(toRemove.map((b) => b.id))
+ } catch (err) {
+ addNotification({
+ type: 'error',
+ title: `Failed to delete ${toRemove.length} backup${toRemove.length === 1 ? '' : 's'}`,
+ text: err instanceof Error ? err.message : String(err),
+ })
+ } finally {
+ deselectAll()
+ isBulkOperating.value = false
+ bulkTotal.value = 0
+ }
+}
+
function triggerDownloadAnimation() {
overTheTopDownloadAnimation.value = true
setTimeout(() => (overTheTopDownloadAnimation.value = false), 500)
}
-const retryBackup = (backupId: string) => {
- retryMutation.mutate(backupId, {
- onError: (err) => {
- console.error('Failed to retry backup:', err)
- },
- })
+function useQueueDeleteFor(backup: Archon.BackupsQueue.v1.BackupQueueBackup) {
+ return backup.status === 'done'
}
-function deleteBackup(backup?: Archon.Backups.v1.Backup) {
+function deleteBackup(backup?: Archon.BackupsQueue.v1.BackupQueueBackup) {
if (!backup) {
addNotification({
type: 'error',
@@ -350,7 +557,9 @@ function deleteBackup(backup?: Archon.Backups.v1.Backup) {
return
}
- deleteMutation.mutate(backup.id, {
+ const mutation = useQueueDeleteFor(backup) ? deleteQueueMutation : deleteLegacyMutation
+
+ mutation.mutate(backup.id, {
onError: (err) => {
const message = err instanceof Error ? err.message : String(err)
addNotification({
@@ -396,6 +605,21 @@ function deleteBackup(backup?: Archon.Backups.v1.Backup) {
transition: transform 200ms ease-in-out;
}
+@keyframes indeterminate {
+ 0% {
+ width: 20%;
+ margin-left: -20%;
+ }
+ 100% {
+ width: 60%;
+ margin-left: 100%;
+ }
+}
+
+.animate-indeterminate {
+ animation: indeterminate 1.5s ease-in-out infinite;
+}
+
.over-the-top-download-animation {
position: fixed;
z-index: 100;
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
index e72ce06f64..cec7a66f1d 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue
@@ -2,11 +2,10 @@
import type { Archon, Labrinth } from '@modrinth/api-client'
import { ClipboardCopyIcon } from '@modrinth/assets'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
-import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
-import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
+import { computed, nextTick, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
import ReadyTransition from '#ui/components/base/ReadyTransition.vue'
-import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
import { useReadyState } from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import {
@@ -22,10 +21,7 @@ import ConfirmUnlinkModal from '../../../shared/content-tab/components/modals/Co
import ContentUpdaterModal from '../../../shared/content-tab/components/modals/ContentUpdaterModal.vue'
import ModpackContentModal from '../../../shared/content-tab/components/modals/ModpackContentModal.vue'
import ContentPageLayout from '../../../shared/content-tab/layout.vue'
-import type {
- ContentModpackData,
- UploadState,
-} from '../../../shared/content-tab/providers/content-manager'
+import type { ContentModpackData } from '../../../shared/content-tab/providers/content-manager'
import { provideContentManager } from '../../../shared/content-tab/providers/content-manager'
import type {
ContentItem,
@@ -85,20 +81,9 @@ const messages = defineMessages({
},
})
-const leaveMessages = defineMessages({
- uploadInProgress: {
- id: 'instances.confirm-leave-modal.upload-in-progress',
- defaultMessage: 'Upload in progress',
- },
- leavePageBody: {
- id: 'instances.confirm-leave-modal.body',
- defaultMessage:
- 'Files are still being uploaded. Leaving this page will cancel the upload and your changes may be lost.',
- },
-})
-
const client = injectModrinthClient()
-const { server, worldId, busyReasons, isSyncingContent } = injectModrinthServerContext()
+const { server, worldId, busyReasons, isSyncingContent, uploadState, cancelUpload } =
+ injectModrinthServerContext()
const { addNotification } = injectNotificationManager()
const { openServerSettings, browseServerContent } = injectServerSettingsModal()
const route = useRoute()
@@ -352,57 +337,10 @@ async function handleBulkDisable(items: ContentItem[]) {
}
}
-const uploadState = ref({
- isUploading: false,
- currentFileName: null,
- currentFileProgress: 0,
- uploadedBytes: 0,
- totalBytes: 0,
- completedFiles: 0,
- totalFiles: 0,
-})
-
-const confirmLeaveModal = ref>()
const modpackUnlinkModal = ref>()
const modpackContentModal = ref>()
const contentUpdaterModal = ref>()
-let activeUploadCancel: (() => void) | null = null
-
-const isUploading = computed(() => uploadState.value.isUploading)
-
-function handleBeforeUnload(e: BeforeUnloadEvent) {
- if (isUploading.value) {
- e.preventDefault()
- return ''
- }
-}
-
-if (typeof window !== 'undefined') {
- watch(isUploading, (uploading) => {
- if (uploading) {
- window.addEventListener('beforeunload', handleBeforeUnload)
- } else {
- window.removeEventListener('beforeunload', handleBeforeUnload)
- }
- })
-
- onBeforeUnmount(() => {
- window.removeEventListener('beforeunload', handleBeforeUnload)
- })
-
- onBeforeRouteLeave(async () => {
- if (isUploading.value) {
- const shouldLeave = (await confirmLeaveModal.value?.prompt()) ?? false
- if (shouldLeave) {
- activeUploadCancel?.()
- }
- return shouldLeave
- }
- return true
- })
-}
-
const updatingProject = ref(null)
const updatingModpack = ref(false)
const loadingChangelog = ref(false)
@@ -486,7 +424,7 @@ function handleUploadFiles() {
uploadState.value.totalBytes = p.total
},
})
- activeUploadCancel = () => handle.cancel()
+ cancelUpload.value = () => handle.cancel()
try {
await handle.promise
@@ -500,7 +438,7 @@ function handleUploadFiles() {
text: err instanceof Error ? err.message : undefined,
})
} finally {
- activeUploadCancel = null
+ cancelUpload.value = null
uploadState.value = {
isUploading: false,
currentFileName: null,
@@ -875,7 +813,6 @@ provideContentManager({
},
browse: handleBrowseContent,
uploadFiles: handleUploadFiles,
- uploadState,
deletionContext: 'server',
hasUpdateSupport: true,
updateItem: handleUpdateItem,
@@ -911,7 +848,7 @@ provideContentManager({
-
+
-
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/index.vue b/packages/ui/src/layouts/wrapped/hosting/manage/index.vue
index d409c30473..a5bf18e625 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/index.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/index.vue
@@ -89,6 +89,7 @@
@@ -559,6 +560,11 @@ const serverList = computed
(() => {
return serverResponse.value.servers
})
+const showEmptyState = computed(
+ () =>
+ !showServersListLoading.value && serverList.value.length === 0 && !isPollingForNewServers.value,
+)
+
const searchInput = ref('')
const fuse = computed(() => {
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/overview.vue b/packages/ui/src/layouts/wrapped/hosting/manage/overview.vue
index 9cb3dc26b1..0985b699d3 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/overview.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/overview.vue
@@ -117,7 +117,12 @@ provideConsoleManager({
},
showCommandInput: true,
disableCommandInput: computed(() => serverPowerState.value !== 'running'),
- loading: computed(() => !isConnected.value || isWsAuthIncorrect.value),
+ loading: computed(
+ () =>
+ !isConnected.value ||
+ modrinthServersConsole.isInitialLogHydrating.value ||
+ isWsAuthIncorrect.value,
+ ),
onClear: async () => {
modrinthServersConsole.clear()
try {
diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
index ea7a67b666..f9159ad088 100644
--- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
+++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue
@@ -99,7 +99,7 @@
@@ -309,66 +307,13 @@
Hang on, we're reconnecting to your server.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Uploading files ({{ uploadState.completedFiles }}/{{ uploadState.totalFiles }})
-
- — {{ uploadState.currentFileName }}
-
-
-
- {{ formatBytes(uploadState.uploadedBytes) }} /
- {{ formatBytes(uploadState.totalBytes) }} ({{
- Math.round(uploadOverallProgress * 100)
- }}%)
-
-
-
- Cancel
-
-
-
-
-
-
-
-
-
+
@@ -417,11 +362,9 @@ import {
SettingsIcon,
TransferIcon,
TriangleAlertIcon,
- UploadIcon,
XIcon,
} from '@modrinth/assets'
import type { Stats } from '@modrinth/utils'
-import { formatBytes } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { useStorage, useTimeoutFn } from '@vueuse/core'
import DOMPurify from 'dompurify'
@@ -429,16 +372,12 @@ import { Tooltip } from 'floating-vue'
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
-import Admonition from '#ui/components/base/Admonition.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import ErrorInformationCard from '#ui/components/base/ErrorInformationCard.vue'
import NavTabs from '#ui/components/base/NavTabs.vue'
-import ProgressBar from '#ui/components/base/ProgressBar.vue'
import ServerNotice from '#ui/components/base/ServerNotice.vue'
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
-import BackupProgressAdmonitions from '#ui/components/servers/backups/BackupProgressAdmonitions.vue'
-import { ServerIcon } from '#ui/components/servers/icons'
-import InstallingBanner from '#ui/components/servers/InstallingBanner.vue'
+import ServerPanelAdmonitions from '#ui/components/servers/admonitions/ServerPanelAdmonitions.vue'
import MedalServerCountdown from '#ui/components/servers/marketing/MedalServerCountdown.vue'
import {
PanelServerActionButton,
@@ -455,6 +394,7 @@ import {
useServerProject,
} from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
+import { useServerBackupsQueue } from '#ui/composables/server-backups-queue'
import { useServerManageCoreRuntime } from '#ui/composables/server-manage-core-runtime'
import type { LogLine } from '#ui/layouts/shared/console'
import type { ServerSettingsTabId } from '#ui/layouts/shared/server-settings'
@@ -465,7 +405,6 @@ import {
} from '#ui/providers'
import { formatLoaderLabel } from '#ui/utils/loaders'
-import FileOperationAdmonitions from '../../../shared/files-tab/components/FileOperationAdmonitions.vue'
import ServerOnboardingPanelPage from './[id]/onboarding.vue'
interface Tab {
@@ -618,17 +557,17 @@ const worldId = computed(() => {
return activeWorld?.id ?? serverFull.value.worlds[0]?.id ?? null
})
+const { handleWsBackupProgress, busyReasons: backupsBusy } = useServerBackupsQueue(
+ computed(() => props.serverId),
+ worldId,
+)
+
const { image: serverImage } = useServerImage(
props.serverId,
computed(() => serverData.value?.upstream ?? null),
)
const { data: serverProject } = useServerProject(computed(() => serverData.value?.upstream ?? null))
-const cancelledBackups = new Set()
-const markBackupCancelled = (backupId: string) => {
- cancelledBackups.add(backupId)
-}
-
const syncProgress = ref(null)
const contentError = ref(null)
const syncProgressActive = ref(false)
@@ -686,7 +625,6 @@ const onStateEvent = (data: Archon.Websocket.v0.WSStateEvent) => {
}
const {
- backupsState,
cancelUpload,
cleanupCoreRuntime,
connectSocket,
@@ -704,8 +642,7 @@ const {
worldId,
server: serverData,
isSyncingContent,
- markBackupCancelled,
- includeBackupBusyReasons: true,
+ extraBusyReasons: backupsBusy,
setDisconnectedOnAuthIncorrect: false,
syncUptimeFromState: true,
incrementUptimeLocally: true,
@@ -713,12 +650,6 @@ const {
onStateEvent,
})
-const uploadOverallProgress = computed(() => {
- const state = uploadState.value
- if (!state.isUploading || state.totalFiles === 0) return 0
- return Math.min((state.completedFiles + state.currentFileProgress) / state.totalFiles, 1)
-})
-
const isUploading = computed(() => uploadState.value.isUploading)
function handleBeforeUnload(e: BeforeUnloadEvent) {
@@ -981,69 +912,7 @@ async function handleContentRetry() {
}
const handleBackupProgress = (data: Archon.Websocket.v0.WSBackupProgressEvent) => {
- if (data.task === 'file') return
-
- const backupId = data.id
-
- if (cancelledBackups.has(backupId)) return
-
- const current = backupsState.get(backupId) ?? {}
- const currentTaskState = current[data.task]?.state
- const isIncomingTerminal =
- data.state === 'done' || data.state === 'failed' || data.state === 'cancelled'
-
- if (currentTaskState === data.state && isIncomingTerminal) return
-
- const previousProgress = current[data.task]?.progress
- if (currentTaskState !== data.state || previousProgress !== data.progress) {
- backupsState.set(backupId, {
- ...current,
- [data.task]: {
- progress: data.progress,
- state: data.state,
- },
- })
- }
-
- if (isIncomingTerminal) {
- const attemptCleanup = (attempt: number = 1) => {
- queryClient.invalidateQueries({ queryKey: ['backups', 'list', props.serverId] }).then(() => {
- const backupData = queryClient.getQueryData([
- 'backups',
- 'list',
- props.serverId,
- ])
- const backup = backupData?.find((b) => b.id === backupId)
- const isStillActive =
- backup && (backup.status === 'in_progress' || backup.status === 'pending')
-
- if (isStillActive && attempt < 6) {
- setTimeout(() => attemptCleanup(attempt + 1), 1000 * Math.pow(2, attempt - 1))
- return
- }
-
- if (isStillActive) {
- queryClient.setQueryData(
- ['backups', 'list', props.serverId],
- (old) =>
- old?.map((b) => {
- if (b.id !== backupId) return b
- return {
- ...b,
- status: data.state === 'done' ? ('done' as const) : ('error' as const),
- ongoing: false,
- interrupted: data.state === 'failed',
- }
- }),
- )
- }
-
- backupsState.delete(backupId)
- })
- }
-
- attemptCleanup()
- }
+ handleWsBackupProgress(data)
}
const handleFilesystemOps = (data: Archon.Websocket.v0.WSFilesystemOpsEvent) => {
@@ -1455,20 +1324,23 @@ function initializeServer() {
}
}
+let intercomInitialized = false
+
const cleanup = () => {
isMounted.value = false
saveWsStateToCache()
- shutdown()
+ if (intercomInitialized) {
+ shutdown()
+ intercomInitialized = false
+ }
cleanupCoreRuntime(props.serverId)
isReconnecting.value = false
isLoading.value = true
- cancelledBackups.clear()
-
DOMPurify.removeHook('afterSanitizeAttributes')
}
@@ -1486,19 +1358,36 @@ onMounted(() => {
})
}
- let intercomInitialized = false
const tryInitIntercom = () => {
if (intercomInitialized) return
- if (!props.authUser || !props.fetchIntercomToken) return
+ if (!props.authUser || !props.fetchIntercomToken) {
+ console.debug('[PYROSERVERS][INTERCOM] waiting for auth user and token fetcher', {
+ hasAuthUser: !!props.authUser,
+ hasFetchIntercomToken: !!props.fetchIntercomToken,
+ })
+ return
+ }
intercomInitialized = true
+ console.debug('[PYROSERVERS][INTERCOM] initializing secure support chat')
props
.fetchIntercomToken()
.then(({ token }) => {
+ console.debug('[PYROSERVERS][INTERCOM] fetched messenger JWT, booting widget')
Intercom({
app_id: props.intercomAppId!,
intercom_user_jwt: token,
session_duration: 1000 * 60 * 60 * 24,
})
+ window.setTimeout(() => {
+ const hasWidget = !!document.querySelector(
+ '.intercom-lightweight-app, #intercom-container, #intercom-frame',
+ )
+ if (!hasWidget) {
+ console.warn(
+ '[PYROSERVERS][INTERCOM] boot completed but no Intercom widget was detected',
+ )
+ }
+ }, 2500)
})
.catch((error) => {
intercomInitialized = false
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index 764dbdd55f..cadb575b23 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -452,9 +452,6 @@
"content.page-layout.upload-files": {
"defaultMessage": "Upload files"
},
- "content.page-layout.uploading-files": {
- "defaultMessage": "Uploading files ({completed}/{total})"
- },
"content.selection-bar.all-already-disabled": {
"defaultMessage": "All selected content is already disabled"
},
@@ -974,14 +971,20 @@
"files.navbar.upload-from-zip-url": {
"defaultMessage": "Upload from .zip URL"
},
+ "files.operations.current-file": {
+ "defaultMessage": "Current file: {file}"
+ },
"files.operations.extracted": {
"defaultMessage": "{size} extracted"
},
"files.operations.extracting": {
"defaultMessage": "Extracting {source}"
},
- "files.operations.failed": {
- "defaultMessage": "Failed"
+ "files.operations.extracting-completed": {
+ "defaultMessage": "Extracting {source} finished"
+ },
+ "files.operations.extracting-failed": {
+ "defaultMessage": "Extracting {source} failed"
},
"files.operations.modpack-from-url": {
"defaultMessage": "modpack from URL"
@@ -1487,12 +1490,6 @@
"instance.worlds.game_mode.unknown": {
"defaultMessage": "Unknown game mode"
},
- "instances.confirm-leave-modal.body": {
- "defaultMessage": "Files are still being uploaded. Leaving this page will cancel the upload and your changes may be lost."
- },
- "instances.confirm-leave-modal.upload-in-progress": {
- "defaultMessage": "Upload in progress"
- },
"instances.content-install.compatible-count": {
"defaultMessage": "{count} compatible {count, plural, one {instance} other {instances}}"
},
@@ -2837,6 +2834,9 @@
"project.visibility.unlisted-by-staff": {
"defaultMessage": "Unlisted by staff"
},
+ "s.bg": {
+ "defaultMessage": "Background task running"
+ },
"search.filter.locked.default": {
"defaultMessage": "Filter locked"
},
@@ -2924,6 +2924,21 @@
"search.server_content_type.vanilla": {
"defaultMessage": "Vanilla"
},
+ "servers.admonitions.background-task-running": {
+ "defaultMessage": "Background task running"
+ },
+ "servers.backups.admonition.backup-cancelled.description": {
+ "defaultMessage": "Backup {backupName} was cancelled."
+ },
+ "servers.backups.admonition.backup-cancelled.title": {
+ "defaultMessage": "Backup cancelled"
+ },
+ "servers.backups.admonition.backup-completed.description": {
+ "defaultMessage": "{backupName} finished successfully."
+ },
+ "servers.backups.admonition.backup-completed.title": {
+ "defaultMessage": "Backup finished"
+ },
"servers.backups.admonition.backup-failed.description": {
"defaultMessage": "Something went wrong while creating {backupName}. Please try again or contact support if the issue continues."
},
@@ -2936,6 +2951,12 @@
"servers.backups.admonition.backup-queued.title": {
"defaultMessage": "Backup queued"
},
+ "servers.backups.admonition.backup-timed-out.description": {
+ "defaultMessage": "Creating {backupName} timed out. You can try again or contact support if the issue continues."
+ },
+ "servers.backups.admonition.backup-timed-out.title": {
+ "defaultMessage": "Backup timed out"
+ },
"servers.backups.admonition.creating-backup.description": {
"defaultMessage": "Saving world data and server configuration for {backupName}. This can take a few minutes."
},
@@ -2945,23 +2966,35 @@
"servers.backups.admonition.fallback-name": {
"defaultMessage": "Your backup"
},
+ "servers.backups.admonition.restore-cancelled.description": {
+ "defaultMessage": "Restoring from {backupName} was cancelled."
+ },
+ "servers.backups.admonition.restore-cancelled.title": {
+ "defaultMessage": "Restore cancelled"
+ },
"servers.backups.admonition.restore-failed.description": {
"defaultMessage": "Something went wrong while restoring from {backupName}. Please try again or contact support if the issue continues."
},
"servers.backups.admonition.restore-failed.title": {
- "defaultMessage": "Restoring from backup failed"
+ "defaultMessage": "Restore failed"
},
"servers.backups.admonition.restore-queued.description": {
"defaultMessage": "Restoring from {backupName} is queued and will start shortly."
},
"servers.backups.admonition.restore-queued.title": {
- "defaultMessage": "Restoring from backup queued"
+ "defaultMessage": "Restore queued"
},
"servers.backups.admonition.restore-successful.description": {
"defaultMessage": "Your server has been restored to {backupName} and is ready to start."
},
"servers.backups.admonition.restore-successful.title": {
- "defaultMessage": "Restoring from backup successful"
+ "defaultMessage": "Restore finished"
+ },
+ "servers.backups.admonition.restore-timed-out.description": {
+ "defaultMessage": "Restoring from {backupName} timed out. You can try again or contact support if the issue continues."
+ },
+ "servers.backups.admonition.restore-timed-out.title": {
+ "defaultMessage": "Restore timed out"
},
"servers.backups.admonition.restoring-backup.description": {
"defaultMessage": "Restoring your server from {backupName}. This may take a couple of minutes."
@@ -2969,18 +3002,51 @@
"servers.backups.admonition.restoring-backup.title": {
"defaultMessage": "Restoring from backup"
},
+ "servers.backups.bulk-bar.aria-label": {
+ "defaultMessage": "{count, plural, one {Bulk actions for one selected backup} other {Bulk actions for # selected backups}}"
+ },
+ "servers.backups.bulk-bar.deleting": {
+ "defaultMessage": "Deleting {total, plural, one {# backup} other {# backups}}..."
+ },
+ "servers.backups.bulk-bar.selected-count": {
+ "defaultMessage": "{count, plural, one {# backup selected} other {# backups selected}}"
+ },
+ "servers.backups.delete-modal.admonition-body": {
+ "defaultMessage": "Once deleted, {count, plural, one {this backup cannot} other {these backups cannot}} be recovered. Deletion is permanent."
+ },
+ "servers.backups.delete-modal.admonition-header": {
+ "defaultMessage": "Deletion warning"
+ },
+ "servers.backups.delete-modal.backups-label": {
+ "defaultMessage": "{count, plural, one {Backup} other {Backups ({count})}}"
+ },
+ "servers.backups.delete-modal.confirm": {
+ "defaultMessage": "Delete {count, plural, one {backup} other {# backups}}"
+ },
+ "servers.backups.delete-modal.header": {
+ "defaultMessage": "Delete {count, plural, one {backup} other {backups}}"
+ },
+ "servers.backups.empty.description": {
+ "defaultMessage": "Create your first backup"
+ },
+ "servers.backups.empty.heading": {
+ "defaultMessage": "No backups yet"
+ },
+ "servers.backups.filtered-empty.clear-filters": {
+ "defaultMessage": "Clear filters"
+ },
+ "servers.backups.filtered-empty.description": {
+ "defaultMessage": "Try a different filter or clear filters to see all backups."
+ },
+ "servers.backups.filtered-empty.heading": {
+ "defaultMessage": "No backups match"
+ },
"servers.backups.item.auto": {
"defaultMessage": "Auto"
},
"servers.backups.item.backup-schedule": {
"defaultMessage": "Backup schedule"
},
- "servers.backups.item.failed-to-create-backup": {
- "defaultMessage": "Failed to create backup"
- },
- "servers.backups.item.failed-to-restore-backup": {
- "defaultMessage": "Failed to restore from backup"
- },
"servers.backups.item.manual-backup": {
"defaultMessage": "Manual backup"
},
@@ -2990,6 +3056,21 @@
"servers.backups.item.restore": {
"defaultMessage": "Restore"
},
+ "servers.backups.select-backup-aria": {
+ "defaultMessage": "Select backup {name}"
+ },
+ "servers.backups.toolbar.create-backup": {
+ "defaultMessage": "Create backup"
+ },
+ "servers.backups.toolbar.filter-auto": {
+ "defaultMessage": "Auto"
+ },
+ "servers.backups.toolbar.filter-manual": {
+ "defaultMessage": "Manual"
+ },
+ "servers.backups.toolbar.select-all": {
+ "defaultMessage": "Select all"
+ },
"servers.busy.backup-creating": {
"defaultMessage": "Backup creation in progress"
},
@@ -3919,5 +4000,11 @@
},
"ui.confirm-leave-modal.title": {
"defaultMessage": "Leave page?"
+ },
+ "ui.stacked-admonitions.alert-count": {
+ "defaultMessage": "{count, plural, one {# alert} other {# alerts}}"
+ },
+ "ui.stacked-admonitions.dismiss-all": {
+ "defaultMessage": "Dismiss all"
}
}
diff --git a/packages/ui/src/providers/server-context.ts b/packages/ui/src/providers/server-context.ts
index 2c6cd84e5e..5b786d98d6 100644
--- a/packages/ui/src/providers/server-context.ts
+++ b/packages/ui/src/providers/server-context.ts
@@ -1,6 +1,6 @@
import type { Archon, UploadState } from '@modrinth/api-client'
import type { Stats } from '@modrinth/utils'
-import type { ComputedRef, Reactive, Ref } from 'vue'
+import type { ComputedRef, Ref } from 'vue'
import type { MessageDescriptor } from '#ui/composables/i18n'
import type { FileOperation } from '#ui/layouts/shared/files-tab/types'
@@ -11,19 +11,6 @@ export interface BusyReason {
reason: MessageDescriptor
}
-export type BackupTaskState = {
- progress: number
- state: Archon.Backups.v1.BackupState
-}
-
-export type BackupProgressEntry = {
- file?: BackupTaskState
- create?: BackupTaskState
- restore?: BackupTaskState
-}
-
-export type BackupsState = Map
-
export interface FilesystemAuth {
url: string
token: string
@@ -42,8 +29,6 @@ export interface ModrinthServerContext {
readonly isServerRunning: ComputedRef
readonly stats: Ref
readonly uptimeSeconds: Ref
- readonly backupsState: Reactive
- markBackupCancelled: (backupId: string) => void
// Content sync state
readonly isSyncingContent: Ref
diff --git a/packages/ui/src/stories/base/Admonition.stories.ts b/packages/ui/src/stories/base/Admonition.stories.ts
index f04ea9e81f..413e2a1b6d 100644
--- a/packages/ui/src/stories/base/Admonition.stories.ts
+++ b/packages/ui/src/stories/base/Admonition.stories.ts
@@ -1,8 +1,8 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
+import { ref } from 'vue'
import Admonition from '../../components/base/Admonition.vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
-import ProgressBar from '../../components/base/ProgressBar.vue'
const meta = {
title: 'Base/Admonition',
@@ -57,37 +57,56 @@ export const Dismissible: Story = {
},
}
+export const HeaderWithTimestamp: Story = {
+ render: () => ({
+ components: { Admonition },
+ setup() {
+ const t = ref(Date.now() - 3600_000)
+ return { t }
+ },
+ template: /*html*/ `
+
+ Saving world data for my-world.
+
+ `,
+ }),
+}
+
export const WithTopRightActions: Story = {
render: () => ({
components: { Admonition, ButtonStyled },
template: /*html*/ `
-
+
Uploading server files...
- Cancel
+ Cancel
-
+
Something went wrong while extracting the archive.
- Retry
-
-
- ✕
+ Retry
-
+
All files have been extracted successfully.
-
-
- ✕
-
-
`,
@@ -96,41 +115,55 @@ export const WithTopRightActions: Story = {
export const WithProgressBar: Story = {
render: () => ({
- components: { Admonition, ButtonStyled, ProgressBar },
+ components: { Admonition, ButtonStyled },
template: /*html*/ `
-
+
128 KB / 1.2 MB (45%)
- Cancel
+ Cancel
-
-
-
-
+
24 MB extracted — config/settings.yml
- Cancel
+ Cancel
-
-
-
-
+
56 MB extracted
-
-
- ✕
-
-
-
-
-
+
+
+ Queued and waiting for available bandwidth.
`,
diff --git a/packages/ui/src/stories/base/Checkbox.stories.ts b/packages/ui/src/stories/base/Checkbox.stories.ts
index a8e7f8b187..6f868fbdde 100644
--- a/packages/ui/src/stories/base/Checkbox.stories.ts
+++ b/packages/ui/src/stories/base/Checkbox.stories.ts
@@ -40,6 +40,14 @@ export const Indeterminate: Story = {
},
}
+export const LabelClass: Story = {
+ args: {
+ label: 'Custom label class',
+ labelClass: 'text-brand font-bold',
+ modelValue: true,
+ },
+}
+
export const AllStates: StoryObj = {
render: () => ({
components: { Checkbox },
diff --git a/packages/ui/src/stories/base/StackedAdmonitions.stories.ts b/packages/ui/src/stories/base/StackedAdmonitions.stories.ts
new file mode 100644
index 0000000000..c0683ad7df
--- /dev/null
+++ b/packages/ui/src/stories/base/StackedAdmonitions.stories.ts
@@ -0,0 +1,507 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+import { ref } from 'vue'
+
+import Admonition from '../../components/base/Admonition.vue'
+import ButtonStyled from '../../components/base/ButtonStyled.vue'
+import StackedAdmonitionsRaw, {
+ type StackedAdmonitionItem,
+} from '../../components/base/StackedAdmonitions.vue'
+
+// The generic type signature of StackedAdmonitions breaks Storybook's Meta
+// inference and Vue's components record type. Cast to `any` for story wiring;
+// runtime behavior is unchanged.
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const StackedAdmonitions = StackedAdmonitionsRaw as any
+
+interface DemoItem extends StackedAdmonitionItem {
+ header: string
+ body: string
+}
+
+const meta: Meta = {
+ title: 'Base/StackedAdmonitions',
+ component: StackedAdmonitions,
+}
+
+export default meta
+type Story = StoryObj
+
+const initialItems: DemoItem[] = [
+ {
+ id: 'backup-failed',
+ type: 'critical',
+ header: 'Backup failed',
+ body: 'Something went wrong while creating your backup. Try again or contact support.',
+ },
+ {
+ id: 'storage-nearly-full',
+ type: 'warning',
+ header: 'Storage nearly full',
+ body: 'Your server is using 92% of available storage.',
+ },
+ {
+ id: 'scheduled-maintenance',
+ type: 'info',
+ header: 'Scheduled maintenance',
+ body: 'Routine maintenance will begin in 30 minutes.',
+ },
+]
+
+const variedHeightItems: DemoItem[] = [
+ initialItems[0],
+ {
+ id: 'storage-nearly-full',
+ type: 'warning',
+ header: 'Storage nearly full',
+ body: 'Your server is using 92% of available storage. Large world backups, installation artifacts, and cached uploads are all contributing here, so this card is intentionally taller to exercise the collapse animation between mixed-height banners.',
+ },
+ initialItems[2],
+]
+
+const completedBackupItem: DemoItem = {
+ id: 'backup-completed',
+ type: 'success',
+ header: 'Backup completed',
+ body: 'Backup 38298832 finished successfully.',
+}
+
+export const Empty: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ template: /* html */ `
+
+
+
+
+
+
+
+ Nothing should render above this line.
+
+
+ `,
+ }),
+}
+
+export const SingleItem: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const items = ref([completedBackupItem])
+ function dismiss(id: string) {
+ items.value = items.value.filter((i) => i.id !== id)
+ }
+ function reset() {
+ items.value = [completedBackupItem]
+ }
+ return { items, dismiss, reset }
+ },
+ template: /* html */ `
+
+ `,
+ }),
+}
+
+export const TwoItems: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const items = ref(initialItems.slice(0, 2))
+ return { items }
+ },
+ template: /* html */ `
+
+
+
+
+
+ `,
+ }),
+}
+
+export const FiveItems: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const items = ref([
+ ...initialItems,
+ {
+ id: 'update-available',
+ type: 'success',
+ header: 'Update available',
+ body: 'A new version of your server software is ready to install.',
+ },
+ {
+ id: 'memory-pressure',
+ type: 'warning',
+ header: 'High memory usage',
+ body: 'Your server is using 89% of allocated memory.',
+ },
+ ])
+ return { items }
+ },
+ template: /* html */ `
+
+
+
+
+
+ `,
+ }),
+}
+
+export const MixedTypes: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const items = ref([
+ {
+ id: 'critical',
+ type: 'critical',
+ header: 'Critical admonition',
+ body: 'Red background placeholder when not front.',
+ },
+ {
+ id: 'warning',
+ type: 'warning',
+ header: 'Warning admonition',
+ body: 'Orange background placeholder when not front.',
+ },
+ {
+ id: 'info',
+ type: 'info',
+ header: 'Info admonition',
+ body: 'Blue background placeholder when not front.',
+ },
+ {
+ id: 'success',
+ type: 'success',
+ header: 'Success admonition',
+ body: 'Green background placeholder when not front.',
+ },
+ ])
+ return { items }
+ },
+ template: /* html */ `
+
+
+
+
+
+ `,
+ }),
+}
+
+export const VariedHeights: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const items = ref(variedHeightItems)
+ const expanded = ref(false)
+ let nextId = 1
+
+ function addAlert() {
+ const type = ['info', 'warning', 'critical', 'success'][nextId % 4] as DemoItem['type']
+ items.value = [
+ ...items.value,
+ {
+ id: `new-alert-${nextId}`,
+ type,
+ header: `New alert ${nextId}`,
+ body:
+ nextId % 2 === 0
+ ? 'A short dynamically added alert.'
+ : 'A dynamically added alert with a longer body so the stack can exercise measurement updates when new mixed-height items enter.',
+ },
+ ]
+ nextId += 1
+ }
+
+ function reset() {
+ items.value = variedHeightItems
+ expanded.value = false
+ nextId = 1
+ }
+
+ return { items, expanded, addAlert, reset }
+ },
+ template: /* html */ `
+
+
+
+ Add alert
+
+
+ Reset
+
+
+
+
+
+
+
+
+ `,
+ }),
+}
+
+export const ForceExpanded: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const items = ref(variedHeightItems)
+ const expanded = ref(true)
+ return { items, expanded }
+ },
+ template: /* html */ `
+
+
+
+
+
+ `,
+ }),
+}
+
+export const DismissIndividual: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const items = ref([...initialItems])
+ function dismiss(id: string) {
+ items.value = items.value.filter((i) => i.id !== id)
+ }
+ function reset() {
+ items.value = [...initialItems]
+ }
+ return { items, dismiss, reset }
+ },
+ template: /* html */ `
+
+ `,
+ }),
+}
+
+export const DismissAll: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const items = ref([...initialItems])
+ function dismiss(id: string) {
+ items.value = items.value.filter((i) => i.id !== id)
+ }
+ function dismissAll() {
+ items.value = []
+ }
+ function reset() {
+ items.value = [...initialItems]
+ }
+ return { items, dismiss, dismissAll, reset }
+ },
+ template: /* html */ `
+
+ `,
+ }),
+}
+
+interface RichItem extends StackedAdmonitionItem {
+ header: string
+ body: string
+ progress?: number
+ canRetry?: boolean
+ canCancel?: boolean
+}
+
+export const RichContent: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition, ButtonStyled },
+ setup() {
+ const items = ref([
+ {
+ id: 'upload-1',
+ type: 'info',
+ header: 'Uploading files (2/5)',
+ body: '128 KB / 1.2 MB (45%)',
+ progress: 0.45,
+ canCancel: true,
+ },
+ {
+ id: 'extract-1',
+ type: 'critical',
+ header: 'Extraction failed',
+ body: 'Something went wrong while extracting the archive.',
+ canRetry: true,
+ },
+ {
+ id: 'install-1',
+ type: 'success',
+ header: 'Installation complete',
+ body: 'All files have been installed successfully.',
+ },
+ ])
+ function dismiss(id: string) {
+ items.value = items.value.filter((i) => i.id !== id)
+ }
+ function dismissAll() {
+ items.value = []
+ }
+ return { items, dismiss, dismissAll }
+ },
+ template: /* html */ `
+
+
+
+ {{ item.body }}
+
+
+ Cancel
+
+
+ Retry
+
+
+
+
+
+ `,
+ }),
+}
+
+export const KeyboardAndA11y: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const items = ref([...initialItems])
+ function dismiss(id: string) {
+ items.value = items.value.filter((i) => i.id !== id)
+ }
+ return { items, dismiss }
+ },
+ template: /* html */ `
+
+
+ Tab to the stack and press Enter or Space to expand.
+ While collapsed, only the front card's buttons are focusable (inert on back cards).
+
+
+
+
+
+
+
+ `,
+ }),
+}
+
+export const TwoInstances: Story = {
+ render: () => ({
+ components: { StackedAdmonitions, Admonition },
+ setup() {
+ const stackA = ref([initialItems[0], initialItems[1]])
+ const stackB = ref([initialItems[2]])
+ function dismissA(id: string) {
+ stackA.value = stackA.value.filter((i) => i.id !== id)
+ }
+ function dismissB(id: string) {
+ stackB.value = stackB.value.filter((i) => i.id !== id)
+ }
+ return { stackA, stackB, dismissA, dismissB }
+ },
+ template: /* html */ `
+
+ `,
+ }),
+}
diff --git a/packages/ui/src/stories/servers/BackupItem.stories.ts b/packages/ui/src/stories/servers/BackupItem.stories.ts
new file mode 100644
index 0000000000..7add6b91fc
--- /dev/null
+++ b/packages/ui/src/stories/servers/BackupItem.stories.ts
@@ -0,0 +1,99 @@
+import type { Archon } from '@modrinth/api-client'
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import BackupItem from '../../components/servers/backups/BackupItem.vue'
+
+const meta = {
+ title: 'Servers/BackupItem',
+ component: BackupItem,
+ args: {
+ preview: false,
+ showCopyIdAction: false,
+ showDebugInfo: false,
+ restoreDisabled: undefined,
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+function makeBackup(
+ overrides: Partial = {},
+): Archon.BackupsQueue.v1.BackupQueueBackup {
+ return {
+ id: 'backup-001',
+ name: 'Backup #5',
+ created_at: new Date(Date.now() - 1000 * 60 * 10).toISOString(),
+ automated: false,
+ status: 'done',
+ locked: false,
+ history: [],
+ ...overrides,
+ }
+}
+
+export const Default: Story = {
+ name: 'Default (manual)',
+ args: {
+ backup: makeBackup({ name: 'Base finished!!' }),
+ },
+}
+
+export const Automated: Story = {
+ name: 'Automated',
+ args: {
+ backup: makeBackup({ automated: true, name: 'Backup #2' }),
+ },
+}
+
+export const Preview: Story = {
+ name: 'Preview (compact, used in delete modal)',
+ args: {
+ backup: makeBackup({ name: 'Base finished!!' }),
+ preview: true,
+ },
+}
+
+export const RestoreDisabled: Story = {
+ name: 'Restore disabled (server running)',
+ args: {
+ backup: makeBackup({ name: 'Backup #5', automated: true }),
+ restoreDisabled: 'Cannot restore backup while server is running',
+ },
+}
+
+export const CommonStates: Story = {
+ render: () => ({
+ components: { BackupItem },
+ setup() {
+ const now = new Date(Date.now() - 1000 * 60 * 10).toISOString()
+
+ function makeBackup(
+ overrides: Partial,
+ ): Archon.BackupsQueue.v1.BackupQueueBackup {
+ return {
+ id: 'backup-001',
+ name: 'Backup #5',
+ created_at: now,
+ automated: false,
+ status: 'done',
+ locked: false,
+ history: [],
+ ...overrides,
+ }
+ }
+
+ return {
+ manual: makeBackup({ name: 'Base finished!!' }),
+ automated: makeBackup({ automated: true, name: 'Backup #2' }),
+ }
+ },
+ template: /* html */ `
+
+
+
+
+
+ `,
+ }),
+}
diff --git a/packages/ui/src/stories/servers/EditServerIcon.stories.ts b/packages/ui/src/stories/servers/EditServerIcon.stories.ts
index e93ddc154b..a9b0bfdf6f 100644
--- a/packages/ui/src/stories/servers/EditServerIcon.stories.ts
+++ b/packages/ui/src/stories/servers/EditServerIcon.stories.ts
@@ -1,7 +1,7 @@
import type { Archon, UploadState } from '@modrinth/api-client'
import type { Stats } from '@modrinth/utils'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
-import { computed, reactive, ref } from 'vue'
+import { computed, ref } from 'vue'
import EditServerIcon from '../../components/servers/edit-server-icon/EditServerIcon.vue'
import { provideModrinthServerContext } from '../../providers'
@@ -66,8 +66,6 @@ const meta = {
isServerRunning: computed(() => true),
stats,
uptimeSeconds: ref(0),
- backupsState: reactive(new Map()),
- markBackupCancelled: () => {},
isSyncingContent: ref(false),
busyReasons: computed(() => []),
fsAuth: ref(null),
diff --git a/packages/ui/src/stories/servers/InstallingBanner.stories.ts b/packages/ui/src/stories/servers/InstallingBanner.stories.ts
index f04431bc36..cdf65e9da2 100644
--- a/packages/ui/src/stories/servers/InstallingBanner.stories.ts
+++ b/packages/ui/src/stories/servers/InstallingBanner.stories.ts
@@ -23,6 +23,15 @@ export const WithProgress: Story = {
},
}
+export const IndeterminateLoaderInstall: Story = {
+ args: {
+ progress: {
+ phase: 'InstallingLoader',
+ percent: 0,
+ },
+ },
+}
+
export const InstallingModpack: Story = {
args: {
progress: {
@@ -97,6 +106,7 @@ export const AllStates: Story = {
template: /*html*/ `
+
diff --git a/packages/ui/src/stories/servers/ServerPanelAdmonitionCopyDraft.stories.ts b/packages/ui/src/stories/servers/ServerPanelAdmonitionCopyDraft.stories.ts
new file mode 100644
index 0000000000..6ec76ee2c2
--- /dev/null
+++ b/packages/ui/src/stories/servers/ServerPanelAdmonitionCopyDraft.stories.ts
@@ -0,0 +1,270 @@
+import { RotateCounterClockwiseIcon } from '@modrinth/assets'
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import Admonition from '../../components/base/Admonition.vue'
+import ButtonStyled from '../../components/base/ButtonStyled.vue'
+
+type AdmonitionType = 'info' | 'warning' | 'critical' | 'success'
+type ActionType = 'Cancel' | 'Retry' | 'Dismiss'
+type ProgressColor = 'blue' | 'green' | 'red'
+
+interface CopyExample {
+ title: string
+ body: string
+ type: AdmonitionType
+ action?: ActionType
+ dismissible?: boolean
+ progress?: number
+ progressColor?: ProgressColor
+ waiting?: boolean
+}
+
+interface CopySection {
+ title: string
+ items: CopyExample[]
+}
+
+const meta = {
+ title: 'Servers/ServerPanelAdmonitionCopyDraft',
+ component: Admonition,
+ parameters: {
+ layout: 'padded',
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+const sections: CopySection[] = [
+ {
+ title: 'Installation and content sync',
+ items: [
+ {
+ type: 'info',
+ title: "We're preparing your server",
+ body: 'Installing platform...',
+ progress: 45,
+ progressColor: 'blue',
+ },
+ {
+ type: 'info',
+ title: "We're preparing your server",
+ body: 'Installing modpack...',
+ progress: 72,
+ progressColor: 'blue',
+ },
+ {
+ type: 'critical',
+ title: 'Installation failed',
+ body: 'The specified loader or Minecraft version could not be installed. It may be invalid or unsupported.',
+ action: 'Retry',
+ dismissible: true,
+ },
+ {
+ type: 'critical',
+ title: 'Installation failed',
+ body: 'This modpack version does not include a downloadable file. It may have been packaged incorrectly.',
+ action: 'Retry',
+ dismissible: true,
+ },
+ ],
+ },
+ {
+ title: 'Uploads and file operations',
+ items: [
+ {
+ type: 'info',
+ title: 'Uploading resourcepack.zip (1/3)',
+ body: '20 KB / 100 KB (20%)',
+ action: 'Cancel',
+ progress: 0.2,
+ progressColor: 'blue',
+ },
+ {
+ type: 'info',
+ title: 'Extracting story-modpack.mrpack',
+ body: '2 MB extracted. Current file: server.properties',
+ action: 'Cancel',
+ progress: 0.35,
+ progressColor: 'blue',
+ },
+ {
+ type: 'success',
+ title: 'Extracting story-modpack.mrpack finished',
+ body: '12 MB extracted',
+ progress: 1,
+ progressColor: 'green',
+ },
+ {
+ type: 'critical',
+ title: 'Extracting story-modpack.mrpack failed',
+ body: '2 MB extracted',
+ action: 'Dismiss',
+ dismissible: true,
+ progress: 0.35,
+ progressColor: 'red',
+ },
+ ],
+ },
+ {
+ title: 'Backup creation',
+ items: [
+ {
+ type: 'info',
+ title: 'Backup queued',
+ body: 'World backup is queued and will start shortly.',
+ action: 'Cancel',
+ },
+ {
+ type: 'info',
+ title: 'Creating backup',
+ body: 'Saving world data and server configuration for World backup. This can take a few minutes.',
+ action: 'Cancel',
+ progress: 0.42,
+ progressColor: 'blue',
+ },
+ {
+ type: 'critical',
+ title: 'Backup failed',
+ body: 'Something went wrong while creating World backup. Please try again or contact support if the issue continues.',
+ action: 'Retry',
+ dismissible: true,
+ },
+ {
+ type: 'success',
+ title: 'Backup finished',
+ body: 'World backup finished successfully.',
+ action: 'Dismiss',
+ dismissible: true,
+ },
+ ],
+ },
+ {
+ title: 'Backup restore',
+ items: [
+ {
+ type: 'info',
+ title: 'Restore queued',
+ body: 'Restoring from World backup is queued and will start shortly.',
+ action: 'Cancel',
+ },
+ {
+ type: 'info',
+ title: 'Restoring from backup',
+ body: 'Restoring your server from World backup. This may take a couple of minutes.',
+ action: 'Cancel',
+ progress: 0.65,
+ progressColor: 'blue',
+ },
+ {
+ type: 'critical',
+ title: 'Restore failed',
+ body: 'Something went wrong while restoring from World backup. Please try again or contact support if the issue continues.',
+ action: 'Retry',
+ dismissible: true,
+ },
+ {
+ type: 'success',
+ title: 'Restore finished',
+ body: 'Your server has been restored to World backup and is ready to start.',
+ action: 'Dismiss',
+ dismissible: true,
+ },
+ ],
+ },
+ {
+ title: 'Busy states',
+ items: [
+ {
+ type: 'warning',
+ title: 'Background task running',
+ body: 'Please wait for the operation to complete before editing content.',
+ },
+ {
+ type: 'warning',
+ title: 'Background task running',
+ body: 'File operations are disabled while the operation is in progress.',
+ },
+ ],
+ },
+]
+
+export const AllCopy: Story = {
+ render: () => ({
+ components: { Admonition, ButtonStyled, RotateCounterClockwiseIcon },
+ setup() {
+ return { sections }
+ },
+ template: /* html */ `
+
+
+
+
+ {{ section.title }}
+
+
+
+ {{ item.body }}
+
+
+ Cancel
+
+
+
+
+ Retry
+
+
+
+
+
+
+
+
+ `,
+ }),
+}
+
+export const TitleTreatmentExperiment: Story = {
+ render: () => ({
+ components: { Admonition, ButtonStyled, RotateCounterClockwiseIcon },
+ template: /* html */ `
+
+
+ Something went wrong while creating World backup. Please try again or contact support if the issue continues.
+
+
+
+
+ Retry
+
+
+
+
+
+ `,
+ }),
+}
diff --git a/packages/ui/src/stories/servers/ServerPanelAdmonitions.stories.ts b/packages/ui/src/stories/servers/ServerPanelAdmonitions.stories.ts
new file mode 100644
index 0000000000..9afdf5b429
--- /dev/null
+++ b/packages/ui/src/stories/servers/ServerPanelAdmonitions.stories.ts
@@ -0,0 +1,116 @@
+import type { Archon, UploadState } from '@modrinth/api-client'
+import type { Stats } from '@modrinth/utils'
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+import { computed, onMounted, ref } from 'vue'
+import { useRouter } from 'vue-router'
+
+import ServerPanelAdmonitions from '../../components/servers/admonitions/ServerPanelAdmonitions.vue'
+import { defineMessage } from '../../composables/i18n'
+import type { FileOperation } from '../../layouts/shared/files-tab/types'
+import { provideModrinthServerContext } from '../../providers'
+import type { ModrinthServerContext } from '../../providers/server-context'
+
+const meta = {
+ title: 'Servers/ServerPanelAdmonitions',
+ component: ServerPanelAdmonitions,
+ parameters: {
+ layout: 'padded',
+ },
+ decorators: [
+ (story) => ({
+ components: { story },
+ setup() {
+ const router = useRouter()
+ onMounted(() => {
+ router.replace('/hosting/manage/demo-server/content')
+ })
+
+ const server = ref({
+ server_id: 'demo-server',
+ status: 'running',
+ upstream: null,
+ } as Archon.Servers.v0.Server)
+
+ const stats = ref({
+ current: {
+ cpu_percent: 0,
+ ram_usage_bytes: 0,
+ ram_total_bytes: 1,
+ storage_usage_bytes: 0,
+ storage_total_bytes: 0,
+ },
+ past: {
+ cpu_percent: 0,
+ ram_usage_bytes: 0,
+ ram_total_bytes: 1,
+ storage_usage_bytes: 0,
+ storage_total_bytes: 0,
+ },
+ graph: { cpu: [], ram: [] },
+ })
+
+ const uploadState = ref({
+ isUploading: true,
+ currentFileName: 'resourcepack.zip',
+ currentFileProgress: 0.2,
+ uploadedBytes: 20_000,
+ totalBytes: 100_000,
+ completedFiles: 1,
+ totalFiles: 3,
+ })
+
+ const fileOp = ref([
+ {
+ id: 'fs-op-1',
+ op: 'extract',
+ src: 'story-modpack.mrpack',
+ state: 'running',
+ progress: 0.35,
+ bytes_processed: 2_000_000,
+ },
+ ])
+
+ const serverContext: ModrinthServerContext = {
+ get serverId() {
+ return 'demo-server'
+ },
+ worldId: ref(null),
+ server,
+ isConnected: ref(true),
+ isWsAuthIncorrect: ref(false),
+ powerState: ref('running'),
+ powerStateDetails: ref(undefined),
+ isServerRunning: computed(() => true),
+ stats,
+ uptimeSeconds: ref(0),
+ isSyncingContent: ref(false),
+ busyReasons: computed(() => [
+ { reason: defineMessage({ id: 's.bg', defaultMessage: 'Background task running' }) },
+ ]),
+ fsAuth: ref(null),
+ fsOps: ref([]),
+ fsQueuedOps: ref([]),
+ refreshFsAuth: async () => {},
+ uploadState,
+ cancelUpload: ref(() => {
+ uploadState.value = { ...uploadState.value, isUploading: false }
+ }),
+ activeOperations: computed(() => fileOp.value),
+ dismissOperation: async (id) => {
+ fileOp.value = fileOp.value.filter((o) => o.id !== id)
+ },
+ }
+
+ provideModrinthServerContext(serverContext)
+ return {}
+ },
+ template: '
',
+ }),
+ ],
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const WithUploadFileOpAndBusy: Story = {}
diff --git a/packages/ui/src/utils/server-search.ts b/packages/ui/src/utils/server-search.ts
index 86a4e082b3..cc029aa7cb 100644
--- a/packages/ui/src/utils/server-search.ts
+++ b/packages/ui/src/utils/server-search.ts
@@ -1,7 +1,7 @@
import type { Labrinth } from '@modrinth/api-client'
import { getCategoryIcon, GlobeIcon, SERVER_CATEGORY_ICON_MAP, UserIcon } from '@modrinth/assets'
import { sortedCategories } from '@modrinth/utils'
-import { computed, type Ref, ref, shallowRef } from 'vue'
+import { computed, type ComputedRef, type Ref, ref, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
import { defineMessage, LOCALES, useVIntl } from '../composables/i18n'
@@ -128,6 +128,7 @@ export function useServerSearch(opts: {
query: Ref
maxResults: Ref
currentPage: Ref
+ providedFilters?: ComputedRef
}) {
const { tags, query, maxResults, currentPage } = opts
@@ -371,6 +372,31 @@ export function useServerSearch(opts: {
}
}
+ const providedProjectIds = (opts.providedFilters?.value ?? [])
+ .filter((filter) => filter.type === 'project_id')
+ .map((filter) => ({
+ projectId: filter.option.startsWith('project_id:')
+ ? filter.option.slice('project_id:'.length)
+ : filter.option,
+ negative: !!filter.negative,
+ }))
+ .filter((filter) => filter.projectId.length > 0)
+ const excludedProjectIds = providedProjectIds
+ .filter((filter) => filter.negative)
+ .map((filter) => filter.projectId)
+ const includedProjectIds = providedProjectIds
+ .filter((filter) => !filter.negative)
+ .map((filter) => filter.projectId)
+
+ if (includedProjectIds.length > 0) {
+ const values = includedProjectIds.map((projectId) => `"${projectId}"`).join(', ')
+ parts.push(`project_id IN [${values}]`)
+ }
+ if (excludedProjectIds.length > 0) {
+ const values = excludedProjectIds.map((projectId) => `"${projectId}"`).join(', ')
+ parts.push(`project_id NOT IN [${values}]`)
+ }
+
return parts.join(' AND ')
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f3906ee746..39c0a11b9a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,6 +71,9 @@ importers:
apps/app-frontend:
dependencies:
+ '@intercom/messenger-js-sdk':
+ specifier: ^0.0.14
+ version: 0.0.14
'@modrinth/api-client':
specifier: workspace:^
version: link:../../packages/api-client
@@ -167,7 +170,7 @@ importers:
devDependencies:
'@eslint/compat':
specifier: ^1.1.1
- version: 1.4.1(eslint@9.39.2(jiti@1.21.7))
+ version: 1.4.1(eslint@9.39.2(jiti@2.6.1))
'@formatjs/cli':
specifier: ^6.2.12
version: 6.12.2(@vue/compiler-core@3.5.27)(vue@3.5.27(typescript@5.9.3))
@@ -176,22 +179,22 @@ importers:
version: link:../../packages/tooling-config
'@nuxt/eslint-config':
specifier: ^0.5.6
- version: 0.5.7(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ version: 0.5.7(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@taijased/vue-render-tracker':
specifier: ^1.0.7
version: 1.0.7(vue@3.5.27(typescript@5.9.3))
'@vitejs/plugin-vue':
specifier: ^6.0.3
- version: 6.0.4(vite@8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
+ version: 6.0.4(vite@8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
autoprefixer:
specifier: ^10.4.19
version: 10.4.24(postcss@8.5.6)
eslint:
specifier: ^9.9.1
- version: 9.39.2(jiti@1.21.7)
+ version: 9.39.2(jiti@2.6.1)
eslint-plugin-turbo:
specifier: ^2.5.4
- version: 2.8.2(eslint@9.39.2(jiti@1.21.7))(turbo@2.8.2)
+ version: 2.8.2(eslint@9.39.2(jiti@2.6.1))(turbo@2.8.2)
postcss:
specifier: ^8.4.39
version: 8.5.6
@@ -209,7 +212,7 @@ importers:
version: 5.9.3
vite:
specifier: ^8.0.0
- version: 8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+ version: 8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
vue-component-type-helpers:
specifier: ^3.1.8
version: 3.2.4
@@ -694,6 +697,9 @@ importers:
markdown-it:
specifier: ^13.0.2
version: 13.0.2
+ motion-v:
+ specifier: ^2.2.1
+ version: 2.2.1(@vueuse/core@11.3.0(vue@3.5.27(typescript@5.9.3)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vue@3.5.27(typescript@5.9.3))
postprocessing:
specifier: ^6.37.6
version: 6.38.2(three@0.172.0)
@@ -751,7 +757,7 @@ importers:
version: 5.2.4(vite@5.4.21(@types/node@20.19.31)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0))(vue@3.5.27(typescript@5.9.3))
eslint-plugin-storybook:
specifier: ^10.1.10
- version: 10.2.4(eslint@9.39.2(jiti@2.6.1))(storybook@10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
+ version: 10.2.4(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
storybook:
specifier: ^10.1.10
version: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -6208,6 +6214,20 @@ packages:
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
+ framer-motion@12.38.0:
+ resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
@@ -6426,6 +6446,9 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
+ hey-listen@1.0.8:
+ resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
+
highlight.js@11.11.1:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
@@ -7397,6 +7420,18 @@ packages:
module-details-from-path@1.0.4:
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
+ motion-dom@12.38.0:
+ resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
+
+ motion-utils@12.36.0:
+ resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
+
+ motion-v@2.2.1:
+ resolution: {integrity: sha512-BYbABe1Ep/u33dHOrK+8SoVU2MuiQqT94JOYsgrge8QbrwkKf2lS6rHW2QyzP6t89wcyBvzZeLQQwfrx76dj9A==}
+ peerDependencies:
+ '@vueuse/core': '>=10.0.0'
+ vue: '>=3.0.0'
+
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -10967,11 +11002,11 @@ snapshots:
'@eslint-community/regexpp@4.12.2': {}
- '@eslint/compat@1.4.1(eslint@9.39.2(jiti@1.21.7))':
+ '@eslint/compat@1.4.1(eslint@9.39.2(jiti@2.6.1))':
dependencies:
'@eslint/core': 0.17.0
optionalDependencies:
- eslint: 9.39.2(jiti@1.21.7)
+ eslint: 9.39.2(jiti@2.6.1)
'@eslint/config-array@0.21.1':
dependencies:
@@ -11592,36 +11627,36 @@ snapshots:
- utf-8-validate
- vue
- '@nuxt/eslint-config@0.5.7(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
+ '@nuxt/eslint-config@0.5.7(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint/js': 9.39.2
- '@nuxt/eslint-plugin': 0.5.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@stylistic/eslint-plugin': 2.13.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- eslint: 9.39.2(jiti@1.21.7)
- eslint-config-flat-gitignore: 0.3.0(eslint@9.39.2(jiti@1.21.7))
+ '@nuxt/eslint-plugin': 0.5.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@stylistic/eslint-plugin': 2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
+ eslint-config-flat-gitignore: 0.3.0(eslint@9.39.2(jiti@2.6.1))
eslint-flat-config-utils: 0.4.0
- eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))
- eslint-plugin-jsdoc: 50.8.0(eslint@9.39.2(jiti@1.21.7))
- eslint-plugin-regexp: 2.10.0(eslint@9.39.2(jiti@1.21.7))
- eslint-plugin-unicorn: 55.0.0(eslint@9.39.2(jiti@1.21.7))
- eslint-plugin-vue: 9.33.0(eslint@9.39.2(jiti@1.21.7))
+ eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-jsdoc: 50.8.0(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-regexp: 2.10.0(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-unicorn: 55.0.0(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-vue: 9.33.0(eslint@9.39.2(jiti@2.6.1))
globals: 15.15.0
local-pkg: 0.5.1
pathe: 1.1.2
- vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@1.21.7))
+ vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- '@typescript-eslint/utils'
- eslint-import-resolver-node
- supports-color
- typescript
- '@nuxt/eslint-plugin@0.5.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
+ '@nuxt/eslint-plugin@0.5.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.54.0
- '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- eslint: 9.39.2(jiti@1.21.7)
+ '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
transitivePeerDependencies:
- supports-color
- typescript
@@ -13091,18 +13126,6 @@ snapshots:
'@stripe/stripe-js@7.9.0': {}
- '@stylistic/eslint-plugin@2.13.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- eslint: 9.39.2(jiti@1.21.7)
- eslint-visitor-keys: 4.2.1
- espree: 10.4.0
- estraverse: 5.3.0
- picomatch: 4.0.3
- transitivePeerDependencies:
- - supports-color
- - typescript
-
'@stylistic/eslint-plugin@2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
@@ -13114,7 +13137,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- typescript
- optional: true
'@svgdotjs/svg.draggable.js@3.0.6(@svgdotjs/svg.js@3.2.5)':
dependencies:
@@ -13516,22 +13538,6 @@ snapshots:
dependencies:
'@types/node': 20.19.31
- '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
- dependencies:
- '@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.54.0
- '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.54.0
- eslint: 9.39.2(jiti@1.21.7)
- ignore: 7.0.5
- natural-compare: 1.4.0
- ts-api-utils: 2.4.0(typescript@5.9.3)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
'@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -13548,18 +13554,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/scope-manager': 8.54.0
- '@typescript-eslint/types': 8.54.0
- '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.54.0
- debug: 4.4.3
- eslint: 9.39.2(jiti@1.21.7)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
'@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.54.0
@@ -13590,18 +13584,6 @@ snapshots:
dependencies:
typescript: 5.9.3
- '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/types': 8.54.0
- '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
- '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- debug: 4.4.3
- eslint: 9.39.2(jiti@1.21.7)
- ts-api-utils: 2.4.0(typescript@5.9.3)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
'@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.54.0
@@ -13772,10 +13754,10 @@ snapshots:
vite: 7.3.1(@types/node@20.19.31)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
vue: 3.5.27(typescript@5.9.3)
- '@vitejs/plugin-vue@6.0.4(vite@8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))':
+ '@vitejs/plugin-vue@6.0.4(vite@8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.2
- vite: 8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
+ vite: 8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)
vue: 3.5.27(typescript@5.9.3)
'@vitest/expect@3.2.4':
@@ -15301,10 +15283,10 @@ snapshots:
escape-string-regexp@5.0.0: {}
- eslint-config-flat-gitignore@0.3.0(eslint@9.39.2(jiti@1.21.7)):
+ eslint-config-flat-gitignore@0.3.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
- '@eslint/compat': 1.4.1(eslint@9.39.2(jiti@1.21.7))
- eslint: 9.39.2(jiti@1.21.7)
+ '@eslint/compat': 1.4.1(eslint@9.39.2(jiti@2.6.1))
+ eslint: 9.39.2(jiti@2.6.1)
find-up-simple: 1.0.1
eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)):
@@ -15322,12 +15304,12 @@ snapshots:
optionalDependencies:
unrs-resolver: 1.11.1
- eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)):
+ eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@typescript-eslint/types': 8.54.0
comment-parser: 1.4.5
debug: 4.4.3
- eslint: 9.39.2(jiti@1.21.7)
+ eslint: 9.39.2(jiti@2.6.1)
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
is-glob: 4.0.3
minimatch: 10.1.2
@@ -15335,18 +15317,18 @@ snapshots:
stable-hash-x: 0.2.0
unrs-resolver: 1.11.1
optionalDependencies:
- '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
transitivePeerDependencies:
- supports-color
- eslint-plugin-jsdoc@50.8.0(eslint@9.39.2(jiti@1.21.7)):
+ eslint-plugin-jsdoc@50.8.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@es-joy/jsdoccomment': 0.50.2
are-docs-informative: 0.0.2
comment-parser: 1.4.1
debug: 4.4.3
escape-string-regexp: 4.0.0
- eslint: 9.39.2(jiti@1.21.7)
+ eslint: 9.39.2(jiti@2.6.1)
espree: 10.4.0
esquery: 1.7.0
parse-imports-exports: 0.2.4
@@ -15364,12 +15346,12 @@ snapshots:
optionalDependencies:
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-regexp@2.10.0(eslint@9.39.2(jiti@1.21.7)):
+ eslint-plugin-regexp@2.10.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
'@eslint-community/regexpp': 4.12.2
comment-parser: 1.4.5
- eslint: 9.39.2(jiti@1.21.7)
+ eslint: 9.39.2(jiti@2.6.1)
jsdoc-type-pratt-parser: 4.8.0
refa: 0.12.1
regexp-ast-analysis: 0.7.1
@@ -15379,29 +15361,29 @@ snapshots:
dependencies:
eslint: 9.39.2(jiti@2.6.1)
- eslint-plugin-storybook@10.2.4(eslint@9.39.2(jiti@2.6.1))(storybook@10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3):
+ eslint-plugin-storybook@10.2.4(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3):
dependencies:
- '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
+ '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@1.21.7)
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- supports-color
- typescript
- eslint-plugin-turbo@2.8.2(eslint@9.39.2(jiti@1.21.7))(turbo@2.8.2):
+ eslint-plugin-turbo@2.8.2(eslint@9.39.2(jiti@2.6.1))(turbo@2.8.2):
dependencies:
dotenv: 16.0.3
- eslint: 9.39.2(jiti@1.21.7)
+ eslint: 9.39.2(jiti@2.6.1)
turbo: 2.8.2
- eslint-plugin-unicorn@55.0.0(eslint@9.39.2(jiti@1.21.7)):
+ eslint-plugin-unicorn@55.0.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@babel/helper-validator-identifier': 7.28.5
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
ci-info: 4.4.0
clean-regexp: 1.0.0
core-js-compat: 3.48.0
- eslint: 9.39.2(jiti@1.21.7)
+ eslint: 9.39.2(jiti@2.6.1)
esquery: 1.7.0
globals: 15.15.0
indent-string: 4.0.0
@@ -15428,16 +15410,16 @@ snapshots:
'@stylistic/eslint-plugin': 2.13.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-vue@9.33.0(eslint@9.39.2(jiti@1.21.7)):
+ eslint-plugin-vue@9.33.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
- eslint: 9.39.2(jiti@1.21.7)
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
+ eslint: 9.39.2(jiti@2.6.1)
globals: 13.24.0
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 6.1.2
semver: 7.7.3
- vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@1.21.7))
+ vue-eslint-parser: 9.4.3(eslint@9.39.2(jiti@2.6.1))
xml-name-validator: 4.0.0
transitivePeerDependencies:
- supports-color
@@ -15755,6 +15737,15 @@ snapshots:
fraction.js@5.3.4: {}
+ framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ motion-dom: 12.38.0
+ motion-utils: 12.36.0
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
fresh@2.0.0: {}
fsevents@2.3.3:
@@ -16108,6 +16099,8 @@ snapshots:
he@1.2.0: {}
+ hey-listen@1.0.8: {}
+
highlight.js@11.11.1: {}
highlightjs-mcfunction@https://codeload.github.com/modrinth/better-highlightjs-mcfunction/tar.gz/aa999b763fd792ffb950d28347eeb6811c83ea8e: {}
@@ -17302,6 +17295,25 @@ snapshots:
module-details-from-path@1.0.4: {}
+ motion-dom@12.38.0:
+ dependencies:
+ motion-utils: 12.36.0
+
+ motion-utils@12.36.0: {}
+
+ motion-v@2.2.1(@vueuse/core@11.3.0(vue@3.5.27(typescript@5.9.3)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vue@3.5.27(typescript@5.9.3)):
+ dependencies:
+ '@vueuse/core': 11.3.0(vue@3.5.27(typescript@5.9.3))
+ framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ hey-listen: 1.0.8
+ motion-dom: 12.38.0
+ motion-utils: 12.36.0
+ vue: 3.5.27(typescript@5.9.3)
+ transitivePeerDependencies:
+ - '@emotion/is-prop-valid'
+ - react
+ - react-dom
+
mrmime@2.0.1: {}
ms@2.1.3: {}
@@ -19817,7 +19829,7 @@ snapshots:
terser: 5.46.0
yaml: 2.8.2
- vite@8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2):
+ vite@8.0.3(@types/node@20.19.31)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -19828,7 +19840,7 @@ snapshots:
'@types/node': 20.19.31
esbuild: 0.27.3
fsevents: 2.3.3
- jiti: 1.21.7
+ jiti: 2.6.1
sass: 1.97.3
terser: 5.46.0
yaml: 2.8.2
@@ -19993,10 +20005,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- vue-eslint-parser@9.4.3(eslint@9.39.2(jiti@1.21.7)):
+ vue-eslint-parser@9.4.3(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 4.4.3
- eslint: 9.39.2(jiti@1.21.7)
+ eslint: 9.39.2(jiti@2.6.1)
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
espree: 9.6.1