feat:移除了弹窗,服务器添加sls

This commit is contained in:
2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 changed files with 1322534 additions and 0 deletions

View File

@ -0,0 +1,955 @@
<script setup lang="ts">
import { DatabaseIcon, RefreshCwIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
NewButton as Button,
NewModal,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { getVersion } from '@tauri-apps/api/app'
import { invoke } from '@tauri-apps/api/core'
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
import { inject, nextTick, ref, watch } from 'vue'
import UpdateAnnouncementHistory from '@/components/ui/announcement/UpdateAnnouncementHistory.vue'
import {
betaDatabaseExists,
copyReleaseDatabaseToBeta,
copyDatabaseBetweenChannels,
getCurrentAppDatabasePath,
getUpdateChannel,
getUpdatePreferences,
setUpdateChannel,
setUpdatePreferences,
type UpdateChannel,
} from '@/helpers/settings.ts'
import { isDev, restartApp } from '@/helpers/utils.js'
import { type AppUpdateCheckResult, checkForAppUpdate } from '@/providers/app-update.ts'
import SettingsRow from './SettingsRow.vue'
import SettingsSection from './SettingsSection.vue'
const { formatMessage } = useVIntl()
const { addNotification, handleError } = injectNotificationManager()
const [
activeChannel,
initialUpdatePreferences,
currentVersion,
isDevEnvironment,
databasePath,
portable,
] = await Promise.all([
getUpdateChannel(),
getUpdatePreferences(),
getVersion(),
isDev(),
getCurrentAppDatabasePath().catch(() => ''),
invoke<boolean>('is_portable_mode').catch(() => false),
])
const selectedChannel = ref<UpdateChannel>(activeChannel)
const updatePreferences = ref(initialUpdatePreferences)
const checking = ref(false)
const checkResult = ref<AppUpdateCheckResult | 'failed' | 'portable' | null>(null)
const currentDatabasePath = ref(databasePath)
const latestChannelVersions = ref<Partial<Record<UpdateChannel, string>>>({})
const latestChannelVersionsLoaded = ref(false)
const isPortable = ref(portable)
const restartModal = ref<InstanceType<typeof NewModal>>()
const copyDatabaseModal = ref<InstanceType<typeof NewModal>>()
const databaseOperationModal = ref<InstanceType<typeof NewModal>>()
const pendingChannel = ref<UpdateChannel | null>(null)
const databaseOperation = ref<'release-to-beta' | 'beta-to-release' | ''>('')
const databaseOperationBusy = ref(false)
let restoringChannelSelection = false
const messages = defineMessages({
title: {
id: 'app.settings.updates.channel.title',
defaultMessage: 'Update channel',
},
preferencesTitle: {
id: 'app.settings.updates.preferences.title',
defaultMessage: 'Update behavior',
},
checkTitle: {
id: 'app.settings.updates.check.title',
defaultMessage: 'Check for updates',
},
description: {
id: 'app.settings.updates.channel.description',
defaultMessage: 'Choose which launcher versions Axolotl receives.',
},
channelLabel: {
id: 'app.settings.updates.channel.label',
defaultMessage: 'Channel',
},
release: {
id: 'app.settings.updates.channel.release',
defaultMessage: 'Release',
},
releaseDescription: {
id: 'app.settings.updates.channel.release-description',
defaultMessage: 'Stable, tested features and fixes. Updates arrive less often.',
},
beta: {
id: 'app.settings.updates.channel.beta',
defaultMessage: 'Beta',
},
betaDescription: {
id: 'app.settings.updates.channel.beta-description',
defaultMessage: 'New, less stable updates with the latest features. Updates arrive more often.',
},
betaImmediateFetch: {
id: 'app.settings.updates.immediate-fetch.beta-description',
defaultMessage: 'Beta updates are always available immediately.',
},
check: {
id: 'app.settings.updates.check',
defaultMessage: 'Check for updates',
},
checking: {
id: 'app.settings.updates.checking',
defaultMessage: 'Checking for updates…',
},
available: {
id: 'app.settings.updates.available',
defaultMessage: 'An update is available.',
},
upToDate: {
id: 'app.settings.updates.up-to-date',
defaultMessage: 'Axolotl is up to date.',
},
disabled: {
id: 'app.settings.updates.disabled',
defaultMessage: 'Updates are disabled in this build.',
},
offline: {
id: 'app.settings.updates.offline',
defaultMessage: 'Connect to the internet to check for updates.',
},
failed: {
id: 'app.settings.updates.failed',
defaultMessage: 'Could not check for updates.',
},
portable: {
id: 'app.settings.updates.portable',
defaultMessage:
'Portable mode cannot update automatically. Please download the latest version manually.',
},
security: {
id: 'app.settings.updates.security',
defaultMessage: 'Updates are installed only when their cryptographic signature is valid.',
},
currentVersion: {
id: 'app.settings.updates.current-version',
defaultMessage: 'Current version {version}',
},
latestVersion: {
id: 'app.settings.updates.latest-version',
defaultMessage: 'Latest {channel} version: {version}',
},
latestVersionUnavailable: {
id: 'app.settings.updates.latest-version-unavailable',
defaultMessage: 'Latest version unavailable',
},
latestVersionLoading: {
id: 'app.settings.updates.latest-version-loading',
defaultMessage: 'Fetching latest version…',
},
restartTitle: {
id: 'app.settings.updates.channel.restart-title',
defaultMessage: 'Restart required',
},
restartDescription: {
id: 'app.settings.updates.channel.restart-description',
defaultMessage:
'Restart Axolotl now to start using the new update channel, or restart manually later.',
},
restartDevelopmentDescription: {
id: 'app.settings.updates.channel.restart-development-description',
defaultMessage:
'The new update channel will be used after you manually restart the development session.',
},
restartNow: {
id: 'app.settings.updates.channel.restart-now',
defaultMessage: 'Restart now',
},
restartLater: {
id: 'app.settings.updates.channel.restart-later',
defaultMessage: 'Restart manually later',
},
immediateFetch: {
id: 'app.settings.updates.immediate-fetch',
defaultMessage: 'Get updates as soon as they are available',
},
immediateFetchDescription: {
id: 'app.settings.updates.immediate-fetch-description',
defaultMessage:
'After the latest stable changes and fixes are released, be among the first to receive them.',
},
pause: {
id: 'app.settings.updates.pause',
defaultMessage: 'Pause updates',
},
pauseDescription: {
id: 'app.settings.updates.pause-description',
defaultMessage:
'Stop automatic update checks, downloads, and update notifications until you resume updates.',
},
paused: {
id: 'app.settings.updates.paused',
defaultMessage: 'Updates are paused.',
},
copyDatabaseTitle: {
id: 'app.settings.updates.channel.copy-database-title',
defaultMessage: 'Copy Release data to Beta?',
},
copyDatabaseDescription: {
id: 'app.settings.updates.channel.copy-database-description',
defaultMessage:
'Would you like to copy your Release database into the Beta channel? This cannot be undone automatically.',
},
copyDatabase: {
id: 'app.settings.updates.channel.copy-database',
defaultMessage: 'Copy database',
},
startEmpty: {
id: 'app.settings.updates.channel.start-empty',
defaultMessage: 'Start with empty database',
},
databaseIsolationTitle: {
id: 'app.settings.updates.database-isolation.title',
defaultMessage: 'Database isolation',
},
currentDatabase: {
id: 'app.settings.updates.database-isolation.current-database',
defaultMessage: 'Database currently in use',
},
databaseIsolationDescription: {
id: 'app.settings.updates.database-isolation.description',
defaultMessage:
'Release and Beta use separate databases, so testing Beta does not change your Release data.',
},
releaseDatabase: {
id: 'app.settings.updates.database-isolation.release',
defaultMessage: 'Release database',
},
betaDatabase: {
id: 'app.settings.updates.database-isolation.beta',
defaultMessage: 'Beta database',
},
activeDatabase: {
id: 'app.settings.updates.database-isolation.active',
defaultMessage: 'Active',
},
databaseOperation: {
id: 'app.settings.updates.database-operation.label',
defaultMessage: 'Database operation',
},
releaseToBeta: {
id: 'app.settings.updates.database-operation.release-to-beta',
defaultMessage: 'Copy Release to Beta',
},
betaToRelease: {
id: 'app.settings.updates.database-operation.beta-to-release',
defaultMessage: 'Copy Beta to Release',
},
databaseOperationTitle: {
id: 'app.settings.updates.database-operation.confirm-title',
defaultMessage: 'Overwrite database?',
},
databaseOperationDescription: {
id: 'app.settings.updates.database-operation.confirm-description',
defaultMessage:
'This will completely replace the inactive {target} database with the contents of the {source} database. This cannot be undone.',
},
databaseOperationConfirm: {
id: 'app.settings.updates.database-operation.confirm',
defaultMessage: 'Overwrite database',
},
databaseOperationActiveTarget: {
id: 'app.settings.updates.database-operation.active-target',
defaultMessage:
'Cannot overwrite the database currently in use. Restart Axolotl and switch channels first.',
},
databaseOperationFailed: {
id: 'app.settings.updates.database-operation.failed',
defaultMessage:
'The database could not be copied. Please make sure Axolotl is not using the target database.',
},
databaseOperationSuccess: {
id: 'app.settings.updates.database-operation.success',
defaultMessage: '{source} database was copied to {target} successfully.',
},
cancel: {
id: 'app.settings.updates.database-operation.cancel',
defaultMessage: 'Cancel',
},
})
async function loadLatestChannelVersions() {
const versions = await Promise.all(
(['release', 'beta'] as const).map(async (channel) => {
try {
const response = await tauriFetch(`https://update.axlmc.org/latest?channel=${channel}`)
if (!response.ok) return [channel, undefined] as const
const payload = (await response.json()) as { version?: string }
return [channel, payload.version] as const
} catch {
return [channel, undefined] as const
}
}),
)
latestChannelVersions.value = Object.fromEntries(versions.filter(([, version]) => version))
latestChannelVersionsLoaded.value = true
}
void loadLatestChannelVersions()
const resultMessages: Record<AppUpdateCheckResult | 'failed' | 'portable', keyof typeof messages> =
{
available: 'available',
'up-to-date': 'upToDate',
disabled: 'disabled',
offline: 'offline',
failed: 'failed',
portable: 'portable',
paused: 'paused',
}
watch(selectedChannel, async (channel, previousChannel) => {
if (restoringChannelSelection) return
if (channel === 'beta') updatePreferences.value.immediateUpdateFetch = true
if (channel === 'beta' && previousChannel === 'release') {
try {
if (!(await betaDatabaseExists())) {
pendingChannel.value = channel
restoringChannelSelection = true
selectedChannel.value = previousChannel
await nextTick()
restoringChannelSelection = false
copyDatabaseModal.value?.show()
return
}
} catch (error) {
restoringChannelSelection = true
selectedChannel.value = previousChannel
await nextTick()
restoringChannelSelection = false
handleError(error)
return
}
}
await applyChannel(channel)
checkResult.value = null
})
async function applyChannel(channel: UpdateChannel, copyDatabase = false) {
try {
if (copyDatabase) await copyReleaseDatabaseToBeta()
await setUpdateChannel(channel)
restartModal.value?.show()
return true
} catch (error) {
handleError(error)
return false
}
}
async function chooseBetaDatabase(copyDatabase: boolean) {
copyDatabaseModal.value?.hide()
const channel = pendingChannel.value
pendingChannel.value = null
if (channel && (await applyChannel(channel, copyDatabase))) {
restoringChannelSelection = true
selectedChannel.value = channel
await nextTick()
restoringChannelSelection = false
}
}
async function restartForChannelChange() {
restartModal.value?.hide()
try {
await restartApp()
} catch (error) {
handleError(error)
}
}
async function saveUpdatePreferences() {
try {
await setUpdatePreferences(updatePreferences.value)
} catch (error) {
handleError(error)
}
}
async function checkForUpdates() {
checking.value = true
checkResult.value = null
if (isPortable.value) {
checkResult.value = 'portable'
checking.value = false
return
}
try {
checkResult.value = await checkForAppUpdate()
} catch (error) {
checkResult.value = 'failed'
handleError(error)
} finally {
checking.value = false
}
}
function requestDatabaseOperation() {
if (!databaseOperation.value) return
databaseOperationModal.value?.show()
}
function selectDatabaseOperation(operation: 'release-to-beta' | 'beta-to-release') {
databaseOperation.value = operation
requestDatabaseOperation()
}
async function confirmDatabaseOperation() {
if (!databaseOperation.value || databaseOperationBusy.value) return
const [sourceChannel, targetChannel] =
databaseOperation.value === 'release-to-beta'
? (['release', 'beta'] as const)
: (['beta', 'release'] as const)
databaseOperationBusy.value = true
databaseOperationModal.value?.hide()
try {
await copyDatabaseBetweenChannels(sourceChannel, targetChannel)
addNotification({
type: 'success',
title: formatMessage(messages.databaseOperationSuccess, {
source:
sourceChannel === 'release'
? formatMessage(messages.releaseDatabase)
: formatMessage(messages.betaDatabase),
target:
targetChannel === 'release'
? formatMessage(messages.releaseDatabase)
: formatMessage(messages.betaDatabase),
}),
})
} catch (error) {
handleError(
new Error(
targetChannel === activeChannel
? formatMessage(messages.databaseOperationActiveTarget)
: formatMessage(messages.databaseOperationFailed),
),
)
} finally {
databaseOperationBusy.value = false
}
}
function cancelDatabaseOperation() {
databaseOperationModal.value?.hide()
databaseOperation.value = ''
}
</script>
<template>
<div class="flex flex-col gap-6">
<SettingsSection :title="formatMessage(messages.title)">
<div class="update-channel-panel">
<p id="settings-target-updates-channel" class="m-0 text-sm leading-[1.45] text-secondary">
{{ formatMessage(messages.description) }}
</p>
<div
class="update-channel-options"
role="radiogroup"
:aria-label="formatMessage(messages.channelLabel)"
>
<button
type="button"
class="update-channel-card"
:class="{ 'update-channel-card-selected': selectedChannel === 'release' }"
role="radio"
:aria-checked="selectedChannel === 'release'"
@click="selectedChannel = 'release'"
>
<span class="update-channel-card-title">{{ formatMessage(messages.release) }}</span>
<span class="update-channel-card-description">
{{ formatMessage(messages.releaseDescription) }}
</span>
<span class="update-channel-card-version">
{{
!latestChannelVersionsLoaded
? formatMessage(messages.latestVersionLoading)
: latestChannelVersions.release
? formatMessage(messages.latestVersion, {
channel: formatMessage(messages.release),
version: latestChannelVersions.release,
})
: formatMessage(messages.latestVersionUnavailable)
}}
</span>
</button>
<button
type="button"
class="update-channel-card"
:class="{ 'update-channel-card-selected': selectedChannel === 'beta' }"
role="radio"
:aria-checked="selectedChannel === 'beta'"
@click="selectedChannel = 'beta'"
>
<span class="update-channel-card-title">{{ formatMessage(messages.beta) }}</span>
<span class="update-channel-card-description">
{{ formatMessage(messages.betaDescription) }}
</span>
<span class="update-channel-card-version">
{{
!latestChannelVersionsLoaded
? formatMessage(messages.latestVersionLoading)
: latestChannelVersions.beta
? formatMessage(messages.latestVersion, {
channel: formatMessage(messages.beta),
version: latestChannelVersions.beta,
})
: formatMessage(messages.latestVersionUnavailable)
}}
</span>
</button>
</div>
</div>
<div class="database-isolation">
<div class="database-isolation-copy">
<h3 class="m-0 text-base font-semibold text-contrast">
{{ formatMessage(messages.databaseIsolationTitle) }}
</h3>
<p class="m-0 text-sm text-secondary">
{{ formatMessage(messages.databaseIsolationDescription) }}
</p>
<div class="database-path">
<span>{{ formatMessage(messages.currentDatabase) }}</span>
<code>{{ currentDatabasePath || '—' }}</code>
</div>
<div class="database-operation">
<span>{{ formatMessage(messages.databaseOperation) }}</span>
<div class="database-operation-buttons">
<ButtonStyled type="outlined" :disabled="activeChannel === 'beta'">
<button
type="button"
:disabled="activeChannel === 'beta'"
@click="selectDatabaseOperation('release-to-beta')"
>
<DatabaseIcon />
{{ formatMessage(messages.releaseToBeta) }}
</button>
</ButtonStyled>
<ButtonStyled type="outlined" :disabled="activeChannel === 'release'">
<button
type="button"
:disabled="activeChannel === 'release'"
@click="selectDatabaseOperation('beta-to-release')"
>
<DatabaseIcon />
{{ formatMessage(messages.betaToRelease) }}
</button>
</ButtonStyled>
</div>
</div>
</div>
<div
class="database-diagram"
:aria-label="formatMessage(messages.databaseIsolationDescription)"
>
<div class="database-channel database-channel-release">
<span>R</span>
<strong>{{ formatMessage(messages.releaseDatabase) }}</strong>
<small>{{
activeChannel === 'release' ? formatMessage(messages.activeDatabase) : '\u00a0'
}}</small>
</div>
<div class="database-branch" aria-hidden="true"></div>
<div class="database-channel database-channel-beta">
<span>B</span>
<strong>{{ formatMessage(messages.betaDatabase) }}</strong>
<small>{{
activeChannel === 'beta' ? formatMessage(messages.activeDatabase) : '\u00a0'
}}</small>
</div>
</div>
</div>
</SettingsSection>
<SettingsSection :title="formatMessage(messages.preferencesTitle)">
<SettingsRow>
<template #label>{{ formatMessage(messages.immediateFetch) }}</template>
<template #description>
{{
selectedChannel === 'beta'
? formatMessage(messages.betaImmediateFetch)
: formatMessage(messages.immediateFetchDescription)
}}
</template>
<template #control>
<Toggle
id="immediate-update-fetch"
v-model="updatePreferences.immediateUpdateFetch"
:disabled="selectedChannel === 'beta'"
@update:model-value="saveUpdatePreferences"
/>
</template>
</SettingsRow>
<SettingsRow>
<template #label>{{ formatMessage(messages.pause) }}</template>
<template #description>{{ formatMessage(messages.pauseDescription) }}</template>
<template #control>
<Toggle
id="pause-updates"
v-model="updatePreferences.updatesPaused"
@update:model-value="saveUpdatePreferences"
/>
</template>
</SettingsRow>
</SettingsSection>
<SettingsSection :title="formatMessage(messages.checkTitle)">
<div class="update-check-panel">
<div class="update-check-heading">
<p class="m-0 text-sm text-secondary">
{{ formatMessage(messages.currentVersion, { version: currentVersion }) }}
</p>
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.security) }}</p>
</div>
<div class="flex flex-wrap gap-2">
<Button type="colored" color="brand" :disabled="checking" @click="checkForUpdates">
<RefreshCwIcon :class="{ 'animate-spin': checking }" />
{{ formatMessage(checking ? messages.checking : messages.check) }}
</Button>
</div>
<p
v-if="checkResult"
class="update-check-result"
:class="`update-check-result-${checkResult}`"
role="status"
>
{{ formatMessage(messages[resultMessages[checkResult]]) }}
</p>
</div>
</SettingsSection>
<UpdateAnnouncementHistory :current-version="currentVersion" />
</div>
<NewModal ref="restartModal" :header="formatMessage(messages.restartTitle)" :closable="false">
<p class="m-0">
{{
formatMessage(
isDevEnvironment ? messages.restartDevelopmentDescription : messages.restartDescription,
)
}}
</p>
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button type="button" @click="restartModal?.hide()">
<XIcon />
{{ formatMessage(messages.restartLater) }}
</button>
</ButtonStyled>
<ButtonStyled v-if="!isDevEnvironment" color="brand">
<button type="button" @click="restartForChannelChange">
<RefreshCwIcon />
{{ formatMessage(messages.restartNow) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
<NewModal
ref="copyDatabaseModal"
:header="formatMessage(messages.copyDatabaseTitle)"
:closable="false"
>
<p class="m-0">{{ formatMessage(messages.copyDatabaseDescription) }}</p>
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button type="button" @click="chooseBetaDatabase(false)">
<XIcon />
{{ formatMessage(messages.startEmpty) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button type="button" @click="chooseBetaDatabase(true)">
<RefreshCwIcon />
{{ formatMessage(messages.copyDatabase) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
<NewModal
ref="databaseOperationModal"
:header="formatMessage(messages.databaseOperationTitle)"
:closable="false"
>
<p class="m-0">
{{
formatMessage(messages.databaseOperationDescription, {
source:
databaseOperation === 'release-to-beta'
? formatMessage(messages.releaseDatabase)
: formatMessage(messages.betaDatabase),
target:
databaseOperation === 'release-to-beta'
? formatMessage(messages.betaDatabase)
: formatMessage(messages.releaseDatabase),
})
}}
</p>
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button type="button" :disabled="databaseOperationBusy" @click="cancelDatabaseOperation">
<XIcon />
{{ formatMessage(messages.cancel) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button type="button" :disabled="databaseOperationBusy" @click="confirmDatabaseOperation">
<RefreshCwIcon :class="{ 'animate-spin': databaseOperationBusy }" />
{{ formatMessage(messages.databaseOperationConfirm) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
<style scoped>
.update-check-panel {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--gap-md);
padding: var(--gap-lg);
}
.database-isolation {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(16rem, 0.8fr);
gap: var(--gap-lg);
margin: 0 var(--gap-lg) var(--gap-lg);
padding: var(--gap-md);
border: 1px solid var(--surface-4);
border-radius: var(--radius-md);
background: var(--surface-1);
}
.database-isolation-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--gap-sm);
}
.database-path {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--gap-xs);
color: var(--color-secondary);
font-size: 0.9375rem;
}
.database-path code {
overflow-wrap: anywhere;
color: var(--color-contrast);
font-family: var(--font-mono);
}
.database-operation {
display: flex;
flex-direction: column;
gap: var(--gap-xs);
color: var(--color-secondary);
font-size: 0.9375rem;
}
.database-operation-buttons {
display: flex;
flex-wrap: wrap;
gap: var(--gap-sm);
}
.database-diagram {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: var(--gap-sm);
min-width: 0;
}
.database-channel {
display: flex;
min-width: 0;
flex-direction: column;
align-items: center;
gap: 0.2rem;
padding: var(--gap-sm);
border: 1px solid var(--surface-4);
border-radius: var(--radius-sm);
background: var(--surface-2);
color: var(--color-contrast);
text-align: center;
}
.database-channel > span {
display: grid;
width: 2rem;
height: 2rem;
place-items: center;
border-radius: 50%;
background: var(--color-brand);
color: var(--color-button-text);
font-weight: 700;
}
.database-channel small {
color: var(--color-brand);
font-size: 0.7rem;
font-weight: 600;
}
.database-channel-beta > span {
background: var(--color-purple, var(--color-brand));
}
.database-branch {
width: 2.5rem;
height: 1px;
background: var(--surface-5);
}
.update-channel-panel {
display: flex;
flex-direction: column;
gap: var(--gap-md);
padding: var(--gap-lg);
}
.update-channel-options {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--gap-md);
width: 100%;
}
.update-channel-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
width: 100%;
min-width: 0;
gap: var(--gap-xs);
padding: var(--gap-md);
border: 1px solid var(--surface-4);
border-radius: var(--radius-md);
background: var(--surface-1);
color: var(--color-secondary);
text-align: left;
cursor: pointer;
transition:
border-color 150ms ease,
background-color 150ms ease,
color 150ms ease,
transform 150ms ease;
}
.update-channel-card:hover {
border-color: color-mix(in srgb, var(--color-brand) 55%, var(--surface-4));
background: var(--surface-2);
}
.update-channel-card:active {
transform: scale(0.98);
}
.update-channel-card:focus-visible {
outline: 2px solid var(--color-brand);
outline-offset: 2px;
}
.update-channel-card-selected {
border-color: var(--color-brand);
background: color-mix(in srgb, var(--color-brand) 12%, var(--surface-1));
}
.update-channel-card-title {
color: var(--color-contrast);
font-weight: 600;
}
.update-channel-card-description {
font-size: 0.875rem;
line-height: 1.4;
}
.update-channel-card-version {
grid-column: 2;
grid-row: 1 / span 2;
align-self: center;
color: var(--color-secondary);
font-size: 0.8125rem;
font-weight: 600;
}
.update-check-heading {
display: flex;
flex-direction: column;
gap: var(--gap-xs);
}
.update-check-result {
margin: 0;
padding: var(--gap-sm) var(--gap-md);
border: 1px solid var(--surface-4);
border-radius: var(--radius-sm);
background: var(--surface-1);
color: var(--color-secondary);
font-size: 0.875rem;
line-height: 1.4;
}
.update-check-result-available {
border-color: color-mix(in srgb, var(--color-brand) 45%, var(--surface-4));
color: var(--color-brand);
}
.update-check-result-failed,
.update-check-result-offline {
border-color: color-mix(in srgb, var(--color-red) 45%, var(--surface-4));
color: var(--color-red);
}
.update-check-result-paused,
.update-check-result-portable,
.update-check-result-disabled {
border-color: color-mix(in srgb, var(--color-yellow) 45%, var(--surface-4));
color: var(--color-yellow);
}
@media (max-width: 640px) {
.database-isolation {
grid-template-columns: minmax(0, 1fr);
}
}
</style>