update: 更新前端依赖;修改前端样式

This commit is contained in:
mofeng-git
2026-07-17 01:33:44 +08:00
parent aca847e76c
commit 57d1597264
146 changed files with 3790 additions and 2982 deletions

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"style": "new-york-v4",
"typescript": true,
"tailwind": {
"config": "tailwind.config.js",

1525
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,36 +9,35 @@
"preview": "vite preview"
},
"dependencies": {
"@vueuse/core": "^14.1.0",
"@vueuse/core": "^14.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.556.0",
"opus-decoder": "^0.7.11",
"pinia": "^3.0.4",
"radix-vue": "^1.9.17",
"reka-ui": "^2.6.1",
"simple-keyboard": "^3.8.93",
"tailwind-merge": "^3.4.0",
"reka-ui": "^2.10.1",
"simple-keyboard": "^3.8.163",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"uplot": "^1.6.32",
"vue": "^3.5.24",
"vue": "^3.5.40",
"vue-i18n": "^9.14.5",
"vue-router": "^4.6.3",
"vue-router": "^4.6.4",
"vue-sonner": "^2.0.9"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.20",
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^24.13.3",
"@vitejs/plugin-vue": "^6.0.8",
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.10",
"tailwindcss": "^4.1.17",
"autoprefixer": "^10.5.4",
"postcss": "^8.5.19",
"tailwindcss": "^4.3.3",
"typescript": "~5.9.3",
"vite": "^7.3.2",
"vue-tsc": "^3.1.4"
"vite": "^7.3.6",
"vue-tsc": "^3.3.7"
},
"overrides": {
"defu": "^6.1.5",

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import 'vue-sonner/style.css'
import '@/sonner-overrides.css'
import { computed, KeepAlive, onMounted, watch } from 'vue'
import { computed, KeepAlive, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterView, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useSystemStore } from '@/stores/system'
import { Toaster } from '@/components/ui/sonner'
import { useTheme } from '@/composables/useTheme'
const { t } = useI18n()
@@ -22,17 +23,9 @@ const toasterToastOptions = computed(() => ({
const router = useRouter()
const authStore = useAuthStore()
const systemStore = useSystemStore()
function initTheme() {
const stored = localStorage.getItem('theme')
if (stored === 'dark' || (!stored && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
}
}
const { isDark } = useTheme()
onMounted(async () => {
initTheme()
try {
await authStore.checkSetupStatus()
if (authStore.needsSetup) {
@@ -50,16 +43,6 @@ onMounted(async () => {
} catch {
}
})
watch(
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
(dark) => {
const stored = localStorage.getItem('theme')
if (!stored) {
document.documentElement.classList.toggle('dark', dark)
}
}
)
</script>
<template>
@@ -75,7 +58,7 @@ watch(
expand
position="top-center"
close-button-position="top-right"
theme="system"
:theme="isDark ? 'dark' : 'light'"
:duration="4000"
:gap="14"
:offset="{ top: '1rem', right: '1rem', left: '1rem', bottom: '1rem' }"

View File

@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useSystemStore } from '@/stores/system'
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import {
PopoverContent,
PopoverTrigger,
@@ -276,10 +277,10 @@ const hasRightOverflow = computed(() => {
</script>
<template>
<div class="w-full border-b border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="w-full border-b bg-background">
<div ref="barRef" class="flex items-center px-2 sm:px-4 py-1 sm:py-1.5">
<!-- Left side buttons -->
<div class="left-buttons flex items-center gap-0.5 sm:gap-1.5 flex-1 min-w-0 overflow-hidden">
<ButtonGroup class="left-buttons flex-1 min-w-0 overflow-hidden">
<!-- Video Config - Always visible -->
<VideoConfigPopover
v-model:open="videoPopoverOpen"
@@ -349,10 +350,10 @@ const hasRightOverflow = computed(() => {
</PopoverContent>
</Popover>
</div>
</div>
</ButtonGroup>
<!-- Right side buttons -->
<div class="flex items-center gap-0.5 sm:gap-1.5 shrink-0 ml-1 sm:ml-2">
<ButtonGroup class="shrink-0 ml-1 sm:ml-2">
<!-- Connection Stats - Adaptive -->
<div v-if="isVisible('stats')">
<TooltipProvider>
@@ -393,25 +394,6 @@ const hasRightOverflow = computed(() => {
</TooltipProvider>
</div>
<!-- Settings - Adaptive -->
<div v-if="isVisible('settings')">
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="router.push('/settings')">
<Settings class="h-4 w-4" />
<span v-if="visibleSet.get('settings') === 'label'">{{ t('actionbar.settings') }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('actionbar.settingsTip') }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div v-if="isVisible('stats') || isVisible('terminal') || isVisible('settings')" class="h-5 w-px bg-slate-200 dark:bg-slate-700" />
<!-- Computer Use - Optional -->
<TooltipProvider v-if="props.showComputerUse !== false">
<Tooltip>
@@ -432,6 +414,29 @@ const hasRightOverflow = computed(() => {
</Tooltip>
</TooltipProvider>
<!-- Settings - Adaptive -->
<div v-if="isVisible('settings')">
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="router.push('/settings')">
<Settings class="h-4 w-4" />
<span v-if="visibleSet.get('settings') === 'label'">{{ t('actionbar.settings') }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('actionbar.settingsTip') }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div
v-if="isVisible('settings')"
aria-hidden="true"
class="mr-4 h-5 w-px shrink-0 -translate-x-px self-center bg-border"
/>
<!-- Virtual Keyboard - Always visible -->
<TooltipProvider>
<Tooltip>
@@ -523,7 +528,7 @@ const hasRightOverflow = computed(() => {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</ButtonGroup>
</div>
</div>

View File

@@ -6,6 +6,7 @@ import { useSystemStore } from '@/stores/system'
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
import BrandMark from '@/components/BrandMark.vue'
import { Button } from '@/components/ui/button'
import { useTheme } from '@/composables/useTheme'
import {
DropdownMenu,
DropdownMenuContent,
@@ -23,12 +24,7 @@ const { t } = useI18n()
const router = useRouter()
const authStore = useAuthStore()
const systemStore = useSystemStore()
function toggleTheme() {
const isDark = document.documentElement.classList.contains('dark')
document.documentElement.classList.toggle('dark', !isDark)
localStorage.setItem('theme', isDark ? 'light' : 'dark')
}
const { toggleTheme } = useTheme()
async function handleLogout() {
await authStore.logout()
@@ -40,11 +36,12 @@ async function handleLogout() {
<div class="h-screen h-dvh flex flex-col bg-background overflow-hidden">
<!-- Header -->
<header class="shrink-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div class="flex h-11 sm:h-14 items-center px-3 sm:px-4 max-w-full">
<div class="flex h-10 sm:h-14 items-center px-2 sm:px-4 max-w-full">
<!-- Logo -->
<RouterLink to="/" class="flex items-center gap-1.5 sm:gap-2 font-semibold">
<BrandMark size="sm" />
<span class="hidden sm:inline">One-KVM</span>
<RouterLink to="/" class="flex items-center gap-1.5 sm:gap-2">
<BrandMark size="md" class="hidden sm:block" />
<BrandMark size="sm" class="sm:hidden" />
<span class="font-bold text-sm sm:text-lg">One-KVM</span>
</RouterLink>
<!-- Right Side -->

View File

@@ -46,15 +46,15 @@ const actionBusy = computed(() => activeAction.value !== null)
const powerStateIconColor = computed(() => {
switch (powerState.value) {
case 'on': return 'text-green-600'
case 'off': return 'text-slate-500'
default: return 'text-yellow-600'
case 'on': return 'text-success'
case 'off': return 'text-muted-foreground'
default: return 'text-warning'
}
})
const powerStateTextColor = computed(() => {
switch (powerState.value) {
case 'on': return 'text-green-600'
case 'on': return 'text-success'
default: return ''
}
})
@@ -69,15 +69,15 @@ const powerStateText = computed(() => {
const hddStateIconColor = computed(() => {
switch (hddState.value) {
case 'active': return 'text-green-600'
case 'inactive': return 'text-slate-500'
default: return 'text-yellow-600'
case 'active': return 'text-success'
case 'inactive': return 'text-muted-foreground'
default: return 'text-warning'
}
})
const hddStateTextColor = computed(() => {
switch (hddState.value) {
case 'active': return 'text-green-600'
case 'active': return 'text-success'
default: return ''
}
})
@@ -254,7 +254,7 @@ watch(
size="sm"
:disabled="actionBusy"
:class="[
'w-full justify-start gap-2 h-7 text-xs text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:hover:bg-orange-950',
'h-7 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' : '',
]"
@click="handleAction('long')"
@@ -268,7 +268,7 @@ watch(
size="sm"
:disabled="actionBusy"
:class="[
'w-full justify-start gap-2 h-7 text-xs text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950',
'h-7 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' : '',
]"
@click="handleAction('reset')"
@@ -318,14 +318,16 @@ watch(
<Separator />
<Label class="text-xs text-muted-foreground">{{ t('atx.recentMac') }}</Label>
<div class="space-y-1">
<button
<Button
v-for="mac in wolHistory"
:key="mac"
class="w-full text-left px-2 py-1.5 rounded text-xs font-mono hover:bg-muted transition-colors"
variant="ghost"
size="sm"
class="w-full justify-start font-mono text-xs"
@click="selectFromHistory(mac)"
>
{{ mac }}
</button>
</Button>
</div>
</div>
</TabsContent>

View File

@@ -10,13 +10,7 @@ import {
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
import { Volume2, RefreshCw, Loader2 } from 'lucide-vue-next'
import { audioApi, configApi } from '@/api'
import { useConfigStore } from '@/stores/config'
@@ -232,25 +226,22 @@ watch(() => props.open, (isOpen) => {
<!-- Device Selection -->
<div class="space-y-2">
<Label class="text-xs text-muted-foreground">{{ t('actionbar.audioDevice') }}</Label>
<Select
<NativeSelect
:model-value="selectedDevice"
@update:model-value="(v) => selectedDevice = v as string"
:disabled="loadingDevices || devices.length === 0"
class="h-8 w-full text-xs"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue :placeholder="t('actionbar.selectAudioDevice')" />
</SelectTrigger>
<SelectContent>
<SelectItem
<NativeSelectOption value="">{{ t('actionbar.selectAudioDevice') }}</NativeSelectOption>
<NativeSelectOption
v-for="device in devices"
:key="device.name"
:value="device.name"
class="text-xs"
>
{{ device.description || device.name }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
</div>
<!-- Audio Quality -->

View File

@@ -10,7 +10,7 @@ import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
import { Tabs, TabsContent } from '@/components/ui/tabs'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
const props = defineProps<{
open: boolean
@@ -192,28 +192,10 @@ onMounted(loadConfig)
<Tabs v-model="activeTab" class="flex min-h-0 flex-1 flex-col">
<div class="px-3 py-2">
<div class="grid grid-cols-2 rounded-md bg-muted p-1">
<button
type="button"
:class="[
'rounded-sm px-3 py-1.5 text-sm font-medium transition-colors',
activeTab === 'chat' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
]"
@click="activeTab = 'chat'"
>
对话
</button>
<button
type="button"
:class="[
'rounded-sm px-3 py-1.5 text-sm font-medium transition-colors',
activeTab === 'settings' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
]"
@click="activeTab = 'settings'"
>
设置
</button>
</div>
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="chat">对话</TabsTrigger>
<TabsTrigger value="settings">设置</TabsTrigger>
</TabsList>
</div>
<TabsContent value="chat" class="m-0 flex min-h-0 flex-1 flex-col data-[state=inactive]:hidden">
@@ -248,7 +230,7 @@ onMounted(loadConfig)
</div>
</div>
<div v-else-if="item.type === 'actions_executed'" class="rounded-md border bg-emerald-50 p-2 text-emerald-950 dark:bg-emerald-950/20 dark:text-emerald-100">
<div v-else-if="item.type === 'actions_executed'" class="rounded-md border border-success/35 bg-success/10 p-2 text-success">
<div class="mb-2 text-xs font-medium">已执行</div>
<div class="space-y-1">
<div v-for="(action, index) in item.actions" :key="index" class="rounded-sm bg-background/60 px-2 py-1.5 text-xs">

View File

@@ -6,6 +6,7 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip'
import { HelpCircle } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
defineProps<{
content: string
@@ -19,13 +20,15 @@ defineProps<{
<TooltipProvider :delay-duration="200">
<Tooltip>
<TooltipTrigger as-child>
<button
<Button
type="button"
class="inline-flex items-center justify-center text-muted-foreground/60 hover:text-muted-foreground transition-colors focus:outline-none"
variant="ghost"
size="icon-xs"
class="text-muted-foreground/60 hover:text-muted-foreground"
:class="iconSize === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'"
>
<HelpCircle :class="iconSize === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
</button>
</Button>
</TooltipTrigger>
<TooltipContent
:side="side || 'top'"

View File

@@ -19,6 +19,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
import { MousePointer, Move, Loader2, RefreshCw } from 'lucide-vue-next'
import HelpTooltip from '@/components/HelpTooltip.vue'
import { configApi } from '@/api'
@@ -329,7 +330,7 @@ watch(() => props.open, (isOpen) => {
:model-value="hidBackend"
@update:model-value="handleBackendChange"
>
<SelectTrigger class="h-8 text-xs">
<SelectTrigger class="h-8 w-full text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -343,25 +344,22 @@ watch(() => props.open, (isOpen) => {
<!-- Device Path (OTG or CH9329) -->
<div v-if="hidBackend !== HidBackend.None" class="space-y-2">
<Label class="text-xs text-muted-foreground">{{ t('actionbar.devicePath') }}</Label>
<Select
<NativeSelect
:model-value="devicePath"
@update:model-value="handleDevicePathChange"
:disabled="availableDevicePaths.length === 0"
class="h-8 w-full text-xs"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue :placeholder="t('actionbar.selectDevice')" />
</SelectTrigger>
<SelectContent>
<SelectItem
<NativeSelectOption value="">{{ t('actionbar.selectDevice') }}</NativeSelectOption>
<NativeSelectOption
v-for="device in availableDevicePaths"
:key="device.path"
:value="device.path"
class="text-xs"
>
{{ device.name }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
</div>
<!-- Baudrate (CH9329 only) -->

View File

@@ -2,7 +2,9 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { CanonicalKey } from '@/types/generated'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Kbd } from '@/components/ui/kbd'
import { Separator } from '@/components/ui/separator'
const props = defineProps<{
pressedKeys?: CanonicalKey[]
@@ -40,20 +42,20 @@ const keysDisplay = computed(() => {
</script>
<template>
<div class="w-full border-t border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="w-full border-t bg-background">
<!-- Compact mode (explicit prop or auto on small screens via sm:hidden) -->
<div :class="compact ? '' : 'sm:hidden'">
<div class="flex items-center justify-between text-xs px-2 py-0.5">
<div v-if="keyboardLedEnabled" class="flex items-center gap-1">
<span
:class="capsLock ? 'px-1.5 py-0.5 bg-primary/10 text-primary rounded text-[10px] font-medium' : 'text-muted-foreground/40 text-[10px]'"
>C</span>
<span
:class="numLock ? 'px-1.5 py-0.5 bg-primary/10 text-primary rounded text-[10px] font-medium' : 'text-muted-foreground/40 text-[10px]'"
>N</span>
<span
:class="scrollLock ? 'px-1.5 py-0.5 bg-primary/10 text-primary rounded text-[10px] font-medium' : 'text-muted-foreground/40 text-[10px]'"
>S</span>
<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
</Badge>
<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
</Badge>
<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
</Badge>
</div>
<div v-else class="text-[10px] text-muted-foreground/60">
{{ t('infobar.keyboardLedUnavailable') }}
@@ -71,7 +73,7 @@ const keysDisplay = computed(() => {
<div class="flex items-center gap-4 px-3 py-1 min-w-0 flex-1">
<div class="flex items-center gap-1.5 min-w-0">
<span class="font-medium text-muted-foreground shrink-0">{{ t('infobar.keys') }}:</span>
<span class="text-foreground truncate">{{ keysDisplay || '-' }}</span>
<Kbd class="max-w-full truncate">{{ keysDisplay || '-' }}</Kbd>
</div>
<div v-if="debugMode && mousePosition" class="flex items-center gap-1.5 hidden md:flex">
@@ -82,25 +84,20 @@ const keysDisplay = computed(() => {
<!-- Right side: Keyboard LED states -->
<div class="flex items-center shrink-0">
<Separator orientation="vertical" class="h-5" />
<template v-if="keyboardLedEnabled">
<div
:class="cn(
'px-2 py-1 select-none transition-colors',
capsLock ? 'text-foreground font-medium bg-primary/5' : 'text-muted-foreground/40'
)"
>{{ t('infobar.caps') }}</div>
<div
:class="cn(
'px-2 py-1 select-none transition-colors',
numLock ? 'text-foreground font-medium bg-primary/5' : 'text-muted-foreground/40'
)"
>{{ t('infobar.num') }}</div>
<div
:class="cn(
'px-2 py-1 select-none transition-colors',
scrollLock ? 'text-foreground font-medium bg-primary/5' : 'text-muted-foreground/40'
)"
>{{ t('infobar.scroll') }}</div>
<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'" />
{{ t('infobar.caps') }}
</Badge>
<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'" />
{{ t('infobar.num') }}
</Badge>
<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'" />
{{ t('infobar.scroll') }}
</Badge>
</template>
<div v-else class="px-3 py-1 text-muted-foreground/60">
{{ t('infobar.keyboardLedUnavailable') }}

View File

@@ -29,6 +29,8 @@ import { Label } from '@/components/ui/label'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { Slider } from '@/components/ui/slider'
import { Separator } from '@/components/ui/separator'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia } from '@/components/ui/empty'
import { Skeleton } from '@/components/ui/skeleton'
import {
HardDrive,
Upload,
@@ -151,9 +153,9 @@ const downloadProgress = ref<{
} | null>(null)
const TWO_POINT_TWO_GB = 2.2 * 1024 * 1024 * 1024
const tabTriggerClass = 'h-9 rounded-md border border-transparent bg-transparent text-center text-muted-foreground shadow-none hover:text-foreground data-[state=active]:border-border 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 bg-muted p-1'
const segmentedItemClass = 'h-8 w-full justify-center rounded-md border border-transparent bg-transparent px-3 text-center text-xs text-muted-foreground shadow-none hover:bg-background/60 hover:text-foreground data-[state=on]:border-border data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-sm'
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 segmentedGroupClass = 'grid w-full grid-cols-2 items-center gap-1 rounded-md border border-border bg-muted p-1'
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 slotCapacity = computed(() => systemStore.msd?.slotCapacity ?? 1)
@@ -684,10 +686,10 @@ onUnmounted(() => {
</DialogTitle>
<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="msdConnected ? 'text-green-600 dark:text-green-400' : '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 v-if="msdConnected" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span :class="msdConnected ? 'bg-green-500' : 'bg-muted-foreground'" class="relative inline-flex rounded-full h-2 w-2"></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>
{{ msdConnected ? t('common.connected') : t('common.disconnected') }}
</span>
@@ -696,11 +698,11 @@ onUnmounted(() => {
<Badge
v-if="mediaSlotsFull"
variant="outline"
class="h-6 rounded-md border-amber-500/40 bg-amber-500/10 px-2 text-xs text-amber-700 dark:text-amber-400"
class="h-6 border-warning/40 bg-warning/10 px-2 text-xs text-warning"
>
{{ t('msd.mediaSlotsFull') }}
</Badge>
<span v-if="usbReenumerating" class="text-xs text-amber-600 dark:text-amber-400">
<span v-if="usbReenumerating" class="text-xs text-warning">
{{ t('msd.reenumerating') }}
</span>
</span>
@@ -722,6 +724,7 @@ onUnmounted(() => {
:model-value="diskMode"
type="single"
size="sm"
:spacing="1"
:class="[segmentedGroupClass, 'min-w-0 flex-1 sm:w-[200px] sm:flex-none']"
:disabled="operationInProgress"
@update:model-value="changeDiskMode"
@@ -783,9 +786,13 @@ onUnmounted(() => {
</div>
<Progress v-if="uploading" :model-value="uploadProgress" class="h-1 shrink-0" />
<div v-if="images.length === 0" class="shrink-0 text-center py-6 text-muted-foreground text-sm">
{{ t('msd.noImages') }}
</div>
<Skeleton v-if="loadingImages" class="h-24 w-full" />
<Empty v-else-if="images.length === 0" class="shrink-0 py-6">
<EmptyHeader>
<EmptyMedia variant="icon"><HardDrive /></EmptyMedia>
<EmptyDescription>{{ t('msd.noImages') }}</EmptyDescription>
</EmptyHeader>
</Empty>
<div v-else class="flex-1 min-h-0 overflow-y-auto pr-2 custom-scrollbar">
<div class="space-y-2">
@@ -818,7 +825,7 @@ onUnmounted(() => {
<TooltipTrigger as-child>
<Badge
variant="outline"
class="text-[10px] h-4 px-1.5 border-amber-500/50 text-amber-600 dark:text-amber-400 cursor-help"
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" />
{{ t('msd.largeFileWarning') }}
@@ -1017,8 +1024,10 @@ onUnmounted(() => {
<nav class="flex items-center text-xs min-w-0 overflow-hidden">
<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" />
<button
class="hover:text-primary transition-colors truncate"
<Button
variant="link"
size="sm"
class="h-auto min-w-0 truncate p-0 font-normal"
:class="[
index === breadcrumbs.length - 1 ? 'font-medium' : 'text-muted-foreground',
driveConnectedToTarget ? 'cursor-not-allowed opacity-50' : ''
@@ -1027,7 +1036,7 @@ onUnmounted(() => {
@click="!driveConnectedToTarget && navigateTo(crumb.path)"
>
{{ crumb.name }}
</button>
</Button>
</template>
</nav>
</div>
@@ -1084,12 +1093,13 @@ onUnmounted(() => {
<Progress v-if="uploadingFile" :model-value="fileUploadProgress" class="h-1 shrink-0" />
<!-- File List -->
<div
v-if="driveFiles.length === 0 && !driveConnectedToTarget && !driveError"
class="shrink-0 text-center py-6 text-muted-foreground text-sm"
>
{{ t('msd.emptyFolder') }}
</div>
<Skeleton v-if="loadingDrive" class="h-24 w-full" />
<Empty v-else-if="driveFiles.length === 0 && !driveConnectedToTarget && !driveError" class="shrink-0 py-6">
<EmptyHeader>
<EmptyMedia variant="icon"><Folder /></EmptyMedia>
<EmptyDescription>{{ t('msd.emptyFolder') }}</EmptyDescription>
</EmptyHeader>
</Empty>
<!-- Connected placeholder: file list hidden while drive mounted on target -->
<div
@@ -1110,7 +1120,7 @@ onUnmounted(() => {
class="flex items-center gap-2 cursor-pointer flex-1 min-w-0"
@click="file.is_dir && navigateTo(file.path)"
>
<Folder v-if="file.is_dir" class="h-4 w-4 text-blue-500 shrink-0" />
<Folder v-if="file.is_dir" class="h-4 w-4 shrink-0 text-info" />
<File v-else class="h-4 w-4 text-muted-foreground shrink-0" />
<div class="min-w-0">
<Tooltip>
@@ -1284,6 +1294,7 @@ onUnmounted(() => {
:model-value="mountMode"
type="single"
size="sm"
:spacing="1"
:class="segmentedGroupClass"
@update:model-value="updateMountMode"
>
@@ -1298,6 +1309,7 @@ onUnmounted(() => {
:model-value="accessMode"
type="single"
size="sm"
:spacing="1"
:class="segmentedGroupClass"
@update:model-value="updateAccessMode"
>
@@ -1386,7 +1398,7 @@ onUnmounted(() => {
/ {{ formatBytes(downloadProgress.total_bytes) }}
</span>
</div>
<div v-if="downloadProgress.status === 'completed'" class="text-xs text-green-600">
<div v-if="downloadProgress.status === 'completed'" class="text-xs text-success">
{{ t('msd.downloadComplete') }}
</div>
<div v-else-if="downloadProgress.status.startsWith('failed')" class="text-xs text-destructive">

View File

@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Progress } from '@/components/ui/progress'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { CornerDownLeft, Square, AlertCircle } from 'lucide-vue-next'
import { charToKey, analyzeText } from '@/lib/charToHid'
import { hidApi } from '@/api'
@@ -203,16 +204,14 @@ function handleKeydown(e: KeyboardEvent) {
</div>
<!-- Warning for untypable characters -->
<div v-if="hasUntypableChars && !isPasting" class="flex items-start gap-2 p-2 rounded-md bg-amber-500/10 text-amber-600 dark:text-amber-400">
<Alert v-if="hasUntypableChars && !isPasting" variant="warning">
<AlertCircle class="h-4 w-4 shrink-0 mt-0.5" />
<div class="text-xs">
<p class="font-medium">{{ t('paste.untypableWarning') }}</p>
<p class="text-muted-foreground mt-0.5">
<AlertTitle>{{ t('paste.untypableWarning') }}</AlertTitle>
<AlertDescription>
{{ t('paste.untypableChars', { chars: textAnalysis?.untypableChars.slice(0, 5).map(c => c === '\n' ? '\\n' : c === '\r' ? '\\r' : c === '\t' ? '\\t' : c).join(', ') }) }}
<span v-if="textAnalysis && textAnalysis.untypableChars.length > 5">...</span>
</p>
</div>
</div>
</AlertDescription>
</Alert>
<!-- Progress indicator during paste -->
<div v-if="isPasting" class="space-y-2">

View File

@@ -10,6 +10,9 @@ import {
SheetTitle,
} from '@/components/ui/sheet'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Badge } from '@/components/ui/badge'
import { Item, ItemContent, ItemDescription, ItemTitle } from '@/components/ui/item'
import { Card } from '@/components/ui/card'
import type { WebRTCStats } from '@/composables/useWebRTC'
import { formatFpsValue } from '@/lib/fps'
@@ -405,26 +408,25 @@ onUnmounted(() => {
<Sheet :open="props.open" @update:open="emit('update:open', $event)">
<SheetContent
side="right"
class="w-[90vw] max-w-[440px] p-0 border-l border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950"
class="w-[90vw] max-w-[440px] border-l bg-background p-0"
>
<!-- Header -->
<SheetHeader class="px-6 py-3 border-b border-slate-200 dark:border-slate-800">
<SheetHeader class="border-b px-6 py-3">
<div class="flex items-center gap-2">
<SheetTitle class="text-base">{{ t('stats.title') }}</SheetTitle>
<span class="text-xs px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-muted-foreground">
WebRTC
</span>
<Badge variant="secondary">WebRTC</Badge>
</div>
</SheetHeader>
<ScrollArea class="h-[calc(100dvh-60px)]">
<div class="px-6 py-4 space-y-6">
<!-- Video Section Header -->
<div>
<!-- Video Information -->
<div class="space-y-3">
<h3 class="text-sm font-medium">{{ t('stats.video') }}</h3>
<p class="text-xs text-muted-foreground mt-0.5">
{{ t('stats.videoDesc') }}
</p>
<div class="grid grid-cols-2 gap-3">
<Item variant="muted" size="sm"><ItemContent><ItemDescription>{{ t('stats.resolution') }}</ItemDescription><ItemTitle>{{ currentStats.resolution }}</ItemTitle></ItemContent></Item>
<Item variant="muted" size="sm"><ItemContent><ItemDescription>{{ t('stats.bitrate') }}</ItemDescription><ItemTitle>{{ currentStats.bitrate }} Mbps</ItemTitle></ItemContent></Item>
</div>
</div>
<!-- Network Stability (Jitter) -->
@@ -432,13 +434,10 @@ onUnmounted(() => {
<div class="flex items-center justify-between">
<h4 class="text-sm font-medium">{{ t('stats.stability') }}</h4>
</div>
<p class="text-xs text-muted-foreground">
{{ t('stats.stabilityDesc') }}
</p>
<div class="relative">
<Card class="relative gap-0 p-2 shadow-none">
<div
ref="stabilityChartRef"
class="w-full rounded-lg bg-slate-50 dark:bg-slate-900/50 p-2"
class="w-full"
/>
<div
v-if="activeTooltip.visible && activeTooltip.chartId === 'stability'"
@@ -446,9 +445,9 @@ onUnmounted(() => {
:style="{ left: `${activeTooltip.left + 60}px`, top: `${activeTooltip.top - 40}px` }"
>
<div class="text-xs font-medium">{{ activeTooltip.time }}</div>
<div class="text-xs text-blue-500">{{ activeTooltip.value }} {{ activeTooltip.unit }}</div>
</div>
<div class="text-xs text-info">{{ activeTooltip.value }} {{ activeTooltip.unit }}</div>
</div>
</Card>
</div>
<!-- Playback Delay -->
@@ -459,13 +458,10 @@ onUnmounted(() => {
{{ currentStats.delay }} ms
</span>
</div>
<p class="text-xs text-muted-foreground">
{{ t('stats.delayDesc') }}
</p>
<div class="relative">
<Card class="relative gap-0 p-2 shadow-none">
<div
ref="delayChartRef"
class="w-full rounded-lg bg-slate-50 dark:bg-slate-900/50 p-2"
class="w-full"
/>
<div
v-if="activeTooltip.visible && activeTooltip.chartId === 'delay'"
@@ -473,9 +469,9 @@ onUnmounted(() => {
:style="{ left: `${activeTooltip.left + 60}px`, top: `${activeTooltip.top - 40}px` }"
>
<div class="text-xs font-medium">{{ activeTooltip.time }}</div>
<div class="text-xs text-blue-500">{{ activeTooltip.value }} {{ activeTooltip.unit }}</div>
</div>
<div class="text-xs text-info">{{ activeTooltip.value }} {{ activeTooltip.unit }}</div>
</div>
</Card>
</div>
<!-- Packet Loss -->
@@ -486,13 +482,10 @@ onUnmounted(() => {
{{ currentStats.packetsLost }} {{ t('stats.total') }}
</span>
</div>
<p class="text-xs text-muted-foreground">
{{ t('stats.packetLossDesc') }}
</p>
<div class="relative">
<Card class="relative gap-0 p-2 shadow-none">
<div
ref="packetLossChartRef"
class="w-full rounded-lg bg-slate-50 dark:bg-slate-900/50 p-2"
class="w-full"
/>
<div
v-if="activeTooltip.visible && activeTooltip.chartId === 'packetLoss'"
@@ -500,9 +493,9 @@ onUnmounted(() => {
:style="{ left: `${activeTooltip.left + 60}px`, top: `${activeTooltip.top - 40}px` }"
>
<div class="text-xs font-medium">{{ activeTooltip.time }}</div>
<div class="text-xs text-blue-500">{{ activeTooltip.value }} {{ activeTooltip.unit }}</div>
</div>
<div class="text-xs text-info">{{ activeTooltip.value }} {{ activeTooltip.unit }}</div>
</div>
</Card>
</div>
<!-- FPS -->
@@ -513,13 +506,10 @@ onUnmounted(() => {
{{ formatFpsValue(currentStats.fps) }} fps
</span>
</div>
<p class="text-xs text-muted-foreground">
{{ t('stats.frameRateDesc') }}
</p>
<div class="relative">
<Card class="relative gap-0 p-2 shadow-none">
<div
ref="fpsChartRef"
class="w-full rounded-lg bg-slate-50 dark:bg-slate-900/50 p-2"
class="w-full"
/>
<div
v-if="activeTooltip.visible && activeTooltip.chartId === 'fps'"
@@ -527,52 +517,27 @@ onUnmounted(() => {
:style="{ left: `${activeTooltip.left + 60}px`, top: `${activeTooltip.top - 40}px` }"
>
<div class="text-xs font-medium">{{ activeTooltip.time }}</div>
<div class="text-xs text-blue-500">{{ activeTooltip.value }} {{ activeTooltip.unit }}</div>
</div>
<div class="text-xs text-info">{{ activeTooltip.value }} {{ activeTooltip.unit }}</div>
</div>
</Card>
</div>
<!-- Additional Stats -->
<div class="space-y-3 pt-2 border-t border-slate-200 dark:border-slate-800">
<h4 class="text-sm font-medium">{{ t('stats.additional') }}</h4>
<!-- Connection Information -->
<div class="space-y-3 border-t pt-2">
<h4 class="text-sm font-medium">{{ t('stats.connection') }}</h4>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-lg bg-slate-50 dark:bg-slate-900/50 p-3">
<p class="text-xs text-muted-foreground">{{ t('stats.resolution') }}</p>
<p class="text-sm font-medium mt-1">{{ currentStats.resolution }}</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-900/50 p-3">
<p class="text-xs text-muted-foreground">{{ t('stats.bitrate') }}</p>
<p class="text-sm font-medium mt-1">{{ currentStats.bitrate }} Mbps</p>
</div>
</div>
<!-- Connection Info -->
<h4 class="text-sm font-medium pt-2">{{ t('stats.connection') }}</h4>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-lg bg-slate-50 dark:bg-slate-900/50 p-3">
<p class="text-xs text-muted-foreground">{{ t('stats.connectionType') }}</p>
<p class="text-sm font-medium mt-1 flex items-center gap-1.5">
<Item variant="muted" size="sm"><ItemContent><ItemDescription>{{ t('stats.connectionType') }}</ItemDescription><ItemTitle class="flex items-center gap-1.5">
<span
:class="[
'inline-block w-2 h-2 rounded-full',
currentStats.isRelay ? 'bg-amber-500' : 'bg-green-500'
currentStats.isRelay ? 'bg-warning' : 'bg-success'
]"
/>
{{ currentStats.isRelay ? t('stats.relay') : t('stats.p2p') }}
</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-900/50 p-3">
<p class="text-xs text-muted-foreground">{{ t('stats.transport') }}</p>
<p class="text-sm font-medium mt-1">{{ currentStats.transport }}</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-900/50 p-3">
<p class="text-xs text-muted-foreground">{{ t('stats.localCandidate') }}</p>
<p class="text-sm font-medium mt-1">{{ currentStats.localType }}</p>
</div>
<div class="rounded-lg bg-slate-50 dark:bg-slate-900/50 p-3">
<p class="text-xs text-muted-foreground">{{ t('stats.remoteCandidate') }}</p>
<p class="text-sm font-medium mt-1">{{ currentStats.remoteType }}</p>
</div>
</ItemTitle></ItemContent></Item>
<Item variant="muted" size="sm"><ItemContent><ItemDescription>{{ t('stats.transport') }}</ItemDescription><ItemTitle>{{ currentStats.transport }}</ItemTitle></ItemContent></Item>
<Item variant="muted" size="sm"><ItemContent><ItemDescription>{{ t('stats.localCandidate') }}</ItemDescription><ItemTitle>{{ currentStats.localType }}</ItemTitle></ItemContent></Item>
<Item variant="muted" size="sm"><ItemContent><ItemDescription>{{ t('stats.remoteCandidate') }}</ItemDescription><ItemTitle>{{ currentStats.remoteType }}</ItemTitle></ItemContent></Item>
</div>
</div>
</div>

View File

@@ -13,6 +13,7 @@ import {
PopoverTrigger,
} from '@/components/ui/popover'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { Monitor, Video, Usb, AlertCircle, CheckCircle, Loader2, Volume2, HardDrive } from 'lucide-vue-next'
@@ -50,15 +51,15 @@ onMounted(() => {
const statusColor = computed(() => {
switch (props.status) {
case 'connected':
return 'bg-green-500'
return 'bg-success'
case 'connecting':
return 'bg-yellow-500 animate-pulse'
return 'bg-warning animate-pulse'
case 'disconnected':
return 'bg-slate-400'
return 'bg-muted-foreground'
case 'error':
return 'bg-red-500'
return 'bg-destructive'
default:
return 'bg-slate-400'
return 'bg-muted-foreground'
}
})
@@ -127,15 +128,14 @@ const statusBadgeText = computed(() => {
<HoverCard v-if="!prefersPopover" :open-delay="200" :close-delay="100">
<HoverCardTrigger as-child>
<!-- New layout: vertical with title on top, status+quickInfo on bottom -->
<button
<Button
type="button"
variant="outline"
:aria-label="`${title}: ${quickInfo || subtitle || statusText}`"
:class="cn(
'flex flex-col gap-0.5 rounded-md border cursor-pointer transition-colors text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'h-auto flex-col items-start gap-0.5 text-left',
compact ? 'px-1.5 py-0.5 text-xs' : 'px-3 py-1.5 text-sm min-w-[100px]',
'bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700',
'border-slate-200 dark:border-slate-700',
status === 'error' && 'border-red-300 dark:border-red-800'
status === 'error' && 'border-destructive/50'
)"
>
<template v-if="compact">
@@ -156,7 +156,7 @@ const statusBadgeText = computed(() => {
</span>
</div>
</template>
</button>
</Button>
</HoverCardTrigger>
<HoverCardContent class="w-80" :align="hoverAlign">
@@ -164,10 +164,10 @@ const statusBadgeText = computed(() => {
<!-- Header -->
<div class="flex items-center gap-3">
<div :class="cn(
'p-2 rounded-lg',
status === 'connected' ? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' :
status === 'error' ? 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' :
'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400'
'rounded-md p-2',
status === 'connected' ? 'bg-success/10 text-success' :
status === 'error' ? 'bg-destructive/10 text-destructive' :
'bg-muted text-muted-foreground'
)">
<component :is="StatusIcon" class="h-5 w-5" />
</div>
@@ -179,14 +179,14 @@ const statusBadgeText = computed(() => {
:is="statusIcon"
:class="cn(
'h-3.5 w-3.5',
status === 'connected' ? 'text-green-500' :
status === 'connecting' ? 'text-yellow-500 animate-spin' :
status === 'error' ? 'text-red-500' :
'text-slate-400'
status === 'connected' ? 'text-success' :
status === 'connecting' ? 'text-warning animate-spin' :
status === 'error' ? 'text-destructive' :
'text-muted-foreground'
)"
/>
<Badge
:variant="status === 'connected' ? 'default' : status === 'error' ? 'destructive' : 'secondary'"
:variant="status === 'connected' ? 'success' : status === 'connecting' ? 'warning' : status === 'error' ? 'destructive' : 'secondary'"
class="text-[10px] px-1.5 py-0"
>
{{ statusBadgeText }}
@@ -198,24 +198,14 @@ const statusBadgeText = computed(() => {
<!-- Details -->
<div v-if="details && details.length > 0" class="space-y-2">
<Separator />
<div class="space-y-1.5">
<div class="space-y-1">
<div
v-for="(detail, index) in details"
:key="index"
class="flex items-start justify-between gap-3 text-xs"
class="flex items-baseline justify-between gap-3 py-0.5 text-xs"
>
<span class="text-muted-foreground shrink-0">{{ detail.label }}</span>
<span
:class="cn(
'font-medium text-right break-words min-w-0',
detail.status === 'ok' ? 'text-green-600 dark:text-green-400' :
detail.status === 'warning' ? 'text-yellow-600 dark:text-yellow-400' :
detail.status === 'error' ? 'text-red-600 dark:text-red-400' :
'text-foreground'
)"
>
{{ detail.value }}
</span>
<span class="shrink-0 text-muted-foreground">{{ detail.label }}</span>
<span :class="cn('min-w-0 text-right text-xs font-medium', detail.status === 'ok' ? 'text-success' : detail.status === 'warning' ? 'text-warning' : detail.status === 'error' ? 'text-destructive' : 'text-foreground')">{{ detail.value }}</span>
</div>
</div>
</div>
@@ -226,15 +216,14 @@ const statusBadgeText = computed(() => {
<Popover v-else>
<PopoverTrigger as-child>
<!-- New layout: vertical with title on top, status+quickInfo on bottom -->
<button
<Button
type="button"
variant="outline"
:aria-label="`${title}: ${quickInfo || subtitle || statusText}`"
:class="cn(
'flex flex-col gap-0.5 rounded-md border cursor-pointer transition-colors text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'h-auto flex-col items-start gap-0.5 text-left',
compact ? 'px-1.5 py-0.5 text-xs' : 'px-3 py-1.5 text-sm min-w-[100px]',
'bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700',
'border-slate-200 dark:border-slate-700',
status === 'error' && 'border-red-300 dark:border-red-800'
status === 'error' && 'border-destructive/50'
)"
>
<template v-if="compact">
@@ -255,7 +244,7 @@ const statusBadgeText = computed(() => {
</span>
</div>
</template>
</button>
</Button>
</PopoverTrigger>
<PopoverContent class="w-[min(320px,90vw)]" :align="hoverAlign">
@@ -263,10 +252,10 @@ const statusBadgeText = computed(() => {
<!-- Header -->
<div class="flex items-center gap-3">
<div :class="cn(
'p-2 rounded-lg',
status === 'connected' ? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' :
status === 'error' ? 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' :
'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400'
'rounded-md p-2',
status === 'connected' ? 'bg-success/10 text-success' :
status === 'error' ? 'bg-destructive/10 text-destructive' :
'bg-muted text-muted-foreground'
)">
<component :is="StatusIcon" class="h-5 w-5" />
</div>
@@ -278,14 +267,14 @@ const statusBadgeText = computed(() => {
:is="statusIcon"
:class="cn(
'h-3.5 w-3.5',
status === 'connected' ? 'text-green-500' :
status === 'connecting' ? 'text-yellow-500 animate-spin' :
status === 'error' ? 'text-red-500' :
'text-slate-400'
status === 'connected' ? 'text-success' :
status === 'connecting' ? 'text-warning animate-spin' :
status === 'error' ? 'text-destructive' :
'text-muted-foreground'
)"
/>
<Badge
:variant="status === 'connected' ? 'default' : status === 'error' ? 'destructive' : 'secondary'"
:variant="status === 'connected' ? 'success' : status === 'connecting' ? 'warning' : status === 'error' ? 'destructive' : 'secondary'"
class="text-[10px] px-1.5 py-0"
>
{{ statusBadgeText }}
@@ -297,24 +286,14 @@ const statusBadgeText = computed(() => {
<!-- Details -->
<div v-if="details && details.length > 0" class="space-y-2">
<Separator />
<div class="space-y-1.5">
<div class="space-y-1">
<div
v-for="(detail, index) in details"
:key="index"
class="flex items-start justify-between gap-3 text-xs"
class="flex items-baseline justify-between gap-3 py-0.5 text-xs"
>
<span class="text-muted-foreground shrink-0">{{ detail.label }}</span>
<span
:class="cn(
'font-medium text-right break-words min-w-0',
detail.status === 'ok' ? 'text-green-600 dark:text-green-400' :
detail.status === 'warning' ? 'text-yellow-600 dark:text-yellow-400' :
detail.status === 'error' ? 'text-red-600 dark:text-red-400' :
'text-foreground'
)"
>
{{ detail.value }}
</span>
<span class="shrink-0 text-muted-foreground">{{ detail.label }}</span>
<span :class="cn('min-w-0 text-right text-xs font-medium', detail.status === 'ok' ? 'text-success' : detail.status === 'warning' ? 'text-warning' : detail.status === 'error' ? 'text-destructive' : 'text-foreground')">{{ detail.value }}</span>
</div>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { toast } from 'vue-sonner'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
import {
Popover,
PopoverContent,
@@ -15,9 +16,8 @@ import {
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Monitor, RefreshCw, Loader2, Settings, Zap, Scale, Image, AlertTriangle } from 'lucide-vue-next'
import { Monitor, RefreshCw, Loader2, Zap, Scale, Image } from 'lucide-vue-next'
import HelpTooltip from '@/components/HelpTooltip.vue'
import {
configApi,
@@ -31,7 +31,6 @@ import { getVideoFormatState, isVideoFormatSelectable } from '@/lib/video-format
import { formatFpsLabel, toConfigFps } from '@/lib/fps'
import { formatVideoDeviceLabel } from '@/lib/video-device-label'
import { useConfigStore } from '@/stores/config'
import { useRouter } from 'vue-router'
export type VideoMode = 'mjpeg' | 'h264' | 'h265' | 'vp8' | 'vp9'
@@ -63,7 +62,6 @@ const emit = defineEmits<{
const { t } = useI18n()
const configStore = useConfigStore()
const router = useRouter()
// Device list
const devices = ref<VideoDevice[]>([])
@@ -239,6 +237,14 @@ const selectedFps = ref<number>(30)
const selectedBitratePreset = ref<'Speed' | 'Balanced' | 'Quality'>('Balanced')
const isDirty = ref(false)
const selectedFormatStatus = computed<'recommended' | 'not_recommended' | 'unsupported' | null>(() => {
if (!selectedFormat.value) return null
if (isFormatUnsupported(selectedFormat.value)) return 'unsupported'
if (isFormatRecommended(selectedFormat.value)) return 'recommended'
if (isFormatNotRecommended(selectedFormat.value)) return 'not_recommended'
return null
})
const applying = ref(false)
const applyingBitrate = ref(false)
@@ -317,10 +323,9 @@ const availableFps = computed(() => {
return resolution?.fps || []
})
const selectedFormatInfo = computed(() => {
const format = availableFormatOptions.value.find(f => f.format === selectedFormat.value)
return format
})
const selectedFormatInfo = computed(() =>
availableFormatOptions.value.find(format => format.format === selectedFormat.value) ?? null
)
const selectedCodecInfo = computed(() => {
const codec = availableCodecs.value.find(c => c.id === props.videoMode)
@@ -367,10 +372,6 @@ async function loadConstraints() {
}
}
function goToSettings() {
router.push('/settings?tab=video')
}
function initializeFromCurrent() {
const config = currentConfig.value
selectedDevice.value = config.device
@@ -619,21 +620,24 @@ watch(
<!-- Mode Selection -->
<div class="space-y-2">
<div class="flex items-center gap-1">
<Label class="text-xs">{{ t('actionbar.videoMode') }}</Label>
<HelpTooltip :content="t('actionbar.videoModeHint')" icon-size="sm" side="right" />
</div>
<Select
:model-value="props.videoMode"
@update:model-value="handleVideoModeChange"
:disabled="loadingCodecs || availableCodecs.length === 0 || isRtspCodecLocked"
>
<SelectTrigger class="h-8 text-xs">
<SelectTrigger class="h-8 w-full text-xs">
<div v-if="selectedCodecInfo" class="flex items-center gap-1.5 truncate">
<span class="truncate">{{ selectedCodecInfo.name }}</span>
<span
v-if="selectedCodecInfo.backend && selectedCodecInfo.id !== 'mjpeg'"
class="text-[10px] px-1 py-0.5 rounded shrink-0"
:class="selectedCodecInfo.hardware
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
: 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300'"
? 'bg-info/10 text-info'
: 'bg-warning/10 text-warning'"
>
{{ translateBackendName(selectedCodecInfo.backend) }}
</span>
@@ -655,8 +659,8 @@ watch(
v-if="codec.backend && codec.id !== 'mjpeg'"
class="text-[10px] px-1.5 py-0.5 rounded"
:class="codec.hardware
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
: 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300'"
? 'bg-info/10 text-info'
: 'bg-warning/10 text-warning'"
>
{{ translateBackendName(codec.backend) }}
</span>
@@ -670,10 +674,7 @@ watch(
</SelectItem>
</SelectContent>
</Select>
<p v-if="props.videoMode !== 'mjpeg'" class="text-xs text-muted-foreground">
{{ t('actionbar.webrtcHint') }}
</p>
<p v-if="isCodecLocked" class="text-xs text-amber-600 dark:text-amber-400">
<p v-if="isCodecLocked" class="text-xs text-warning">
{{ codecLockMessage }}
</p>
</div>
@@ -727,30 +728,15 @@ watch(
</div>
</div>
<Button
variant="ghost"
size="sm"
class="w-full h-7 text-xs text-muted-foreground hover:text-foreground justify-start px-0"
@click="goToSettings"
>
<Settings class="h-3.5 w-3.5 mr-1.5" />
{{ t('actionbar.changeEncoderBackend') }}
</Button>
</div>
<!-- Device Settings Section -->
<Separator />
<div class="space-y-3">
<div
v-if="videoParamWarningMessage"
class="rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-2"
>
<p class="flex items-start gap-1.5 text-xs text-amber-700 dark:text-amber-300">
<AlertTriangle class="h-3.5 w-3.5 mt-0.5 shrink-0" />
<span>{{ videoParamWarningMessage }}</span>
<p v-if="videoParamWarningMessage" class="text-xs text-warning">
{{ videoParamWarningMessage }}
</p>
</div>
<div class="flex items-center justify-between">
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.deviceSettings') }}</h5>
@@ -768,25 +754,22 @@ watch(
<!-- Device Selection -->
<div class="space-y-2">
<Label class="text-xs">{{ t('actionbar.videoDevice') }}</Label>
<Select
<NativeSelect
:model-value="selectedDevice"
@update:model-value="handleDeviceChange"
:disabled="loadingDevices || devices.length === 0"
class="h-8 w-full text-xs"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue :placeholder="loadingDevices ? t('common.loading') : t('actionbar.selectDevice')" />
</SelectTrigger>
<SelectContent>
<SelectItem
<NativeSelectOption value="">{{ loadingDevices ? t('common.loading') : t('actionbar.selectDevice') }}</NativeSelectOption>
<NativeSelectOption
v-for="device in devices"
:key="device.path"
:value="device.path"
class="text-xs"
>
{{ formatVideoDeviceLabel(device) }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
</div>
<!-- Format Selection -->
@@ -797,27 +780,27 @@ watch(
@update:model-value="handleFormatChange"
:disabled="!selectedDevice || availableFormats.length === 0"
>
<SelectTrigger class="h-8 text-xs">
<div v-if="selectedFormatInfo" class="flex items-center gap-1.5 truncate">
<SelectTrigger class="h-8 w-full text-xs">
<div v-if="selectedFormatInfo" class="flex min-w-0 items-center gap-1.5">
<span class="truncate">{{ selectedFormatInfo.description }}</span>
<span
v-if="selectedFormatInfo.state === 'unsupported'"
class="shrink-0 text-muted-foreground"
>
{{ t('common.notSupportedYet') }}
</span>
<span
v-if="isFormatRecommended(selectedFormatInfo.format)"
class="text-[10px] px-1 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 shrink-0"
v-if="selectedFormatStatus === 'recommended'"
class="shrink-0 rounded bg-info/10 px-1 py-0.5 text-[10px] text-info"
>
{{ t('actionbar.recommended') }}
</span>
<span
v-else-if="isFormatNotRecommended(selectedFormatInfo.format)"
class="text-[10px] px-1 py-0.5 rounded bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300 shrink-0"
v-else-if="selectedFormatStatus === 'not_recommended'"
class="shrink-0 rounded bg-warning/10 px-1 py-0.5 text-[10px] text-warning"
>
{{ t('actionbar.notRecommended') }}
</span>
<span
v-else-if="selectedFormatStatus === 'unsupported'"
class="shrink-0 rounded bg-muted px-1 py-0.5 text-[10px] text-muted-foreground"
>
{{ t('common.notSupportedYet') }}
</span>
</div>
<span v-else class="text-muted-foreground">{{ t('actionbar.selectFormat') }}</span>
</SelectTrigger>
@@ -827,25 +810,19 @@ watch(
:key="format.format"
:value="format.format"
:disabled="format.disabled"
:class="['text-xs', { 'opacity-50': format.disabled }]"
class="text-xs"
>
<div class="flex items-center gap-2">
<span>{{ format.description }}</span>
<span
v-if="format.state === 'unsupported'"
class="text-muted-foreground"
>
{{ t('common.notSupportedYet') }}
</span>
<span
v-if="isFormatRecommended(format.format)"
class="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"
class="rounded bg-info/10 px-1.5 py-0.5 text-[10px] text-info"
>
{{ t('actionbar.recommended') }}
</span>
<span
v-else-if="isFormatNotRecommended(format.format)"
class="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300"
class="rounded bg-warning/10 px-1.5 py-0.5 text-[10px] text-warning"
>
{{ t('actionbar.notRecommended') }}
</span>
@@ -858,49 +835,43 @@ watch(
<!-- Resolution Selection -->
<div class="space-y-2">
<Label class="text-xs">{{ t('actionbar.videoResolution') }}</Label>
<Select
<NativeSelect
:model-value="selectedResolution"
@update:model-value="handleResolutionChange"
:disabled="!selectedFormat || availableResolutions.length === 0"
class="h-8 w-full text-xs"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue :placeholder="t('actionbar.selectResolution')" />
</SelectTrigger>
<SelectContent>
<SelectItem
<NativeSelectOption value="">{{ t('actionbar.selectResolution') }}</NativeSelectOption>
<NativeSelectOption
v-for="res in availableResolutions"
:key="`${res.width}x${res.height}`"
:value="`${res.width}x${res.height}`"
class="text-xs"
>
{{ res.width }} x {{ res.height }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
</div>
<!-- FPS Selection -->
<div class="space-y-2">
<Label class="text-xs">{{ t('actionbar.videoFps') }}</Label>
<Select
<NativeSelect
:model-value="String(selectedFps)"
@update:model-value="handleFpsChange"
:disabled="!selectedResolution || availableFps.length === 0"
class="h-8 w-full text-xs"
>
<SelectTrigger class="h-8 text-xs">
<SelectValue :placeholder="t('actionbar.selectFps')" />
</SelectTrigger>
<SelectContent>
<SelectItem
<NativeSelectOption value="">{{ t('actionbar.selectFps') }}</NativeSelectOption>
<NativeSelectOption
v-for="fps in availableFps"
:key="fps"
:value="String(fps)"
class="text-xs"
>
{{ formatFpsLabel(fps) }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
</div>
<!-- Apply Button -->

View File

@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
import Keyboard from 'simple-keyboard'
import 'simple-keyboard/build/css/index.css'
import { hidApi } from '@/api'
import { Button } from '@/components/ui/button'
import { CanonicalKey } from '@/types/generated'
import {
keys,
@@ -608,39 +609,43 @@ onUnmounted(() => {
@touchstart="startDrag"
>
<div class="vkb-header-left">
<button class="vkb-btn" @click="toggleAttached">
<Button variant="ghost" size="sm" class="vkb-btn" @click="toggleAttached">
{{ isAttached ? t('virtualKeyboard.detach') : t('virtualKeyboard.attach') }}
</button>
</Button>
<div class="vkb-os-selector">
<button
<Button
v-for="os in (['windows', 'mac', 'android'] as KeyboardOsType[])"
:key="os"
class="vkb-os-btn"
variant="ghost"
size="sm"
:class="{ 'vkb-os-btn--active': selectedOs === os }"
@click.stop="switchOsLayout(os)"
>
{{ os === 'windows' ? 'Win' : os === 'mac' ? 'Mac' : 'Android' }}
</button>
</Button>
</div>
</div>
<span class="vkb-title">{{ t('virtualKeyboard.title') }}</span>
<button class="vkb-btn" @click="close">
<Button variant="ghost" size="sm" class="vkb-btn" @click="close">
{{ t('virtualKeyboard.hide') }}
</button>
</Button>
</div>
<!-- Keyboard body -->
<div class="vkb-body">
<!-- Media keys row -->
<div v-if="props.consumerEnabled !== false" class="vkb-media-row">
<button
<Button
v-for="key in mediaKeys"
:key="key"
class="vkb-media-btn"
variant="outline"
size="sm"
@click="onMediaKeyPress(key)"
>
{{ mediaKeyLabels[key] || key }}
</button>
</Button>
</div>
<div class="vkb-keyboards">
<div :id="`${keyboardId}-main`" class="kb-main-container"></div>
@@ -660,12 +665,12 @@ onUnmounted(() => {
.vkb .simple-keyboard .hg-button {
height: 36px;
border-radius: 6px;
background: var(--keyboard-button-bg, white);
color: var(--keyboard-button-color, #1f2937);
border: 1px solid var(--keyboard-button-border, #e5e7eb);
border-radius: var(--radius-md);
background: var(--keyboard-button-bg);
color: var(--keyboard-button-color);
border: 1px solid var(--keyboard-button-border);
border-bottom-width: 2px;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
box-shadow: 0 1px 2px 0 color-mix(in oklch, var(--foreground) 5%, transparent);
font-size: 12px;
font-weight: 500;
padding: 0 6px;
@@ -678,12 +683,12 @@ onUnmounted(() => {
}
.vkb .simple-keyboard .hg-button:hover {
background: var(--keyboard-button-hover-bg, #f3f4f6);
background: var(--keyboard-button-hover-bg);
}
.vkb .simple-keyboard .hg-button:active {
background: #3b82f6;
color: white;
background: var(--primary);
color: var(--primary-foreground);
border-bottom-width: 1px;
margin-top: 1px;
}
@@ -706,10 +711,10 @@ onUnmounted(() => {
/* Pressed keys */
.vkb .simple-keyboard .hg-button.down-key {
background: #3b82f6;
color: white;
background: var(--primary);
color: var(--primary-foreground);
font-weight: 600;
border-color: #2563eb;
border-color: var(--primary);
}
/* Space bar */
@@ -843,17 +848,17 @@ onUnmounted(() => {
:root.dark .hg-theme-default .hg-button,
html.dark .hg-theme-default .hg-button,
.dark .hg-theme-default .hg-button {
background: #374151 !important;
color: #f9fafb !important;
border-color: #4b5563 !important;
border-bottom-color: #4b5563 !important;
background: var(--keyboard-button-bg) !important;
color: var(--keyboard-button-color) !important;
border-color: var(--keyboard-button-border) !important;
border-bottom-color: var(--keyboard-button-border) !important;
box-shadow: none !important;
}
:root.dark .hg-theme-default .hg-button:hover,
html.dark .hg-theme-default .hg-button:hover,
.dark .hg-theme-default .hg-button:hover {
background: #4b5563 !important;
background: var(--keyboard-button-hover-bg) !important;
}
:root.dark .hg-theme-default .hg-button:active,
@@ -862,17 +867,17 @@ html.dark .hg-theme-default .hg-button:active,
:root.dark .hg-theme-default .hg-button.hg-activeButton,
html.dark .hg-theme-default .hg-button.hg-activeButton,
.dark .hg-theme-default .hg-button.hg-activeButton {
background: #3b82f6 !important;
color: white !important;
background: var(--primary) !important;
color: var(--primary-foreground) !important;
}
:root.dark .hg-theme-default .hg-button.down-key,
html.dark .hg-theme-default .hg-button.down-key,
.dark .hg-theme-default .hg-button.down-key {
background: #3b82f6 !important;
color: white !important;
border-color: #2563eb !important;
border-bottom-color: #2563eb !important;
background: var(--primary) !important;
color: var(--primary-foreground) !important;
border-color: var(--primary) !important;
border-bottom-color: var(--primary) !important;
}
</style>
@@ -880,13 +885,13 @@ html.dark .hg-theme-default .hg-button.down-key,
.vkb {
z-index: 100;
user-select: none;
background: white;
border: 1px solid #e5e7eb;
background: var(--background);
border: 1px solid var(--border);
}
:global(.dark .vkb) {
background: #1f2937;
border-color: #374151;
background: var(--background);
border-color: var(--border);
}
.vkb--attached {
@@ -904,8 +909,8 @@ html.dark .hg-theme-default .hg-button.down-key,
min-width: 1200px;
max-width: 1600px;
width: auto;
border-radius: 8px;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
border-radius: var(--radius-lg);
box-shadow: 0 25px 50px -12px color-mix(in oklch, var(--foreground) 25%, transparent);
}
.vkb--dragging {
@@ -918,20 +923,20 @@ html.dark .hg-theme-default .hg-button.down-key,
align-items: center;
justify-content: space-between;
padding: 4px 8px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
border-bottom: 1px solid var(--border);
background: var(--muted);
min-height: 28px;
position: relative;
}
:global(.dark .vkb-header) {
background: #111827;
border-color: #374151;
background: var(--muted);
border-color: var(--border);
}
.vkb--floating .vkb-header {
cursor: move;
border-radius: 8px 8px 0 0;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.vkb-header-left {
@@ -946,84 +951,84 @@ html.dark .hg-theme-default .hg-button.down-key,
transform: translateX(-50%);
font-size: 12px;
font-weight: 500;
color: #374151;
color: var(--foreground);
}
:global(.dark .vkb-title) {
color: #d1d5db;
color: var(--foreground);
}
.vkb-btn {
padding: 2px 8px;
font-size: 11px;
font-weight: 500;
color: #374151;
background: white;
border: 1px solid #d1d5db;
border-radius: 4px;
color: var(--foreground);
background: var(--background);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
line-height: 1.4;
}
.vkb-btn:hover {
background: #f3f4f6;
background: var(--accent);
}
:global(.dark .vkb-btn) {
color: #d1d5db;
background: #374151;
border-color: #4b5563;
color: var(--foreground);
background: var(--background);
border-color: var(--border);
}
:global(.dark .vkb-btn:hover) {
background: #4b5563;
background: var(--accent);
}
/* OS selector */
.vkb-os-selector {
display: flex;
gap: 2px;
background: #e5e7eb;
border-radius: 4px;
background: var(--muted);
border-radius: var(--radius-sm);
padding: 2px;
}
:global(.dark .vkb-os-selector) {
background: #374151;
background: var(--muted);
}
.vkb-os-btn {
padding: 2px 8px;
font-size: 10px;
font-weight: 500;
color: #6b7280;
color: var(--muted-foreground);
background: transparent;
border: none;
border-radius: 3px;
border-radius: var(--radius-sm);
cursor: pointer;
}
.vkb-os-btn:hover {
color: #374151;
color: var(--foreground);
}
.vkb-os-btn--active {
background: white;
color: #374151;
box-shadow: 0 1px 2px rgb(0 0 0 / 0.1);
background: var(--background);
color: var(--foreground);
box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 10%, transparent);
}
:global(.dark .vkb-os-btn) {
color: #9ca3af;
color: var(--muted-foreground);
}
:global(.dark .vkb-os-btn:hover) {
color: #d1d5db;
color: var(--foreground);
}
:global(.dark .vkb-os-btn--active) {
background: #4b5563;
color: #f9fafb;
background: var(--background);
color: var(--foreground);
}
/* Keyboard body */
@@ -1032,15 +1037,15 @@ html.dark .hg-theme-default .hg-button.down-key,
flex-direction: column;
padding: 8px;
gap: 8px;
background: #f3f4f6;
background: color-mix(in oklch, var(--muted) 65%, var(--background));
}
:global(.dark .vkb-body) {
background: #111827;
background: color-mix(in oklch, var(--muted) 65%, var(--background));
}
.vkb--floating .vkb-body {
border-radius: 0 0 8px 8px;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
/* Media keys row */
@@ -1049,40 +1054,40 @@ html.dark .hg-theme-default .hg-button.down-key,
gap: 4px;
justify-content: center;
padding-bottom: 4px;
border-bottom: 1px solid #e5e7eb;
border-bottom: 1px solid var(--border);
}
:global(.dark .vkb-media-row) {
border-color: #374151;
border-color: var(--border);
}
.vkb-media-btn {
padding: 4px 12px;
font-size: 16px;
background: white;
border: 1px solid #d1d5db;
border-radius: 6px;
background: var(--background);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
min-width: 40px;
}
.vkb-media-btn:hover {
background: #f3f4f6;
background: var(--accent);
}
.vkb-media-btn:active {
background: #3b82f6;
color: white;
background: var(--primary);
color: var(--primary-foreground);
}
:global(.dark .vkb-media-btn) {
background: #374151;
border-color: #4b5563;
color: #f9fafb;
background: var(--background);
border-color: var(--border);
color: var(--foreground);
}
:global(.dark .vkb-media-btn:hover) {
background: #4b5563;
background: var(--accent);
}
/* Keyboards container */

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { AlertDialogEmits, AlertDialogProps } from "reka-ui"
import { AlertDialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<AlertDialogProps>()
const emits = defineEmits<AlertDialogEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AlertDialogRoot v-slot="slotProps" data-slot="alert-dialog" v-bind="forwarded">
<slot v-bind="slotProps" />
</AlertDialogRoot>
</template>

View File

@@ -1,14 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { AlertDialogAction, type AlertDialogActionProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import type { AlertDialogActionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { AlertDialogAction } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<AlertDialogAction :class="cn(buttonVariants(), props.class)">
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
<slot />
</AlertDialogAction>
</template>

View File

@@ -1,14 +1,25 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { AlertDialogCancel, type AlertDialogCancelProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import type { AlertDialogCancelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { AlertDialogCancel } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<AlertDialogCancel :class="cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)">
<AlertDialogCancel
v-bind="delegatedProps"
:class="cn(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
props.class,
)"
>
<slot />
</AlertDialogCancel>
</template>

View File

@@ -1,30 +1,42 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { AlertDialogContentEmits, AlertDialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
AlertDialogContent,
AlertDialogOverlay,
AlertDialogPortal,
type AlertDialogContentEmits,
type AlertDialogContentProps,
} from 'reka-ui'
import { cn } from '@/lib/utils'
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes['class'] }>()
defineOptions({
inheritAttrs: false,
})
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<AlertDialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<AlertDialogPortal>
<AlertDialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
data-slot="alert-dialog-overlay"
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
/>
<AlertDialogContent
v-bind="props"
:class="cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class
)"
@escape-key-down="emits('escapeKeyDown', $event)"
data-slot="alert-dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class,
)
"
>
<slot />
</AlertDialogContent>

View File

@@ -1,13 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { AlertDialogDescription, type AlertDialogDescriptionProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import type { AlertDialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
AlertDialogDescription,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<AlertDialogDescription :class="cn('text-sm text-muted-foreground', props.class)">
<AlertDialogDescription
data-slot="alert-dialog-description"
v-bind="delegatedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</AlertDialogDescription>
</template>

View File

@@ -1,14 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes['class']
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', props.class)">
<div
data-slot="alert-dialog-footer"
:class="
cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@@ -1,14 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes['class']
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('flex flex-col space-y-2 text-center sm:text-left', props.class)">
<div
data-slot="alert-dialog-header"
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View File

@@ -1,13 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { AlertDialogTitle, type AlertDialogTitleProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import type { AlertDialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { AlertDialogTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<AlertDialogTitle :class="cn('text-lg font-semibold', props.class)">
<AlertDialogTitle
data-slot="alert-dialog-title"
v-bind="delegatedProps"
:class="cn('text-lg font-semibold', props.class)"
>
<slot />
</AlertDialogTitle>
</template>

View File

@@ -1,11 +1,8 @@
export {
AlertDialogRoot as AlertDialog,
AlertDialogTrigger,
} from 'reka-ui'
export { default as AlertDialogContent } from './AlertDialogContent.vue'
export { default as AlertDialogHeader } from './AlertDialogHeader.vue'
export { default as AlertDialogFooter } from './AlertDialogFooter.vue'
export { default as AlertDialogTitle } from './AlertDialogTitle.vue'
export { default as AlertDialogDescription } from './AlertDialogDescription.vue'
export { default as AlertDialogAction } from './AlertDialogAction.vue'
export { default as AlertDialogCancel } from './AlertDialogCancel.vue'
export { default as AlertDialog } from "./AlertDialog.vue"
export { default as AlertDialogAction } from "./AlertDialogAction.vue"
export { default as AlertDialogCancel } from "./AlertDialogCancel.vue"
export { default as AlertDialogContent } from "./AlertDialogContent.vue"
export { default as AlertDialogDescription } from "./AlertDialogDescription.vue"
export { default as AlertDialogFooter } from "./AlertDialogFooter.vue"
export { default as AlertDialogHeader } from "./AlertDialogHeader.vue"
export { default as AlertDialogTitle } from "./AlertDialogTitle.vue"

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { AlertVariants } from "."
import { cn } from "@/lib/utils"
import { alertVariants } from "."
const props = defineProps<{
class?: HTMLAttributes["class"]
variant?: AlertVariants["variant"]
}>()
</script>
<template>
<div
data-slot="alert"
:class="cn(alertVariants({ variant }), props.class)"
role="alert"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="alert-description"
:class="cn('text-muted-foreground col-start-2 text-sm [&_p]:leading-relaxed', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="alert-title"
:class="cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,30 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Alert } from "./Alert.vue"
export { default as AlertDescription } from "./AlertDescription.vue"
export { default as AlertTitle } from "./AlertTitle.vue"
export const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
success:
"border-success/35 bg-success/10 text-success [&>svg]:text-current *:data-[slot=alert-description]:text-success/90",
warning:
"border-warning/35 bg-warning/10 text-warning [&>svg]:text-current *:data-[slot=alert-description]:text-warning/90",
info:
"border-info/35 bg-info/10 text-info [&>svg]:text-current *:data-[slot=alert-description]:text-info/90",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type AlertVariants = VariantProps<typeof alertVariants>

View File

@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority"
export { default as Badge } from "./Badge.vue"
export const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none 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 transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
@@ -14,6 +14,12 @@ export const badgeVariants = cva(
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
success:
"border-transparent bg-success text-success-foreground [a&]:hover:bg-success/90 focus-visible:ring-success/20",
warning:
"border-transparent bg-warning text-warning-foreground [a&]:hover:bg-warning/90 focus-visible:ring-warning/20",
info:
"border-transparent bg-info text-info-foreground [a&]:hover:bg-info/90 focus-visible:ring-info/20",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { ButtonGroupVariants } from "."
import { cn } from "@/lib/utils"
import { buttonGroupVariants } from "."
const props = defineProps<{
class?: HTMLAttributes["class"]
orientation?: ButtonGroupVariants["orientation"]
}>()
</script>
<template>
<div
role="group"
data-slot="button-group"
:data-orientation="props.orientation"
:class="cn(buttonGroupVariants({ orientation: props.orientation }), props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,23 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as ButtonGroup } from "./ButtonGroup.vue"
export const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
},
)
export type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>

View File

@@ -20,6 +20,8 @@ const props = withDefaults(defineProps<Props>(), {
<template>
<Primitive
data-slot="button"
:data-variant="variant"
:data-size="size"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"

View File

@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority"
export { default as Button } from "./Button.vue"
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none 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",
{
variants: {
variant: {
@@ -22,9 +22,11 @@ export const buttonVariants = cva(
},
size: {
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
"xs": "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
"icon": "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui"
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<CollapsibleRootProps>()
const emits = defineEmits<CollapsibleRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<CollapsibleRoot
v-slot="slotProps"
data-slot="collapsible"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</CollapsibleRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { CollapsibleContentProps } from "reka-ui"
import { CollapsibleContent } from "reka-ui"
const props = defineProps<CollapsibleContentProps>()
</script>
<template>
<CollapsibleContent
data-slot="collapsible-content"
v-bind="props"
>
<slot />
</CollapsibleContent>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { CollapsibleTriggerProps } from "reka-ui"
import { CollapsibleTrigger } from "reka-ui"
const props = defineProps<CollapsibleTriggerProps>()
</script>
<template>
<CollapsibleTrigger
data-slot="collapsible-trigger"
v-bind="props"
>
<slot />
</CollapsibleTrigger>
</template>

View File

@@ -0,0 +1,3 @@
export { default as Collapsible } from "./Collapsible.vue"
export { default as CollapsibleContent } from "./CollapsibleContent.vue"
export { default as CollapsibleTrigger } from "./CollapsibleTrigger.vue"

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import {
DialogClose,
DialogContent,

View File

@@ -1,8 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { DialogClose } from "reka-ui"
import { cn } from "@/lib/utils"
import { Button } from '@/components/ui/button'
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
showCloseButton?: boolean
}>(), {
showCloseButton: false,
})
</script>
<template>
@@ -11,5 +18,10 @@ const props = defineProps<{ class?: HTMLAttributes["class"] }>()
:class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)"
>
<slot />
<DialogClose v-if="showCloseButton" as-child>
<Button variant="outline">
Close
</Button>
</DialogClose>
</div>
</template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import {
DialogClose,
DialogContent,

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,

View File

@@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps)
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="cn('focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
:class="cn(`relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!`, props.class)"
>
<slot />
</DropdownMenuItem>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Circle } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,

View File

@@ -20,7 +20,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] max-w-(--reka-dropdown-menu-content-available-width) origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
>
<slot />
</DropdownMenuSubContent>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { DropdownMenuSubTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronRight } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSubTrigger,
useForwardProps,
@@ -19,8 +19,9 @@ const forwardedProps = useForwardProps(delegatedProps)
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:data-inset="inset ? '' : undefined"
:class="cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
`relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!`,
props.class,
)"
>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="empty"
:class="cn(
'flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance rounded-lg border-dashed p-6 text-center md:p-12',
props.class,
)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<p
data-slot="empty-description"
:class="cn(
'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
$attrs.class ?? '',
)"
>
<slot />
</p>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="empty-header"
:class="cn(
'flex max-w-sm flex-col items-center gap-2 text-center',
props.class,
)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { EmptyMediaVariants } from "."
import { cn } from "@/lib/utils"
import { emptyMediaVariants } from "."
const props = defineProps<{
class?: HTMLAttributes["class"]
variant?: EmptyMediaVariants["variant"]
}>()
</script>
<template>
<div
data-slot="empty-icon"
:data-variant="variant"
:class="cn(emptyMediaVariants({ variant }), props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,24 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Empty } from "./Empty.vue"
export { default as EmptyDescription } from "./EmptyDescription.vue"
export { default as EmptyHeader } from "./EmptyHeader.vue"
export { default as EmptyMedia } from "./EmptyMedia.vue"
export const emptyMediaVariants = cva(
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type EmptyMediaVariants = VariantProps<typeof emptyMediaVariants>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { FieldVariants } from "."
import { cn } from "@/lib/utils"
import { fieldVariants } from "."
const props = defineProps<{
class?: HTMLAttributes["class"]
orientation?: FieldVariants["orientation"]
}>()
</script>
<template>
<div
role="group"
data-slot="field"
:data-orientation="orientation"
:class="cn(
fieldVariants({ orientation }),
props.class,
)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="field-group"
:class="cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
props.class,
)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { Label } from '@/components/ui/label'
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<Label
data-slot="field-label"
:class="cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
props.class,
)"
>
<slot />
</Label>
</template>

View File

@@ -0,0 +1,32 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
},
)
export type FieldVariants = VariantProps<typeof fieldVariants>
export { default as Field } from "./Field.vue"
export { default as FieldGroup } from "./FieldGroup.vue"
export { default as FieldLabel } from "./FieldLabel.vue"

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { HoverCardRootEmits, HoverCardRootProps } from "reka-ui"
import { HoverCardRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<HoverCardRootProps>()
const emits = defineEmits<HoverCardRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<HoverCardRoot
v-slot="slotProps"
data-slot="hover-card"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</HoverCardRoot>
</template>

View File

@@ -1,28 +1,41 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { HoverCardContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
HoverCardContent,
HoverCardPortal,
type HoverCardContentProps,
} from 'reka-ui'
import { cn } from '@/lib/utils'
useForwardProps,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<HoverCardContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<HoverCardContentProps & { class?: HTMLAttributes["class"] }>(),
{
sideOffset: 4,
},
)
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<HoverCardPortal>
<HoverCardContent
v-bind="delegatedProps"
:class="cn(
'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class
)"
data-slot="hover-card-content"
v-bind="{ ...$attrs, ...forwardedProps }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border p-4 shadow-md outline-hidden',
props.class,
)
"
>
<slot />
</HoverCardContent>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HoverCardTriggerProps } from "reka-ui"
import { HoverCardTrigger } from "reka-ui"
const props = defineProps<HoverCardTriggerProps>()
</script>
<template>
<HoverCardTrigger
data-slot="hover-card-trigger"
v-bind="props"
>
<slot />
</HoverCardTrigger>
</template>

View File

@@ -1,5 +1,3 @@
export {
HoverCardRoot as HoverCard,
HoverCardTrigger,
} from 'reka-ui'
export { default as HoverCardContent } from './HoverCardContent.vue'
export { default as HoverCard } from "./HoverCard.vue"
export { default as HoverCardContent } from "./HoverCardContent.vue"
export { default as HoverCardTrigger } from "./HoverCardTrigger.vue"

View File

@@ -25,7 +25,7 @@ const modelValue = useVModel(props, "modelValue", emits, {
data-slot="input"
: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',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'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',
props.class,
)"

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import type { ItemVariants } from "."
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { itemVariants } from "."
const props = withDefaults(defineProps<PrimitiveProps & {
class?: HTMLAttributes["class"]
variant?: ItemVariants["variant"]
size?: ItemVariants["size"]
}>(), {
as: "div",
})
</script>
<template>
<Primitive
data-slot="item"
:data-variant="variant"
:data-size="size"
:as="as"
:as-child="asChild"
:class="cn(itemVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="item-content"
:class="cn('flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<p
data-slot="item-description"
:class="cn(
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
props.class,
)"
>
<slot />
</p>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="item-title"
:class="cn('flex w-fit items-center gap-2 text-sm leading-snug font-medium', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,48 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Item } from "./Item.vue"
export { default as ItemContent } from "./ItemContent.vue"
export { default as ItemDescription } from "./ItemDescription.vue"
export { default as ItemTitle } from "./ItemTitle.vue"
export const itemVariants = cva(
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "p-4 gap-4 ",
sm: "py-3 px-4 gap-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
{
variants: {
variant: {
default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type ItemVariants = VariantProps<typeof itemVariants>
export type ItemMediaVariants = VariantProps<typeof itemMediaVariants>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<kbd
:class="cn(
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
'[&_svg:not([class*=\'size-\'])]:size-3',
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
props.class,
)"
>
<slot />
</kbd>
</template>

View File

@@ -0,0 +1 @@
export { default as Kbd } from "./Kbd.vue"

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import type { AcceptableValue } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { ChevronDownIcon } from "lucide-vue-next"
import { reactiveOmit, useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = defineProps<{ modelValue?: AcceptableValue | AcceptableValue[], class?: HTMLAttributes["class"] }>()
const emit = defineEmits<{
"update:modelValue": [value: AcceptableValue]
}>()
const modelValue = useVModel(props, "modelValue", emit, {
passive: true,
defaultValue: "",
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<div
class="group/native-select relative w-full has-[select:disabled]:opacity-50"
data-slot="native-select-wrapper"
>
<select
v-bind="{ ...$attrs, ...delegatedProps }"
v-model="modelValue"
data-slot="native-select"
: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',
'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',
props.class,
)"
>
<slot />
</select>
<ChevronDownIcon
class="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
aria-hidden="true"
data-slot="native-select-icon"
/>
</div>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<option data-slot="native-select-option" :class="cn('bg-popover text-popover-foreground', props.class)">
<slot />
</option>
</template>

View File

@@ -0,0 +1,2 @@
export { default as NativeSelect } from "./NativeSelect.vue"
export { default as NativeSelectOption } from "./NativeSelectOption.vue"

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { PopoverRootEmits, PopoverRootProps } from "reka-ui"
import { PopoverRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<PopoverRootProps>()
const emits = defineEmits<PopoverRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<PopoverRoot
v-slot="slotProps"
data-slot="popover"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</PopoverRoot>
</template>

View File

@@ -1,28 +1,43 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { PopoverContentEmits, PopoverContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
PopoverContent,
PopoverPortal,
type PopoverContentProps,
} from 'reka-ui'
import { cn } from '@/lib/utils'
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<PopoverContentProps & { class?: HTMLAttributes["class"] }>(),
{
align: "center",
sideOffset: 4,
},
)
const emits = defineEmits<PopoverContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<PopoverPortal>
<PopoverContent
v-bind="delegatedProps"
:class="cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class
)"
data-slot="popover-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 max-w-(--reka-popover-content-available-width) rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
props.class,
)
"
>
<slot />
</PopoverContent>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { PopoverTriggerProps } from "reka-ui"
import { PopoverTrigger } from "reka-ui"
const props = defineProps<PopoverTriggerProps>()
</script>
<template>
<PopoverTrigger
data-slot="popover-trigger"
v-bind="props"
>
<slot />
</PopoverTrigger>
</template>

View File

@@ -1,5 +1,3 @@
export {
PopoverRoot as Popover,
PopoverTrigger,
} from 'reka-ui'
export { default as PopoverContent } from './PopoverContent.vue'
export { default as Popover } from "./Popover.vue"
export { default as PopoverContent } from "./PopoverContent.vue"
export { default as PopoverTrigger } from "./PopoverTrigger.vue"

View File

@@ -15,9 +15,11 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template>
<RadioGroupRoot
:class="cn('grid gap-2', props.class)"
v-slot="slotProps"
data-slot="radio-group"
:class="cn('grid gap-3', props.class)"
v-bind="forwarded"
>
<slot />
<slot v-bind="slotProps" />
</RadioGroupRoot>
</template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { RadioGroupItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { CircleIcon } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import {
RadioGroupIndicator,
RadioGroupItem,
@@ -19,16 +19,22 @@ const forwardedProps = useForwardProps(delegatedProps)
<template>
<RadioGroupItem
data-slot="radio-group-item"
v-bind="forwardedProps"
:class="
cn(
'peer aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"
>
<RadioGroupIndicator class="flex items-center justify-center">
<Check class="h-3.5 w-3.5 text-primary" />
<RadioGroupIndicator
data-slot="radio-group-indicator"
class="relative flex items-center justify-center"
>
<slot>
<CircleIcon class="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</slot>
</RadioGroupIndicator>
</RadioGroupItem>
</template>

View File

@@ -1,40 +1,33 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { ScrollAreaRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
type ScrollAreaRootProps,
} from 'reka-ui'
import { cn } from '@/lib/utils'
} from "reka-ui"
import { cn } from "@/lib/utils"
import ScrollBar from "./ScrollBar.vue"
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<ScrollAreaRoot v-bind="delegatedProps" :class="cn('relative overflow-hidden', props.class)">
<ScrollAreaViewport class="h-full w-full rounded-[inherit]">
<ScrollAreaRoot
data-slot="scroll-area"
v-bind="delegatedProps"
:class="cn('relative', props.class)"
>
<ScrollAreaViewport
data-slot="scroll-area-viewport"
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-3 focus-visible:outline-1"
>
<slot />
</ScrollAreaViewport>
<ScrollAreaScrollbar
class="flex touch-none select-none transition-colors data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:border-t data-[orientation=vertical]:border-l data-[orientation=horizontal]:border-t-transparent data-[orientation=vertical]:border-l-transparent data-[orientation=horizontal]:p-[1px] data-[orientation=vertical]:p-[1px]"
orientation="vertical"
>
<ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
</ScrollAreaScrollbar>
<ScrollAreaScrollbar
class="flex touch-none select-none transition-colors data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:border-t data-[orientation=vertical]:border-l data-[orientation=horizontal]:border-t-transparent data-[orientation=vertical]:border-l-transparent data-[orientation=horizontal]:p-[1px] data-[orientation=vertical]:p-[1px]"
orientation="horizontal"
>
<ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
</ScrollAreaScrollbar>
<ScrollBar />
<ScrollAreaCorner />
</ScrollAreaRoot>
</template>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { ScrollAreaScrollbarProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes["class"] }>(), {
orientation: "vertical",
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
v-bind="delegatedProps"
:class="
cn('flex touch-none p-px transition-colors select-none',
orientation === 'vertical'
&& 'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal'
&& 'h-2.5 flex-col border-t border-t-transparent',
props.class)"
>
<ScrollAreaThumb
data-slot="scroll-area-thumb"
class="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaScrollbar>
</template>

View File

@@ -1 +1,2 @@
export { default as ScrollArea } from './ScrollArea.vue'
export { default as ScrollArea } from "./ScrollArea.vue"
export { default as ScrollBar } from "./ScrollBar.vue"

View File

@@ -9,7 +9,11 @@ const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<SelectRoot v-bind="forwarded">
<slot />
<SelectRoot
v-slot="slotProps"
data-slot="select"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</SelectRoot>
</template>

View File

@@ -31,8 +31,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template>
<SelectPortal>
<SelectContent
v-bind="{ ...forwarded, ...$attrs }" :class="cn(
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
data-slot="select-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper'
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class,
@@ -40,7 +42,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
"
>
<SelectScrollUpButton />
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-[--reka-select-trigger-height] w-full min-w-[--reka-select-trigger-width]')">
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-(--reka-select-trigger-height) w-full min-w-(--reka-select-trigger-width) scroll-my-1')">
<slot />
</SelectViewport>
<SelectScrollDownButton />

View File

@@ -1,17 +1,15 @@
<script setup lang="ts">
import type { SelectGroupProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { SelectGroup } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const props = defineProps<SelectGroupProps>()
</script>
<template>
<SelectGroup :class="cn('p-1 w-full', props.class)" v-bind="delegatedProps">
<SelectGroup
data-slot="select-group"
v-bind="props"
>
<slot />
</SelectGroup>
</template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { SelectItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import {
SelectItem,
SelectItemIndicator,
@@ -20,17 +20,20 @@ const forwardedProps = useForwardProps(delegatedProps)
<template>
<SelectItem
data-slot="select-item"
v-bind="forwardedProps"
:class="
cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
props.class,
)
"
>
<span class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<span class="absolute right-2 flex size-3.5 items-center justify-center">
<SelectItemIndicator>
<Check class="h-4 w-4" />
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</SelectItemIndicator>
</span>

View File

@@ -6,7 +6,10 @@ const props = defineProps<SelectItemTextProps>()
</script>
<template>
<SelectItemText v-bind="props">
<SelectItemText
data-slot="select-item-text"
v-bind="props"
>
<slot />
</SelectItemText>
</template>

View File

@@ -8,7 +8,10 @@ const props = defineProps<SelectLabelProps & { class?: HTMLAttributes["class"] }
</script>
<template>
<SelectLabel :class="cn('px-2 py-1.5 text-sm font-semibold', props.class)">
<SelectLabel
data-slot="select-label"
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
>
<slot />
</SelectLabel>
</template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { SelectScrollDownButtonProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronDown } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import { SelectScrollDownButton, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
@@ -14,9 +14,13 @@ const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollDownButton v-bind="forwardedProps" :class="cn('flex cursor-default items-center justify-center py-1', props.class)">
<SelectScrollDownButton
data-slot="select-scroll-down-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronDown />
<ChevronDown class="size-4" />
</slot>
</SelectScrollDownButton>
</template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { SelectScrollUpButtonProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronUp } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import { SelectScrollUpButton, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
@@ -14,9 +14,13 @@ const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollUpButton v-bind="forwardedProps" :class="cn('flex cursor-default items-center justify-center py-1', props.class)">
<SelectScrollUpButton
data-slot="select-scroll-up-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronUp />
<ChevronUp class="size-4" />
</slot>
</SelectScrollUpButton>
</template>

View File

@@ -11,5 +11,9 @@ const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<SelectSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
<SelectSeparator
data-slot="select-separator"
v-bind="delegatedProps"
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
/>
</template>

View File

@@ -1,29 +1,33 @@
<script setup lang="ts">
import type { SelectTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronDown } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const props = withDefaults(
defineProps<SelectTriggerProps & { class?: HTMLAttributes["class"], size?: "sm" | "default" }>(),
{ size: "default" },
)
const delegatedProps = reactiveOmit(props, "class", "size")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectTrigger
data-slot="select-trigger"
:data-size="size"
v-bind="forwardedProps"
:class="cn(
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)"
>
<slot />
<SelectIcon as-child>
<ChevronDown class="w-4 h-4 opacity-50 shrink-0" />
<ChevronDown class="size-4 opacity-50" />
</SelectIcon>
</SelectTrigger>
</template>

View File

@@ -6,7 +6,10 @@ const props = defineProps<SelectValueProps>()
</script>
<template>
<SelectValue v-bind="props">
<SelectValue
data-slot="select-value"
v-bind="props"
>
<slot />
</SelectValue>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot
v-slot="slotProps"
data-slot="sheet"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DialogRoot>
</template>

View File

@@ -1,69 +1,60 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { X } from "lucide-vue-next"
import { reactiveOmit } from "@vueuse/core"
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
type DialogContentProps,
type DialogContentEmits,
} from 'reka-ui'
import { X } from 'lucide-vue-next'
import { type VariantProps, cva } from 'class-variance-authority'
import { cn } from '@/lib/utils'
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import SheetOverlay from "./SheetOverlay.vue"
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
},
)
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes["class"]
side?: "top" | "right" | "bottom" | "left"
}
type SheetVariants = VariantProps<typeof sheetVariants>
const props = defineProps<DialogContentProps & {
class?: HTMLAttributes['class']
side?: SheetVariants['side']
}>()
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SheetContentProps>(), {
side: "right",
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => {
const { class: _, side: __, ...delegated } = props
return delegated
})
const delegatedProps = reactiveOmit(props, "class", "side")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<SheetOverlay />
<DialogContent
v-bind="delegatedProps"
:class="cn(sheetVariants({ side: props.side }), props.class)"
@escape-key-down="emits('escapeKeyDown', $event)"
@pointer-down-outside="emits('pointerDownOutside', $event)"
@focus-outside="emits('focusOutside', $event)"
@interact-outside="emits('interactOutside', $event)"
data-slot="sheet-content"
:class="cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right'
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left'
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top'
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom'
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
props.class)"
v-bind="{ ...$attrs, ...forwarded }"
>
<slot />
<DialogClose
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
>
<X class="w-4 h-4" />
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>

View File

@@ -1,13 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { DialogDescription, type DialogDescriptionProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogDescription :class="cn('text-sm text-muted-foreground', props.class)">
<DialogDescription
data-slot="sheet-description"
:class="cn('text-muted-foreground text-sm', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div :class="cn('flex flex-col space-y-2 text-center sm:text-left', props.class)">
<div
data-slot="sheet-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogOverlay
data-slot="sheet-overlay"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogOverlay>
</template>

View File

@@ -1,13 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { DialogTitle, type DialogTitleProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogTitle :class="cn('text-lg font-semibold text-foreground', props.class)">
<DialogTitle
data-slot="sheet-title"
:class="cn('text-foreground font-semibold', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>

View File

@@ -1,9 +1,5 @@
export {
DialogRoot as Sheet,
DialogTrigger as SheetTrigger,
DialogClose as SheetClose,
} from 'reka-ui'
export { default as SheetContent } from './SheetContent.vue'
export { default as SheetHeader } from './SheetHeader.vue'
export { default as SheetTitle } from './SheetTitle.vue'
export { default as SheetDescription } from './SheetDescription.vue'
export { default as Sheet } from "./Sheet.vue"
export { default as SheetContent } from "./SheetContent.vue"
export { default as SheetDescription } from "./SheetDescription.vue"
export { default as SheetHeader } from "./SheetHeader.vue"
export { default as SheetTitle } from "./SheetTitle.vue"

View File

@@ -0,0 +1,96 @@
<script setup lang="ts">
import type { SidebarProps } from "."
import { cn } from "@/lib/utils"
import { Sheet, SheetContent } from '@/components/ui/sheet'
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
import SheetHeader from '@/components/ui/sheet/SheetHeader.vue'
import SheetTitle from '@/components/ui/sheet/SheetTitle.vue'
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from "./utils"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SidebarProps>(), {
side: "left",
variant: "sidebar",
collapsible: "offcanvas",
})
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
</script>
<template>
<div
v-if="collapsible === 'none'"
data-slot="sidebar"
:class="cn('bg-sidebar text-sidebar-foreground flex w-(--sidebar-width) flex-col', props.class)"
v-bind="$attrs"
>
<slot />
</div>
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
:side="side"
class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
:style="{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
}"
>
<SheetHeader class="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div class="flex h-full w-full flex-col">
<slot />
</div>
</SheetContent>
</Sheet>
<div
v-else
class="group peer text-sidebar-foreground hidden md:block"
data-slot="sidebar"
:data-state="state"
:data-collapsible="state === 'collapsed' ? collapsible : ''"
:data-variant="variant"
:data-side="side"
>
<!-- This is what handles the sidebar gap on desktop -->
<div
:class="cn(
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
)"
/>
<div
:class="cn(
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
props.class,
)"
v-bind="$attrs"
>
<div
data-sidebar="sidebar"
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
<slot />
</div>
</div>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More