feat: AgentBadge component, force mobile mode, and transcript UX improvements
- Extract agent badge into AgentBadge component with dropdown (TODO placeholder) - Realtime connection indicated by badge color (green=connected, indigo=disconnected) - Remove Transcript label and chat bubble icon from titlebar - Add force mobile mode button (bottom sheet panel on desktop) - Size toggle persisted in localStorage and controls sheet height in mobile mode - Replace full titlebar drag with thin 5px top edge grip - Remove sheet-handle touch bar, size controlled via toggle button only - Large mobile mode respects app header height - Slide-up/down animation for mobile panel enter/exit
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useTranscriptDebug } from '@/composables/transcript-debug'
|
||||
import { ChatContainer, AquaticBackground } from '@/components/transcript-debug'
|
||||
import { ChatContainer, AquaticBackground, AgentBadge } from '@/components/transcript-debug'
|
||||
import type { AgentName } from '@/types/transcript-debug'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -56,7 +56,7 @@ const isFocusWithin = ref(false)
|
||||
|
||||
const showChrome = computed(() =>
|
||||
isHovered.value || isFocusWithin.value || isDragging.value ||
|
||||
isResizing.value || showSelector.value || isDraggingSheet.value
|
||||
isResizing.value || showSelector.value
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
@@ -78,21 +78,41 @@ const zoom = ref(1)
|
||||
|
||||
// Size mode: pin (small, anchored to FAB), medium (default), large
|
||||
type SizeMode = 'pin' | 'medium' | 'large'
|
||||
const sizeMode = ref<SizeMode>('medium')
|
||||
const savedSize = localStorage.getItem('transcript-size-mode') as SizeMode | null
|
||||
const sizeMode = ref<SizeMode>(savedSize && ['pin', 'medium', 'large'].includes(savedSize) ? savedSize : 'medium')
|
||||
|
||||
// Force mobile (bottom sheet) mode on desktop
|
||||
const forceMobile = ref(false)
|
||||
const effectiveMobile = computed(() => isMobile.value || forceMobile.value)
|
||||
|
||||
function toggleForceMobile() {
|
||||
if (isMobile.value) return // already native mobile
|
||||
forceMobile.value = !forceMobile.value
|
||||
if (forceMobile.value) {
|
||||
// Enter mobile: set sheet height based on current sizeMode
|
||||
const mobileSnaps: Record<SizeMode, number> = { pin: 20, medium: 55, large: 100 }
|
||||
sheetHeight.value = mobileSnaps[sizeMode.value]
|
||||
hasCustomPosition.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function cycleSizeMode() {
|
||||
const modes: SizeMode[] = ['pin', 'medium', 'large']
|
||||
const i = modes.indexOf(sizeMode.value)
|
||||
sizeMode.value = modes[(i + 1) % modes.length]
|
||||
localStorage.setItem('transcript-size-mode', sizeMode.value)
|
||||
hasCustomPosition.value = false
|
||||
|
||||
// In mobile mode, also update sheet height
|
||||
if (effectiveMobile.value) {
|
||||
const mobileSnaps: Record<SizeMode, number> = { pin: 20, medium: 55, large: 100 }
|
||||
sheetHeight.value = mobileSnaps[sizeMode.value]
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile bottom sheet state
|
||||
const isMobile = ref(false)
|
||||
const sheetHeight = ref(55)
|
||||
const isDraggingSheet = ref(false)
|
||||
const sheetDragStart = ref({ y: 0, height: 0 })
|
||||
const snapPoints = [20, 55, 85]
|
||||
|
||||
// ============================================================================
|
||||
// MOBILE DETECTION
|
||||
@@ -104,78 +124,29 @@ function checkMobile() {
|
||||
isMobile.value = isTouchDevice && isSmallScreen
|
||||
}
|
||||
|
||||
function findNearestSnap(height: number): number {
|
||||
return snapPoints.reduce((prev, curr) =>
|
||||
Math.abs(curr - height) < Math.abs(prev - height) ? curr : prev
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MOBILE SHEET DRAG
|
||||
// ============================================================================
|
||||
|
||||
function startSheetDrag(e: TouchEvent) {
|
||||
if (!isMobile.value) return
|
||||
const touch = e.touches[0]
|
||||
if (!touch) return
|
||||
isDraggingSheet.value = true
|
||||
sheetDragStart.value = { y: touch.clientY, height: sheetHeight.value }
|
||||
}
|
||||
|
||||
function onSheetDrag(e: TouchEvent) {
|
||||
if (!isDraggingSheet.value || !isMobile.value) return
|
||||
const touch = e.touches[0]
|
||||
if (!touch) return
|
||||
const deltaY = sheetDragStart.value.y - touch.clientY
|
||||
const deltaPercent = (deltaY / window.innerHeight) * 100
|
||||
sheetHeight.value = Math.max(15, Math.min(90, sheetDragStart.value.height + deltaPercent))
|
||||
}
|
||||
|
||||
function stopSheetDrag() {
|
||||
if (!isDraggingSheet.value) return
|
||||
isDraggingSheet.value = false
|
||||
sheetHeight.value = findNearestSnap(sheetHeight.value)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WINDOW DRAG
|
||||
// ============================================================================
|
||||
|
||||
function startDrag(e: MouseEvent | TouchEvent) {
|
||||
if ((e.target as HTMLElement).closest('.window-controls')) return
|
||||
if ((e.target as HTMLElement).closest('.header-selector')) return
|
||||
|
||||
if (isMobile.value) {
|
||||
if (e instanceof TouchEvent) startSheetDrag(e)
|
||||
return
|
||||
}
|
||||
function startDrag(e: MouseEvent) {
|
||||
if (effectiveMobile.value) return
|
||||
|
||||
isDragging.value = true
|
||||
const rect = windowRef.value?.getBoundingClientRect()
|
||||
const touch = e instanceof TouchEvent ? e.touches[0] : null
|
||||
const clientX = e instanceof MouseEvent ? e.clientX : (touch?.clientX ?? 0)
|
||||
const clientY = e instanceof MouseEvent ? e.clientY : (touch?.clientY ?? 0)
|
||||
|
||||
if (rect) {
|
||||
if (!hasCustomPosition.value) {
|
||||
position.value = { x: rect.left, y: rect.top }
|
||||
}
|
||||
dragOffset.value = { x: clientX - rect.left, y: clientY - rect.top }
|
||||
dragOffset.value = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onDrag)
|
||||
document.addEventListener('mouseup', stopDrag)
|
||||
document.addEventListener('touchmove', onDrag, { passive: false })
|
||||
document.addEventListener('touchend', stopDrag)
|
||||
}
|
||||
|
||||
function onDrag(e: MouseEvent | TouchEvent) {
|
||||
function onDrag(e: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
if (e instanceof TouchEvent) e.preventDefault()
|
||||
|
||||
const touch = e instanceof TouchEvent ? e.touches[0] : null
|
||||
const clientX = e instanceof MouseEvent ? e.clientX : (touch?.clientX ?? 0)
|
||||
const clientY = e instanceof MouseEvent ? e.clientY : (touch?.clientY ?? 0)
|
||||
|
||||
const w = windowRef.value?.offsetWidth || 480
|
||||
const h = windowRef.value?.offsetHeight || 600
|
||||
@@ -185,8 +156,8 @@ function onDrag(e: MouseEvent | TouchEvent) {
|
||||
const maxY = window.innerHeight - h * 0.25
|
||||
|
||||
position.value = {
|
||||
x: Math.max(minX, Math.min(clientX - dragOffset.value.x, maxX)),
|
||||
y: Math.max(minY, Math.min(clientY - dragOffset.value.y, maxY))
|
||||
x: Math.max(minX, Math.min(e.clientX - dragOffset.value.x, maxX)),
|
||||
y: Math.max(minY, Math.min(e.clientY - dragOffset.value.y, maxY))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,8 +166,6 @@ function stopDrag() {
|
||||
hasCustomPosition.value = true
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('touchmove', onDrag)
|
||||
document.removeEventListener('touchend', stopDrag)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -239,16 +208,25 @@ const sizeModePresets: Record<SizeMode, { w: number; h: number }> = {
|
||||
}
|
||||
|
||||
const windowStyle = computed((): Record<string, string> => {
|
||||
if (isMobile.value) {
|
||||
return {
|
||||
if (effectiveMobile.value) {
|
||||
const isFullScreen = sheetHeight.value >= 100
|
||||
const style: Record<string, string> = {
|
||||
position: 'fixed',
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
width: '100%',
|
||||
height: `${sheetHeight.value}vh`,
|
||||
maxHeight: '90vh'
|
||||
}
|
||||
if (isFullScreen) {
|
||||
// Full height but respect app header
|
||||
const headerEl = document.querySelector('.app-header') as HTMLElement | null
|
||||
const headerH = headerEl ? headerEl.offsetHeight : 40
|
||||
style.top = `${headerH}px`
|
||||
style.height = 'auto'
|
||||
} else {
|
||||
style.height = `${sheetHeight.value}dvh`
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
const preset = sizeModePresets[sizeMode.value]
|
||||
@@ -306,7 +284,7 @@ function close() {
|
||||
}
|
||||
|
||||
function openAtCursor(x: number, y: number) {
|
||||
if (isMobile.value) {
|
||||
if (effectiveMobile.value) {
|
||||
isOpen.value = !isOpen.value
|
||||
return
|
||||
}
|
||||
@@ -399,8 +377,6 @@ onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', handleZoomKey)
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('touchmove', onDrag)
|
||||
document.removeEventListener('touchend', stopDrag)
|
||||
document.removeEventListener('mousemove', onResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
@@ -417,8 +393,7 @@ onBeforeUnmount(() => {
|
||||
:class="{
|
||||
dragging: isDragging,
|
||||
resizing: isResizing,
|
||||
mobile: isMobile,
|
||||
'sheet-dragging': isDraggingSheet,
|
||||
mobile: effectiveMobile,
|
||||
'chrome-visible': showChrome,
|
||||
'selector-open': showSelector
|
||||
}"
|
||||
@@ -429,34 +404,33 @@ onBeforeUnmount(() => {
|
||||
@focusout="isFocusWithin = false"
|
||||
>
|
||||
<div class="glass" :style="{ zoom: zoom !== 1 ? zoom : undefined }">
|
||||
<!-- Mobile drag handle -->
|
||||
<!-- Thin drag edge (replaces full titlebar drag) -->
|
||||
<div
|
||||
v-if="isMobile"
|
||||
class="sheet-handle"
|
||||
@touchstart.passive="startSheetDrag"
|
||||
@touchmove.prevent="onSheetDrag"
|
||||
@touchend="stopSheetDrag"
|
||||
>
|
||||
<div class="handle-bar"></div>
|
||||
</div>
|
||||
v-if="!effectiveMobile"
|
||||
class="drag-edge"
|
||||
@mousedown="startDrag"
|
||||
></div>
|
||||
|
||||
<!-- Titlebar -->
|
||||
<div
|
||||
class="titlebar"
|
||||
@mousedown="startDrag"
|
||||
@touchstart.passive="startDrag"
|
||||
@touchmove="onSheetDrag"
|
||||
@touchend="stopSheetDrag"
|
||||
>
|
||||
<div class="titlebar">
|
||||
<div class="left">
|
||||
<svg class="title-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
<span class="title-label">Transcript</span>
|
||||
<i class="dot" :class="{ on: isRealtime }"></i>
|
||||
<span v-if="selectedAgent" class="agent-badge">{{ selectedAgent }}</span>
|
||||
<AgentBadge v-if="selectedAgent" :agent="selectedAgent" :connected="isRealtime" />
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
<button
|
||||
v-if="!isMobile"
|
||||
@click.stop="toggleForceMobile"
|
||||
class="mobile-btn"
|
||||
:class="{ active: forceMobile }"
|
||||
title="Toggle mobile panel"
|
||||
>
|
||||
<!-- Phone/panel-from-bottom icon -->
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" shape-rendering="crispEdges">
|
||||
<rect x="2" y="0" width="6" height="10" fill="currentColor" opacity="0.25"/>
|
||||
<rect x="3" y="1" width="4" height="7" fill="currentColor"/>
|
||||
<rect x="4" y="9" width="2" height="1" fill="currentColor" opacity="0.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click.stop="cycleSizeMode" class="size-btn" :title="`Size: ${sizeMode}`">
|
||||
<!-- Pin icon: small square bottom-left -->
|
||||
<svg v-if="sizeMode === 'pin'" width="10" height="10" viewBox="0 0 10 10" shape-rendering="crispEdges">
|
||||
@@ -531,7 +505,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<!-- Resize handle -->
|
||||
<div v-if="!isMobile" class="resize-handle" @mousedown="startResize"></div>
|
||||
<div v-if="!effectiveMobile" class="resize-handle" @mousedown="startResize"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -615,6 +589,11 @@ onBeforeUnmount(() => {
|
||||
transition: opacity 0.35s ease, max-height 0.35s ease, padding 0.35s ease;
|
||||
}
|
||||
|
||||
/* Idle: hide drag edge */
|
||||
.aero-win:not(.chrome-visible) .drag-edge {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Idle: slide titlebar up and fade out */
|
||||
.aero-win:not(.chrome-visible) .titlebar {
|
||||
opacity: 0;
|
||||
@@ -704,6 +683,25 @@ onBeforeUnmount(() => {
|
||||
transition: opacity 0.35s ease, transform 0.35s ease !important;
|
||||
}
|
||||
|
||||
/* ── Drag edge: thin strip at top for window dragging ── */
|
||||
.drag-edge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 5px;
|
||||
z-index: 11;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.drag-edge:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.aero-win.dragging .drag-edge {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* ── Titlebar: absolute overlay at top of .glass ── */
|
||||
.titlebar {
|
||||
position: absolute;
|
||||
@@ -723,13 +721,10 @@ onBeforeUnmount(() => {
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: opacity 0.35s ease, transform 0.35s ease;
|
||||
}
|
||||
|
||||
.aero-win.dragging .titlebar { cursor: grabbing; }
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -738,48 +733,6 @@ onBeforeUnmount(() => {
|
||||
font: 500 10px/1 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
opacity: 0.4;
|
||||
flex-shrink: 0;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
|
||||
.title-label {
|
||||
font-weight: 700;
|
||||
font-size: 10px;
|
||||
color: #818cf8;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Courier New', monospace;
|
||||
/* No gradient - pixel art aesthetic = flat color */
|
||||
-webkit-text-fill-color: #818cf8;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 0;
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.dot.on {
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 6px #22c55e;
|
||||
}
|
||||
|
||||
.agent-badge {
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
padding: 1px 4px;
|
||||
border-radius: 0;
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
color: #a5b4fc;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
@@ -810,6 +763,22 @@ onBeforeUnmount(() => {
|
||||
color: #a5b4fc;
|
||||
}
|
||||
|
||||
.window-controls .mobile-btn {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.window-controls .mobile-btn:hover {
|
||||
color: #c4b5fd;
|
||||
background: rgba(167, 139, 250, 0.15);
|
||||
border-color: rgba(167, 139, 250, 0.25);
|
||||
}
|
||||
|
||||
.window-controls .mobile-btn.active {
|
||||
background: rgba(167, 139, 250, 0.2);
|
||||
border-color: rgba(167, 139, 250, 0.3);
|
||||
color: #c4b5fd;
|
||||
}
|
||||
|
||||
.window-controls .size-btn {
|
||||
color: #0ea5e9;
|
||||
}
|
||||
@@ -1181,40 +1150,11 @@ onBeforeUnmount(() => {
|
||||
transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.aero-win.mobile.sheet-dragging {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.aero-win.mobile .glass {
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.sheet-handle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
|
||||
.sheet-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.handle-bar {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
background: rgba(255,255,255,0.12);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.aero-win.mobile .titlebar {
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.aero-win.mobile .resize-handle {
|
||||
display: none;
|
||||
@@ -1227,9 +1167,17 @@ onBeforeUnmount(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aero-win.mobile.win-slide-enter-active {
|
||||
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
||||
}
|
||||
|
||||
.aero-win.mobile.win-slide-leave-active {
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 1, 1) !important;
|
||||
}
|
||||
|
||||
.aero-win.mobile.win-slide-enter-from,
|
||||
.aero-win.mobile.win-slide-leave-to {
|
||||
transform: translateY(100%);
|
||||
opacity: 1;
|
||||
transform: translateY(100%) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
141
frontend/src/components/transcript-debug/AgentBadge.vue
Normal file
141
frontend/src/components/transcript-debug/AgentBadge.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
agent: string
|
||||
connected: boolean
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const wrapperRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function toggle(e: Event) {
|
||||
e.stopPropagation()
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
function onClickOutside(e: MouseEvent) {
|
||||
if (!isOpen.value) return
|
||||
if (wrapperRef.value && !wrapperRef.value.contains(e.target as Node)) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('mousedown', onClickOutside))
|
||||
onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="wrapperRef" class="agent-badge-wrapper" :class="{ connected }" @click="toggle">
|
||||
<span class="agent-label">{{ agent }}</span>
|
||||
<svg class="caret" :class="{ open: isOpen }" width="6" height="6" viewBox="0 0 6 6" shape-rendering="crispEdges">
|
||||
<rect x="2" y="4" width="2" height="2" fill="currentColor"/>
|
||||
<rect x="0" y="2" width="2" height="2" fill="currentColor"/>
|
||||
<rect x="4" y="2" width="2" height="2" fill="currentColor"/>
|
||||
</svg>
|
||||
<Transition name="dropdown">
|
||||
<div v-if="isOpen" class="dropdown">
|
||||
<div class="dropdown-item todo">TODO</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-badge-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 3px 7px;
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
transition: background 0.15s, border-color 0.15s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.agent-badge-wrapper:hover {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.agent-badge-wrapper.connected {
|
||||
border-color: rgba(34, 197, 94, 0.35);
|
||||
background: rgba(34, 197, 94, 0.08);
|
||||
box-shadow: 0 0 8px rgba(34, 197, 94, 0.15);
|
||||
}
|
||||
|
||||
.agent-badge-wrapper.connected:hover {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border-color: rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.agent-label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #a5b4fc;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.connected .agent-label {
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgba(165, 180, 252, 0.5);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.connected .caret {
|
||||
color: rgba(134, 239, 172, 0.5);
|
||||
}
|
||||
|
||||
.caret.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
min-width: 120px;
|
||||
background: rgba(8, 8, 18, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
z-index: 100;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 5px 10px;
|
||||
font-size: 9px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dropdown-item.todo {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Transition */
|
||||
.dropdown-enter-active {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
transition: opacity 0.1s ease, transform 0.1s ease;
|
||||
}
|
||||
|
||||
.dropdown-enter-from,
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
</style>
|
||||
@@ -12,4 +12,5 @@ export { default as UserInput } from './UserInput.vue'
|
||||
export { default as PermissionApproval } from './PermissionApproval.vue'
|
||||
export { default as PlanApproval } from './PlanApproval.vue'
|
||||
export { default as CodeBlock } from './CodeBlock.vue'
|
||||
export { default as AgentBadge } from './AgentBadge.vue'
|
||||
export { AquaticBackground } from './aquaticBackground'
|
||||
|
||||
Reference in New Issue
Block a user