feat: 统一和完善 UI

This commit is contained in:
mofeng-git
2026-07-19 21:43:27 +08:00
parent 5d871e8b21
commit 275c2ca343
25 changed files with 431 additions and 402 deletions

View File

@@ -304,7 +304,7 @@ const hasRightOverflow = computed(() => {
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="msdDialogOpen = true"> <Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="msdDialogOpen = true">
<HardDrive class="h-4 w-4" /> <HardDrive class="size-4" />
<span v-if="visibleSet.get('msd') === 'label'">{{ t('actionbar.virtualMedia') }}</span> <span v-if="visibleSet.get('msd') === 'label'">{{ t('actionbar.virtualMedia') }}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -320,7 +320,7 @@ const hasRightOverflow = computed(() => {
<Popover v-model:open="atxOpen"> <Popover v-model:open="atxOpen">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"> <Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs">
<Power class="h-4 w-4" /> <Power class="size-4" />
<span v-if="visibleSet.get('atx') === 'label'">{{ t('actionbar.power') }}</span> <span v-if="visibleSet.get('atx') === 'label'">{{ t('actionbar.power') }}</span>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -341,7 +341,7 @@ const hasRightOverflow = computed(() => {
<Popover v-model:open="pasteOpen"> <Popover v-model:open="pasteOpen">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"> <Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs">
<ClipboardPaste class="h-4 w-4" /> <ClipboardPaste class="size-4" />
<span v-if="visibleSet.get('paste') === 'label'">{{ t('actionbar.paste') }}</span> <span v-if="visibleSet.get('paste') === 'label'">{{ t('actionbar.paste') }}</span>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -360,7 +360,7 @@ const hasRightOverflow = computed(() => {
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="emit('toggleStats')"> <Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="emit('toggleStats')">
<BarChart3 class="h-4 w-4" /> <BarChart3 class="size-4" />
<span v-if="visibleSet.get('stats') === 'label'">{{ t('actionbar.stats') }}</span> <span v-if="visibleSet.get('stats') === 'label'">{{ t('actionbar.stats') }}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -383,7 +383,7 @@ const hasRightOverflow = computed(() => {
:disabled="!props.ttydRunning" :disabled="!props.ttydRunning"
@click="emit('openTerminal')" @click="emit('openTerminal')"
> >
<Terminal class="h-4 w-4" /> <Terminal class="size-4" />
<span v-if="visibleSet.get('terminal') === 'label'">{{ t('actionbar.webTerminal') }}</span> <span v-if="visibleSet.get('terminal') === 'label'">{{ t('actionbar.webTerminal') }}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -401,10 +401,10 @@ const hasRightOverflow = computed(() => {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs" class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"
@click="emit('openComputerUse')" @click="emit('openComputerUse')"
> >
<Bot class="h-3.5 w-3.5 sm:h-4 sm:w-4" /> <Bot class="size-3.5 sm:size-4" />
<span class="hidden xl:inline">AI</span> <span class="hidden xl:inline">AI</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -420,7 +420,7 @@ const hasRightOverflow = computed(() => {
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="router.push('/settings')"> <Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="router.push('/settings')">
<Settings class="h-4 w-4" /> <Settings class="size-4" />
<span v-if="visibleSet.get('settings') === 'label'">{{ t('actionbar.settings') }}</span> <span v-if="visibleSet.get('settings') === 'label'">{{ t('actionbar.settings') }}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -444,10 +444,10 @@ const hasRightOverflow = computed(() => {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs" class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"
@click="emit('toggleVirtualKeyboard')" @click="emit('toggleVirtualKeyboard')"
> >
<Keyboard class="h-3.5 w-3.5 sm:h-4 sm:w-4" /> <Keyboard class="size-3.5 sm:size-4" />
<span class="hidden xl:inline">{{ t('actionbar.keyboard') }}</span> <span class="hidden xl:inline">{{ t('actionbar.keyboard') }}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -464,10 +464,10 @@ const hasRightOverflow = computed(() => {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs" class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"
@click="emit('toggleFullscreen')" @click="emit('toggleFullscreen')"
> >
<Maximize class="h-3.5 w-3.5 sm:h-4 sm:w-4" /> <Maximize class="size-3.5 sm:size-4" />
<span class="hidden xl:inline">{{ t('actionbar.fullscreen') }}</span> <span class="hidden xl:inline">{{ t('actionbar.fullscreen') }}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -480,26 +480,26 @@ const hasRightOverflow = computed(() => {
<!-- Overflow Menu - Only show if there are overflowed items --> <!-- Overflow Menu - Only show if there are overflowed items -->
<DropdownMenu v-if="hasOverflow" v-model:open="overflowMenuOpen"> <DropdownMenu v-if="hasOverflow" v-model:open="overflowMenuOpen">
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<Button variant="ghost" size="sm" class="h-7 w-7 sm:h-8 sm:w-8 p-0"> <Button variant="ghost" size="sm" class="size-8 p-0">
<MoreHorizontal class="h-3.5 w-3.5 sm:h-4 sm:w-4" /> <MoreHorizontal class="size-3.5 sm:size-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-48"> <DropdownMenuContent align="end" class="w-48">
<!-- MSD --> <!-- MSD -->
<DropdownMenuItem v-if="showMsd && !isVisible('msd')" @click="openFromOverflow(() => msdDialogOpen = true)"> <DropdownMenuItem v-if="showMsd && !isVisible('msd')" @click="openFromOverflow(() => msdDialogOpen = true)">
<HardDrive class="h-4 w-4 mr-2" /> <HardDrive class="size-4 mr-2" />
{{ t('actionbar.virtualMedia') }} {{ t('actionbar.virtualMedia') }}
</DropdownMenuItem> </DropdownMenuItem>
<!-- ATX --> <!-- ATX -->
<DropdownMenuItem v-if="showAtx && !isVisible('atx')" @click="openMobileAtx"> <DropdownMenuItem v-if="showAtx && !isVisible('atx')" @click="openMobileAtx">
<Power class="h-4 w-4 mr-2" /> <Power class="size-4 mr-2" />
{{ t('actionbar.power') }} {{ t('actionbar.power') }}
</DropdownMenuItem> </DropdownMenuItem>
<!-- Paste --> <!-- Paste -->
<DropdownMenuItem v-if="showPasteText && !isVisible('paste')" @click="openMobilePaste"> <DropdownMenuItem v-if="showPasteText && !isVisible('paste')" @click="openMobilePaste">
<ClipboardPaste class="h-4 w-4 mr-2" /> <ClipboardPaste class="size-4 mr-2" />
{{ t('actionbar.paste') }} {{ t('actionbar.paste') }}
</DropdownMenuItem> </DropdownMenuItem>
@@ -507,7 +507,7 @@ const hasRightOverflow = computed(() => {
<!-- Stats --> <!-- Stats -->
<DropdownMenuItem v-if="showStats && !isVisible('stats')" @click="openFromOverflow(() => emit('toggleStats'))"> <DropdownMenuItem v-if="showStats && !isVisible('stats')" @click="openFromOverflow(() => emit('toggleStats'))">
<BarChart3 class="h-4 w-4 mr-2" /> <BarChart3 class="size-4 mr-2" />
{{ t('actionbar.stats') }} {{ t('actionbar.stats') }}
</DropdownMenuItem> </DropdownMenuItem>
@@ -517,13 +517,13 @@ const hasRightOverflow = computed(() => {
:disabled="!props.ttydRunning" :disabled="!props.ttydRunning"
@click="openFromOverflow(() => emit('openTerminal'))" @click="openFromOverflow(() => emit('openTerminal'))"
> >
<Terminal class="h-4 w-4 mr-2" /> <Terminal class="size-4 mr-2" />
{{ t('actionbar.webTerminal') }} {{ t('actionbar.webTerminal') }}
</DropdownMenuItem> </DropdownMenuItem>
<!-- Settings --> <!-- Settings -->
<DropdownMenuItem v-if="!isVisible('settings')" @click="openFromOverflow(() => router.push('/settings'))"> <DropdownMenuItem v-if="!isVisible('settings')" @click="openFromOverflow(() => router.push('/settings'))">
<Settings class="h-4 w-4 mr-2" /> <Settings class="size-4 mr-2" />
{{ t('actionbar.settings') }} {{ t('actionbar.settings') }}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@@ -577,30 +577,30 @@ const hasRightOverflow = computed(() => {
<div ref="measureRef" aria-hidden="true" class="fixed pointer-events-none" style="visibility: hidden; top: -9999px; left: -9999px; white-space: nowrap;"> <div ref="measureRef" aria-hidden="true" class="fixed pointer-events-none" style="visibility: hidden; top: -9999px; left: -9999px; white-space: nowrap;">
<div class="flex items-center gap-0.5 sm:gap-1.5 px-2 sm:px-4 py-1 sm:py-1.5"> <div class="flex items-center gap-0.5 sm:gap-1.5 px-2 sm:px-4 py-1 sm:py-1.5">
<!-- MSD --> <!-- MSD -->
<Button data-measure="msd-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button> <Button data-measure="msd-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
<Button data-measure="msd-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" />{{ t('actionbar.virtualMedia') }}</Button> <Button data-measure="msd-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" />{{ t('actionbar.virtualMedia') }}</Button>
<!-- ATX --> <!-- ATX -->
<Button data-measure="atx-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Power class="h-4 w-4" /></Button> <Button data-measure="atx-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Power class="size-4" /></Button>
<Button data-measure="atx-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Power class="h-4 w-4" />{{ t('actionbar.power') }}</Button> <Button data-measure="atx-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Power class="size-4" />{{ t('actionbar.power') }}</Button>
<!-- Paste --> <!-- Paste -->
<Button data-measure="paste-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><ClipboardPaste class="h-4 w-4" /></Button> <Button data-measure="paste-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><ClipboardPaste class="size-4" /></Button>
<Button data-measure="paste-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><ClipboardPaste class="h-4 w-4" />{{ t('actionbar.paste') }}</Button> <Button data-measure="paste-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><ClipboardPaste class="size-4" />{{ t('actionbar.paste') }}</Button>
<!-- Stats --> <!-- Stats -->
<Button data-measure="stats-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><BarChart3 class="h-4 w-4" /></Button> <Button data-measure="stats-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><BarChart3 class="size-4" /></Button>
<Button data-measure="stats-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><BarChart3 class="h-4 w-4" />{{ t('actionbar.stats') }}</Button> <Button data-measure="stats-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><BarChart3 class="size-4" />{{ t('actionbar.stats') }}</Button>
<!-- Web Terminal --> <!-- Web Terminal -->
<Button data-measure="terminal-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="h-4 w-4" /></Button> <Button data-measure="terminal-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="size-4" /></Button>
<Button data-measure="terminal-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="h-4 w-4" />{{ t('actionbar.webTerminal') }}</Button> <Button data-measure="terminal-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="size-4" />{{ t('actionbar.webTerminal') }}</Button>
<!-- Settings --> <!-- Settings -->
<Button data-measure="settings-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="h-4 w-4" /></Button> <Button data-measure="settings-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="size-4" /></Button>
<Button data-measure="settings-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="h-4 w-4" />{{ t('actionbar.settings') }}</Button> <Button data-measure="settings-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="size-4" />{{ t('actionbar.settings') }}</Button>
<!-- Always-visible items (for measuring their actual width) --> <!-- Always-visible items (for measuring their actual width) -->
<Button data-measure="video-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button> <Button data-measure="video-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
<Button data-measure="video-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button> <Button data-measure="video-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
<Button data-measure="audio-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button> <Button data-measure="audio-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
<Button data-measure="audio-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button> <Button data-measure="audio-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
<Button data-measure="hid-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button> <Button data-measure="hid-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
<Button data-measure="hid-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button> <Button data-measure="hid-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -52,9 +52,9 @@ async function handleLogout() {
</span> </span>
<!-- Theme Toggle --> <!-- Theme Toggle -->
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.toggleTheme')" @click="toggleTheme"> <Button variant="ghost" size="icon-sm" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
<Sun class="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Sun class="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon class="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <Moon class="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">{{ t('common.toggleTheme') }}</span> <span class="sr-only">{{ t('common.toggleTheme') }}</span>
</Button> </Button>
@@ -64,21 +64,21 @@ async function handleLogout() {
<!-- Mobile Menu --> <!-- Mobile Menu -->
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger as-child class="md:hidden"> <DropdownMenuTrigger as-child class="md:hidden">
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.menu')"> <Button variant="ghost" size="icon-sm" :aria-label="t('common.menu')">
<Menu class="h-4 w-4" /> <Menu class="size-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem @click="handleLogout"> <DropdownMenuItem @click="handleLogout">
<LogOut class="h-4 w-4 mr-2" /> <LogOut class="size-4 mr-2" />
{{ t('nav.logout') }} {{ t('nav.logout') }}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<!-- Logout Button (Desktop) --> <!-- Logout Button (Desktop) -->
<Button variant="ghost" size="icon" class="hidden md:flex h-8 w-8" :aria-label="t('nav.logout')" @click="handleLogout"> <Button variant="ghost" size="icon-sm" class="hidden md:flex" :aria-label="t('nav.logout')" @click="handleLogout">
<LogOut class="h-4 w-4" /> <LogOut class="size-4" />
<span class="sr-only">{{ t('nav.logout') }}</span> <span class="sr-only">{{ t('nav.logout') }}</span>
</Button> </Button>
</div> </div>

View File

@@ -30,7 +30,7 @@ const emit = defineEmits<{
const { t } = useI18n() const { t } = useI18n()
const activeTab = ref('atx') const activeTab = ref('atx')
const tabTriggerClass = 'h-9 rounded-md border-0 bg-transparent text-center text-xs text-muted-foreground shadow-none hover:text-foreground data-[state=active]:border-0 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm' const tabTriggerClass = 'h-8 rounded-md border-0 bg-transparent text-center text-xs text-muted-foreground shadow-none hover:text-foreground data-[state=active]:border-0 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm'
const powerState = ref<'on' | 'off' | 'unknown'>('unknown') const powerState = ref<'on' | 'off' | 'unknown'>('unknown')
const hddState = ref<'active' | 'inactive' | 'unknown'>('unknown') const hddState = ref<'active' | 'inactive' | 'unknown'>('unknown')
@@ -199,19 +199,19 @@ watch(
<template> <template>
<div class="p-2.5 space-y-2.5"> <div class="p-2.5 space-y-2.5">
<Tabs v-model="activeTab"> <Tabs v-model="activeTab">
<TabsList class="grid h-auto w-full grid-cols-2 gap-1 rounded-md border border-border bg-muted p-1"> <TabsList class="grid h-auto w-full grid-cols-2 gap-1 rounded-md border border-border bg-muted p-0.5">
<TabsTrigger <TabsTrigger
value="atx" value="atx"
:class="tabTriggerClass" :class="tabTriggerClass"
> >
<Power class="h-3 w-3 mr-1" /> <Power class="size-3 mr-1" />
{{ t('atx.title') }} {{ t('atx.title') }}
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="wol" value="wol"
:class="tabTriggerClass" :class="tabTriggerClass"
> >
<Wifi class="h-3 w-3 mr-1" /> <Wifi class="size-3 mr-1" />
WOL WOL
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -221,14 +221,14 @@ watch(
<!-- Status --> <!-- Status -->
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5"> <div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5">
<Power :class="['h-4 w-4 shrink-0', powerStateIconColor]" /> <Power :class="['size-4 shrink-0', powerStateIconColor]" />
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.powerState') }}</p> <p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.powerState') }}</p>
<p :class="['mt-1 truncate text-xs font-medium leading-none', powerStateTextColor]">{{ powerStateText }}</p> <p :class="['mt-1 truncate text-xs font-medium leading-none', powerStateTextColor]">{{ powerStateText }}</p>
</div> </div>
</div> </div>
<div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5"> <div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5">
<HardDrive :class="['h-4 w-4 shrink-0', hddStateIconColor]" /> <HardDrive :class="['size-4 shrink-0', hddStateIconColor]" />
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.hddState') }}</p> <p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.hddState') }}</p>
<p :class="['mt-1 truncate text-xs font-medium leading-none', hddStateTextColor]">{{ hddStateText }}</p> <p :class="['mt-1 truncate text-xs font-medium leading-none', hddStateTextColor]">{{ hddStateText }}</p>
@@ -245,12 +245,12 @@ watch(
size="sm" size="sm"
:disabled="actionBusy" :disabled="actionBusy"
:class="[ :class="[
'w-full justify-start gap-2 h-7 text-xs', 'w-full justify-start gap-2 h-8 text-xs',
activeAction === 'short' ? 'bg-muted text-muted-foreground' : '', activeAction === 'short' ? 'bg-muted text-muted-foreground' : '',
]" ]"
@click="handleAction('short')" @click="handleAction('short')"
> >
<Power class="h-3 w-3" /> <Power class="size-3" />
{{ t('atx.shortPress') }} {{ t('atx.shortPress') }}
</Button> </Button>
@@ -259,12 +259,12 @@ watch(
size="sm" size="sm"
:disabled="actionBusy" :disabled="actionBusy"
:class="[ :class="[
'h-7 w-full justify-start gap-2 text-xs text-warning hover:bg-warning/10 hover:text-warning', 'h-8 w-full justify-start gap-2 text-xs text-warning hover:bg-warning/10 hover:text-warning',
activeAction === 'long' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '', activeAction === 'long' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
]" ]"
@click="handleAction('long')" @click="handleAction('long')"
> >
<CircleDot class="h-3 w-3" /> <CircleDot class="size-3" />
{{ t('atx.longPress') }} {{ t('atx.longPress') }}
</Button> </Button>
@@ -273,12 +273,12 @@ watch(
size="sm" size="sm"
:disabled="actionBusy" :disabled="actionBusy"
:class="[ :class="[
'h-7 w-full justify-start gap-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive', 'h-8 w-full justify-start gap-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive',
activeAction === 'reset' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '', activeAction === 'reset' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
]" ]"
@click="handleAction('reset')" @click="handleAction('reset')"
> >
<RotateCcw class="h-3 w-3" /> <RotateCcw class="size-3" />
{{ t('atx.reset') }} {{ t('atx.reset') }}
</Button> </Button>
</div> </div>
@@ -302,7 +302,7 @@ watch(
:disabled="!isValidMac || wolSending" :disabled="!isValidMac || wolSending"
@click="sendWol" @click="sendWol"
> >
<Send class="h-3.5 w-3.5" /> <Send class="size-3.5" />
</Button> </Button>
</div> </div>
<p v-if="wolMacAddress && !isValidMac" class="text-xs text-destructive"> <p v-if="wolMacAddress && !isValidMac" class="text-xs text-destructive">

View File

@@ -142,8 +142,8 @@ watch(() => props.open, (isOpen) => {
<template> <template>
<Popover :open="open" @update:open="emit('update:open', $event)"> <Popover :open="open" @update:open="emit('update:open', $event)">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button variant="ghost" size="sm" class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"> <Button variant="ghost" size="sm" class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs">
<Volume2 class="h-3.5 w-3.5 sm:h-4 sm:w-4" /> <Volume2 class="size-3.5 sm:size-4" />
<span class="hidden sm:inline">{{ t('actionbar.audioConfig') }}</span> <span class="hidden sm:inline">{{ t('actionbar.audioConfig') }}</span>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -166,7 +166,7 @@ watch(() => props.open, (isOpen) => {
<span class="text-xs font-mono">{{ Math.round(localVolume[0] ?? 0) }}%</span> <span class="text-xs font-mono">{{ Math.round(localVolume[0] ?? 0) }}%</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Volume2 class="h-3.5 w-3.5 text-muted-foreground opacity-50" /> <Volume2 class="size-3.5 text-muted-foreground opacity-50" />
<Slider <Slider
:model-value="localVolume" :model-value="localVolume"
@update:model-value="handleVolumeChange" @update:model-value="handleVolumeChange"
@@ -176,7 +176,7 @@ watch(() => props.open, (isOpen) => {
:disabled="!systemStore.audio?.streaming" :disabled="!systemStore.audio?.streaming"
class="flex-1" class="flex-1"
/> />
<Volume2 class="h-3.5 w-3.5 text-muted-foreground" /> <Volume2 class="size-3.5 text-muted-foreground" />
</div> </div>
</div> </div>
</div> </div>
@@ -191,12 +191,11 @@ watch(() => props.open, (isOpen) => {
</h5> </h5>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon-xs"
class="h-6 w-6"
:disabled="loadingDevices" :disabled="loadingDevices"
@click="loadDevices" @click="loadDevices"
> >
<RefreshCw :class="['h-3.5 w-3.5', loadingDevices && 'animate-spin']" /> <RefreshCw :class="['size-3.5', loadingDevices && 'animate-spin']" />
</Button> </Button>
</div> </div>
@@ -230,7 +229,7 @@ watch(() => props.open, (isOpen) => {
:model-value="selectedDevice" :model-value="selectedDevice"
@update:model-value="(v) => selectedDevice = v as string" @update:model-value="(v) => selectedDevice = v as string"
:disabled="loadingDevices || devices.length === 0" :disabled="loadingDevices || devices.length === 0"
class="h-8 w-full text-xs" size="sm" class="w-full text-xs"
> >
<NativeSelectOption value="">{{ t('actionbar.selectAudioDevice') }}</NativeSelectOption> <NativeSelectOption value="">{{ t('actionbar.selectAudioDevice') }}</NativeSelectOption>
<NativeSelectOption <NativeSelectOption
@@ -281,7 +280,7 @@ watch(() => props.open, (isOpen) => {
:disabled="applying" :disabled="applying"
@click="applyConfig" @click="applyConfig"
> >
<Loader2 v-if="applying" class="h-3.5 w-3.5 mr-1.5 animate-spin" /> <Loader2 v-if="applying" class="size-3.5 mr-1.5 animate-spin" />
<span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span> <span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span>
</Button> </Button>
</div> </div>

View File

@@ -13,17 +13,17 @@ const props = withDefaults(
const dim = computed(() => { const dim = computed(() => {
switch (props.size) { switch (props.size) {
case 'xs': case 'xs':
return 'h-4 w-4' return 'size-4'
case 'sm': case 'sm':
return 'h-5 w-5' return 'size-5'
case 'md': case 'md':
return 'h-6 w-6' return 'size-6'
case 'lg': case 'lg':
return 'h-10 w-10' return 'size-10'
case 'xl': case 'xl':
return 'h-14 w-14' return 'size-14'
default: default:
return 'h-6 w-6' return 'size-6'
} }
}) })
</script> </script>

View File

@@ -18,6 +18,7 @@ import {
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { computerUseApi, type ComputerUseAction, type ComputerUseConfig, type ComputerUseSession } from '@/api' import { computerUseApi, type ComputerUseAction, type ComputerUseConfig, type ComputerUseSession } from '@/api'
import { ApiError } from '@/api/request'
import type { ComputerUseTimelineItem } from '@/types/computerUseTimeline' import type { ComputerUseTimelineItem } from '@/types/computerUseTimeline'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -107,6 +108,8 @@ async function loadConfig() {
try { try {
config.value = await computerUseApi.config() config.value = await computerUseApi.config()
} catch (err) { } catch (err) {
// Auth failures are handled globally (redirect to login); don't toast over it.
if (err instanceof ApiError && err.status === 401) return
toast.error(t('computerUse.errors.configLoadFailed'), { toast.error(t('computerUse.errors.configLoadFailed'), {
description: err instanceof Error ? err.message : undefined, description: err instanceof Error ? err.message : undefined,
}) })
@@ -217,15 +220,15 @@ onMounted(loadConfig)
> >
<div class="flex h-full min-h-0 flex-col"> <div class="flex h-full min-h-0 flex-col">
<header class="flex h-12 shrink-0 items-center gap-2 border-b px-3"> <header class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
<Bot class="h-5 w-5 shrink-0" /> <Bot class="size-5 shrink-0" />
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="truncate text-sm font-semibold">{{ t('computerUse.title') }}</div> <div class="truncate text-sm font-semibold">{{ t('computerUse.title') }}</div>
<div <div
class="flex min-w-0 items-center gap-1 text-[11px]" class="flex min-w-0 items-center gap-1 text-[11px]"
:class="connected ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'" :class="connected ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'"
> >
<Wifi v-if="connected" class="h-3 w-3 shrink-0" /> <Wifi v-if="connected" class="size-3 shrink-0" />
<WifiOff v-else class="h-3 w-3 shrink-0" /> <WifiOff v-else class="size-3 shrink-0" />
<span class="truncate"> <span class="truncate">
{{ connected ? t('computerUse.connection.connected') : (wsError || t('computerUse.connection.disconnected')) }} {{ connected ? t('computerUse.connection.connected') : (wsError || t('computerUse.connection.disconnected')) }}
</span> </span>
@@ -235,39 +238,39 @@ onMounted(loadConfig)
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8" class="size-8"
:title="t('computerUse.buttons.settings')" :title="t('computerUse.buttons.settings')"
:aria-label="t('computerUse.buttons.settings')" :aria-label="t('computerUse.buttons.settings')"
@click="settingsOpen = true" @click="settingsOpen = true"
> >
<Settings class="h-4 w-4" /> <Settings class="size-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8" class="size-8"
:title="t('computerUse.buttons.clear')" :title="t('computerUse.buttons.clear')"
:aria-label="t('computerUse.buttons.clear')" :aria-label="t('computerUse.buttons.clear')"
:disabled="isRunning || timeline.length === 0" :disabled="isRunning || timeline.length === 0"
@click="emit('clear')" @click="emit('clear')"
> >
<Trash2 class="h-4 w-4" /> <Trash2 class="size-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8" class="size-8"
:title="t('computerUse.buttons.close')" :title="t('computerUse.buttons.close')"
:aria-label="t('computerUse.buttons.close')" :aria-label="t('computerUse.buttons.close')"
@click="emit('update:open', false)" @click="emit('update:open', false)"
> >
<X class="h-4 w-4" /> <X class="size-4" />
</Button> </Button>
</header> </header>
<div ref="messagesRef" class="min-h-0 flex-1 overflow-y-auto px-4 py-4"> <div ref="messagesRef" class="min-h-0 flex-1 overflow-y-auto px-4 py-4">
<div v-if="timeline.length === 0" class="flex h-full items-center justify-center text-muted-foreground/45"> <div v-if="timeline.length === 0" class="flex h-full items-center justify-center text-muted-foreground/45">
<Bot class="h-10 w-10" /> <Bot class="size-10" />
</div> </div>
<div v-else class="space-y-5"> <div v-else class="space-y-5">
@@ -288,9 +291,9 @@ onMounted(loadConfig)
class="flex w-full items-center gap-1.5 py-1 text-left font-medium hover:text-foreground" class="flex w-full items-center gap-1.5 py-1 text-left font-medium hover:text-foreground"
@click="toggleReasoning(item)" @click="toggleReasoning(item)"
> >
<ChevronDown v-if="reasoningOpen(item)" class="h-3.5 w-3.5 shrink-0" /> <ChevronDown v-if="reasoningOpen(item)" class="size-3.5 shrink-0" />
<ChevronRight v-else class="h-3.5 w-3.5 shrink-0" /> <ChevronRight v-else class="size-3.5 shrink-0" />
<BrainCircuit class="h-3.5 w-3.5 shrink-0" /> <BrainCircuit class="size-3.5 shrink-0" />
<span> <span>
{{ item.failed {{ item.failed
? t('computerUse.reasoning.failed') ? t('computerUse.reasoning.failed')
@@ -308,7 +311,7 @@ onMounted(loadConfig)
<div v-else-if="item.type === 'screenshot'" class="overflow-hidden rounded-md border bg-card"> <div v-else-if="item.type === 'screenshot'" class="overflow-hidden rounded-md border bg-card">
<div class="flex items-center justify-between border-b px-2.5 py-1.5 text-[11px] text-muted-foreground"> <div class="flex items-center justify-between border-b px-2.5 py-1.5 text-[11px] text-muted-foreground">
<span class="inline-flex items-center gap-1.5"> <span class="inline-flex items-center gap-1.5">
<Image class="h-3.5 w-3.5" />{{ t('computerUse.trace.screenshot') }} <Image class="size-3.5" />{{ t('computerUse.trace.screenshot') }}
</span> </span>
<span>{{ item.screenshot.width }}x{{ item.screenshot.height }}</span> <span>{{ item.screenshot.width }}x{{ item.screenshot.height }}</span>
</div> </div>
@@ -363,24 +366,24 @@ onMounted(loadConfig)
<Button <Button
v-if="!isRunning" v-if="!isRunning"
size="icon" size="icon"
class="absolute bottom-2 right-2 h-8 w-8" class="absolute bottom-2 right-2 size-8"
:title="t('computerUse.buttons.send')" :title="t('computerUse.buttons.send')"
:aria-label="t('computerUse.buttons.send')" :aria-label="t('computerUse.buttons.send')"
:disabled="!canStart || starting" :disabled="!canStart || starting"
@click="start" @click="start"
> >
<SendHorizontal class="h-4 w-4" /> <SendHorizontal class="size-4" />
</Button> </Button>
<Button <Button
v-else v-else
size="icon" size="icon"
variant="destructive" variant="destructive"
class="absolute bottom-2 right-2 h-8 w-8" class="absolute bottom-2 right-2 size-8"
:title="t('computerUse.buttons.stop')" :title="t('computerUse.buttons.stop')"
:aria-label="t('computerUse.buttons.stop')" :aria-label="t('computerUse.buttons.stop')"
@click="emit('stop')" @click="emit('stop')"
> >
<Square class="h-3.5 w-3.5 fill-current" /> <Square class="size-3.5 fill-current" />
</Button> </Button>
</div> </div>
<p v-if="config && !config.api_key_configured" class="mt-2 text-xs text-muted-foreground"> <p v-if="config && !config.api_key_configured" class="mt-2 text-xs text-muted-foreground">
@@ -422,7 +425,7 @@ onMounted(loadConfig)
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="cua-key" class="flex items-center gap-1.5"> <Label for="cua-key" class="flex items-center gap-1.5">
<KeyRound class="h-3.5 w-3.5" /> <KeyRound class="size-3.5" />
{{ t('computerUse.settings.apiKey') }} {{ t('computerUse.settings.apiKey') }}
</Label> </Label>
<Input <Input

View File

@@ -25,9 +25,9 @@ defineProps<{
variant="ghost" variant="ghost"
size="icon-xs" size="icon-xs"
class="text-muted-foreground/60 hover:text-muted-foreground" class="text-muted-foreground/60 hover:text-muted-foreground"
:class="iconSize === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'" :class="iconSize === 'sm' ? 'size-3.5' : 'size-4'"
> >
<HelpCircle :class="iconSize === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'" /> <HelpCircle :class="iconSize === 'sm' ? 'size-3.5' : 'size-4'" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent

View File

@@ -232,9 +232,9 @@ watch(() => props.open, (isOpen) => {
<template> <template>
<Popover :open="open" @update:open="emit('update:open', $event)"> <Popover :open="open" @update:open="emit('update:open', $event)">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button variant="ghost" size="sm" class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"> <Button variant="ghost" size="sm" class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs">
<MousePointer v-if="mouseMode === 'absolute'" class="h-3.5 w-3.5 sm:h-4 sm:w-4" /> <MousePointer v-if="mouseMode === 'absolute'" class="size-3.5 sm:size-4" />
<Move v-else class="h-3.5 w-3.5 sm:h-4 sm:w-4" /> <Move v-else class="size-3.5 sm:size-4" />
<span class="hidden sm:inline">{{ buttonText }}</span> <span class="hidden sm:inline">{{ buttonText }}</span>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -261,7 +261,7 @@ watch(() => props.open, (isOpen) => {
class="flex-1 h-8 text-xs" class="flex-1 h-8 text-xs"
@click="toggleMouseMode" @click="toggleMouseMode"
> >
<MousePointer class="h-3.5 w-3.5 mr-1" /> <MousePointer class="size-3.5 mr-1" />
{{ t('actionbar.absolute') }} {{ t('actionbar.absolute') }}
</Button> </Button>
<Button <Button
@@ -270,7 +270,7 @@ watch(() => props.open, (isOpen) => {
class="flex-1 h-8 text-xs" class="flex-1 h-8 text-xs"
@click="toggleMouseMode" @click="toggleMouseMode"
> >
<Move class="h-3.5 w-3.5 mr-1" /> <Move class="size-3.5 mr-1" />
{{ t('actionbar.relative') }} {{ t('actionbar.relative') }}
</Button> </Button>
</div> </div>
@@ -314,12 +314,11 @@ watch(() => props.open, (isOpen) => {
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.hidDeviceSettings') }}</h5> <h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.hidDeviceSettings') }}</h5>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon-xs"
class="h-6 w-6"
:disabled="loadingDevices" :disabled="loadingDevices"
@click="loadDevices" @click="loadDevices"
> >
<RefreshCw :class="['h-3.5 w-3.5', loadingDevices && 'animate-spin']" /> <RefreshCw :class="['size-3.5', loadingDevices && 'animate-spin']" />
</Button> </Button>
</div> </div>
@@ -330,7 +329,7 @@ watch(() => props.open, (isOpen) => {
:model-value="hidBackend" :model-value="hidBackend"
@update:model-value="handleBackendChange" @update:model-value="handleBackendChange"
> >
<SelectTrigger class="h-8 w-full text-xs"> <SelectTrigger size="sm" class="w-full text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -348,7 +347,7 @@ watch(() => props.open, (isOpen) => {
:model-value="devicePath" :model-value="devicePath"
@update:model-value="handleDevicePathChange" @update:model-value="handleDevicePathChange"
:disabled="availableDevicePaths.length === 0" :disabled="availableDevicePaths.length === 0"
class="h-8 w-full text-xs" size="sm" class="w-full text-xs"
> >
<NativeSelectOption value="">{{ t('actionbar.selectDevice') }}</NativeSelectOption> <NativeSelectOption value="">{{ t('actionbar.selectDevice') }}</NativeSelectOption>
<NativeSelectOption <NativeSelectOption
@@ -369,7 +368,7 @@ watch(() => props.open, (isOpen) => {
:model-value="String(baudrate)" :model-value="String(baudrate)"
@update:model-value="handleBaudrateChange" @update:model-value="handleBaudrateChange"
> >
<SelectTrigger class="h-8 text-xs"> <SelectTrigger size="sm" class="text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -388,7 +387,7 @@ watch(() => props.open, (isOpen) => {
:disabled="applying" :disabled="applying"
@click="applyHidConfig" @click="applyHidConfig"
> >
<Loader2 v-if="applying" class="h-3.5 w-3.5 mr-1.5 animate-spin" /> <Loader2 v-if="applying" class="size-3.5 mr-1.5 animate-spin" />
<span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span> <span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span>
</Button> </Button>
</div> </div>

View File

@@ -48,13 +48,13 @@ const keysDisplay = computed(() => {
<div class="flex items-center justify-between text-xs px-2 py-0.5"> <div class="flex items-center justify-between text-xs px-2 py-0.5">
<div v-if="keyboardLedEnabled" class="flex items-center gap-1"> <div v-if="keyboardLedEnabled" class="flex items-center gap-1">
<Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground"> <Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground">
<span class="h-1.5 w-1.5 rounded-full" :class="capsLock ? 'bg-success' : 'bg-warning'" />C <span class="size-1.5 rounded-full" :class="capsLock ? 'bg-success' : 'bg-warning'" />C
</Badge> </Badge>
<Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground"> <Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground">
<span class="h-1.5 w-1.5 rounded-full" :class="numLock ? 'bg-success' : 'bg-warning'" />N <span class="size-1.5 rounded-full" :class="numLock ? 'bg-success' : 'bg-warning'" />N
</Badge> </Badge>
<Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground"> <Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground">
<span class="h-1.5 w-1.5 rounded-full" :class="scrollLock ? 'bg-success' : 'bg-warning'" />S <span class="size-1.5 rounded-full" :class="scrollLock ? 'bg-success' : 'bg-warning'" />S
</Badge> </Badge>
</div> </div>
<div v-else class="text-[10px] text-muted-foreground/60"> <div v-else class="text-[10px] text-muted-foreground/60">
@@ -87,15 +87,15 @@ const keysDisplay = computed(() => {
<Separator orientation="vertical" class="h-5" /> <Separator orientation="vertical" class="h-5" />
<template v-if="keyboardLedEnabled"> <template v-if="keyboardLedEnabled">
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground"> <Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
<span class="h-1.5 w-1.5 rounded-full" :class="capsLock ? 'bg-success' : 'bg-warning'" /> <span class="size-1.5 rounded-full" :class="capsLock ? 'bg-success' : 'bg-warning'" />
{{ t('infobar.caps') }} {{ t('infobar.caps') }}
</Badge> </Badge>
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground"> <Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
<span class="h-1.5 w-1.5 rounded-full" :class="numLock ? 'bg-success' : 'bg-warning'" /> <span class="size-1.5 rounded-full" :class="numLock ? 'bg-success' : 'bg-warning'" />
{{ t('infobar.num') }} {{ t('infobar.num') }}
</Badge> </Badge>
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground"> <Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
<span class="h-1.5 w-1.5 rounded-full" :class="scrollLock ? 'bg-success' : 'bg-warning'" /> <span class="size-1.5 rounded-full" :class="scrollLock ? 'bg-success' : 'bg-warning'" />
{{ t('infobar.scroll') }} {{ t('infobar.scroll') }}
</Badge> </Badge>
</template> </template>

View File

@@ -43,7 +43,7 @@ function toggleLanguage() {
:aria-label="t('common.toggleLanguage')" :aria-label="t('common.toggleLanguage')"
@click="toggleLanguage" @click="toggleLanguage"
> >
<Languages class="h-4 w-4" /> <Languages class="size-4" />
<span v-if="props.labelMode !== 'hidden'">{{ buttonLabel }}</span> <span v-if="props.labelMode !== 'hidden'">{{ buttonLabel }}</span>
<span class="sr-only">{{ t('common.toggleLanguage') }}</span> <span class="sr-only">{{ t('common.toggleLanguage') }}</span>
</Button> </Button>

View File

@@ -153,8 +153,8 @@ const downloadProgress = ref<{
} | null>(null) } | null>(null)
const TWO_POINT_TWO_GB = 2.2 * 1024 * 1024 * 1024 const TWO_POINT_TWO_GB = 2.2 * 1024 * 1024 * 1024
const tabTriggerClass = 'h-9 rounded-md border-0 bg-transparent text-center text-muted-foreground shadow-none hover:text-foreground data-[state=active]:border-0 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm' const tabTriggerClass = 'h-8 rounded-md border-0 bg-transparent text-center text-muted-foreground shadow-none hover:text-foreground data-[state=active]:border-0 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm'
const segmentedGroupClass = 'grid w-full grid-cols-2 items-center gap-1 rounded-md border border-border bg-muted p-1' const segmentedGroupClass = 'grid w-full grid-cols-2 items-center gap-1 rounded-md border border-border bg-muted p-0.5'
const segmentedItemClass = 'h-8 w-full justify-center rounded-md border-0 bg-transparent px-3 text-center text-xs text-muted-foreground shadow-none hover:bg-transparent hover:text-foreground data-[state=on]:border-0 data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-sm data-[state=on]:hover:bg-background' const segmentedItemClass = 'h-8 w-full justify-center rounded-md border-0 bg-transparent px-3 text-center text-xs text-muted-foreground shadow-none hover:bg-transparent hover:text-foreground data-[state=on]:border-0 data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-sm data-[state=on]:hover:bg-background'
const diskMode = computed(() => systemStore.msd?.diskMode ?? 'single') const diskMode = computed(() => systemStore.msd?.diskMode ?? 'single')
@@ -688,17 +688,17 @@ onUnmounted(() => {
<TooltipProvider> <TooltipProvider>
<Dialog :open="open" @update:open="emit('update:open', $event)"> <Dialog :open="open" @update:open="emit('update:open', $event)">
<DialogContent class="max-h-[90vh] overflow-hidden p-0 sm:max-w-[760px] flex flex-col"> <DialogContent class="max-h-[90vh] overflow-hidden p-0 sm:max-w-[760px] flex flex-col">
<DialogHeader class="px-6 pt-6 shrink-0"> <DialogHeader class="px-5 pt-4 shrink-0">
<DialogTitle class="flex items-center gap-2"> <DialogTitle class="flex items-center gap-2">
<HardDrive class="h-5 w-5" /> <HardDrive class="size-5" />
{{ t('msd.title') }} {{ t('msd.title') }}
</DialogTitle> </DialogTitle>
<DialogDescription as="div" class="mt-1 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <DialogDescription as="div" class="mt-1 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<span class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1"> <span class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<span :class="msdConnected ? 'text-success' : 'text-muted-foreground'" class="flex items-center gap-1.5"> <span :class="msdConnected ? 'text-success' : 'text-muted-foreground'" class="flex items-center gap-1.5">
<span class="relative flex h-2 w-2"> <span class="relative flex size-2">
<span v-if="msdConnected" class="absolute inline-flex h-full w-full animate-ping rounded-full bg-success opacity-75"></span> <span v-if="msdConnected" class="absolute inline-flex h-full w-full animate-ping rounded-full bg-success opacity-75"></span>
<span :class="msdConnected ? 'bg-success' : 'bg-muted-foreground'" class="relative inline-flex h-2 w-2 rounded-full"></span> <span :class="msdConnected ? 'bg-success' : 'bg-muted-foreground'" class="relative inline-flex size-2 rounded-full"></span>
</span> </span>
{{ msdConnected ? t('common.connected') : t('common.disconnected') }} {{ msdConnected ? t('common.connected') : t('common.disconnected') }}
</span> </span>
@@ -720,8 +720,8 @@ onUnmounted(() => {
<span class="font-medium text-foreground">{{ t('msd.diskMode') }}</span> <span class="font-medium text-foreground">{{ t('msd.diskMode') }}</span>
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<span class="inline-flex h-4 w-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground"> <span class="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground">
<Info class="h-3.5 w-3.5" /> <Info class="size-3.5" />
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -747,15 +747,15 @@ onUnmounted(() => {
<Separator class="shrink-0" /> <Separator class="shrink-0" />
<div class="flex-1 min-h-0 flex flex-col px-6 pb-6 pt-4"> <div class="flex-1 min-h-0 flex flex-col px-5 pb-4 pt-3">
<Tabs v-model="activeTab" class="flex-1 flex flex-col min-h-0"> <Tabs v-model="activeTab" class="flex-1 flex flex-col min-h-0">
<TabsList class="grid h-auto w-full shrink-0 grid-cols-2 gap-1 rounded-md border border-border bg-muted p-1"> <TabsList class="grid h-auto w-full shrink-0 grid-cols-2 gap-1 rounded-md border border-border bg-muted p-0.5">
<TabsTrigger value="images" :class="tabTriggerClass"> <TabsTrigger value="images" :class="tabTriggerClass">
<Disc class="h-4 w-4 mr-1.5" /> <Disc class="size-4 mr-1.5" />
{{ t('msd.images') }} {{ t('msd.images') }}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="drive" :class="tabTriggerClass"> <TabsTrigger value="drive" :class="tabTriggerClass">
<HardDrive class="h-4 w-4 mr-1.5" /> <HardDrive class="size-4 mr-1.5" />
{{ t('msd.drive') }} {{ t('msd.drive') }}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -775,7 +775,7 @@ onUnmounted(() => {
@change="handleImageUpload" @change="handleImageUpload"
/> />
<Button variant="outline" size="sm" as="span" class="h-8 cursor-pointer px-2.5 text-xs"> <Button variant="outline" size="sm" as="span" class="h-8 cursor-pointer px-2.5 text-xs">
<Upload class="mr-1.5 h-3.5 w-3.5" /> <Upload class="mr-1.5 size-3.5" />
{{ t('msd.uploadImage') }} {{ t('msd.uploadImage') }}
</Button> </Button>
</label> </label>
@@ -785,11 +785,11 @@ onUnmounted(() => {
class="h-8 px-2.5 text-xs" class="h-8 px-2.5 text-xs"
@click="showUrlDialog = true" @click="showUrlDialog = true"
> >
<Globe class="mr-1.5 h-3.5 w-3.5" /> <Globe class="mr-1.5 size-3.5" />
{{ t('msd.downloadFromUrl') }} {{ t('msd.downloadFromUrl') }}
</Button> </Button>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="loadImages"> <Button variant="ghost" size="icon-sm" @click="loadImages">
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingImages }" /> <RefreshCw class="size-3.5" :class="{ 'animate-spin': loadingImages }" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -817,8 +817,8 @@ onUnmounted(() => {
> >
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div class="flex min-w-0 flex-1 items-start gap-2"> <div class="flex min-w-0 flex-1 items-start gap-2">
<span v-if="mountedImage(image.id)" class="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-primary" /> <span v-if="mountedImage(image.id)" class="mt-1.5 size-2 shrink-0 rounded-full bg-primary" />
<Disc v-else class="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" /> <Disc v-else class="mt-0.5 size-4 shrink-0 text-muted-foreground" />
<div class="w-0 flex-1"> <div class="w-0 flex-1">
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
@@ -836,7 +836,7 @@ onUnmounted(() => {
variant="outline" variant="outline"
class="h-4 cursor-help border-warning/50 px-1.5 text-[10px] text-warning" class="h-4 cursor-help border-warning/50 px-1.5 text-[10px] text-warning"
> >
<AlertCircle class="h-2.5 w-2.5 mr-0.5" /> <AlertCircle class="size-2.5 mr-0.5" />
{{ t('msd.largeFileWarning') }} {{ t('msd.largeFileWarning') }}
</Badge> </Badge>
</TooltipTrigger> </TooltipTrigger>
@@ -856,7 +856,7 @@ onUnmounted(() => {
:disabled="operationInProgress" :disabled="operationInProgress"
@click="unmountImageById(image.id)" @click="unmountImageById(image.id)"
> >
<Unlink class="h-3.5 w-3.5 mr-1" /> <Unlink class="size-3.5 mr-1" />
{{ t('msd.disconnect') }} {{ t('msd.disconnect') }}
</Button> </Button>
</template> </template>
@@ -868,7 +868,7 @@ onUnmounted(() => {
:disabled="operationInProgress || mediaSlotsFull" :disabled="operationInProgress || mediaSlotsFull"
@click="connectImage(image)" @click="connectImage(image)"
> >
<Link v-if="!connecting" class="h-3.5 w-3.5 mr-1" /> <Link v-if="!connecting" class="size-3.5 mr-1" />
<span v-if="connecting">{{ t('common.connecting') }}...</span> <span v-if="connecting">{{ t('common.connecting') }}...</span>
<span v-else>{{ t('msd.connect') }}</span> <span v-else>{{ t('msd.connect') }}</span>
</Button> </Button>
@@ -876,11 +876,11 @@ onUnmounted(() => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive" class="size-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
:disabled="operationInProgress || !!mountedImage(image.id)" :disabled="operationInProgress || !!mountedImage(image.id)"
@click="confirmDelete('image', image.id, image.name)" @click="confirmDelete('image', image.id, image.name)"
> >
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="size-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -900,7 +900,7 @@ onUnmounted(() => {
<TabsContent value="drive" class="m-0 flex min-h-0 flex-1 flex-col space-y-4 pt-3"> <TabsContent value="drive" class="m-0 flex min-h-0 flex-1 flex-col space-y-4 pt-3">
<template v-if="!driveInitialized"> <template v-if="!driveInitialized">
<div class="shrink-0 text-center py-8 space-y-4"> <div class="shrink-0 text-center py-8 space-y-4">
<HardDrive class="h-10 w-10 mx-auto text-muted-foreground" /> <HardDrive class="size-10 mx-auto text-muted-foreground" />
<p class="text-sm text-muted-foreground">{{ t('msd.driveNotInitialized') }}</p> <p class="text-sm text-muted-foreground">{{ t('msd.driveNotInitialized') }}</p>
<Button size="sm" @click="initializeDrive"> <Button size="sm" @click="initializeDrive">
{{ t('msd.initializeDrive') }} {{ t('msd.initializeDrive') }}
@@ -920,7 +920,7 @@ onUnmounted(() => {
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<HardDrive class="h-4 w-4 text-muted-foreground" /> <HardDrive class="size-4 text-muted-foreground" />
<span class="text-sm font-medium">{{ t('msd.drive') }}</span> <span class="text-sm font-medium">{{ t('msd.drive') }}</span>
<!-- Show size badge only when info is available --> <!-- Show size badge only when info is available -->
<Badge v-if="driveInfo" variant="outline" class="text-xs"> <Badge v-if="driveInfo" variant="outline" class="text-xs">
@@ -933,8 +933,8 @@ onUnmounted(() => {
</Badge> </Badge>
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<span class="inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground"> <span class="inline-flex size-4 items-center justify-center text-muted-foreground hover:text-foreground">
<Info class="h-3.5 w-3.5" /> <Info class="size-3.5" />
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -958,9 +958,9 @@ onUnmounted(() => {
</template> </template>
<template v-else-if="driveConnectedToTarget"> <template v-else-if="driveConnectedToTarget">
<Badge variant="default" class="h-8 px-2 text-xs"> <Badge variant="default" class="h-8 px-2 text-xs">
<span class="relative flex h-1.5 w-1.5 mr-1.5"> <span class="relative flex size-1.5 mr-1.5">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary-foreground opacity-75"></span> <span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary-foreground opacity-75"></span>
<span class="relative inline-flex h-1.5 w-1.5 rounded-full bg-primary-foreground"></span> <span class="relative inline-flex size-1.5 rounded-full bg-primary-foreground"></span>
</span> </span>
{{ t('common.connected') }} {{ t('common.connected') }}
</Badge> </Badge>
@@ -972,7 +972,7 @@ onUnmounted(() => {
:disabled="operationInProgress" :disabled="operationInProgress"
@click="unmountMedia(driveMedia)" @click="unmountMedia(driveMedia)"
> >
<Unlink class="h-3.5 w-3.5 mr-1" /> <Unlink class="size-3.5 mr-1" />
{{ t('msd.disconnect') }} {{ t('msd.disconnect') }}
</Button> </Button>
</template> </template>
@@ -984,7 +984,7 @@ onUnmounted(() => {
:disabled="operationInProgress || mediaSlotsFull || !!driveError" :disabled="operationInProgress || mediaSlotsFull || !!driveError"
@click="connectDrive" @click="connectDrive"
> >
<Link v-if="!connecting" class="h-3.5 w-3.5 mr-1" /> <Link v-if="!connecting" class="size-3.5 mr-1" />
<span v-if="connecting">{{ t('common.connecting') }}...</span> <span v-if="connecting">{{ t('common.connecting') }}...</span>
<span v-else>{{ t('msd.connect') }}</span> <span v-else>{{ t('msd.connect') }}</span>
</Button> </Button>
@@ -992,11 +992,11 @@ onUnmounted(() => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive" class="size-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
:disabled="operationInProgress || driveConnectedToTarget" :disabled="operationInProgress || driveConnectedToTarget"
@click="showDeleteDriveDialog = true" @click="showDeleteDriveDialog = true"
> >
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="size-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -1024,15 +1024,15 @@ onUnmounted(() => {
v-if="currentPath !== '/'" v-if="currentPath !== '/'"
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8 shrink-0" class="size-8 shrink-0"
:disabled="driveConnectedToTarget" :disabled="driveConnectedToTarget"
@click="navigateUp" @click="navigateUp"
> >
<ArrowLeft class="h-3.5 w-3.5" /> <ArrowLeft class="size-3.5" />
</Button> </Button>
<nav class="flex items-center text-xs min-w-0 overflow-hidden"> <nav class="flex items-center text-xs min-w-0 overflow-hidden">
<template v-for="(crumb, index) in breadcrumbs" :key="crumb.path"> <template v-for="(crumb, index) in breadcrumbs" :key="crumb.path">
<ChevronRight v-if="index > 0" class="h-3 w-3 text-muted-foreground mx-0.5 shrink-0" /> <ChevronRight v-if="index > 0" class="size-3 text-muted-foreground mx-0.5 shrink-0" />
<Button <Button
variant="link" variant="link"
size="sm" size="sm"
@@ -1059,10 +1059,10 @@ onUnmounted(() => {
variant="ghost" variant="ghost"
size="icon" size="icon"
as="span" as="span"
class="h-8 w-8" class="size-8"
:class="driveConnectedToTarget ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'" :class="driveConnectedToTarget ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'"
> >
<Upload class="h-3.5 w-3.5" /> <Upload class="size-3.5" />
</Button> </Button>
</label> </label>
</TooltipTrigger> </TooltipTrigger>
@@ -1076,11 +1076,11 @@ onUnmounted(() => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8" class="size-8"
:disabled="driveConnectedToTarget" :disabled="driveConnectedToTarget"
@click="showNewFolderDialog = true" @click="showNewFolderDialog = true"
> >
<FolderPlus class="h-3.5 w-3.5" /> <FolderPlus class="size-3.5" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent v-if="driveConnectedToTarget"> <TooltipContent v-if="driveConnectedToTarget">
@@ -1090,11 +1090,11 @@ onUnmounted(() => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8" class="size-8"
:disabled="driveConnectedToTarget" :disabled="driveConnectedToTarget"
@click="loadDriveFiles" @click="loadDriveFiles"
> >
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingDrive }" /> <RefreshCw class="size-3.5" :class="{ 'animate-spin': loadingDrive }" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -1129,8 +1129,8 @@ onUnmounted(() => {
class="flex items-center gap-2 cursor-pointer flex-1 min-w-0" class="flex items-center gap-2 cursor-pointer flex-1 min-w-0"
@click="file.is_dir && navigateTo(file.path)" @click="file.is_dir && navigateTo(file.path)"
> >
<Folder v-if="file.is_dir" class="h-4 w-4 shrink-0 text-info" /> <Folder v-if="file.is_dir" class="size-4 shrink-0 text-info" />
<File v-else class="h-4 w-4 text-muted-foreground shrink-0" /> <File v-else class="size-4 text-muted-foreground shrink-0" />
<div class="min-w-0"> <div class="min-w-0">
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
@@ -1150,22 +1150,22 @@ onUnmounted(() => {
v-if="!file.is_dir" v-if="!file.is_dir"
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8" class="size-8"
as="a" as="a"
:href="msdApi.downloadDriveFile(file.path)" :href="msdApi.downloadDriveFile(file.path)"
download download
> >
<Download class="h-3.5 w-3.5" /> <Download class="size-3.5" />
</Button> </Button>
<!-- ③ Delete disabled when drive connected to target --> <!-- ③ Delete disabled when drive connected to target -->
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive" class="size-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
:disabled="driveConnectedToTarget" :disabled="driveConnectedToTarget"
@click="confirmDelete('file', file.path, file.name)" @click="confirmDelete('file', file.path, file.name)"
> >
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="size-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -1339,7 +1339,7 @@ onUnmounted(() => {
{{ t('common.cancel') }} {{ t('common.cancel') }}
</Button> </Button>
<Button @click="confirmImageMount" :disabled="connecting || mediaSlotsFull"> <Button @click="confirmImageMount" :disabled="connecting || mediaSlotsFull">
<Link v-if="!connecting" class="h-4 w-4 mr-1" /> <Link v-if="!connecting" class="size-4 mr-1" />
<span v-if="connecting">{{ t('common.connecting') }}...</span> <span v-if="connecting">{{ t('common.connecting') }}...</span>
<span v-else>{{ t('msd.connect') }}</span> <span v-else>{{ t('msd.connect') }}</span>
</Button> </Button>
@@ -1366,7 +1366,7 @@ onUnmounted(() => {
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle class="flex items-center gap-2"> <DialogTitle class="flex items-center gap-2">
<Globe class="h-5 w-5" /> <Globe class="size-5" />
{{ t('msd.downloadFromUrl') }} {{ t('msd.downloadFromUrl') }}
</DialogTitle> </DialogTitle>
<DialogDescription>{{ t('msd.downloadFromUrlDesc') }}</DialogDescription> <DialogDescription>{{ t('msd.downloadFromUrlDesc') }}</DialogDescription>
@@ -1425,11 +1425,11 @@ onUnmounted(() => {
:disabled="!downloadUrl.trim()" :disabled="!downloadUrl.trim()"
@click="startUrlDownload" @click="startUrlDownload"
> >
<Download class="h-4 w-4 mr-1" /> <Download class="size-4 mr-1" />
{{ t('msd.download') }} {{ t('msd.download') }}
</Button> </Button>
<Button v-else variant="destructive" @click="cancelUrlDownload"> <Button v-else variant="destructive" @click="cancelUrlDownload">
<X class="h-4 w-4 mr-1" /> <X class="size-4 mr-1" />
{{ t('common.cancel') }} {{ t('common.cancel') }}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -205,7 +205,7 @@ function handleKeydown(e: KeyboardEvent) {
<!-- Warning for untypable characters --> <!-- Warning for untypable characters -->
<Alert v-if="hasUntypableChars && !isPasting" variant="warning"> <Alert v-if="hasUntypableChars && !isPasting" variant="warning">
<AlertCircle class="h-4 w-4 shrink-0 mt-0.5" /> <AlertCircle class="size-4 shrink-0 mt-0.5" />
<AlertTitle>{{ t('paste.untypableWarning') }}</AlertTitle> <AlertTitle>{{ t('paste.untypableWarning') }}</AlertTitle>
<AlertDescription> <AlertDescription>
{{ t('paste.untypableChars', { chars: textAnalysis?.untypableChars.slice(0, 5).map(c => c === '\n' ? '\\n' : c === '\r' ? '\\r' : c === '\t' ? '\\t' : c).join(', ') }) }} {{ t('paste.untypableChars', { chars: textAnalysis?.untypableChars.slice(0, 5).map(c => c === '\n' ? '\\n' : c === '\r' ? '\\r' : c === '\t' ? '\\t' : c).join(', ') }) }}
@@ -234,7 +234,7 @@ function handleKeydown(e: KeyboardEvent) {
{{ t('common.cancel') }} {{ t('common.cancel') }}
</Button> </Button>
<Button v-else variant="ghost" size="sm" @click="cancelPaste"> <Button v-else variant="ghost" size="sm" @click="cancelPaste">
<Square class="h-3 w-3 mr-1.5 fill-current" /> <Square class="size-3 mr-1.5 fill-current" />
{{ t('paste.stop') }} {{ t('paste.stop') }}
</Button> </Button>
<Button <Button
@@ -242,7 +242,7 @@ function handleKeydown(e: KeyboardEvent) {
:disabled="!text.trim() || isPasting" :disabled="!text.trim() || isPasting"
@click="handlePaste" @click="handlePaste"
> >
<CornerDownLeft class="h-4 w-4 mr-1.5" /> <CornerDownLeft class="size-4 mr-1.5" />
{{ t('paste.confirm') }} {{ t('paste.confirm') }}
</Button> </Button>
</div> </div>

View File

@@ -141,7 +141,7 @@ const statusBadgeText = computed(() => {
<template v-if="compact"> <template v-if="compact">
<!-- Compact: single row with dot + abbreviated title --> <!-- Compact: single row with dot + abbreviated title -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span :class="cn('h-1.5 w-1.5 rounded-full shrink-0', statusColor)" /> <span :class="cn('size-1.5 rounded-full shrink-0', statusColor)" />
<span class="text-[10px] text-muted-foreground leading-tight truncate">{{ title }}</span> <span class="text-[10px] text-muted-foreground leading-tight truncate">{{ title }}</span>
</div> </div>
</template> </template>
@@ -150,7 +150,7 @@ const statusBadgeText = computed(() => {
<span class="font-medium text-foreground text-xs truncate">{{ title }}</span> <span class="font-medium text-foreground text-xs truncate">{{ title }}</span>
<!-- Bottom: Status dot + Quick info --> <!-- Bottom: Status dot + Quick info -->
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span :class="cn('h-2 w-2 rounded-full shrink-0', statusColor)" /> <span :class="cn('size-2 rounded-full shrink-0', statusColor)" />
<span class="text-[11px] text-muted-foreground leading-tight truncate"> <span class="text-[11px] text-muted-foreground leading-tight truncate">
{{ quickInfo || subtitle || statusText }} {{ quickInfo || subtitle || statusText }}
</span> </span>
@@ -169,7 +169,7 @@ const statusBadgeText = computed(() => {
status === 'error' ? 'bg-destructive/10 text-destructive' : status === 'error' ? 'bg-destructive/10 text-destructive' :
'bg-muted text-muted-foreground' 'bg-muted text-muted-foreground'
)"> )">
<component :is="StatusIcon" class="h-5 w-5" /> <component :is="StatusIcon" class="size-5" />
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h4 class="font-semibold text-sm">{{ title }}</h4> <h4 class="font-semibold text-sm">{{ title }}</h4>
@@ -178,7 +178,7 @@ const statusBadgeText = computed(() => {
v-if="statusIcon" v-if="statusIcon"
:is="statusIcon" :is="statusIcon"
:class="cn( :class="cn(
'h-3.5 w-3.5', 'size-3.5',
status === 'connected' ? 'text-success' : status === 'connected' ? 'text-success' :
status === 'connecting' ? 'text-warning animate-spin' : status === 'connecting' ? 'text-warning animate-spin' :
status === 'error' ? 'text-destructive' : status === 'error' ? 'text-destructive' :
@@ -229,7 +229,7 @@ const statusBadgeText = computed(() => {
<template v-if="compact"> <template v-if="compact">
<!-- Compact: single row with dot + abbreviated title --> <!-- Compact: single row with dot + abbreviated title -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span :class="cn('h-1.5 w-1.5 rounded-full shrink-0', statusColor)" /> <span :class="cn('size-1.5 rounded-full shrink-0', statusColor)" />
<span class="text-[10px] text-muted-foreground leading-tight truncate">{{ title }}</span> <span class="text-[10px] text-muted-foreground leading-tight truncate">{{ title }}</span>
</div> </div>
</template> </template>
@@ -238,7 +238,7 @@ const statusBadgeText = computed(() => {
<span class="font-medium text-foreground text-xs truncate">{{ title }}</span> <span class="font-medium text-foreground text-xs truncate">{{ title }}</span>
<!-- Bottom: Status dot + Quick info --> <!-- Bottom: Status dot + Quick info -->
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span :class="cn('h-2 w-2 rounded-full shrink-0', statusColor)" /> <span :class="cn('size-2 rounded-full shrink-0', statusColor)" />
<span class="text-[11px] text-muted-foreground leading-tight truncate"> <span class="text-[11px] text-muted-foreground leading-tight truncate">
{{ quickInfo || subtitle || statusText }} {{ quickInfo || subtitle || statusText }}
</span> </span>
@@ -257,7 +257,7 @@ const statusBadgeText = computed(() => {
status === 'error' ? 'bg-destructive/10 text-destructive' : status === 'error' ? 'bg-destructive/10 text-destructive' :
'bg-muted text-muted-foreground' 'bg-muted text-muted-foreground'
)"> )">
<component :is="StatusIcon" class="h-5 w-5" /> <component :is="StatusIcon" class="size-5" />
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h4 class="font-semibold text-sm">{{ title }}</h4> <h4 class="font-semibold text-sm">{{ title }}</h4>
@@ -266,7 +266,7 @@ const statusBadgeText = computed(() => {
v-if="statusIcon" v-if="statusIcon"
:is="statusIcon" :is="statusIcon"
:class="cn( :class="cn(
'h-3.5 w-3.5', 'size-3.5',
status === 'connected' ? 'text-success' : status === 'connected' ? 'text-success' :
status === 'connecting' ? 'text-warning animate-spin' : status === 'connecting' ? 'text-warning animate-spin' :
status === 'error' ? 'text-destructive' : status === 'error' ? 'text-destructive' :

View File

@@ -70,23 +70,22 @@ function hideFrameScrollbars(event: Event) {
<DialogHeader class="px-3 sm:px-4 py-1 border-b shrink-0"> <DialogHeader class="px-3 sm:px-4 py-1 border-b shrink-0">
<DialogTitle class="flex h-8 items-center justify-between w-full"> <DialogTitle class="flex h-8 items-center justify-between w-full">
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<Terminal class="h-4 w-4 shrink-0" /> <Terminal class="size-4 shrink-0" />
<span class="truncate text-sm font-semibold">{{ t('extensions.ttyd.title') }}</span> <span class="truncate text-sm font-semibold">{{ t('extensions.ttyd.title') }}</span>
</div> </div>
<div class="flex items-center gap-1 shrink-0"> <div class="flex items-center gap-1 shrink-0">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon-sm"
class="h-7 w-7"
@click="openTerminalInNewTab" @click="openTerminalInNewTab"
:aria-label="t('extensions.ttyd.openInNewTab')" :aria-label="t('extensions.ttyd.openInNewTab')"
:title="t('extensions.ttyd.openInNewTab')" :title="t('extensions.ttyd.openInNewTab')"
> >
<ExternalLink class="h-3.5 w-3.5" /> <ExternalLink class="size-3.5" />
</Button> </Button>
<DialogClose as-child> <DialogClose as-child>
<Button variant="ghost" size="icon" class="h-7 w-7"> <Button variant="ghost" size="icon-sm">
<X class="h-3.5 w-3.5" /> <X class="size-3.5" />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
</Button> </Button>
</DialogClose> </DialogClose>

View File

@@ -249,11 +249,11 @@ onUnmounted(() => {
</CardContent> </CardContent>
<CardFooter class="border-t pt-4 justify-end"> <CardFooter class="border-t pt-4 justify-end">
<Button v-if="!enabled" :disabled="statusLoading" @click="enrollmentOpen = true"> <Button v-if="!enabled" :disabled="statusLoading" @click="enrollmentOpen = true">
<ShieldCheck class="h-4 w-4" /> <ShieldCheck class="size-4" />
{{ t('settings.totp.enable') }} {{ t('settings.totp.enable') }}
</Button> </Button>
<Button v-else variant="destructive" :disabled="statusLoading" @click="disableOpen = true"> <Button v-else variant="destructive" :disabled="statusLoading" @click="disableOpen = true">
<ShieldOff class="h-4 w-4" /> <ShieldOff class="size-4" />
{{ t('settings.totp.disable') }} {{ t('settings.totp.disable') }}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -283,7 +283,7 @@ onUnmounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<Label for="totp-enrollment-code">{{ t('auth.totpCode') }}</Label> <Label for="totp-enrollment-code">{{ t('auth.totpCode') }}</Label>
<div class="relative"> <div class="relative">
<KeyRound class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <KeyRound class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input id="totp-enrollment-code" v-model="enrollmentCode" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="pl-10 font-mono" @input="enrollmentCode = normalizeCode(enrollmentCode)" /> <Input id="totp-enrollment-code" v-model="enrollmentCode" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="pl-10 font-mono" @input="enrollmentCode = normalizeCode(enrollmentCode)" />
</div> </div>
</div> </div>
@@ -296,11 +296,11 @@ onUnmounted(() => {
<DialogFooter> <DialogFooter>
<Button variant="outline" :disabled="enrollmentLoading" @click="enrollmentOpen = false">{{ t('common.cancel') }}</Button> <Button variant="outline" :disabled="enrollmentLoading" @click="enrollmentOpen = false">{{ t('common.cancel') }}</Button>
<Button v-if="enrollmentStep === 'password'" :disabled="enrollmentLoading" @click="beginEnrollment"> <Button v-if="enrollmentStep === 'password'" :disabled="enrollmentLoading" @click="beginEnrollment">
<Loader2 v-if="enrollmentLoading" class="h-4 w-4 animate-spin" /> <Loader2 v-if="enrollmentLoading" class="size-4 animate-spin" />
{{ t('common.next') }} {{ t('common.next') }}
</Button> </Button>
<Button v-else :disabled="enrollmentLoading" @click="confirmEnrollment"> <Button v-else :disabled="enrollmentLoading" @click="confirmEnrollment">
<Loader2 v-if="enrollmentLoading" class="h-4 w-4 animate-spin" /> <Loader2 v-if="enrollmentLoading" class="size-4 animate-spin" />
{{ t('common.confirm') }} {{ t('common.confirm') }}
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -329,7 +329,7 @@ onUnmounted(() => {
<DialogFooter> <DialogFooter>
<Button variant="outline" :disabled="disableLoading" @click="disableOpen = false">{{ t('common.cancel') }}</Button> <Button variant="outline" :disabled="disableLoading" @click="disableOpen = false">{{ t('common.cancel') }}</Button>
<Button variant="destructive" :disabled="disableLoading" @click="disableTotp"> <Button variant="destructive" :disabled="disableLoading" @click="disableTotp">
<Loader2 v-if="disableLoading" class="h-4 w-4 animate-spin" /> <Loader2 v-if="disableLoading" class="size-4 animate-spin" />
{{ t('settings.totp.disable') }} {{ t('settings.totp.disable') }}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -594,8 +594,8 @@ watch(
<template> <template>
<Popover :open="open" @update:open="emit('update:open', $event)"> <Popover :open="open" @update:open="emit('update:open', $event)">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button variant="ghost" size="sm" class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"> <Button variant="ghost" size="sm" class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs">
<Monitor class="h-3.5 w-3.5 sm:h-4 sm:w-4" /> <Monitor class="size-3.5 sm:size-4" />
<span class="hidden sm:inline">{{ buttonText }}</span> <span class="hidden sm:inline">{{ buttonText }}</span>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -612,7 +612,7 @@ watch(
<!-- Mode Selection --> <!-- Mode Selection -->
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Label class="text-xs">{{ t('actionbar.videoMode') }}</Label> <Label class="text-xs text-muted-foreground">{{ t('actionbar.videoMode') }}</Label>
<HelpTooltip :content="t('actionbar.videoModeHint')" icon-size="sm" side="right" /> <HelpTooltip :content="t('actionbar.videoModeHint')" icon-size="sm" side="right" />
</div> </div>
<Select <Select
@@ -620,7 +620,7 @@ watch(
@update:model-value="handleVideoModeChange" @update:model-value="handleVideoModeChange"
:disabled="loadingCodecs || availableCodecs.length === 0 || isRtspCodecLocked" :disabled="loadingCodecs || availableCodecs.length === 0 || isRtspCodecLocked"
> >
<SelectTrigger class="h-8 w-full text-xs"> <SelectTrigger size="sm" class="w-full text-xs">
<div v-if="selectedCodecInfo" class="flex items-center gap-1.5 truncate"> <div v-if="selectedCodecInfo" class="flex items-center gap-1.5 truncate">
<span class="truncate">{{ selectedCodecInfo.name }}</span> <span class="truncate">{{ selectedCodecInfo.name }}</span>
<span <span
@@ -673,7 +673,7 @@ watch(
<!-- Bitrate Preset - Only shown for WebRTC modes --> <!-- Bitrate Preset - Only shown for WebRTC modes -->
<div v-if="props.videoMode !== 'mjpeg'" class="space-y-2"> <div v-if="props.videoMode !== 'mjpeg'" class="space-y-2">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Label class="text-xs">{{ t('actionbar.bitratePreset') }}</Label> <Label class="text-xs text-muted-foreground">{{ t('actionbar.bitratePreset') }}</Label>
<HelpTooltip :content="t('help.videoBitratePreset')" icon-size="sm" /> <HelpTooltip :content="t('help.videoBitratePreset')" icon-size="sm" />
</div> </div>
<div class="grid grid-cols-3 gap-1.5"> <div class="grid grid-cols-3 gap-1.5">
@@ -687,7 +687,7 @@ watch(
:disabled="applyingBitrate" :disabled="applyingBitrate"
@click="handleBitratePresetChange('Speed')" @click="handleBitratePresetChange('Speed')"
> >
<Zap class="h-3.5 w-3.5" /> <Zap class="size-3.5" />
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateSpeed') }}</span> <span class="text-[10px] font-medium">{{ t('actionbar.bitrateSpeed') }}</span>
</Button> </Button>
<Button <Button
@@ -700,7 +700,7 @@ watch(
:disabled="applyingBitrate" :disabled="applyingBitrate"
@click="handleBitratePresetChange('Balanced')" @click="handleBitratePresetChange('Balanced')"
> >
<Scale class="h-3.5 w-3.5" /> <Scale class="size-3.5" />
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateBalanced') }}</span> <span class="text-[10px] font-medium">{{ t('actionbar.bitrateBalanced') }}</span>
</Button> </Button>
<Button <Button
@@ -713,7 +713,7 @@ watch(
:disabled="applyingBitrate" :disabled="applyingBitrate"
@click="handleBitratePresetChange('Quality')" @click="handleBitratePresetChange('Quality')"
> >
<Image class="h-3.5 w-3.5" /> <Image class="size-3.5" />
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateQuality') }}</span> <span class="text-[10px] font-medium">{{ t('actionbar.bitrateQuality') }}</span>
</Button> </Button>
</div> </div>
@@ -733,23 +733,22 @@ watch(
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.deviceSettings') }}</h5> <h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.deviceSettings') }}</h5>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon-xs"
class="h-6 w-6"
:disabled="loadingDevices" :disabled="loadingDevices"
@click="loadDevices" @click="loadDevices"
> >
<RefreshCw :class="['h-3.5 w-3.5', loadingDevices && 'animate-spin']" /> <RefreshCw :class="['size-3.5', loadingDevices && 'animate-spin']" />
</Button> </Button>
</div> </div>
<!-- Device Selection --> <!-- Device Selection -->
<div class="space-y-2"> <div class="space-y-2">
<Label class="text-xs">{{ t('actionbar.videoDevice') }}</Label> <Label class="text-xs text-muted-foreground">{{ t('actionbar.videoDevice') }}</Label>
<NativeSelect <NativeSelect
:model-value="selectedDevice" :model-value="selectedDevice"
@update:model-value="handleDeviceChange" @update:model-value="handleDeviceChange"
:disabled="loadingDevices || devices.length === 0" :disabled="loadingDevices || devices.length === 0"
class="h-8 w-full text-xs" size="sm" class="w-full text-xs"
> >
<NativeSelectOption value="">{{ loadingDevices ? t('common.loading') : t('actionbar.selectDevice') }}</NativeSelectOption> <NativeSelectOption value="">{{ loadingDevices ? t('common.loading') : t('actionbar.selectDevice') }}</NativeSelectOption>
<NativeSelectOption <NativeSelectOption
@@ -765,13 +764,13 @@ watch(
<!-- Format Selection --> <!-- Format Selection -->
<div class="space-y-2"> <div class="space-y-2">
<Label class="text-xs">{{ t('actionbar.videoFormat') }}</Label> <Label class="text-xs text-muted-foreground">{{ t('actionbar.videoFormat') }}</Label>
<Select <Select
:model-value="selectedFormat" :model-value="selectedFormat"
@update:model-value="handleFormatChange" @update:model-value="handleFormatChange"
:disabled="!selectedDevice || availableFormats.length === 0" :disabled="!selectedDevice || availableFormats.length === 0"
> >
<SelectTrigger class="h-8 w-full text-xs"> <SelectTrigger size="sm" class="w-full text-xs">
<div v-if="selectedFormatInfo" class="flex min-w-0 items-center gap-1.5"> <div v-if="selectedFormatInfo" class="flex min-w-0 items-center gap-1.5">
<span class="truncate">{{ selectedFormatInfo.description }}</span> <span class="truncate">{{ selectedFormatInfo.description }}</span>
<span <span
@@ -825,12 +824,12 @@ watch(
<!-- Resolution Selection --> <!-- Resolution Selection -->
<div class="space-y-2"> <div class="space-y-2">
<Label class="text-xs">{{ t('actionbar.videoResolution') }}</Label> <Label class="text-xs text-muted-foreground">{{ t('actionbar.videoResolution') }}</Label>
<NativeSelect <NativeSelect
:model-value="selectedResolution" :model-value="selectedResolution"
@update:model-value="handleResolutionChange" @update:model-value="handleResolutionChange"
:disabled="!selectedFormat || availableResolutions.length === 0" :disabled="!selectedFormat || availableResolutions.length === 0"
class="h-8 w-full text-xs" size="sm" class="w-full text-xs"
> >
<NativeSelectOption value="">{{ t('actionbar.selectResolution') }}</NativeSelectOption> <NativeSelectOption value="">{{ t('actionbar.selectResolution') }}</NativeSelectOption>
<NativeSelectOption <NativeSelectOption
@@ -846,12 +845,12 @@ watch(
<!-- FPS Selection --> <!-- FPS Selection -->
<div class="space-y-2"> <div class="space-y-2">
<Label class="text-xs">{{ t('actionbar.videoFps') }}</Label> <Label class="text-xs text-muted-foreground">{{ t('actionbar.videoFps') }}</Label>
<NativeSelect <NativeSelect
:model-value="String(selectedFps)" :model-value="String(selectedFps)"
@update:model-value="handleFpsChange" @update:model-value="handleFpsChange"
:disabled="!selectedResolution || availableFps.length === 0" :disabled="!selectedResolution || availableFps.length === 0"
class="h-8 w-full text-xs" size="sm" class="w-full text-xs"
> >
<NativeSelectOption value="">{{ t('actionbar.selectFps') }}</NativeSelectOption> <NativeSelectOption value="">{{ t('actionbar.selectFps') }}</NativeSelectOption>
<NativeSelectOption <NativeSelectOption
@@ -871,7 +870,7 @@ watch(
:disabled="applying || !selectedDevice || !selectedFormat" :disabled="applying || !selectedDevice || !selectedFormat"
@click="applyVideoConfig" @click="applyVideoConfig"
> >
<Loader2 v-if="applying" class="h-3.5 w-3.5 mr-1.5 animate-spin" /> <Loader2 v-if="applying" class="size-3.5 mr-1.5 animate-spin" />
<span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span> <span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span>
</Button> </Button>
</div> </div>

View File

@@ -3,11 +3,15 @@ import type { HTMLAttributes } from "vue"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const props = defineProps<{ const props = withDefaults(
defineProps<{
defaultValue?: string | number defaultValue?: string | number
modelValue?: string | number modelValue?: string | number
class?: HTMLAttributes["class"] class?: HTMLAttributes["class"]
}>() size?: "sm" | "default"
}>(),
{ size: "default" },
)
const emits = defineEmits<{ const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void (e: "update:modelValue", payload: string | number): void
@@ -23,8 +27,9 @@ const modelValue = useVModel(props, "modelValue", emits, {
<input <input
v-model="modelValue" v-model="modelValue"
data-slot="input" data-slot="input"
:data-size="size"
:class="cn( :class="cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input data-[size=default]:h-9 data-[size=sm]:h-8 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class, props.class,

View File

@@ -9,7 +9,10 @@ defineOptions({
inheritAttrs: false, inheritAttrs: false,
}) })
const props = defineProps<{ modelValue?: AcceptableValue | AcceptableValue[], class?: HTMLAttributes["class"] }>() const props = withDefaults(
defineProps<{ modelValue?: AcceptableValue | AcceptableValue[], class?: HTMLAttributes["class"], size?: "sm" | "default" }>(),
{ size: "default" },
)
const emit = defineEmits<{ const emit = defineEmits<{
"update:modelValue": [value: AcceptableValue] "update:modelValue": [value: AcceptableValue]
@@ -20,7 +23,7 @@ const modelValue = useVModel(props, "modelValue", emit, {
defaultValue: "", defaultValue: "",
}) })
const delegatedProps = reactiveOmit(props, "class") const delegatedProps = reactiveOmit(props, "class", "size")
</script> </script>
<template> <template>
@@ -32,8 +35,9 @@ const delegatedProps = reactiveOmit(props, "class")
v-bind="{ ...$attrs, ...delegatedProps }" v-bind="{ ...$attrs, ...delegatedProps }"
v-model="modelValue" v-model="modelValue"
data-slot="native-select" data-slot="native-select"
:data-size="size"
:class="cn( :class="cn(
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed', 'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 data-[size=default]:h-9 data-[size=sm]:h-8 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class, props.class,

View File

@@ -18,7 +18,7 @@ const { toggleSidebar } = useSidebar()
data-slot="sidebar-trigger" data-slot="sidebar-trigger"
variant="ghost" variant="ghost"
size="icon" size="icon"
:class="cn('h-7 w-7', props.class)" :class="cn('size-7', props.class)"
@click="toggleSidebar" @click="toggleSidebar"
> >
<PanelLeft /> <PanelLeft />

View File

@@ -3,7 +3,7 @@ import { createContext } from "reka-ui"
export const SIDEBAR_COOKIE_NAME = "sidebar_state" export const SIDEBAR_COOKIE_NAME = "sidebar_state"
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
export const SIDEBAR_WIDTH = "16rem" export const SIDEBAR_WIDTH = "14rem"
export const SIDEBAR_WIDTH_MOBILE = "18rem" export const SIDEBAR_WIDTH_MOBILE = "18rem"
export const SIDEBAR_WIDTH_ICON = "3rem" export const SIDEBAR_WIDTH_ICON = "3rem"
export const SIDEBAR_KEYBOARD_SHORTCUT = "b" export const SIDEBAR_KEYBOARD_SHORTCUT = "b"

View File

@@ -155,3 +155,30 @@ body {
#app { #app {
min-height: 100vh; min-height: 100vh;
} }
@layer components {
/* Shared dotted backdrop: console video area, login & setup pages */
.dot-grid-bg {
background-color: var(--muted);
background-image: radial-gradient(circle, color-mix(in oklab, var(--muted-foreground) 35%, transparent) 1px, transparent 1px);
background-size: 20px 20px;
}
}
/* Settings page density: compact cards.
Unlayered on purpose — must outrank Tailwind's @layer utilities (py-6/px-6/gap-6). */
.settings-dense [data-slot="card"] {
gap: 1rem;
padding-block: 1rem;
}
.settings-dense [data-slot="card-header"],
.settings-dense [data-slot="card-content"],
.settings-dense [data-slot="card-footer"] {
padding-inline: 1rem;
}
.settings-dense [data-slot="card-header"].border-b {
padding-bottom: 1rem;
}
.settings-dense [data-slot="card-footer"].border-t {
padding-top: 1rem;
}

View File

@@ -3117,22 +3117,22 @@ onUnmounted(() => {
/> />
</div> </div>
<div class="mx-1 hidden h-6 w-px bg-border md:block" /> <div class="mx-1 hidden h-6 w-px bg-border md:block" />
<Button variant="ghost" size="icon" class="h-8 w-8 hidden md:flex" :aria-label="t('common.toggleTheme')" @click="toggleTheme"> <Button variant="ghost" size="icon-sm" class="hidden md:flex" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
<Sun v-if="isDark" class="h-4 w-4" /> <Sun v-if="isDark" class="size-4" />
<Moon v-else class="h-4 w-4" /> <Moon v-else class="size-4" />
</Button> </Button>
<LanguageToggleButton class="h-8 w-8 hidden md:flex" /> <LanguageToggleButton class="size-8 hidden md:flex" />
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<Button variant="outline" size="sm" class="gap-1 sm:gap-1.5 h-7 sm:h-9 px-2 sm:px-3"> <Button variant="outline" size="sm" class="gap-1 sm:gap-1.5 px-2 sm:px-3">
<span class="text-xs max-w-[60px] sm:max-w-[100px] truncate">{{ authStore.user || 'admin' }}</span> <span class="text-xs max-w-[60px] sm:max-w-[100px] truncate">{{ authStore.user || 'admin' }}</span>
<ChevronDown class="h-3 w-3 sm:h-3.5 sm:w-3.5" /> <ChevronDown class="size-3 sm:size-3.5" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem class="md:hidden" @click="toggleTheme"> <DropdownMenuItem class="md:hidden" @click="toggleTheme">
<Sun v-if="isDark" class="h-4 w-4 mr-2" /> <Sun v-if="isDark" class="size-4 mr-2" />
<Moon v-else class="h-4 w-4 mr-2" /> <Moon v-else class="size-4 mr-2" />
{{ isDark ? t('settings.lightMode') : t('settings.darkMode') }} {{ isDark ? t('settings.lightMode') : t('settings.darkMode') }}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem as-child class="md:hidden p-0"> <DropdownMenuItem as-child class="md:hidden p-0">
@@ -3145,12 +3145,12 @@ onUnmounted(() => {
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator class="md:hidden" /> <DropdownMenuSeparator class="md:hidden" />
<DropdownMenuItem @click="changePasswordDialogOpen = true"> <DropdownMenuItem @click="changePasswordDialogOpen = true">
<KeyRound class="h-4 w-4 mr-2" /> <KeyRound class="size-4 mr-2" />
{{ t('auth.changePassword') }} {{ t('auth.changePassword') }}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem @click="logout"> <DropdownMenuItem @click="logout">
<LogOut class="h-4 w-4 mr-2" /> <LogOut class="size-4 mr-2" />
{{ t('auth.logout') }} {{ t('auth.logout') }}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@@ -3179,13 +3179,7 @@ onUnmounted(() => {
@open-computer-use="openComputerUse" @open-computer-use="openComputerUse"
/> />
<div class="flex-1 overflow-hidden relative"> <div class="flex-1 overflow-hidden relative">
<div <div class="absolute inset-0 dot-grid-bg" />
class="absolute inset-0 bg-slate-100/80 dark:bg-slate-800/40 opacity-80"
style="
background-image: radial-gradient(circle, rgb(148 163 184 / 0.4) 1px, transparent 1px);
background-size: 20px 20px;
"
/>
<div class="relative flex h-full w-full min-w-0 items-stretch gap-3 p-1 sm:p-4"> <div class="relative flex h-full w-full min-w-0 items-stretch gap-3 p-1 sm:p-4">
<div <div
class="flex min-w-0 flex-1 items-center justify-center transition-all duration-300" class="flex min-w-0 flex-1 items-center justify-center transition-all duration-300"
@@ -3293,7 +3287,7 @@ onUnmounted(() => {
<div class="absolute w-full h-0.5 bg-gradient-to-r from-transparent via-primary/40 to-transparent animate-pulse" style="top: 50%; animation-duration: 1.5s;" /> <div class="absolute w-full h-0.5 bg-gradient-to-r from-transparent via-primary/40 to-transparent animate-pulse" style="top: 50%; animation-duration: 1.5s;" />
</div> </div>
<Spinner class="h-10 w-10 sm:h-16 sm:w-16 text-white mb-2 sm:mb-4" /> <Spinner class="size-10 sm:size-16 text-white mb-2 sm:mb-4" />
<p class="text-white/90 text-sm sm:text-lg font-medium text-center px-4 flex items-baseline justify-center gap-2 flex-wrap"> <p class="text-white/90 text-sm sm:text-lg font-medium text-center px-4 flex items-baseline justify-center gap-2 flex-wrap">
<span>{{ webrtcLoadingMessage }}</span> <span>{{ webrtcLoadingMessage }}</span>
<span <span
@@ -3339,7 +3333,7 @@ onUnmounted(() => {
}" }"
> >
<MonitorOff <MonitorOff
class="h-10 w-10 sm:h-16 sm:w-16" class="size-10 sm:size-16"
:class="{ :class="{
'text-slate-200': signalOverlayInfo.tone === 'info', 'text-slate-200': signalOverlayInfo.tone === 'info',
'text-red-300': signalOverlayInfo.tone === 'error', 'text-red-300': signalOverlayInfo.tone === 'error',
@@ -3368,7 +3362,7 @@ onUnmounted(() => {
v-if="videoError && !videoLoading" v-if="videoError && !videoLoading"
class="absolute inset-0 flex flex-col items-center justify-center bg-black/85 text-white gap-4 transition-opacity duration-300 p-4" class="absolute inset-0 flex flex-col items-center justify-center bg-black/85 text-white gap-4 transition-opacity duration-300 p-4"
> >
<MonitorOff class="h-10 w-10 sm:h-16 sm:w-16 text-slate-400" /> <MonitorOff class="size-10 sm:size-16 text-slate-400" />
<div class="text-center max-w-md px-2"> <div class="text-center max-w-md px-2">
<p class="font-medium text-sm sm:text-lg mb-1 sm:mb-2">{{ t('console.connectionFailed') }}</p> <p class="font-medium text-sm sm:text-lg mb-1 sm:mb-2">{{ t('console.connectionFailed') }}</p>
<p class="text-xs sm:text-sm text-slate-300 mb-2 sm:mb-3">{{ t('console.connectionFailedDesc') }}</p> <p class="text-xs sm:text-sm text-slate-300 mb-2 sm:mb-3">{{ t('console.connectionFailedDesc') }}</p>
@@ -3379,7 +3373,7 @@ onUnmounted(() => {
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<Button variant="secondary" size="sm" @click="reloadPage"> <Button variant="secondary" size="sm" @click="reloadPage">
<RefreshCw class="h-4 w-4 mr-2" /> <RefreshCw class="size-4 mr-2" />
{{ t('console.reconnect') }} {{ t('console.reconnect') }}
</Button> </Button>
</div> </div>
@@ -3470,7 +3464,7 @@ onUnmounted(() => {
{{ t('common.cancel') }} {{ t('common.cancel') }}
</Button> </Button>
<Button @click="handleChangePassword" :disabled="changingPassword"> <Button @click="handleChangePassword" :disabled="changingPassword">
<Loader2 v-if="changingPassword" class="h-4 w-4 mr-2 animate-spin" /> <Loader2 v-if="changingPassword" class="size-4 mr-2 animate-spin" />
{{ t('common.confirm') }} {{ t('common.confirm') }}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -120,27 +120,27 @@ function returnToPassword(keepError = false) {
</script> </script>
<template> <template>
<div class="min-h-screen min-h-dvh flex items-center justify-center bg-background p-4"> <div class="min-h-screen min-h-dvh flex items-center justify-center dot-grid-bg p-4">
<Card class="relative w-full max-w-sm"> <Card class="relative w-full max-w-sm">
<div class="absolute top-4 right-4"> <div class="absolute top-4 right-4">
<LanguageToggleButton /> <LanguageToggleButton />
</div> </div>
<CardHeader class="space-y-2 pt-10 text-center sm:pt-12"> <CardHeader class="space-y-2 pt-8 text-center">
<div class="mx-auto flex justify-center"> <div class="mx-auto flex justify-center">
<BrandMark size="xl" /> <BrandMark size="lg" />
</div> </div>
<CardTitle class="text-xl sm:text-2xl">One-KVM</CardTitle> <CardTitle class="text-xl">One-KVM</CardTitle>
<CardDescription>{{ step === 'password' ? t('auth.login') : t('auth.totpPrompt') }}</CardDescription> <CardDescription>{{ step === 'password' ? t('auth.login') : t('auth.totpPrompt') }}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form v-if="step === 'password'" @submit.prevent="handleLogin"> <form v-if="step === 'password'" @submit.prevent="handleLogin">
<FieldGroup> <FieldGroup class="gap-5">
<Field> <Field>
<FieldLabel for="username">{{ t('auth.username') }}</FieldLabel> <FieldLabel for="username">{{ t('auth.username') }}</FieldLabel>
<div class="relative"> <div class="relative">
<User class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <User class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
id="username" id="username"
v-model="username" v-model="username"
@@ -164,7 +164,7 @@ function returnToPassword(keepError = false) {
class="h-auto gap-1 p-0 text-xs text-muted-foreground" class="h-auto gap-1 p-0 text-xs text-muted-foreground"
> >
{{ t('auth.forgotPassword') }} {{ t('auth.forgotPassword') }}
<CircleHelp class="h-3.5 w-3.5" /> <CircleHelp class="size-3.5" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent class="w-80 p-3" align="end"> <PopoverContent class="w-80 p-3" align="end">
@@ -175,7 +175,7 @@ function returnToPassword(keepError = false) {
</Popover> </Popover>
</div> </div>
<div class="relative"> <div class="relative">
<Lock class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Lock class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
id="password" id="password"
v-model="password" v-model="password"
@@ -211,11 +211,11 @@ function returnToPassword(keepError = false) {
</FieldGroup> </FieldGroup>
</form> </form>
<form v-else @submit.prevent="handleTotpLogin"> <form v-else @submit.prevent="handleTotpLogin">
<FieldGroup> <FieldGroup class="gap-5">
<Field> <Field>
<FieldLabel for="totp-code">{{ t('auth.totpCode') }}</FieldLabel> <FieldLabel for="totp-code">{{ t('auth.totpCode') }}</FieldLabel>
<div class="relative"> <div class="relative">
<KeyRound class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <KeyRound class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
id="totp-code" id="totp-code"
v-model="totpCode" v-model="totpCode"
@@ -240,7 +240,7 @@ function returnToPassword(keepError = false) {
<span v-else>{{ t('auth.verifyAndLogin') }}</span> <span v-else>{{ t('auth.verifyAndLogin') }}</span>
</Button> </Button>
<Button type="button" variant="ghost" class="w-full" :disabled="loading" @click="returnToPassword()"> <Button type="button" variant="ghost" class="w-full" :disabled="loading" @click="returnToPassword()">
<ArrowLeft class="h-4 w-4" /> <ArrowLeft class="size-4" />
{{ t('auth.backToPassword') }} {{ t('auth.backToPassword') }}
</Button> </Button>
</FieldGroup> </FieldGroup>

View File

@@ -2700,19 +2700,19 @@ watch(isWindows, () => {
<AppLayout> <AppLayout>
<SidebarProvider class="h-full min-h-0 overflow-hidden"> <SidebarProvider class="h-full min-h-0 overflow-hidden">
<Sidebar class="top-10 h-[calc(100dvh-2.5rem)] sm:top-14 sm:h-[calc(100dvh-3.5rem)]" collapsible="offcanvas"> <Sidebar class="top-10 h-[calc(100dvh-2.5rem)] sm:top-14 sm:h-[calc(100dvh-3.5rem)]" collapsible="offcanvas">
<SidebarHeader class="gap-1 px-6 pt-6 pb-3 text-foreground md:pt-10"> <SidebarHeader class="gap-1 px-6 pt-4 pb-2 text-foreground md:pt-6">
<h1 class="text-xl font-semibold">{{ t('settings.title') }}</h1> <h1 class="text-xl font-semibold">{{ t('settings.title') }}</h1>
<p class="text-xs text-muted-foreground">{{ t('settings.sidebarSubtitle') }}</p> <p class="text-xs text-muted-foreground">{{ t('settings.sidebarSubtitle') }}</p>
</SidebarHeader> </SidebarHeader>
<SidebarContent class="px-3 pb-6 md:pb-10"> <SidebarContent class="px-3 pb-6 md:pb-10">
<SidebarGroup v-for="group in navGroups" :key="group.title" class="px-0 py-2"> <SidebarGroup v-for="group in navGroups" :key="group.title" class="px-0 py-1">
<SidebarGroupLabel class="uppercase">{{ group.title }}</SidebarGroupLabel> <SidebarGroupLabel class="uppercase">{{ group.title }}</SidebarGroupLabel>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem v-for="item in group.items" :key="item.id"> <SidebarMenuItem v-for="item in group.items" :key="item.id">
<SidebarMenuButton <SidebarMenuButton
:is-active="activeSection === item.id" :is-active="activeSection === item.id"
:tooltip="item.label" :tooltip="item.label"
class="h-10 px-3 text-foreground data-[active=true]:bg-primary data-[active=true]:text-primary-foreground data-[active=true]:shadow-sm data-[active=true]:hover:bg-primary data-[active=true]:hover:text-primary-foreground" class="h-8 px-3 text-foreground data-[active=true]:bg-primary data-[active=true]:text-primary-foreground data-[active=true]:shadow-sm data-[active=true]:hover:bg-primary data-[active=true]:hover:text-primary-foreground"
@click="selectSection(item.id)" @click="selectSection(item.id)"
> >
<component :is="item.icon" /> <component :is="item.icon" />
@@ -2727,19 +2727,19 @@ watch(isWindows, () => {
<SidebarInset class="min-w-0 overflow-y-auto"> <SidebarInset class="min-w-0 overflow-y-auto">
<!-- Mobile Header --> <!-- Mobile Header -->
<div class="md:hidden sticky top-0 z-20 flex items-center px-3 sm:px-4 py-2 sm:py-3 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/70"> <div class="md:hidden sticky top-0 z-20 flex items-center px-3 sm:px-4 py-2 sm:py-3 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/70">
<SidebarTrigger class="mr-1.5 h-8 w-8 sm:mr-2 sm:h-9 sm:w-9" /> <SidebarTrigger class="mr-1.5 size-8 sm:mr-2 sm:size-9" />
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<component :is="sectionMeta.icon" class="h-4 w-4 text-muted-foreground shrink-0" /> <component :is="sectionMeta.icon" class="size-4 text-muted-foreground shrink-0" />
<h1 class="text-sm sm:text-base font-semibold truncate">{{ sectionMeta.title }}</h1> <h1 class="text-sm sm:text-base font-semibold truncate">{{ sectionMeta.title }}</h1>
</div> </div>
</div> </div>
<div class="mx-auto w-full max-w-3xl px-3 sm:px-6 lg:px-8 pt-6 md:pt-10 pb-10 space-y-6"> <div class="mx-auto w-full max-w-4xl px-3 sm:px-6 lg:px-8 pt-6 pb-10 space-y-4 settings-dense">
<!-- Section Header --> <!-- Section Header -->
<header class="space-y-1.5 pb-2 border-b"> <header class="space-y-1.5 pb-2 border-b">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<component :is="sectionMeta.icon" class="h-5 w-5 text-muted-foreground" /> <component :is="sectionMeta.icon" class="size-5 text-muted-foreground" />
<h1 class="text-xl sm:text-2xl font-semibold tracking-tight">{{ sectionMeta.title }}</h1> <h1 class="text-xl font-semibold tracking-tight">{{ sectionMeta.title }}</h1>
</div> </div>
<p v-if="sectionMeta.description" class="text-sm text-muted-foreground"> <p v-if="sectionMeta.description" class="text-sm text-muted-foreground">
{{ sectionMeta.description }} {{ sectionMeta.description }}
@@ -2747,7 +2747,7 @@ watch(isWindows, () => {
</header> </header>
<!-- Appearance Section --> <!-- Appearance Section -->
<div v-show="activeSection === 'appearance'" class="space-y-6"> <div v-show="activeSection === 'appearance'" class="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>{{ t('settings.theme') }}</CardTitle> <CardTitle>{{ t('settings.theme') }}</CardTitle>
@@ -2756,13 +2756,13 @@ watch(isWindows, () => {
<CardContent> <CardContent>
<div class="grid grid-cols-3 gap-2 sm:max-w-md"> <div class="grid grid-cols-3 gap-2 sm:max-w-md">
<Button :variant="theme === 'light' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('light')"> <Button :variant="theme === 'light' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('light')">
<Sun class="h-4 w-4 mr-1.5" />{{ t('settings.lightMode') }} <Sun class="size-4 mr-1.5" />{{ t('settings.lightMode') }}
</Button> </Button>
<Button :variant="theme === 'dark' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('dark')"> <Button :variant="theme === 'dark' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('dark')">
<Moon class="h-4 w-4 mr-1.5" />{{ t('settings.darkMode') }} <Moon class="size-4 mr-1.5" />{{ t('settings.darkMode') }}
</Button> </Button>
<Button :variant="theme === 'system' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('system')"> <Button :variant="theme === 'system' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('system')">
<Monitor class="h-4 w-4 mr-1.5" />{{ t('settings.systemMode') }} <Monitor class="size-4 mr-1.5" />{{ t('settings.systemMode') }}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -2786,21 +2786,21 @@ watch(isWindows, () => {
<CardContent class="space-y-1"> <CardContent class="space-y-1">
<div class="flex items-center justify-between gap-4 px-3 py-3"> <div class="flex items-center justify-between gap-4 px-3 py-3">
<Label for="feature-web-terminal" class="flex min-w-0 items-center gap-2 font-normal"> <Label for="feature-web-terminal" class="flex min-w-0 items-center gap-2 font-normal">
<Terminal class="h-4 w-4 shrink-0 text-muted-foreground" /> <Terminal class="size-4 shrink-0 text-muted-foreground" />
<span class="truncate">{{ t('actionbar.webTerminal') }}</span> <span class="truncate">{{ t('actionbar.webTerminal') }}</span>
</Label> </Label>
<Switch id="feature-web-terminal" v-model="featureVisibility.webTerminal" /> <Switch id="feature-web-terminal" v-model="featureVisibility.webTerminal" />
</div> </div>
<div class="flex items-center justify-between gap-4 px-3 py-3"> <div class="flex items-center justify-between gap-4 px-3 py-3">
<Label for="feature-computer-use" class="flex min-w-0 items-center gap-2 font-normal"> <Label for="feature-computer-use" class="flex min-w-0 items-center gap-2 font-normal">
<Bot class="h-4 w-4 shrink-0 text-muted-foreground" /> <Bot class="size-4 shrink-0 text-muted-foreground" />
<span class="truncate">{{ t('settings.computerUseAgent') }}</span> <span class="truncate">{{ t('settings.computerUseAgent') }}</span>
</Label> </Label>
<Switch id="feature-computer-use" v-model="featureVisibility.computerUse" /> <Switch id="feature-computer-use" v-model="featureVisibility.computerUse" />
</div> </div>
<div class="flex items-center justify-between gap-4 px-3 py-3"> <div class="flex items-center justify-between gap-4 px-3 py-3">
<Label for="feature-paste-text" class="flex min-w-0 items-center gap-2 font-normal"> <Label for="feature-paste-text" class="flex min-w-0 items-center gap-2 font-normal">
<ClipboardPaste class="h-4 w-4 shrink-0 text-muted-foreground" /> <ClipboardPaste class="size-4 shrink-0 text-muted-foreground" />
<span class="truncate">{{ t('settings.pasteText') }}</span> <span class="truncate">{{ t('settings.pasteText') }}</span>
</Label> </Label>
<Switch id="feature-paste-text" v-model="featureVisibility.pasteText" /> <Switch id="feature-paste-text" v-model="featureVisibility.pasteText" />
@@ -2810,7 +2810,7 @@ watch(isWindows, () => {
</div> </div>
<!-- Account Section --> <!-- Account Section -->
<div v-show="activeSection === 'account'" class="space-y-6"> <div v-show="activeSection === 'account'" class="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>{{ t('settings.username') }}</CardTitle> <CardTitle>{{ t('settings.username') }}</CardTitle>
@@ -2826,12 +2826,12 @@ watch(isWindows, () => {
<Input id="account-username-password" v-model="usernamePassword" type="password" autocomplete="current-password" /> <Input id="account-username-password" v-model="usernamePassword" type="password" autocomplete="current-password" />
</div> </div>
<p v-if="usernameError" class="text-xs text-destructive">{{ usernameError }}</p> <p v-if="usernameError" class="text-xs text-destructive">{{ usernameError }}</p>
<p v-else-if="usernameSaved" class="flex items-center gap-1.5 text-xs text-success"><Check class="h-3.5 w-3.5" />{{ t('common.success') }}</p> <p v-else-if="usernameSaved" class="flex items-center gap-1.5 text-xs text-success"><Check class="size-3.5" />{{ t('common.success') }}</p>
</CardContent> </CardContent>
<CardFooter class="border-t pt-4 justify-end"> <CardFooter class="border-t pt-4 justify-end">
<Button @click="changeUsername" :disabled="usernameSaving"> <Button @click="changeUsername" :disabled="usernameSaving">
<Loader2 v-if="usernameSaving" class="h-4 w-4 mr-2 animate-spin" /> <Loader2 v-if="usernameSaving" class="size-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" /> <Save v-else class="size-4 mr-2" />
{{ t('common.save') }} {{ t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -2856,12 +2856,12 @@ watch(isWindows, () => {
<Input id="account-confirm-password" v-model="confirmPassword" type="password" autocomplete="new-password" /> <Input id="account-confirm-password" v-model="confirmPassword" type="password" autocomplete="new-password" />
</div> </div>
<p v-if="passwordError" class="text-xs text-destructive">{{ passwordError }}</p> <p v-if="passwordError" class="text-xs text-destructive">{{ passwordError }}</p>
<p v-else-if="passwordSaved" class="flex items-center gap-1.5 text-xs text-success"><Check class="h-3.5 w-3.5" />{{ t('common.success') }}</p> <p v-else-if="passwordSaved" class="flex items-center gap-1.5 text-xs text-success"><Check class="size-3.5" />{{ t('common.success') }}</p>
</CardContent> </CardContent>
<CardFooter class="border-t pt-4 justify-end"> <CardFooter class="border-t pt-4 justify-end">
<Button @click="changePassword" :disabled="passwordSaving"> <Button @click="changePassword" :disabled="passwordSaving">
<Loader2 v-if="passwordSaving" class="h-4 w-4 mr-2 animate-spin" /> <Loader2 v-if="passwordSaving" class="size-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" /> <Save v-else class="size-4 mr-2" />
{{ t('common.save') }} {{ t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -2885,8 +2885,8 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter class="border-t pt-4 justify-end"> <CardFooter class="border-t pt-4 justify-end">
<Button @click="saveAuthConfig" :disabled="authConfigLoading"> <Button @click="saveAuthConfig" :disabled="authConfigLoading">
<Loader2 v-if="authConfigLoading" class="h-4 w-4 mr-2 animate-spin" /> <Loader2 v-if="authConfigLoading" class="size-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" /> <Save v-else class="size-4 mr-2" />
{{ t('common.save') }} {{ t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -2894,7 +2894,7 @@ watch(isWindows, () => {
</div> </div>
<!-- Video Section --> <!-- Video Section -->
<div v-show="activeSection === 'video'" class="space-y-6"> <div v-show="activeSection === 'video'" class="space-y-4">
<!-- Video Device Settings --> <!-- Video Device Settings -->
<Card> <Card>
<CardHeader class="flex flex-row items-start justify-between space-y-0"> <CardHeader class="flex flex-row items-start justify-between space-y-0">
@@ -2902,8 +2902,8 @@ watch(isWindows, () => {
<CardTitle>{{ t('settings.videoSettings') }}</CardTitle> <CardTitle>{{ t('settings.videoSettings') }}</CardTitle>
<CardDescription>{{ t('settings.videoSettingsDesc') }}</CardDescription> <CardDescription>{{ t('settings.videoSettingsDesc') }}</CardDescription>
</div> </div>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadDevices"> <Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadDevices">
<RefreshCw class="h-4 w-4" /> <RefreshCw class="size-4" />
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent class="space-y-4"> <CardContent class="space-y-4">
@@ -3023,8 +3023,8 @@ watch(isWindows, () => {
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')" :aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords" @click="showPasswords = !showPasswords"
> >
<Eye v-if="!showPasswords" class="h-4 w-4" /> <Eye v-if="!showPasswords" class="size-4" />
<EyeOff v-else class="h-4 w-4" /> <EyeOff v-else class="size-4" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -3034,15 +3034,15 @@ watch(isWindows, () => {
</div> </div>
<!-- HID Section --> <!-- HID Section -->
<div v-show="activeSection === 'hid'" class="space-y-6"> <div v-show="activeSection === 'hid'" class="space-y-4">
<Card> <Card>
<CardHeader class="flex flex-row items-start justify-between space-y-0"> <CardHeader class="flex flex-row items-start justify-between space-y-0">
<div class="space-y-1.5"> <div class="space-y-1.5">
<CardTitle>{{ t('settings.hidSettings') }}</CardTitle> <CardTitle>{{ t('settings.hidSettings') }}</CardTitle>
<CardDescription>{{ t('settings.hidSettingsDesc') }}</CardDescription> <CardDescription>{{ t('settings.hidSettingsDesc') }}</CardDescription>
</div> </div>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadDevices"> <Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadDevices">
<RefreshCw class="h-4 w-4" /> <RefreshCw class="size-4" />
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent class="space-y-4"> <CardContent class="space-y-4">
@@ -3090,13 +3090,13 @@ watch(isWindows, () => {
<h4 class="text-sm font-medium">{{ t('settings.ch9329Descriptor') }}</h4> <h4 class="text-sm font-medium">{{ t('settings.ch9329Descriptor') }}</h4>
<p class="text-sm text-muted-foreground">{{ t('settings.ch9329DescriptorDesc') }}</p> <p class="text-sm text-muted-foreground">{{ t('settings.ch9329DescriptorDesc') }}</p>
</div> </div>
<Button variant="ghost" size="icon" class="h-8 w-8 shrink-0" :aria-label="t('common.refresh')" :disabled="ch9329DescriptorLoading" @click="loadCh9329Descriptor"> <Button variant="ghost" size="icon-sm" class="shrink-0" :aria-label="t('common.refresh')" :disabled="ch9329DescriptorLoading" @click="loadCh9329Descriptor">
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': ch9329DescriptorLoading }" /> <RefreshCw class="size-4" :class="{ 'animate-spin': ch9329DescriptorLoading }" />
</Button> </Button>
</div> </div>
</div> </div>
<p v-if="ch9329DescriptorLoading" class="text-sm text-muted-foreground flex items-center gap-2"> <p v-if="ch9329DescriptorLoading" class="text-sm text-muted-foreground flex items-center gap-2">
<Loader2 class="h-4 w-4 animate-spin" /> <Loader2 class="size-4 animate-spin" />
{{ t('settings.ch9329DescriptorLoading') }} {{ t('settings.ch9329DescriptorLoading') }}
</p> </p>
<p v-else-if="ch9329DescriptorError" class="text-sm text-destructive"> <p v-else-if="ch9329DescriptorError" class="text-sm text-destructive">
@@ -3355,7 +3355,7 @@ watch(isWindows, () => {
</div> </div>
<!-- Environment Section --> <!-- Environment Section -->
<div v-show="activeSection === 'environment'" class="space-y-4 max-w-3xl"> <div v-show="activeSection === 'environment'" class="space-y-4">
<Card v-if="systemStore.deviceInfo"> <Card v-if="systemStore.deviceInfo">
<CardHeader> <CardHeader>
<CardTitle>{{ t('settings.deviceInfo') }}</CardTitle> <CardTitle>{{ t('settings.deviceInfo') }}</CardTitle>
@@ -3408,7 +3408,7 @@ watch(isWindows, () => {
]" ]"
@click="onRunOtgSelfCheckClick" @click="onRunOtgSelfCheckClick"
> >
<RefreshCw class="h-4 w-4 mr-2" :class="{ 'animate-spin': otgSelfCheckLoading }" /> <RefreshCw class="size-4 mr-2" :class="{ 'animate-spin': otgSelfCheckLoading }" />
{{ t('settings.otgSelfCheck.run') }} {{ t('settings.otgSelfCheck.run') }}
</Button> </Button>
</CardHeader> </CardHeader>
@@ -3445,7 +3445,7 @@ watch(isWindows, () => {
> >
<summary class="list-none cursor-pointer px-4 py-3 flex items-center justify-between gap-3 hover:bg-muted/40"> <summary class="list-none cursor-pointer px-4 py-3 flex items-center justify-between gap-3 hover:bg-muted/40">
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<span class="inline-block h-2 w-2 rounded-full shrink-0" :class="otgGroupStatusClass(group.status)" /> <span class="inline-block size-2 rounded-full shrink-0" :class="otgGroupStatusClass(group.status)" />
<span class="text-sm font-medium truncate leading-6">{{ t(group.titleKey) }}</span> <span class="text-sm font-medium truncate leading-6">{{ t(group.titleKey) }}</span>
</div> </div>
<div class="flex items-center gap-2 shrink-0"> <div class="flex items-center gap-2 shrink-0">
@@ -3461,7 +3461,7 @@ watch(isWindows, () => {
class="px-4 py-3 border-b last:border-b-0" class="px-4 py-3 border-b last:border-b-0"
> >
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="inline-block h-2 w-2 rounded-full mt-1.5 shrink-0" :class="otgCheckLevelClass(item.level)" /> <span class="inline-block size-2 rounded-full mt-1.5 shrink-0" :class="otgCheckLevelClass(item.level)" />
<div class="min-w-0 space-y-1"> <div class="min-w-0 space-y-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p class="text-sm leading-5">{{ otgCheckMessage(item) }}</p> <p class="text-sm leading-5">{{ otgCheckMessage(item) }}</p>
@@ -3503,7 +3503,7 @@ watch(isWindows, () => {
]" ]"
@click="onRunVideoEncoderSelfCheckClick" @click="onRunVideoEncoderSelfCheckClick"
> >
<RefreshCw class="h-4 w-4 mr-2" :class="{ 'animate-spin': videoEncoderSelfCheckLoading }" /> <RefreshCw class="size-4 mr-2" :class="{ 'animate-spin': videoEncoderSelfCheckLoading }" />
{{ t('settings.encoderSelfCheck.run') }} {{ t('settings.encoderSelfCheck.run') }}
</Button> </Button>
</CardHeader> </CardHeader>
@@ -3569,7 +3569,7 @@ watch(isWindows, () => {
</div> </div>
<div v-show="activeSection === 'other'" class="space-y-6"> <div v-show="activeSection === 'other'" class="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
@@ -3587,8 +3587,8 @@ watch(isWindows, () => {
</CardHeader> </CardHeader>
<CardContent class="space-y-4"> <CardContent class="space-y-4">
<div class="flex items-center gap-2 text-sm"> <div class="flex items-center gap-2 text-sm">
<Loader2 v-if="watchdogLoading" class="h-4 w-4 animate-spin text-muted-foreground" /> <Loader2 v-if="watchdogLoading" class="size-4 animate-spin text-muted-foreground" />
<span v-else class="h-2.5 w-2.5 rounded-full" :class="watchdogStatusClass" /> <span v-else class="size-2.5 rounded-full" :class="watchdogStatusClass" />
<span class="font-medium"> <span class="font-medium">
{{ watchdogLoading {{ watchdogLoading
? t('common.loading') ? t('common.loading')
@@ -3617,7 +3617,7 @@ watch(isWindows, () => {
:disabled="usbDevicesLoading" :disabled="usbDevicesLoading"
@click="fetchUsbDevices()" @click="fetchUsbDevices()"
> >
<RefreshCw class="h-4 w-4 mr-2" :class="{ 'animate-spin': usbDevicesLoading }" /> <RefreshCw class="size-4 mr-2" :class="{ 'animate-spin': usbDevicesLoading }" />
{{ t('settings.usbDevices.refresh') }} {{ t('settings.usbDevices.refresh') }}
</Button> </Button>
</CardHeader> </CardHeader>
@@ -3657,7 +3657,7 @@ watch(isWindows, () => {
v-if="dev.authorized != null" v-if="dev.authorized != null"
variant="outline" variant="outline"
size="sm" size="sm"
class="h-7 text-xs" class="text-xs"
:disabled="usbResetLoading" :disabled="usbResetLoading"
@click="usbResetTarget = dev" @click="usbResetTarget = dev"
> >
@@ -3701,14 +3701,14 @@ watch(isWindows, () => {
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
<div v-show="activeSection === 'network'" class="space-y-6"> <div v-show="activeSection === 'network'" class="space-y-4">
<!-- Auto-restart: restarting progress --> <!-- Auto-restart: restarting progress -->
<div <div
v-if="autoRestarting" v-if="autoRestarting"
class="flex items-center gap-3 rounded-lg border bg-card px-4 py-3 text-sm shadow-sm" class="flex items-center gap-3 rounded-lg border bg-card px-4 py-3 text-sm shadow-sm"
> >
<RefreshCw class="h-4 w-4 animate-spin text-primary shrink-0" /> <RefreshCw class="size-4 animate-spin text-primary shrink-0" />
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="font-medium">{{ t('settings.autoRestarting') }}</p> <p class="font-medium">{{ t('settings.autoRestarting') }}</p>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
@@ -3739,7 +3739,7 @@ watch(isWindows, () => {
<AlertDescription class="flex items-center justify-between gap-3"> <AlertDescription class="flex items-center justify-between gap-3">
<span>{{ t('settings.autoRestartFailed') }}</span> <span>{{ t('settings.autoRestartFailed') }}</span>
<Button variant="outline" size="sm" class="text-foreground" @click="triggerAutoRestart"> <Button variant="outline" size="sm" class="text-foreground" @click="triggerAutoRestart">
<RefreshCw class="h-3 w-3 mr-1" /> <RefreshCw class="size-3 mr-1" />
{{ t('common.retry') }} {{ t('common.retry') }}
</Button> </Button>
</AlertDescription> </AlertDescription>
@@ -3807,33 +3807,33 @@ watch(isWindows, () => {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<code class="font-mono text-xs sm:text-sm break-all flex-1 min-w-0">{{ previewAccessUrl }}</code> <code class="font-mono text-xs sm:text-sm break-all flex-1 min-w-0">{{ previewAccessUrl }}</code>
<Button <Button
variant="ghost" size="icon" class="h-7 w-7 shrink-0" variant="ghost" size="icon-sm" class="shrink-0"
:title="t('settings.copyUrl')" :title="t('settings.copyUrl')"
:aria-label="t('settings.copyUrl')" :aria-label="t('settings.copyUrl')"
@click="copyPreviewUrl" @click="copyPreviewUrl"
> >
<Check v-if="previewUrlCopied" class="h-3.5 w-3.5 text-success" /> <Check v-if="previewUrlCopied" class="size-3.5 text-success" />
<Copy v-else class="h-3.5 w-3.5" /> <Copy v-else class="size-3.5" />
</Button> </Button>
<Button <Button
variant="ghost" size="icon" class="h-7 w-7 shrink-0" variant="ghost" size="icon-sm" class="shrink-0"
:title="t('settings.openInBrowser')" :title="t('settings.openInBrowser')"
:aria-label="t('settings.openInBrowser')" :aria-label="t('settings.openInBrowser')"
@click="openPreviewUrl" @click="openPreviewUrl"
> >
<ExternalLink class="h-3.5 w-3.5" /> <ExternalLink class="size-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
</CardContent> </CardContent>
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4"> <CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
<p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground"> <p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<AlertTriangle class="h-3.5 w-3.5 shrink-0 text-warning" /> <AlertTriangle class="size-3.5 shrink-0 text-warning" />
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span> <span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
</p> </p>
<Button @click="saveWebServerConfig" :disabled="webServerLoading || autoRestarting"> <Button @click="saveWebServerConfig" :disabled="webServerLoading || autoRestarting">
<RefreshCw v-if="autoRestarting" class="h-4 w-4 mr-2 animate-spin" /> <RefreshCw v-if="autoRestarting" class="size-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" /> <Save v-else class="size-4 mr-2" />
{{ autoRestarting ? t('settings.restarting') : t('common.save') }} {{ autoRestarting ? t('settings.restarting') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -3891,11 +3891,11 @@ watch(isWindows, () => {
<div v-for="(_, i) in bindAddressList" :key="`bind-${i}`" class="flex gap-2"> <div v-for="(_, i) in bindAddressList" :key="`bind-${i}`" class="flex gap-2">
<Input v-model="bindAddressList[i]" placeholder="192.168.1.10" /> <Input v-model="bindAddressList[i]" placeholder="192.168.1.10" />
<Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeBindAddress(i)"> <Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeBindAddress(i)">
<Trash2 class="h-4 w-4" /> <Trash2 class="size-4" />
</Button> </Button>
</div> </div>
<Button variant="outline" size="sm" @click="addBindAddress"> <Button variant="outline" size="sm" @click="addBindAddress">
<Plus class="h-4 w-4 mr-1" /> <Plus class="size-4 mr-1" />
{{ t('settings.addBindAddress') }} {{ t('settings.addBindAddress') }}
</Button> </Button>
<p v-if="bindAddressError" class="text-xs text-destructive">{{ bindAddressError }}</p> <p v-if="bindAddressError" class="text-xs text-destructive">{{ bindAddressError }}</p>
@@ -3911,12 +3911,12 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4"> <CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
<p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground"> <p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<AlertTriangle class="h-3.5 w-3.5 shrink-0 text-warning" /> <AlertTriangle class="size-3.5 shrink-0 text-warning" />
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span> <span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
</p> </p>
<Button @click="saveWebServerConfig" :disabled="webServerLoading || !!bindAddressError || autoRestarting"> <Button @click="saveWebServerConfig" :disabled="webServerLoading || !!bindAddressError || autoRestarting">
<RefreshCw v-if="autoRestarting" class="h-4 w-4 mr-2 animate-spin" /> <RefreshCw v-if="autoRestarting" class="size-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" /> <Save v-else class="size-4 mr-2" />
{{ autoRestarting ? t('settings.restarting') : t('common.save') }} {{ autoRestarting ? t('settings.restarting') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -3942,12 +3942,12 @@ watch(isWindows, () => {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
class="text-destructive hover:text-destructive h-7 text-xs" class="text-destructive hover:text-destructive text-xs"
:disabled="certClearing || autoRestarting" :disabled="certClearing || autoRestarting"
@click="clearCertificate" @click="clearCertificate"
> >
<RefreshCw v-if="certClearing || autoRestarting" class="h-3 w-3 mr-1 animate-spin" /> <RefreshCw v-if="certClearing || autoRestarting" class="size-3 mr-1 animate-spin" />
<Trash2 v-else class="h-3 w-3 mr-1" /> <Trash2 v-else class="size-3 mr-1" />
{{ autoRestarting ? t('settings.restarting') : t('settings.sslCertClear') }} {{ autoRestarting ? t('settings.restarting') : t('settings.sslCertClear') }}
</Button> </Button>
</AlertDescription> </AlertDescription>
@@ -3980,15 +3980,15 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4"> <CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
<p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground"> <p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<AlertTriangle class="h-3.5 w-3.5 shrink-0 text-warning" /> <AlertTriangle class="size-3.5 shrink-0 text-warning" />
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span> <span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
</p> </p>
<Button <Button
:disabled="certSaving || autoRestarting || !sslCertPem.trim() || !sslKeyPem.trim()" :disabled="certSaving || autoRestarting || !sslCertPem.trim() || !sslKeyPem.trim()"
@click="saveCertificate" @click="saveCertificate"
> >
<RefreshCw v-if="certSaving || autoRestarting" class="h-4 w-4 mr-2 animate-spin" /> <RefreshCw v-if="certSaving || autoRestarting" class="size-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" /> <Save v-else class="size-4 mr-2" />
{{ autoRestarting ? t('settings.restarting') : t('settings.sslCertSave') }} {{ autoRestarting ? t('settings.restarting') : t('settings.sslCertSave') }}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -3996,7 +3996,7 @@ watch(isWindows, () => {
</div> </div>
<!-- ATX Section --> <!-- ATX Section -->
<div v-show="activeSection === 'atx'" class="space-y-6"> <div v-show="activeSection === 'atx'" class="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>{{ t('settings.atxPowerManagement') }}</CardTitle> <CardTitle>{{ t('settings.atxPowerManagement') }}</CardTitle>
@@ -4012,8 +4012,8 @@ watch(isWindows, () => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<Button variant="ghost" size="icon" class="h-9 w-9 shrink-0" :aria-label="t('common.refresh')" @click="loadAtxDevices"> <Button variant="ghost" size="icon-sm" class="shrink-0" :aria-label="t('common.refresh')" @click="loadAtxDevices">
<RefreshCw class="h-4 w-4" /> <RefreshCw class="size-4" />
</Button> </Button>
</div> </div>
@@ -4167,7 +4167,7 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter class="border-t pt-4 justify-end"> <CardFooter class="border-t pt-4 justify-end">
<Button :disabled="atxSaving" @click="saveAtxSettings"> <Button :disabled="atxSaving" @click="saveAtxSettings">
<Loader2 v-if="atxSaving" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="atxSaved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ atxSaving ? t('actionbar.applying') : atxSaved ? t('common.success') : t('common.save') }} <Loader2 v-if="atxSaving" class="size-4 mr-2 animate-spin" /><Check v-else-if="atxSaved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ atxSaving ? t('actionbar.applying') : atxSaved ? t('common.success') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
@@ -4190,14 +4190,14 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter class="border-t pt-4 justify-end"> <CardFooter class="border-t pt-4 justify-end">
<Button :disabled="wolSaving" @click="saveWolSettings"> <Button :disabled="wolSaving" @click="saveWolSettings">
<Loader2 v-if="wolSaving" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="wolSaved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ wolSaving ? t('actionbar.applying') : wolSaved ? t('common.success') : t('common.save') }} <Loader2 v-if="wolSaving" class="size-4 mr-2 animate-spin" /><Check v-else-if="wolSaved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ wolSaving ? t('actionbar.applying') : wolSaved ? t('common.success') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
<!-- ttyd Section --> <!-- ttyd Section -->
<div v-show="activeSection === 'ext-ttyd'" class="space-y-6"> <div v-show="activeSection === 'ext-ttyd'" class="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -4228,7 +4228,7 @@ watch(isWindows, () => {
variant="default" variant="default"
@click="openTerminal" @click="openTerminal"
> >
<Terminal class="h-4 w-4 mr-1" /> <Terminal class="size-4 mr-1" />
{{ t('extensions.ttyd.open') }} {{ t('extensions.ttyd.open') }}
</Button> </Button>
<Button <Button
@@ -4237,7 +4237,7 @@ watch(isWindows, () => {
@click="startExtension('ttyd')" @click="startExtension('ttyd')"
:disabled="extensionsLoading" :disabled="extensionsLoading"
> >
<Play class="h-4 w-4 mr-1" /> <Play class="size-4 mr-1" />
{{ t('extensions.start') }} {{ t('extensions.start') }}
</Button> </Button>
<Button <Button
@@ -4247,7 +4247,7 @@ watch(isWindows, () => {
@click="stopExtension('ttyd')" @click="stopExtension('ttyd')"
:disabled="extensionsLoading" :disabled="extensionsLoading"
> >
<Square class="h-4 w-4 mr-1" /> <Square class="size-4 mr-1" />
{{ t('extensions.stop') }} {{ t('extensions.stop') }}
</Button> </Button>
</div> </div>
@@ -4267,11 +4267,11 @@ watch(isWindows, () => {
<!-- Logs --> <!-- Logs -->
<div class="space-y-2"> <div class="space-y-2">
<Collapsible v-model:open="showLogs.ttyd" @update:open="open => open && refreshExtensionLogs('ttyd')"> <Collapsible v-model:open="showLogs.ttyd" @update:open="open => open && refreshExtensionLogs('ttyd')">
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['h-4 w-4 transition-transform', showLogs.ttyd ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger> <CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['size-4 transition-transform', showLogs.ttyd ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleContent class="space-y-2 pt-2"> <CollapsibleContent class="space-y-2 pt-2">
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.ttyd || []).join('\n') || t('extensions.noLogs') }}</pre> <pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.ttyd || []).join('\n') || t('extensions.noLogs') }}</pre>
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('ttyd')"> <Button variant="ghost" size="sm" @click="refreshExtensionLogs('ttyd')">
<RefreshCw class="h-3 w-3 mr-1" /> <RefreshCw class="size-3 mr-1" />
{{ t('common.refresh') }} {{ t('common.refresh') }}
</Button> </Button>
</CollapsibleContent> </CollapsibleContent>
@@ -4281,14 +4281,14 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter v-if="extensions?.ttyd?.available" class="border-t pt-4 justify-end"> <CardFooter v-if="extensions?.ttyd?.available" class="border-t pt-4 justify-end">
<Button :disabled="loading || isExtRunning(extensions?.ttyd?.status)" @click="saveExtensionConfig('ttyd')"> <Button :disabled="loading || isExtRunning(extensions?.ttyd?.status)" @click="saveExtensionConfig('ttyd')">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }} <Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
<!-- Remote Access Section --> <!-- Remote Access Section -->
<div v-show="activeSection === 'ext-remote-access'" class="space-y-6"> <div v-show="activeSection === 'ext-remote-access'" class="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -4319,7 +4319,7 @@ watch(isWindows, () => {
@click="startExtension('gostc')" @click="startExtension('gostc')"
:disabled="extensionsLoading || !!gostcValidationMessage" :disabled="extensionsLoading || !!gostcValidationMessage"
> >
<Play class="h-4 w-4 mr-1" /> <Play class="size-4 mr-1" />
{{ t('extensions.start') }} {{ t('extensions.start') }}
</Button> </Button>
<Button <Button
@@ -4329,7 +4329,7 @@ watch(isWindows, () => {
@click="stopExtension('gostc')" @click="stopExtension('gostc')"
:disabled="extensionsLoading" :disabled="extensionsLoading"
> >
<Square class="h-4 w-4 mr-1" /> <Square class="size-4 mr-1" />
{{ t('extensions.stop') }} {{ t('extensions.stop') }}
</Button> </Button>
</div> </div>
@@ -4365,11 +4365,11 @@ watch(isWindows, () => {
<!-- Logs --> <!-- Logs -->
<div class="space-y-2"> <div class="space-y-2">
<Collapsible v-model:open="showLogs.gostc" @update:open="open => open && refreshExtensionLogs('gostc')"> <Collapsible v-model:open="showLogs.gostc" @update:open="open => open && refreshExtensionLogs('gostc')">
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['h-4 w-4 transition-transform', showLogs.gostc ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger> <CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['size-4 transition-transform', showLogs.gostc ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleContent class="space-y-2 pt-2"> <CollapsibleContent class="space-y-2 pt-2">
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.gostc || []).join('\n') || t('extensions.noLogs') }}</pre> <pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.gostc || []).join('\n') || t('extensions.noLogs') }}</pre>
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('gostc')"> <Button variant="ghost" size="sm" @click="refreshExtensionLogs('gostc')">
<RefreshCw class="h-3 w-3 mr-1" /> <RefreshCw class="size-3 mr-1" />
{{ t('common.refresh') }} {{ t('common.refresh') }}
</Button> </Button>
</CollapsibleContent> </CollapsibleContent>
@@ -4379,7 +4379,7 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter v-if="extensions?.gostc?.available" class="border-t pt-4 justify-end"> <CardFooter v-if="extensions?.gostc?.available" class="border-t pt-4 justify-end">
<Button :disabled="loading || isExtRunning(extensions?.gostc?.status)" @click="saveExtensionConfig('gostc')"> <Button :disabled="loading || isExtRunning(extensions?.gostc?.status)" @click="saveExtensionConfig('gostc')">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }} <Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
@@ -4414,7 +4414,7 @@ watch(isWindows, () => {
@click="startExtension('easytier')" @click="startExtension('easytier')"
:disabled="extensionsLoading || !!easytierValidationMessage" :disabled="extensionsLoading || !!easytierValidationMessage"
> >
<Play class="h-4 w-4 mr-1" /> <Play class="size-4 mr-1" />
{{ t('extensions.start') }} {{ t('extensions.start') }}
</Button> </Button>
<Button <Button
@@ -4424,7 +4424,7 @@ watch(isWindows, () => {
@click="stopExtension('easytier')" @click="stopExtension('easytier')"
:disabled="extensionsLoading" :disabled="extensionsLoading"
> >
<Square class="h-4 w-4 mr-1" /> <Square class="size-4 mr-1" />
{{ t('extensions.stop') }} {{ t('extensions.stop') }}
</Button> </Button>
</div> </div>
@@ -4453,11 +4453,11 @@ watch(isWindows, () => {
<div v-for="(_, i) in extConfig.easytier.peer_urls" :key="i" class="flex gap-2"> <div v-for="(_, i) in extConfig.easytier.peer_urls" :key="i" class="flex gap-2">
<Input v-model="extConfig.easytier.peer_urls[i]" placeholder="tcp://1.2.3.4:11010" :disabled="isExtRunning(extensions?.easytier?.status)" /> <Input v-model="extConfig.easytier.peer_urls[i]" placeholder="tcp://1.2.3.4:11010" :disabled="isExtRunning(extensions?.easytier?.status)" />
<Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeEasytierPeer(i)" :disabled="isExtRunning(extensions?.easytier?.status)"> <Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeEasytierPeer(i)" :disabled="isExtRunning(extensions?.easytier?.status)">
<Trash2 class="h-4 w-4" /> <Trash2 class="size-4" />
</Button> </Button>
</div> </div>
<Button variant="outline" size="sm" @click="addEasytierPeer" :disabled="isExtRunning(extensions?.easytier?.status)"> <Button variant="outline" size="sm" @click="addEasytierPeer" :disabled="isExtRunning(extensions?.easytier?.status)">
<Plus class="h-4 w-4 mr-1" /> <Plus class="size-4 mr-1" />
{{ t('extensions.easytier.addPeer') }} {{ t('extensions.easytier.addPeer') }}
</Button> </Button>
</div> </div>
@@ -4472,11 +4472,11 @@ watch(isWindows, () => {
<!-- Logs --> <!-- Logs -->
<div class="space-y-2"> <div class="space-y-2">
<Collapsible v-model:open="showLogs.easytier" @update:open="open => open && refreshExtensionLogs('easytier')"> <Collapsible v-model:open="showLogs.easytier" @update:open="open => open && refreshExtensionLogs('easytier')">
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['h-4 w-4 transition-transform', showLogs.easytier ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger> <CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['size-4 transition-transform', showLogs.easytier ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleContent class="space-y-2 pt-2"> <CollapsibleContent class="space-y-2 pt-2">
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.easytier || []).join('\n') || t('extensions.noLogs') }}</pre> <pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.easytier || []).join('\n') || t('extensions.noLogs') }}</pre>
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('easytier')"> <Button variant="ghost" size="sm" @click="refreshExtensionLogs('easytier')">
<RefreshCw class="h-3 w-3 mr-1" /> <RefreshCw class="size-3 mr-1" />
{{ t('common.refresh') }} {{ t('common.refresh') }}
</Button> </Button>
</CollapsibleContent> </CollapsibleContent>
@@ -4486,7 +4486,7 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter v-if="extensions?.easytier?.available" class="border-t pt-4 justify-end"> <CardFooter v-if="extensions?.easytier?.available" class="border-t pt-4 justify-end">
<Button :disabled="loading || isExtRunning(extensions?.easytier?.status)" @click="saveExtensionConfig('easytier')"> <Button :disabled="loading || isExtRunning(extensions?.easytier?.status)" @click="saveExtensionConfig('easytier')">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }} <Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
@@ -4520,7 +4520,7 @@ watch(isWindows, () => {
@click="startExtension('frpc')" @click="startExtension('frpc')"
:disabled="extensionsLoading || !!frpcValidationMessage" :disabled="extensionsLoading || !!frpcValidationMessage"
> >
<Play class="h-4 w-4 mr-1" /> <Play class="size-4 mr-1" />
{{ t('extensions.start') }} {{ t('extensions.start') }}
</Button> </Button>
<Button <Button
@@ -4530,7 +4530,7 @@ watch(isWindows, () => {
@click="stopExtension('frpc')" @click="stopExtension('frpc')"
:disabled="extensionsLoading" :disabled="extensionsLoading"
> >
<Square class="h-4 w-4 mr-1" /> <Square class="size-4 mr-1" />
{{ t('extensions.stop') }} {{ t('extensions.stop') }}
</Button> </Button>
</div> </div>
@@ -4642,11 +4642,11 @@ watch(isWindows, () => {
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Collapsible v-model:open="showLogs.frpc" @update:open="open => open && refreshExtensionLogs('frpc')"> <Collapsible v-model:open="showLogs.frpc" @update:open="open => open && refreshExtensionLogs('frpc')">
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['h-4 w-4 transition-transform', showLogs.frpc ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger> <CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['size-4 transition-transform', showLogs.frpc ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleContent class="space-y-2 pt-2"> <CollapsibleContent class="space-y-2 pt-2">
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.frpc || []).join('\n') || t('extensions.noLogs') }}</pre> <pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.frpc || []).join('\n') || t('extensions.noLogs') }}</pre>
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('frpc')"> <Button variant="ghost" size="sm" @click="refreshExtensionLogs('frpc')">
<RefreshCw class="h-3 w-3 mr-1" /> <RefreshCw class="size-3 mr-1" />
{{ t('common.refresh') }} {{ t('common.refresh') }}
</Button> </Button>
</CollapsibleContent> </CollapsibleContent>
@@ -4656,14 +4656,14 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter v-if="extensions?.frpc?.available" class="border-t pt-4 justify-end"> <CardFooter v-if="extensions?.frpc?.available" class="border-t pt-4 justify-end">
<Button :disabled="loading || isExtRunning(extensions?.frpc?.status)" @click="saveExtensionConfig('frpc')"> <Button :disabled="loading || isExtRunning(extensions?.frpc?.status)" @click="saveExtensionConfig('frpc')">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }} <Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
<!-- RTSP Section --> <!-- RTSP Section -->
<div v-show="activeSection === 'third-party-access'" class="space-y-6"> <div v-show="activeSection === 'third-party-access'" class="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -4675,8 +4675,8 @@ watch(isWindows, () => {
<Badge :variant="rtspStatus?.service_status === 'running' ? 'default' : 'secondary'"> <Badge :variant="rtspStatus?.service_status === 'running' ? 'default' : 'secondary'">
{{ getRtspServiceStatusText(rtspStatus?.service_status) }} {{ getRtspServiceStatusText(rtspStatus?.service_status) }}
</Badge> </Badge>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadRtspConfig" :disabled="rtspLoading"> <Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadRtspConfig" :disabled="rtspLoading">
<RefreshCw :class="['h-4 w-4', rtspLoading ? 'animate-spin' : '']" /> <RefreshCw :class="['size-4', rtspLoading ? 'animate-spin' : '']" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -4694,7 +4694,7 @@ watch(isWindows, () => {
@click="startRtsp" @click="startRtsp"
:disabled="rtspLoading || rtspStatus?.service_status === 'starting'" :disabled="rtspLoading || rtspStatus?.service_status === 'starting'"
> >
<Play class="h-4 w-4 mr-1" /> <Play class="size-4 mr-1" />
{{ t('extensions.start') }} {{ t('extensions.start') }}
</Button> </Button>
<Button <Button
@@ -4704,7 +4704,7 @@ watch(isWindows, () => {
@click="stopRtsp" @click="stopRtsp"
:disabled="rtspLoading" :disabled="rtspLoading"
> >
<Square class="h-4 w-4 mr-1" /> <Square class="size-4 mr-1" />
{{ t('extensions.stop') }} {{ t('extensions.stop') }}
</Button> </Button>
</div> </div>
@@ -4767,8 +4767,8 @@ watch(isWindows, () => {
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')" :aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords" @click="showPasswords = !showPasswords"
> >
<Eye v-if="!showPasswords" class="h-4 w-4" /> <Eye v-if="!showPasswords" class="size-4" />
<EyeOff v-else class="h-4 w-4" /> <EyeOff v-else class="size-4" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -4782,14 +4782,14 @@ watch(isWindows, () => {
<code class="truncate font-mono">{{ rtspStreamUrl }}</code> <code class="truncate font-mono">{{ rtspStreamUrl }}</code>
</div> </div>
<Button class="shrink-0" :disabled="loading || rtspLoading" @click="saveRtspConfig"> <Button class="shrink-0" :disabled="loading || rtspLoading" @click="saveRtspConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }} <Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
<!-- VNC Section --> <!-- VNC Section -->
<div v-show="activeSection === 'third-party-access'" class="space-y-6"> <div v-show="activeSection === 'third-party-access'" class="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -4801,8 +4801,8 @@ watch(isWindows, () => {
<Badge :variant="vncStatus?.service_status === 'running' ? 'default' : 'secondary'"> <Badge :variant="vncStatus?.service_status === 'running' ? 'default' : 'secondary'">
{{ getVncServiceStatusText(vncStatus?.service_status) }} {{ getVncServiceStatusText(vncStatus?.service_status) }}
</Badge> </Badge>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadVncConfig" :disabled="vncLoading"> <Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadVncConfig" :disabled="vncLoading">
<RefreshCw :class="['h-4 w-4', vncLoading ? 'animate-spin' : '']" /> <RefreshCw :class="['size-4', vncLoading ? 'animate-spin' : '']" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -4824,7 +4824,7 @@ watch(isWindows, () => {
@click="startVnc" @click="startVnc"
:disabled="vncLoading || vncStatus?.service_status === 'starting'" :disabled="vncLoading || vncStatus?.service_status === 'starting'"
> >
<Play class="h-4 w-4 mr-1" /> <Play class="size-4 mr-1" />
{{ t('extensions.start') }} {{ t('extensions.start') }}
</Button> </Button>
<Button <Button
@@ -4834,7 +4834,7 @@ watch(isWindows, () => {
@click="stopVnc" @click="stopVnc"
:disabled="vncLoading" :disabled="vncLoading"
> >
<Square class="h-4 w-4 mr-1" /> <Square class="size-4 mr-1" />
{{ t('extensions.stop') }} {{ t('extensions.stop') }}
</Button> </Button>
</div> </div>
@@ -4889,8 +4889,8 @@ watch(isWindows, () => {
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')" :aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords" @click="showPasswords = !showPasswords"
> >
<Eye v-if="!showPasswords" class="h-4 w-4" /> <Eye v-if="!showPasswords" class="size-4" />
<EyeOff v-else class="h-4 w-4" /> <EyeOff v-else class="size-4" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -4904,14 +4904,14 @@ watch(isWindows, () => {
<code class="truncate font-mono">{{ vncStreamUrl }}</code> <code class="truncate font-mono">{{ vncStreamUrl }}</code>
</div> </div>
<Button class="shrink-0" :disabled="loading || vncLoading" @click="saveVncConfig"> <Button class="shrink-0" :disabled="loading || vncLoading" @click="saveVncConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }} <Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
<!-- RustDesk Section --> <!-- RustDesk Section -->
<div v-show="activeSection === 'third-party-access'" class="space-y-6"> <div v-show="activeSection === 'third-party-access'" class="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -4923,8 +4923,8 @@ watch(isWindows, () => {
<Badge :variant="rustdeskStatus?.service_status === 'running' ? 'default' : 'secondary'"> <Badge :variant="rustdeskStatus?.service_status === 'running' ? 'default' : 'secondary'">
{{ getRustdeskServiceStatusText(rustdeskStatus?.service_status) }} {{ getRustdeskServiceStatusText(rustdeskStatus?.service_status) }}
</Badge> </Badge>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadRustdeskConfig" :disabled="rustdeskLoading"> <Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadRustdeskConfig" :disabled="rustdeskLoading">
<RefreshCw :class="['h-4 w-4', rustdeskLoading ? 'animate-spin' : '']" /> <RefreshCw :class="['size-4', rustdeskLoading ? 'animate-spin' : '']" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -4948,7 +4948,7 @@ watch(isWindows, () => {
@click="startRustdesk" @click="startRustdesk"
:disabled="rustdeskLoading" :disabled="rustdeskLoading"
> >
<Play class="h-4 w-4 mr-1" /> <Play class="size-4 mr-1" />
{{ t('extensions.start') }} {{ t('extensions.start') }}
</Button> </Button>
<Button <Button
@@ -4958,7 +4958,7 @@ watch(isWindows, () => {
@click="stopRustdesk" @click="stopRustdesk"
:disabled="rustdeskLoading" :disabled="rustdeskLoading"
> >
<Square class="h-4 w-4 mr-1" /> <Square class="size-4 mr-1" />
{{ t('extensions.stop') }} {{ t('extensions.stop') }}
</Button> </Button>
</div> </div>
@@ -5022,8 +5022,8 @@ watch(isWindows, () => {
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')" :aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords" @click="showPasswords = !showPasswords"
> >
<Eye v-if="!showPasswords" class="h-4 w-4" /> <Eye v-if="!showPasswords" class="size-4" />
<EyeOff v-else class="h-4 w-4" /> <EyeOff v-else class="size-4" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -5043,16 +5043,16 @@ watch(isWindows, () => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8" class="size-8"
:aria-label="t('extensions.rustdesk.copyId')" :aria-label="t('extensions.rustdesk.copyId')"
@click="copyToClipboard(rustdeskConfig?.device_id || '', 'id')" @click="copyToClipboard(rustdeskConfig?.device_id || '', 'id')"
:disabled="!rustdeskConfig?.device_id" :disabled="!rustdeskConfig?.device_id"
> >
<Check v-if="rustdeskCopied === 'id'" class="h-4 w-4 text-success" /> <Check v-if="rustdeskCopied === 'id'" class="size-4 text-success" />
<Copy v-else class="h-4 w-4" /> <Copy v-else class="size-4" />
</Button> </Button>
<Button variant="outline" size="sm" @click="regenerateRustdeskId" :disabled="rustdeskLoading"> <Button variant="outline" size="sm" @click="regenerateRustdeskId" :disabled="rustdeskLoading">
<RefreshCw class="h-4 w-4 mr-1" /> <RefreshCw class="size-4 mr-1" />
{{ t('extensions.rustdesk.regenerateId') }} {{ t('extensions.rustdesk.regenerateId') }}
</Button> </Button>
</div> </div>
@@ -5066,16 +5066,16 @@ watch(isWindows, () => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8" class="size-8"
:aria-label="t('extensions.rustdesk.copyPassword')" :aria-label="t('extensions.rustdesk.copyPassword')"
@click="copyToClipboard(rustdeskPassword?.device_password || '', 'password')" @click="copyToClipboard(rustdeskPassword?.device_password || '', 'password')"
:disabled="!rustdeskPassword?.device_password" :disabled="!rustdeskPassword?.device_password"
> >
<Check v-if="rustdeskCopied === 'password'" class="h-4 w-4 text-success" /> <Check v-if="rustdeskCopied === 'password'" class="size-4 text-success" />
<Copy v-else class="h-4 w-4" /> <Copy v-else class="size-4" />
</Button> </Button>
<Button variant="outline" size="sm" @click="regenerateRustdeskPassword" :disabled="rustdeskLoading"> <Button variant="outline" size="sm" @click="regenerateRustdeskPassword" :disabled="rustdeskLoading">
<RefreshCw class="h-4 w-4 mr-1" /> <RefreshCw class="size-4 mr-1" />
{{ t('extensions.rustdesk.regeneratePassword') }} {{ t('extensions.rustdesk.regeneratePassword') }}
</Button> </Button>
</div> </div>
@@ -5094,20 +5094,20 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter class="border-t pt-4 justify-end"> <CardFooter class="border-t pt-4 justify-end">
<Button :disabled="loading || rustdeskLoading" @click="saveRustdeskConfig"> <Button :disabled="loading || rustdeskLoading" @click="saveRustdeskConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }} <Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
<!-- Redfish Section --> <!-- Redfish Section -->
<div v-show="activeSection === 'third-party-access'" class="space-y-6"> <div v-show="activeSection === 'third-party-access'" class="space-y-4">
<!-- Auto-restart: restarting progress --> <!-- Auto-restart: restarting progress -->
<div <div
v-if="autoRestarting" v-if="autoRestarting"
class="flex items-center gap-3 rounded-lg border bg-card px-4 py-3 text-sm shadow-sm" class="flex items-center gap-3 rounded-lg border bg-card px-4 py-3 text-sm shadow-sm"
> >
<RefreshCw class="h-4 w-4 animate-spin text-primary shrink-0" /> <RefreshCw class="size-4 animate-spin text-primary shrink-0" />
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="font-medium">{{ t('settings.autoRestarting') }}</p> <p class="font-medium">{{ t('settings.autoRestarting') }}</p>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
@@ -5138,7 +5138,7 @@ watch(isWindows, () => {
<AlertDescription class="flex items-center justify-between gap-3"> <AlertDescription class="flex items-center justify-between gap-3">
<span>{{ t('settings.autoRestartFailed') }}</span> <span>{{ t('settings.autoRestartFailed') }}</span>
<Button variant="outline" size="sm" class="text-foreground" @click="triggerAutoRestart"> <Button variant="outline" size="sm" class="text-foreground" @click="triggerAutoRestart">
<RefreshCw class="h-3 w-3 mr-1" /> <RefreshCw class="size-3 mr-1" />
{{ t('common.retry') }} {{ t('common.retry') }}
</Button> </Button>
</AlertDescription> </AlertDescription>
@@ -5161,12 +5161,12 @@ watch(isWindows, () => {
</CardContent> </CardContent>
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4"> <CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
<p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground"> <p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<AlertTriangle class="h-3.5 w-3.5 shrink-0 text-warning" /> <AlertTriangle class="size-3.5 shrink-0 text-warning" />
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span> <span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
</p> </p>
<Button @click="saveRedfishConfig" :disabled="redfishSaving || autoRestarting"> <Button @click="saveRedfishConfig" :disabled="redfishSaving || autoRestarting">
<RefreshCw v-if="autoRestarting || redfishSaving" class="h-4 w-4 mr-2 animate-spin" /> <RefreshCw v-if="autoRestarting || redfishSaving" class="size-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" /> <Save v-else class="size-4 mr-2" />
{{ autoRestarting ? t('settings.restarting') : t('common.save') }} {{ autoRestarting ? t('settings.restarting') : t('common.save') }}
</Button> </Button>
</CardFooter> </CardFooter>
@@ -5174,7 +5174,7 @@ watch(isWindows, () => {
</div> </div>
<!-- About Section --> <!-- About Section -->
<div v-show="activeSection === 'about'" class="space-y-6"> <div v-show="activeSection === 'about'" class="space-y-4">
<Card> <Card>
<CardHeader class="flex flex-row items-start justify-between space-y-0"> <CardHeader class="flex flex-row items-start justify-between space-y-0">
<div class="space-y-1.5"> <div class="space-y-1.5">
@@ -5184,12 +5184,12 @@ watch(isWindows, () => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class="h-8 w-8" class="size-8"
:aria-label="t('common.refresh')" :aria-label="t('common.refresh')"
:disabled="updateRunning || updateLoading" :disabled="updateRunning || updateLoading"
@click="loadUpdateOverview" @click="loadUpdateOverview"
> >
<RefreshCw :class="['h-4 w-4', (updateLoading || updateRunning) ? 'animate-spin' : '']" /> <RefreshCw :class="['size-4', (updateLoading || updateRunning) ? 'animate-spin' : '']" />
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent class="space-y-4"> <CardContent class="space-y-4">
@@ -5253,7 +5253,7 @@ watch(isWindows, () => {
:disabled="updateRunning || !updateOverview?.upgrade_available" :disabled="updateRunning || !updateOverview?.upgrade_available"
@click="startOnlineUpgrade" @click="startOnlineUpgrade"
> >
<RefreshCw class="h-4 w-4 mr-2" :class="updateRunning ? 'animate-spin' : ''" /> <RefreshCw class="size-4 mr-2" :class="updateRunning ? 'animate-spin' : ''" />
{{ t('settings.startUpgrade') }} {{ t('settings.startUpgrade') }}
</Button> </Button>
</div> </div>
@@ -5267,21 +5267,21 @@ watch(isWindows, () => {
<div v-if="['video', 'hid'].includes(activeSection)" class="sticky bottom-0 pt-3 sm:pt-4 pb-3 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t -mx-3 px-3 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8"> <div v-if="['video', 'hid'].includes(activeSection)" class="sticky bottom-0 pt-3 sm:pt-4 pb-3 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t -mx-3 px-3 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="flex items-center justify-between gap-2 sm:gap-3"> <div class="flex items-center justify-between gap-2 sm:gap-3">
<p v-if="activeSection === 'hid' && !isHidFunctionSelectionValid" class="flex min-w-0 items-center gap-1.5 text-xs text-warning"> <p v-if="activeSection === 'hid' && !isHidFunctionSelectionValid" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" /> <AlertTriangle class="size-3.5 shrink-0" />
<span class="truncate">{{ t('settings.otgFunctionMinWarning') }}</span> <span class="truncate">{{ t('settings.otgFunctionMinWarning') }}</span>
</p> </p>
<p v-else-if="activeSection === 'hid' && !isCh9329DescriptorValid" class="flex min-w-0 items-center gap-1.5 text-xs text-warning"> <p v-else-if="activeSection === 'hid' && !isCh9329DescriptorValid" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" /> <AlertTriangle class="size-3.5 shrink-0" />
<span class="truncate">{{ t('settings.ch9329StringLengthWarning') }}</span> <span class="truncate">{{ t('settings.ch9329StringLengthWarning') }}</span>
</p> </p>
<p v-else-if="activeSection === 'hid' && config.hid_backend === 'ch9329' && ch9329DescriptorLoading" class="flex min-w-0 items-center gap-1.5 text-xs text-warning"> <p v-else-if="activeSection === 'hid' && config.hid_backend === 'ch9329' && ch9329DescriptorLoading" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" /> <AlertTriangle class="size-3.5 shrink-0" />
<span class="truncate">{{ t('settings.ch9329DescriptorLoading') }}</span> <span class="truncate">{{ t('settings.ch9329DescriptorLoading') }}</span>
</p> </p>
<p v-if="saveError" class="text-xs text-destructive">{{ saveError }}</p> <p v-if="saveError" class="text-xs text-destructive">{{ saveError }}</p>
<p v-else class="text-xs text-muted-foreground hidden sm:block">{{ t('settings.unsavedChangesHint') }}</p> <p v-else class="text-xs text-muted-foreground hidden sm:block">{{ t('settings.unsavedChangesHint') }}</p>
<Button class="shrink-0 ml-auto" :disabled="loading || (activeSection === 'hid' && !isHidSettingsValid)" @click="saveConfig"> <Button class="shrink-0 ml-auto" :disabled="loading || (activeSection === 'hid' && !isHidSettingsValid)" @click="saveConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }} <Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button> </Button>
</div> </div>
</div> </div>
@@ -5307,7 +5307,7 @@ watch(isWindows, () => {
{{ t('common.later') }} {{ t('common.later') }}
</Button> </Button>
<Button @click="restartServer" :disabled="restarting"> <Button @click="restartServer" :disabled="restarting">
<RefreshCw v-if="restarting" class="h-4 w-4 mr-2 animate-spin" /> <RefreshCw v-if="restarting" class="size-4 mr-2 animate-spin" />
{{ restarting ? t('settings.restarting') : t('common.restartNow') }} {{ restarting ? t('settings.restarting') : t('common.restartNow') }}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -535,18 +535,18 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
</script> </script>
<template> <template>
<div class="min-h-screen min-h-dvh flex items-start sm:items-center justify-center bg-background px-4 py-6 sm:py-10"> <div class="min-h-screen min-h-dvh flex items-start sm:items-center justify-center dot-grid-bg px-4 py-6 sm:py-10">
<Card class="w-full max-w-lg relative"> <Card class="w-full max-w-lg relative">
<!-- Language Switcher --> <!-- Language Switcher -->
<div class="absolute top-4 right-4"> <div class="absolute top-4 right-4">
<LanguageToggleButton /> <LanguageToggleButton />
</div> </div>
<CardHeader class="text-center space-y-2 pt-10 sm:pt-12"> <CardHeader class="text-center space-y-2 pt-8">
<div class="mx-auto flex justify-center"> <div class="mx-auto flex justify-center">
<BrandMark size="xl" /> <BrandMark size="lg" />
</div> </div>
<CardTitle class="text-xl sm:text-2xl">{{ t('setup.welcome') }}</CardTitle> <CardTitle class="text-xl">{{ t('setup.welcome') }}</CardTitle>
<CardDescription>{{ t('setup.description') }}</CardDescription> <CardDescription>{{ t('setup.description') }}</CardDescription>
</CardHeader> </CardHeader>
@@ -807,7 +807,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
{{ t('setup.advancedEncoder') }} ({{ t('common.optional') }}) {{ t('setup.advancedEncoder') }} ({{ t('common.optional') }})
</span> </span>
<ChevronRight <ChevronRight
class="h-4 w-4 transition-transform duration-200" class="size-4 transition-transform duration-200"
:class="{ 'rotate-90': showAdvancedEncoder }" :class="{ 'rotate-90': showAdvancedEncoder }"
/> />
</Button> </Button>