WIP - UI drawer initial

This commit is contained in:
Francisco Gaona
2026-01-07 22:11:36 +01:00
parent b34da6956c
commit 8ad3fac1b0
4 changed files with 169 additions and 13 deletions

View File

@@ -21,7 +21,7 @@ const handleSend = () => {
</script>
<template>
<div class="ai-chat-area sticky bottom-0 z-20 bg-background border-t border-border p-4 bg-neutral-50">
<div class="ai-chat-area w-full border-t border-border p-4 bg-neutral-50">
<InputGroup>
<InputGroupTextarea
v-model="chatInput"
@@ -50,8 +50,6 @@ const handleSend = () => {
<style scoped>
.ai-chat-area {
height: calc(100vh / 6);
min-height: 140px;
max-height: 200px;
min-height: 120px;
}
</style>

View File

@@ -22,12 +22,19 @@ import { useSoftphone } from '~/composables/useSoftphone'
const { logout } = useAuth()
const { api } = useApi()
const isDrawerOpen = useState<boolean>('bottomDrawerOpen', () => false)
const drawerTab = useState<string>('bottomDrawerTab', () => 'softphone')
const softphone = useSoftphone()
const handleLogout = async () => {
await logout()
}
const openSoftphoneDrawer = () => {
drawerTab.value = 'softphone'
isDrawerOpen.value = true
}
// Check if user is central admin (by checking if we're on a central subdomain)
// Use ref instead of computed to avoid hydration mismatch
const isCentralAdmin = ref(false)
@@ -336,10 +343,13 @@ const centralAdminMenuItems: Array<{
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem v-if="!isCentralAdmin">
<SidebarMenuButton @click="softphone.open" class="cursor-pointer hover:bg-accent">
<SidebarMenuButton @click="openSoftphoneDrawer" class="cursor-pointer hover:bg-accent">
<Phone class="h-4 w-4" />
<span>Softphone</span>
<span v-if="softphone.hasIncomingCall.value" class="ml-auto h-2 w-2 rounded-full bg-red-500 animate-pulse"></span>
<span
v-if="softphone.hasIncomingCall.value"
class="ml-auto h-2 w-2 rounded-full bg-red-500 animate-pulse"
/>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>

View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import AIChatBar from '@/components/AIChatBar.vue'
import { Phone, Sparkles, X } from 'lucide-vue-next'
const isDrawerOpen = useState<boolean>('bottomDrawerOpen', () => false)
const activeTab = useState<string>('bottomDrawerTab', () => 'softphone')
const drawerHeight = ref(240)
const minHeight = 160
const maxHeight = ref(480)
const isResizing = ref(false)
const resizeStartY = ref(0)
const resizeStartHeight = ref(0)
const softphoneEnabled = ref(false)
const softphoneConnected = ref(false)
const statusLabel = computed(() => (softphoneConnected.value ? 'Connected' : 'Offline'))
const clampHeight = (height: number) => Math.min(Math.max(height, minHeight), maxHeight.value)
const updateMaxHeight = () => {
if (!process.client) return
maxHeight.value = Math.round(window.innerHeight * 0.6)
drawerHeight.value = clampHeight(drawerHeight.value)
}
const startResize = (event: MouseEvent | TouchEvent) => {
isResizing.value = true
resizeStartY.value = 'touches' in event ? event.touches[0].clientY : event.clientY
resizeStartHeight.value = drawerHeight.value
}
const handleResize = (event: MouseEvent | TouchEvent) => {
if (!isResizing.value) return
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY
const delta = resizeStartY.value - clientY
drawerHeight.value = clampHeight(resizeStartHeight.value + delta)
}
const stopResize = () => {
isResizing.value = false
}
const closeDrawer = () => {
isDrawerOpen.value = false
}
watch(softphoneEnabled, (enabled) => {
if (!enabled) {
softphoneConnected.value = false
return
}
softphoneConnected.value = true
})
onMounted(() => {
updateMaxHeight()
window.addEventListener('mousemove', handleResize)
window.addEventListener('mouseup', stopResize)
window.addEventListener('touchmove', handleResize, { passive: true })
window.addEventListener('touchend', stopResize)
window.addEventListener('resize', updateMaxHeight)
})
onBeforeUnmount(() => {
window.removeEventListener('mousemove', handleResize)
window.removeEventListener('mouseup', stopResize)
window.removeEventListener('touchmove', handleResize)
window.removeEventListener('touchend', stopResize)
window.removeEventListener('resize', updateMaxHeight)
})
</script>
<template>
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-30 flex justify-center px-4 pb-4">
<div
class="pointer-events-auto w-full max-w-5xl rounded-xl border border-border bg-background shadow-xl"
:class="isDrawerOpen ? 'opacity-100' : 'opacity-0 translate-y-6 pointer-events-none'"
:style="{ height: `${drawerHeight}px` }"
>
<div class="flex items-center justify-between border-b border-border px-4 py-2">
<div
class="flex h-6 flex-1 cursor-row-resize items-center justify-center"
@mousedown="startResize"
@touchstart.prevent="startResize"
>
<span class="h-1.5 w-12 rounded-full bg-muted" />
</div>
<Button variant="ghost" size="icon" class="ml-3" @click="closeDrawer">
<X class="h-4 w-4" />
<span class="sr-only">Close drawer</span>
</Button>
</div>
<Tabs v-model="activeTab" class="flex h-[calc(100%-48px)] flex-col">
<TabsList class="mx-4 mt-3 grid w-fit grid-cols-2">
<TabsTrigger value="softphone" class="flex items-center gap-2">
<Phone class="h-4 w-4" />
Softphone
<span
class="inline-flex h-2 w-2 rounded-full"
:class="softphoneConnected ? 'bg-emerald-500' : 'bg-muted-foreground/40'"
/>
</TabsTrigger>
<TabsTrigger value="ai" class="flex items-center gap-2">
<Sparkles class="h-4 w-4" />
AI Agent
</TabsTrigger>
</TabsList>
<div class="flex-1 overflow-hidden">
<TabsContent value="softphone" class="h-full">
<div class="flex h-full flex-col gap-4 px-6 pb-6 pt-4">
<div class="flex items-center justify-between rounded-lg border border-border bg-muted/30 px-4 py-3">
<div>
<Label class="text-sm font-medium">Softphone availability</Label>
<p class="text-xs text-muted-foreground">
Turn on to initialize the softphone connection.
</p>
</div>
<Switch v-model:checked="softphoneEnabled" />
</div>
<div class="flex items-center gap-3 text-sm text-muted-foreground">
<span
class="inline-flex h-2.5 w-2.5 rounded-full"
:class="softphoneConnected ? 'bg-emerald-500' : 'bg-muted-foreground/40'"
/>
<span>{{ statusLabel }}</span>
</div>
<div class="flex-1 rounded-lg border border-dashed border-border bg-muted/20 p-4 text-sm text-muted-foreground">
Softphone controls will appear here.
</div>
</div>
</TabsContent>
<TabsContent value="ai" class="h-full">
<div class="flex h-full flex-col justify-end">
<AIChatBar />
</div>
</TabsContent>
</div>
</Tabs>
</div>
</div>
</template>

View File

@@ -1,8 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import AppSidebar from '@/components/AppSidebar.vue'
import AIChatBar from '@/components/AIChatBar.vue'
import SoftphoneDialog from '@/components/SoftphoneDialog.vue'
import BottomDrawer from '@/components/BottomDrawer.vue'
import {
Breadcrumb,
BreadcrumbItem,
@@ -74,11 +73,7 @@ const breadcrumbs = computed(() => {
<slot />
</div>
<!-- AI Chat Bar Component -->
<AIChatBar />
<!-- Softphone Dialog (Global) -->
<SoftphoneDialog />
<BottomDrawer />
</SidebarInset>
</SidebarProvider>
</template>