- SectionSummary type with tool names, error count, token usage - Informative inline badge on collapsed sections (tools, errors, tokens) - Auto-collapse keeps older sections collapsed as new messages arrive - Replace scrollbar with pixel art aquatic scroll navigation arrows - Configurable scroll jump percentage in settings - Double chevrons for top/bottom, single for page jump
1828 lines
66 KiB
Vue
1828 lines
66 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
import { useTranscriptDebug } from '@/composables/transcript-debug'
|
|
import { useVoiceInput } from '@/composables/useVoiceInput'
|
|
import { ChatContainer, AquaticBackground, AgentBadge, NewSessionModal } from '@/components/transcript-debug'
|
|
import type { AgentName } from '@/types/transcript-debug'
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
}>()
|
|
|
|
const isOpen = computed({
|
|
get: () => props.modelValue,
|
|
set: (val) => emit('update:modelValue', val)
|
|
})
|
|
|
|
// ============================================================================
|
|
// TRANSCRIPT DEBUG STATE (composable)
|
|
// ============================================================================
|
|
|
|
const {
|
|
selectedAgent,
|
|
sessions,
|
|
selectedSessionId,
|
|
conversation,
|
|
loading,
|
|
error,
|
|
isRealtime,
|
|
processing,
|
|
ephemeral,
|
|
terminalReady,
|
|
hookMeta,
|
|
openTerminals,
|
|
activeTerminalSessionId,
|
|
init,
|
|
switchAgent,
|
|
selectSession,
|
|
createNewSession,
|
|
switchToTerminal,
|
|
closeTerminal,
|
|
disconnectRealtime,
|
|
sendPrompt
|
|
} = useTranscriptDebug()
|
|
|
|
const voice = useVoiceInput({ language: 'es-419' })
|
|
const {
|
|
isRecording: voiceRecording,
|
|
transcript: voiceTranscript,
|
|
interimTranscript: voiceInterim,
|
|
voiceMode,
|
|
whisperStatus,
|
|
audioDevices,
|
|
selectedDeviceId,
|
|
lastAudioUrl,
|
|
isPlayingAudio,
|
|
} = voice
|
|
|
|
const agents: { id: AgentName; label: string }[] = [
|
|
{ id: 'ejecutor', label: 'Ejecutor' },
|
|
{ id: 'nucleo000', label: 'nucleo000' },
|
|
{ id: 'claude', label: 'Claude' }
|
|
]
|
|
|
|
const showSelector = ref(false)
|
|
const showNewSessionModal = ref(false)
|
|
const chatRef = ref<InstanceType<typeof ChatContainer> | null>(null)
|
|
let initialized = false
|
|
|
|
// ============================================================================
|
|
// CHROME VISIBILITY (idle mode: hide UI chrome when not interacting)
|
|
// ============================================================================
|
|
|
|
const isHovered = ref(false)
|
|
const isFocusWithin = ref(false)
|
|
|
|
const showChrome = computed(() =>
|
|
isHovered.value || isFocusWithin.value || isDragging.value ||
|
|
isResizing.value || showSelector.value
|
|
)
|
|
|
|
// ============================================================================
|
|
// OCEAN LIFE STATE
|
|
// ============================================================================
|
|
|
|
const currentHour = ref(new Date().getHours())
|
|
|
|
const isNightTime = computed(() => currentHour.value >= 20 || currentHour.value < 6)
|
|
const isNoonTime = computed(() => currentHour.value >= 11 && currentHour.value <= 13)
|
|
const isDawnTime = computed(() => currentHour.value >= 5 && currentHour.value <= 7)
|
|
const isFridayTime = computed(() => new Date().getDay() === 5)
|
|
const isMidnightTime = computed(() => currentHour.value === 0)
|
|
|
|
const showComet = ref(false)
|
|
const showWhale = ref(false)
|
|
const showBottle = ref(false)
|
|
const showLeviathan = ref(false)
|
|
|
|
const oceanSeeds = Array.from({ length: 20 }, () => Math.random())
|
|
let oceanLifeTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
function tickOceanLife() {
|
|
currentHour.value = new Date().getHours()
|
|
if (!showComet.value && Math.random() < 0.04) {
|
|
showComet.value = true
|
|
setTimeout(() => { showComet.value = false }, 3000)
|
|
}
|
|
if (!showWhale.value && Math.random() < 0.07) {
|
|
showWhale.value = true
|
|
setTimeout(() => { showWhale.value = false }, 95000)
|
|
}
|
|
if (!showBottle.value && Math.random() < 0.025) {
|
|
showBottle.value = true
|
|
setTimeout(() => { showBottle.value = false }, 125000)
|
|
}
|
|
if (!showLeviathan.value && Math.random() < 0.008) {
|
|
showLeviathan.value = true
|
|
setTimeout(() => { showLeviathan.value = false }, 310000)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// DRAG STATE
|
|
// ============================================================================
|
|
|
|
const windowRef = ref<HTMLElement | null>(null)
|
|
const isDragging = ref(false)
|
|
const position = ref({ x: 0, y: 0 })
|
|
const hasCustomPosition = ref(false)
|
|
const dragOffset = ref({ x: 0, y: 0 })
|
|
|
|
// Resize state
|
|
const isResizing = ref(false)
|
|
const size = ref({ w: 480, h: 600 })
|
|
|
|
// Zoom level for content scaling
|
|
const zoom = ref(1)
|
|
|
|
// Size mode: pin (small, anchored to FAB), medium (default), large
|
|
type SizeMode = 'pin' | 'medium' | 'large'
|
|
const savedSize = localStorage.getItem('transcript-size-mode') as SizeMode | null
|
|
const sizeMode = ref<SizeMode>(savedSize && ['pin', 'medium', 'large'].includes(savedSize) ? savedSize : 'medium')
|
|
|
|
// Readability overlay opacity
|
|
const savedOverlay = localStorage.getItem('transcript-overlay-opacity')
|
|
const overlayOpacity = ref(savedOverlay !== null ? parseFloat(savedOverlay) : 0.55)
|
|
|
|
function setOverlayOpacity(val: number) {
|
|
overlayOpacity.value = val
|
|
localStorage.setItem('transcript-overlay-opacity', String(val))
|
|
}
|
|
|
|
// Input textarea max lines
|
|
const savedMaxLines = localStorage.getItem('transcript-input-max-lines')
|
|
const inputMaxLines = ref(savedMaxLines !== null ? parseInt(savedMaxLines) : 6)
|
|
|
|
function setInputMaxLines(val: number) {
|
|
inputMaxLines.value = val
|
|
localStorage.setItem('transcript-input-max-lines', String(val))
|
|
}
|
|
|
|
// Scroll jump percent
|
|
const savedScrollJump = localStorage.getItem('transcript-scroll-jump')
|
|
const scrollJumpPercent = ref(savedScrollJump !== null ? parseInt(savedScrollJump) : 50)
|
|
|
|
function setScrollJumpPercent(val: number) {
|
|
scrollJumpPercent.value = val
|
|
localStorage.setItem('transcript-scroll-jump', String(val))
|
|
}
|
|
|
|
function scrollJump(direction: 'up' | 'down') {
|
|
const el = (chatRef.value as any)?.$el?.querySelector('.messages-scroll') as HTMLElement | null
|
|
if (!el) return
|
|
const amount = el.clientHeight * (scrollJumpPercent.value / 100)
|
|
el.scrollBy({ top: direction === 'up' ? -amount : amount, behavior: 'smooth' })
|
|
}
|
|
|
|
function scrollToEdge(edge: 'top' | 'bottom') {
|
|
const el = (chatRef.value as any)?.$el?.querySelector('.messages-scroll') as HTMLElement | null
|
|
if (!el) return
|
|
el.scrollTo({ top: edge === 'top' ? 0 : el.scrollHeight, behavior: 'smooth' })
|
|
}
|
|
|
|
// 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)
|
|
|
|
// ============================================================================
|
|
// MOBILE DETECTION
|
|
// ============================================================================
|
|
|
|
function checkMobile() {
|
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0
|
|
const isSmallScreen = window.innerWidth <= 1024
|
|
isMobile.value = isTouchDevice && isSmallScreen
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// WINDOW DRAG
|
|
// ============================================================================
|
|
|
|
function startDrag(e: MouseEvent) {
|
|
if (effectiveMobile.value) return
|
|
|
|
isDragging.value = true
|
|
const rect = windowRef.value?.getBoundingClientRect()
|
|
if (rect) {
|
|
if (!hasCustomPosition.value) {
|
|
position.value = { x: rect.left, y: rect.top }
|
|
}
|
|
dragOffset.value = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
|
}
|
|
|
|
document.addEventListener('mousemove', onDrag)
|
|
document.addEventListener('mouseup', stopDrag)
|
|
}
|
|
|
|
function onDrag(e: MouseEvent) {
|
|
if (!isDragging.value) return
|
|
|
|
const w = windowRef.value?.offsetWidth || 480
|
|
const h = windowRef.value?.offsetHeight || 600
|
|
const minX = -w * 0.75
|
|
const maxX = window.innerWidth - w * 0.25
|
|
const minY = -h * 0.75
|
|
const maxY = window.innerHeight - h * 0.25
|
|
|
|
position.value = {
|
|
x: Math.max(minX, Math.min(e.clientX - dragOffset.value.x, maxX)),
|
|
y: Math.max(minY, Math.min(e.clientY - dragOffset.value.y, maxY))
|
|
}
|
|
}
|
|
|
|
function stopDrag() {
|
|
isDragging.value = false
|
|
hasCustomPosition.value = true
|
|
document.removeEventListener('mousemove', onDrag)
|
|
document.removeEventListener('mouseup', stopDrag)
|
|
}
|
|
|
|
// ============================================================================
|
|
// WINDOW RESIZE
|
|
// ============================================================================
|
|
|
|
const resizeStart = ref({ x: 0, y: 0, w: 0, h: 0 })
|
|
|
|
function startResize(e: MouseEvent) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
isResizing.value = true
|
|
resizeStart.value = { x: e.clientX, y: e.clientY, w: size.value.w, h: size.value.h }
|
|
document.addEventListener('mousemove', onResize)
|
|
document.addEventListener('mouseup', stopResize)
|
|
}
|
|
|
|
function onResize(e: MouseEvent) {
|
|
if (!isResizing.value) return
|
|
size.value = {
|
|
w: Math.max(360, Math.min(resizeStart.value.w + e.clientX - resizeStart.value.x, window.innerWidth - 40)),
|
|
h: Math.max(300, Math.min(resizeStart.value.h + e.clientY - resizeStart.value.y, window.innerHeight - 40))
|
|
}
|
|
}
|
|
|
|
function stopResize() {
|
|
isResizing.value = false
|
|
document.removeEventListener('mousemove', onResize)
|
|
document.removeEventListener('mouseup', stopResize)
|
|
}
|
|
|
|
// ============================================================================
|
|
// COMPUTED STYLE
|
|
// ============================================================================
|
|
|
|
const sizeModePresets: Record<SizeMode, { w: number; h: number }> = {
|
|
pin: { w: 240, h: 300 },
|
|
medium: { w: 480, h: 600 },
|
|
large: { w: 800, h: 760 }
|
|
}
|
|
|
|
const windowStyle = computed((): Record<string, string> => {
|
|
if (effectiveMobile.value) {
|
|
const isFullScreen = sheetHeight.value >= 100
|
|
const headerEl = document.querySelector('.app-header') as HTMLElement | null
|
|
const headerH = headerEl ? headerEl.offsetHeight : 40
|
|
const style: Record<string, string> = {
|
|
position: 'fixed',
|
|
left: '0',
|
|
right: '0',
|
|
width: '100%',
|
|
}
|
|
|
|
style.bottom = '0'
|
|
if (isFullScreen) {
|
|
style.top = `${headerH}px`
|
|
style.height = 'auto'
|
|
} else {
|
|
style.height = `${sheetHeight.value}dvh`
|
|
}
|
|
return style
|
|
}
|
|
|
|
const preset = sizeModePresets[sizeMode.value]
|
|
|
|
// Custom position from dragging (uses current resize size or preset)
|
|
if (hasCustomPosition.value) {
|
|
const w = sizeMode.value === 'medium' ? size.value.w : preset.w
|
|
const h = sizeMode.value === 'medium' ? size.value.h : preset.h
|
|
return {
|
|
width: `${w}px`,
|
|
height: `${h}px`,
|
|
top: `${position.value.y}px`,
|
|
left: `${position.value.x}px`,
|
|
bottom: 'auto',
|
|
right: 'auto'
|
|
}
|
|
}
|
|
|
|
// Pin: anchored to FAB button (bottom-left corner aligned)
|
|
if (sizeMode.value === 'pin') {
|
|
return {
|
|
width: `${preset.w}px`,
|
|
height: `${preset.h}px`,
|
|
bottom: '20px',
|
|
left: '80px'
|
|
}
|
|
}
|
|
|
|
// Large: centered-ish, generous size
|
|
if (sizeMode.value === 'large') {
|
|
return {
|
|
width: `${preset.w}px`,
|
|
height: `${preset.h}px`,
|
|
bottom: '16px',
|
|
left: '90px'
|
|
}
|
|
}
|
|
|
|
// Medium (default)
|
|
return {
|
|
width: `${size.value.w}px`,
|
|
height: `${size.value.h}px`,
|
|
bottom: '16px',
|
|
left: '90px'
|
|
}
|
|
})
|
|
|
|
// ============================================================================
|
|
// ACTIONS
|
|
// ============================================================================
|
|
|
|
function close() {
|
|
isOpen.value = false
|
|
showSelector.value = false
|
|
}
|
|
|
|
function openAtCursor(x: number, y: number) {
|
|
if (effectiveMobile.value) {
|
|
isOpen.value = !isOpen.value
|
|
return
|
|
}
|
|
if (isOpen.value) {
|
|
isOpen.value = false
|
|
return
|
|
}
|
|
const preset = sizeModePresets[sizeMode.value]
|
|
const w = sizeMode.value === 'medium' ? size.value.w : preset.w
|
|
const h = sizeMode.value === 'medium' ? size.value.h : preset.h
|
|
const pad = 8
|
|
const vw = window.innerWidth
|
|
const vh = window.innerHeight
|
|
// Respect app header bar (~40px + safe-area)
|
|
const headerEl = document.querySelector('.app-header') as HTMLElement | null
|
|
const topBarrier = headerEl ? headerEl.getBoundingClientRect().bottom + pad : pad + 40
|
|
|
|
// Cursor at bottom-center of window: window extends upward from cursor
|
|
let left = x - w / 2
|
|
let top = y - h
|
|
|
|
// Clamp horizontally
|
|
left = Math.max(pad, Math.min(left, vw - w - pad))
|
|
|
|
// Clamp vertically: never above header, never below viewport
|
|
top = Math.max(topBarrier, Math.min(top, vh - h - pad))
|
|
|
|
position.value = { x: left, y: top }
|
|
hasCustomPosition.value = true
|
|
isOpen.value = true
|
|
nextTick(() => {
|
|
windowRef.value?.querySelector<HTMLTextAreaElement>('.input-field')?.focus()
|
|
})
|
|
}
|
|
|
|
defineExpose({ openAtCursor })
|
|
|
|
function handleAgentSwitch(agent: AgentName) {
|
|
switchAgent(agent)
|
|
}
|
|
|
|
function handleSessionSelect(sessionId: string) {
|
|
selectSession(sessionId)
|
|
showSelector.value = false
|
|
}
|
|
|
|
function handleSend(message: string) {
|
|
voice.clearTranscript()
|
|
sendPrompt(message)
|
|
}
|
|
|
|
function handleCreateSession() {
|
|
showNewSessionModal.value = true
|
|
}
|
|
|
|
async function handleModalCreateNew(agent: AgentName, initialPrompt: string) {
|
|
showNewSessionModal.value = false
|
|
if (agent !== selectedAgent.value) {
|
|
await switchAgent(agent)
|
|
}
|
|
createNewSession(initialPrompt || undefined)
|
|
}
|
|
|
|
async function handleModalResume(sessionId: string, agent: AgentName) {
|
|
showNewSessionModal.value = false
|
|
if (agent !== selectedAgent.value) {
|
|
await switchAgent(agent)
|
|
}
|
|
selectSession(sessionId)
|
|
}
|
|
|
|
// ============================================================================
|
|
// WATCHERS
|
|
// ============================================================================
|
|
|
|
watch(isOpen, async (open) => {
|
|
if (open && !initialized) {
|
|
initialized = true
|
|
await nextTick()
|
|
init()
|
|
}
|
|
})
|
|
|
|
// ============================================================================
|
|
// KEYBOARD SHORTCUTS (Ctrl+1..5 terminal switch, Ctrl+/- zoom)
|
|
// ============================================================================
|
|
|
|
function handleGlobalKeydown(e: KeyboardEvent) {
|
|
if (!e.ctrlKey) return
|
|
|
|
// Ctrl+1..5 → switch to terminal by index
|
|
const num = parseInt(e.key)
|
|
if (num >= 1 && num <= 5) {
|
|
const terminal = openTerminals.value[num - 1]
|
|
if (!terminal) return
|
|
e.preventDefault()
|
|
if (!isOpen.value) isOpen.value = true
|
|
switchToTerminal(terminal.sessionId)
|
|
return
|
|
}
|
|
|
|
// Zoom shortcuts (only when open)
|
|
if (!isOpen.value) return
|
|
if (e.key === '+' || e.key === '=') {
|
|
e.preventDefault()
|
|
zoom.value = Math.min(2, +(zoom.value + 0.1).toFixed(1))
|
|
} else if (e.key === '-') {
|
|
e.preventDefault()
|
|
zoom.value = Math.max(0.5, +(zoom.value - 0.1).toFixed(1))
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// LIFECYCLE
|
|
// ============================================================================
|
|
|
|
onMounted(async () => {
|
|
checkMobile()
|
|
window.addEventListener('resize', checkMobile)
|
|
document.addEventListener('keydown', handleGlobalKeydown)
|
|
oceanLifeTimer = setInterval(tickOceanLife, 20000)
|
|
tickOceanLife()
|
|
await voice.init()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (oceanLifeTimer) clearInterval(oceanLifeTimer)
|
|
disconnectRealtime()
|
|
voice.cleanup()
|
|
document.removeEventListener('keydown', handleGlobalKeydown)
|
|
document.removeEventListener('mousemove', onDrag)
|
|
document.removeEventListener('mouseup', stopDrag)
|
|
document.removeEventListener('mousemove', onResize)
|
|
document.removeEventListener('mouseup', stopResize)
|
|
window.removeEventListener('resize', checkMobile)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<Transition name="win-slide">
|
|
<div
|
|
v-show="isOpen"
|
|
ref="windowRef"
|
|
class="aero-win"
|
|
:class="{
|
|
dragging: isDragging,
|
|
resizing: isResizing,
|
|
mobile: effectiveMobile,
|
|
'chrome-visible': showChrome,
|
|
'selector-open': showSelector
|
|
}"
|
|
:style="windowStyle"
|
|
@mouseenter="isHovered = true"
|
|
@mouseleave="isHovered = false"
|
|
@focusin="isFocusWithin = true"
|
|
@focusout="isFocusWithin = false"
|
|
>
|
|
<div class="glass" :style="{ zoom: zoom !== 1 ? zoom : undefined }">
|
|
<!-- Thin drag edge (replaces full titlebar drag) -->
|
|
<div
|
|
v-if="!effectiveMobile"
|
|
class="drag-edge"
|
|
@mousedown="startDrag"
|
|
></div>
|
|
|
|
<!-- Titlebar -->
|
|
<div class="titlebar">
|
|
<!-- Ocean Life Ecosystem -->
|
|
<div class="ocean-life">
|
|
<!-- Always visible -->
|
|
<i class="cr fish-school" :style="{ animationDelay: `-${oceanSeeds[0] * 10}s` }"></i>
|
|
<i class="cr fish-school-2" :style="{ animationDelay: `-${oceanSeeds[1] * 14}s` }"></i>
|
|
<i class="cr dolphin" :style="{ animationDelay: `-${oceanSeeds[2] * 12}s` }"></i>
|
|
<i class="cr jellyfish" :style="{ animationDelay: `-${oceanSeeds[3] * 25}s` }"></i>
|
|
<i class="cr seahorse" :style="{ animationDelay: `-${oceanSeeds[4] * 35}s` }"></i>
|
|
<i class="cr turtle" :style="{ animationDelay: `-${oceanSeeds[5] * 50}s` }"></i>
|
|
<i class="cr pirate-ship" :style="{ animationDelay: `-${oceanSeeds[6] * 180}s` }"></i>
|
|
<i class="cr bubble b1" :style="{ animationDelay: `-${oceanSeeds[7] * 6}s` }"></i>
|
|
<i class="cr bubble b2" :style="{ animationDelay: `-${oceanSeeds[8] * 9}s` }"></i>
|
|
<i class="cr bubble b3" :style="{ animationDelay: `-${oceanSeeds[9] * 11}s` }"></i>
|
|
<!-- Night only -->
|
|
<template v-if="isNightTime">
|
|
<i class="cr satellite" :style="{ animationDelay: `-${oceanSeeds[10] * 15}s` }"></i>
|
|
<i class="cr anglerfish" :style="{ animationDelay: `-${oceanSeeds[11] * 40}s` }"></i>
|
|
<i class="cr biolum bl1" :style="{ animationDelay: `-${oceanSeeds[12] * 3.5}s` }"></i>
|
|
<i class="cr biolum bl2" :style="{ animationDelay: `-${oceanSeeds[13] * 4.5}s` }"></i>
|
|
<i class="cr biolum bl3" :style="{ animationDelay: `-${oceanSeeds[14] * 5.5}s` }"></i>
|
|
</template>
|
|
<!-- Rare random -->
|
|
<i v-if="showComet" class="cr comet"></i>
|
|
<i v-if="showWhale" class="cr whale" :style="{ animationDelay: `-${oceanSeeds[15] * 90}s` }"></i>
|
|
<i v-if="showBottle" class="cr bottle" :style="{ animationDelay: `-${oceanSeeds[16] * 120}s` }"></i>
|
|
<i v-if="showLeviathan" class="cr leviathan" :style="{ animationDelay: `-${oceanSeeds[17] * 300}s` }"></i>
|
|
<!-- Time-of-day -->
|
|
<i v-if="isNoonTime" class="cr sun-ray"></i>
|
|
<i v-if="isDawnTime" class="cr dawn-glow"></i>
|
|
<!-- Easter eggs -->
|
|
<i v-if="isFridayTime" class="cr party-fish" :style="{ animationDelay: `-${oceanSeeds[18] * 8}s` }"></i>
|
|
<i v-if="isMidnightTime" class="cr ghost-ship" :style="{ animationDelay: `-${oceanSeeds[19] * 240}s` }"></i>
|
|
</div>
|
|
<div class="left">
|
|
<AgentBadge
|
|
v-if="selectedAgent"
|
|
:agent="selectedAgent"
|
|
:connected="isRealtime"
|
|
:terminals="openTerminals"
|
|
:active-session-id="activeTerminalSessionId"
|
|
:model="conversation?.model"
|
|
:version="conversation?.version"
|
|
@switch-terminal="switchToTerminal"
|
|
@close-terminal="closeTerminal"
|
|
/>
|
|
</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">
|
|
<rect x="0" y="6" width="4" height="4" fill="currentColor"/>
|
|
</svg>
|
|
<!-- Medium icon: medium square centered -->
|
|
<svg v-else-if="sizeMode === 'medium'" width="10" height="10" viewBox="0 0 10 10" shape-rendering="crispEdges">
|
|
<rect x="2" y="2" width="6" height="6" fill="currentColor" opacity="0.35"/>
|
|
<rect x="3" y="3" width="4" height="4" fill="currentColor"/>
|
|
</svg>
|
|
<!-- Large icon: full square -->
|
|
<svg v-else width="10" height="10" viewBox="0 0 10 10" shape-rendering="crispEdges">
|
|
<rect x="0" y="0" width="10" height="10" fill="currentColor" opacity="0.35"/>
|
|
<rect x="1" y="1" width="8" height="8" fill="currentColor"/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
@click.stop="chatRef?.collapseAllExceptLast()"
|
|
:class="{ active: chatRef?.allCollapsed }"
|
|
class="collapse-all-btn"
|
|
title="Collapse all except last"
|
|
>
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline v-if="chatRef?.allCollapsed" points="6 9 12 15 18 9"/>
|
|
<template v-else>
|
|
<polyline points="18 15 12 9 6 15"/>
|
|
</template>
|
|
</svg>
|
|
</button>
|
|
<button @click.stop="chatRef?.toggleSelectMode()" :class="{ active: chatRef?.selectMode }" title="Select messages">
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline v-if="chatRef?.selectMode" points="20 6 9 17 4 12"/>
|
|
<template v-else>
|
|
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
|
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
|
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
|
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
|
</template>
|
|
</svg>
|
|
</button>
|
|
<button @click.stop="showSelector = !showSelector" :class="{ active: showSelector }" title="Agent/Session">
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="3"/>
|
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="x" @click="close" title="Close">
|
|
<svg width="8" height="8" viewBox="0 0 10 10">
|
|
<line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/>
|
|
<line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<div v-if="error" class="error-bar">{{ error }}</div>
|
|
|
|
<!-- Content -->
|
|
<div class="content">
|
|
<AquaticBackground />
|
|
<div class="readability-overlay" :style="{ background: `rgba(0, 0, 0, ${overlayOpacity})` }" />
|
|
<ChatContainer
|
|
ref="chatRef"
|
|
v-if="conversation"
|
|
:conversation="conversation"
|
|
:processing="processing"
|
|
:terminal-ready="terminalReady"
|
|
:terminal="ephemeral"
|
|
:show-selector="showSelector"
|
|
:agents="agents"
|
|
:selected-agent="selectedAgent"
|
|
:sessions="sessions"
|
|
:selected-session-id="selectedSessionId"
|
|
:sessions-loading="loading"
|
|
:voice-mode="voiceMode"
|
|
:whisper-status="whisperStatus"
|
|
:audio-devices="audioDevices"
|
|
:selected-device-id="selectedDeviceId"
|
|
:is-recording="voiceRecording"
|
|
:voice-transcript="voiceTranscript + voiceInterim"
|
|
:last-audio-url="lastAudioUrl"
|
|
:is-playing-audio="isPlayingAudio"
|
|
:overlay-opacity="overlayOpacity"
|
|
:input-max-lines="inputMaxLines"
|
|
:scroll-jump-percent="scrollJumpPercent"
|
|
:hook-permission-mode="hookMeta.permissionMode"
|
|
@send="handleSend"
|
|
@switch-agent="handleAgentSwitch"
|
|
@select-session="handleSessionSelect"
|
|
@create-session="handleCreateSession"
|
|
@close-session="closeTerminal"
|
|
@start-recording="voice.startRecording()"
|
|
@stop-recording="voice.stopRecording()"
|
|
@set-voice-mode="voice.setMode($event)"
|
|
@select-microphone="voice.selectMicrophone($event)"
|
|
@play-last-audio="voice.playLastAudio()"
|
|
@update:overlay-opacity="setOverlayOpacity"
|
|
@update:input-max-lines="setInputMaxLines"
|
|
@update:scroll-jump-percent="setScrollJumpPercent"
|
|
/>
|
|
<div v-else class="empty-state">
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<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>Select a session to begin</span>
|
|
<small>{{ sessions.length }} sessions available</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scroll arrows (visible only in idle mode) -->
|
|
<Transition name="scroll-arrows">
|
|
<div v-if="showChrome && conversation" class="scroll-arrows">
|
|
<!-- Top: double chevron up + wave accent -->
|
|
<button class="scroll-arrow sa-surface" @click="scrollToEdge('top')" title="Scroll to top">
|
|
<svg width="20" height="22" viewBox="0 0 20 22" shape-rendering="crispEdges">
|
|
<!-- Wave line -->
|
|
<rect x="0" y="0" width="4" height="2" fill="#0ea5e9" opacity="0.5"/>
|
|
<rect x="4" y="1" width="3" height="2" fill="#22d3ee" opacity="0.4"/>
|
|
<rect x="7" y="0" width="6" height="2" fill="#0ea5e9" opacity="0.5"/>
|
|
<rect x="13" y="1" width="3" height="2" fill="#22d3ee" opacity="0.4"/>
|
|
<rect x="16" y="0" width="4" height="2" fill="#0ea5e9" opacity="0.5"/>
|
|
<rect x="1" y="2" width="2" height="1" fill="white" opacity="0.15"/>
|
|
<rect x="9" y="2" width="2" height="1" fill="white" opacity="0.12"/>
|
|
<!-- Chevron 1 -->
|
|
<rect x="9" y="6" width="2" height="2" fill="#67e8f9" opacity="0.75"/>
|
|
<rect x="7" y="8" width="2" height="2" fill="#67e8f9" opacity="0.6"/>
|
|
<rect x="11" y="8" width="2" height="2" fill="#67e8f9" opacity="0.6"/>
|
|
<rect x="5" y="10" width="2" height="2" fill="#67e8f9" opacity="0.45"/>
|
|
<rect x="13" y="10" width="2" height="2" fill="#67e8f9" opacity="0.45"/>
|
|
<!-- Chevron 2 -->
|
|
<rect x="9" y="13" width="2" height="2" fill="#67e8f9" opacity="0.55"/>
|
|
<rect x="7" y="15" width="2" height="2" fill="#67e8f9" opacity="0.4"/>
|
|
<rect x="11" y="15" width="2" height="2" fill="#67e8f9" opacity="0.4"/>
|
|
<rect x="5" y="17" width="2" height="2" fill="#67e8f9" opacity="0.3"/>
|
|
<rect x="13" y="17" width="2" height="2" fill="#67e8f9" opacity="0.3"/>
|
|
</svg>
|
|
</button>
|
|
<!-- Up: single chevron + bubbles -->
|
|
<button class="scroll-arrow sa-up" @click="scrollJump('up')" title="Scroll up">
|
|
<svg width="20" height="16" viewBox="0 0 20 16" shape-rendering="crispEdges">
|
|
<!-- Chevron -->
|
|
<rect x="9" y="2" width="2" height="2" fill="#67e8f9" opacity="0.75"/>
|
|
<rect x="7" y="4" width="2" height="2" fill="#67e8f9" opacity="0.6"/>
|
|
<rect x="11" y="4" width="2" height="2" fill="#67e8f9" opacity="0.6"/>
|
|
<rect x="5" y="6" width="2" height="2" fill="#67e8f9" opacity="0.45"/>
|
|
<rect x="13" y="6" width="2" height="2" fill="#67e8f9" opacity="0.45"/>
|
|
<!-- Bubble accents -->
|
|
<rect x="15" y="1" width="2" height="2" fill="#67e8f9" opacity="0.25"/>
|
|
<rect x="16" y="1" width="1" height="1" fill="white" opacity="0.2"/>
|
|
<rect x="3" y="3" width="1" height="1" fill="#67e8f9" opacity="0.18"/>
|
|
<!-- Trail -->
|
|
<rect x="9" y="11" width="2" height="1" fill="#67e8f9" opacity="0.18"/>
|
|
<rect x="11" y="13" width="1" height="1" fill="#67e8f9" opacity="0.12"/>
|
|
</svg>
|
|
</button>
|
|
<!-- Down: single chevron + bubbles -->
|
|
<button class="scroll-arrow sa-down" @click="scrollJump('down')" title="Scroll down">
|
|
<svg width="20" height="16" viewBox="0 0 20 16" shape-rendering="crispEdges">
|
|
<!-- Trail -->
|
|
<rect x="8" y="2" width="1" height="1" fill="#67e8f9" opacity="0.12"/>
|
|
<rect x="9" y="4" width="2" height="1" fill="#67e8f9" opacity="0.18"/>
|
|
<!-- Chevron -->
|
|
<rect x="5" y="7" width="2" height="2" fill="#67e8f9" opacity="0.45"/>
|
|
<rect x="13" y="7" width="2" height="2" fill="#67e8f9" opacity="0.45"/>
|
|
<rect x="7" y="9" width="2" height="2" fill="#67e8f9" opacity="0.6"/>
|
|
<rect x="11" y="9" width="2" height="2" fill="#67e8f9" opacity="0.6"/>
|
|
<rect x="9" y="11" width="2" height="2" fill="#67e8f9" opacity="0.75"/>
|
|
<!-- Bubble accents -->
|
|
<rect x="3" y="12" width="2" height="2" fill="#67e8f9" opacity="0.25"/>
|
|
<rect x="4" y="12" width="1" height="1" fill="white" opacity="0.2"/>
|
|
<rect x="16" y="10" width="1" height="1" fill="#67e8f9" opacity="0.18"/>
|
|
</svg>
|
|
</button>
|
|
<!-- Bottom: double chevron down + sand -->
|
|
<button class="scroll-arrow sa-seabed" @click="scrollToEdge('bottom')" title="Scroll to bottom">
|
|
<svg width="20" height="22" viewBox="0 0 20 22" shape-rendering="crispEdges">
|
|
<!-- Chevron 1 (pointing down) -->
|
|
<rect x="5" y="1" width="2" height="2" fill="#67e8f9" opacity="0.3"/>
|
|
<rect x="13" y="1" width="2" height="2" fill="#67e8f9" opacity="0.3"/>
|
|
<rect x="7" y="3" width="2" height="2" fill="#67e8f9" opacity="0.35"/>
|
|
<rect x="11" y="3" width="2" height="2" fill="#67e8f9" opacity="0.35"/>
|
|
<rect x="9" y="5" width="2" height="2" fill="#67e8f9" opacity="0.45"/>
|
|
<!-- Chevron 2 (pointing down) -->
|
|
<rect x="5" y="8" width="2" height="2" fill="#67e8f9" opacity="0.5"/>
|
|
<rect x="13" y="8" width="2" height="2" fill="#67e8f9" opacity="0.5"/>
|
|
<rect x="7" y="10" width="2" height="2" fill="#67e8f9" opacity="0.65"/>
|
|
<rect x="11" y="10" width="2" height="2" fill="#67e8f9" opacity="0.65"/>
|
|
<rect x="9" y="12" width="2" height="2" fill="#67e8f9" opacity="0.75"/>
|
|
<!-- Sand floor -->
|
|
<rect x="0" y="17" width="20" height="2" fill="#d4a06a" opacity="0.3"/>
|
|
<rect x="0" y="19" width="20" height="3" fill="#c4956a" opacity="0.35"/>
|
|
<rect x="4" y="17" width="1" height="1" fill="#e8c088" opacity="0.25"/>
|
|
<rect x="12" y="18" width="1" height="1" fill="#e8c088" opacity="0.2"/>
|
|
<rect x="17" y="17" width="1" height="1" fill="#e8c088" opacity="0.22"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</Transition>
|
|
|
|
<!-- Resize handle -->
|
|
<div v-if="!effectiveMobile" class="resize-handle" @mousedown="startResize"></div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
|
|
<NewSessionModal
|
|
:visible="showNewSessionModal"
|
|
:agents="agents"
|
|
:current-agent="selectedAgent"
|
|
@close="showNewSessionModal = false"
|
|
@create-new="handleModalCreateNew"
|
|
@resume="handleModalResume"
|
|
/>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.aero-win {
|
|
position: fixed;
|
|
min-width: 200px;
|
|
min-height: 200px;
|
|
z-index: 9999;
|
|
transition: width 0.3s ease, height 0.3s ease, top 0.3s ease, left 0.3s ease, bottom 0.3s ease;
|
|
}
|
|
|
|
/* ── Dynamic glow from ocean background ── */
|
|
.aero-win::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: -12px;
|
|
z-index: -1;
|
|
border-radius: 18px;
|
|
background: linear-gradient(
|
|
180deg,
|
|
rgba(0, 12, 35, 0.35) 0%,
|
|
rgba(0, 30, 65, 0.30) 25%,
|
|
rgba(4, 52, 78, 0.35) 50%,
|
|
rgba(6, 58, 72, 0.30) 70%,
|
|
rgba(18, 50, 45, 0.35) 100%
|
|
);
|
|
filter: blur(18px);
|
|
opacity: 0.7;
|
|
transition: opacity 0.5s ease, filter 0.5s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.aero-win:hover::before,
|
|
.aero-win.chrome-visible::before {
|
|
opacity: 1;
|
|
filter: blur(22px);
|
|
}
|
|
|
|
.aero-win.dragging::before,
|
|
.aero-win.resizing::before {
|
|
transition: none;
|
|
}
|
|
|
|
.aero-win.dragging,
|
|
.aero-win.resizing {
|
|
transition: none;
|
|
}
|
|
|
|
.glass {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: rgba(0, 0, 0, 0.78);
|
|
backdrop-filter: blur(16px) saturate(1.2);
|
|
-webkit-backdrop-filter: blur(16px) saturate(1.2);
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(255,255,255,0.05);
|
|
box-shadow:
|
|
0 0 0 1px rgba(0,0,0,0.6),
|
|
0 16px 56px rgba(0,0,0,0.6),
|
|
0 4px 16px rgba(0,0,0,0.4);
|
|
overflow: hidden;
|
|
transition: border-color 0.35s ease, box-shadow 0.35s ease;
|
|
}
|
|
|
|
/* ══════════════════════════════════════════════════════════════════════════
|
|
IDLE MODE: When not hovering/focused, hide all chrome.
|
|
Only the messages + ocean background fill the entire window.
|
|
══════════════════════════════════════════════════════════════════════════ */
|
|
|
|
/* Transition all chrome elements smoothly */
|
|
.titlebar,
|
|
.error-bar,
|
|
.resize-handle {
|
|
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;
|
|
transform: translateY(-100%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Idle: hide error bar */
|
|
.aero-win:not(.chrome-visible) .error-bar {
|
|
opacity: 0;
|
|
max-height: 0;
|
|
padding: 0;
|
|
border-bottom-width: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Idle: hide resize handle */
|
|
.aero-win:not(.chrome-visible) .resize-handle {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Chat-header: only visible when settings/selector is open */
|
|
.aero-win:not(.selector-open) .content :deep(.chat-header) {
|
|
opacity: 0 !important;
|
|
transform: translateY(-150%) !important;
|
|
pointer-events: none !important;
|
|
}
|
|
|
|
/* Idle: slide user-input down and fade out (only if empty) */
|
|
.aero-win:not(.chrome-visible) .content :deep(.user-input) {
|
|
opacity: 0 !important;
|
|
transform: translateY(100%) !important;
|
|
pointer-events: none !important;
|
|
}
|
|
|
|
/* Keep user-input visible when textarea has text */
|
|
.aero-win:not(.chrome-visible) .content :deep(.user-input:has(.input-field:not(:placeholder-shown))) {
|
|
opacity: 1 !important;
|
|
transform: none !important;
|
|
pointer-events: auto !important;
|
|
}
|
|
|
|
/* Idle: hide status bar */
|
|
.aero-win:not(.chrome-visible) .content :deep(.status-bar) {
|
|
opacity: 0 !important;
|
|
transform: translateY(100%) !important;
|
|
pointer-events: none !important;
|
|
}
|
|
|
|
/* Keep status-bar visible when input has text */
|
|
.aero-win:not(.chrome-visible) .content :deep(.status-bar:has(~ .user-input .input-field:not(:placeholder-shown))) {
|
|
opacity: 1 !important;
|
|
transform: none !important;
|
|
pointer-events: auto !important;
|
|
}
|
|
|
|
/* Idle: also hide selection bar */
|
|
.aero-win:not(.chrome-visible) .content :deep(.selection-bar) {
|
|
opacity: 0 !important;
|
|
pointer-events: none !important;
|
|
}
|
|
|
|
/* Idle: softer glass border + dimmed glow */
|
|
.aero-win:not(.chrome-visible) .glass {
|
|
border-color: rgba(255,255,255,0.02);
|
|
box-shadow:
|
|
0 0 0 1px rgba(0,0,0,0.3),
|
|
0 8px 32px rgba(0,0,0,0.4);
|
|
}
|
|
|
|
.aero-win:not(.chrome-visible)::before {
|
|
opacity: 0.4;
|
|
filter: blur(14px);
|
|
}
|
|
|
|
/* Smooth transitions for chrome show/hide */
|
|
.content :deep(.chat-header) {
|
|
transition: opacity 0.35s ease, transform 0.35s ease !important;
|
|
}
|
|
|
|
.content :deep(.user-input) {
|
|
transition: opacity 0.35s ease, transform 0.35s ease !important;
|
|
}
|
|
|
|
.content :deep(.status-bar) {
|
|
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;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 10;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 28px;
|
|
padding: 0 5px 0 8px;
|
|
background:
|
|
/* Pixel art sea surface: sky, moon, waves, fish, bubbles */
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='28' viewBox='0 0 120 28' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='120' height='12' fill='%230a1628' opacity='0.5'/%3E%3Crect x='0' y='12' width='120' height='16' fill='%230c2d4a' opacity='0.45'/%3E%3Crect x='8' y='3' width='1' height='1' fill='white' opacity='0.3'/%3E%3Crect x='25' y='5' width='1' height='1' fill='white' opacity='0.22'/%3E%3Crect x='45' y='2' width='1' height='1' fill='white' opacity='0.18'/%3E%3Crect x='70' y='4' width='1' height='1' fill='white' opacity='0.28'/%3E%3Crect x='95' y='6' width='1' height='1' fill='white' opacity='0.22'/%3E%3Crect x='110' y='3' width='1' height='1' fill='white' opacity='0.18'/%3E%3Crect x='36' y='7' width='1' height='1' fill='%23fde68a' opacity='0.15'/%3E%3Crect x='85' y='2' width='3' height='3' fill='%23fef3c7' opacity='0.3'/%3E%3Crect x='86' y='1' width='2' height='1' fill='%23fef3c7' opacity='0.2'/%3E%3Crect x='86' y='5' width='2' height='1' fill='%23fde68a' opacity='0.15'/%3E%3Crect x='50' y='4' width='4' height='2' fill='%23475569' opacity='0.15'/%3E%3Crect x='51' y='3' width='2' height='1' fill='%23475569' opacity='0.1'/%3E%3Crect x='8' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.3'/%3E%3Crect x='12' y='11' width='10' height='2' fill='%230ea5e9' opacity='0.25'/%3E%3Crect x='22' y='10' width='6' height='2' fill='%2322d3ee' opacity='0.3'/%3E%3Crect x='28' y='11' width='12' height='2' fill='%230ea5e9' opacity='0.25'/%3E%3Crect x='40' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.3'/%3E%3Crect x='44' y='11' width='14' height='2' fill='%230ea5e9' opacity='0.25'/%3E%3Crect x='58' y='10' width='6' height='2' fill='%2322d3ee' opacity='0.3'/%3E%3Crect x='64' y='11' width='10' height='2' fill='%230ea5e9' opacity='0.25'/%3E%3Crect x='74' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.3'/%3E%3Crect x='78' y='11' width='12' height='2' fill='%230ea5e9' opacity='0.25'/%3E%3Crect x='90' y='10' width='6' height='2' fill='%2322d3ee' opacity='0.3'/%3E%3Crect x='96' y='11' width='14' height='2' fill='%230ea5e9' opacity='0.25'/%3E%3Crect x='110' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.3'/%3E%3Crect x='114' y='11' width='6' height='2' fill='%230ea5e9' opacity='0.25'/%3E%3Crect x='0' y='11' width='8' height='2' fill='%230ea5e9' opacity='0.25'/%3E%3Crect x='8' y='10' width='2' height='1' fill='white' opacity='0.12'/%3E%3Crect x='22' y='10' width='2' height='1' fill='white' opacity='0.1'/%3E%3Crect x='40' y='10' width='2' height='1' fill='white' opacity='0.12'/%3E%3Crect x='58' y='10' width='2' height='1' fill='white' opacity='0.1'/%3E%3Crect x='74' y='10' width='2' height='1' fill='white' opacity='0.12'/%3E%3Crect x='90' y='10' width='2' height='1' fill='white' opacity='0.1'/%3E%3Crect x='110' y='10' width='2' height='1' fill='white' opacity='0.12'/%3E%3Crect x='0' y='14' width='120' height='2' fill='%230369a1' opacity='0.18'/%3E%3Crect x='0' y='18' width='120' height='2' fill='%23075985' opacity='0.12'/%3E%3Crect x='0' y='22' width='120' height='2' fill='%230c4a6e' opacity='0.08'/%3E%3Crect x='15' y='16' width='3' height='1' fill='%23f97316' opacity='0.3'/%3E%3Crect x='14' y='16' width='1' height='1' fill='%23fb923c' opacity='0.2'/%3E%3Crect x='55' y='20' width='3' height='1' fill='%23818cf8' opacity='0.22'/%3E%3Crect x='58' y='20' width='1' height='1' fill='%23a78bfa' opacity='0.15'/%3E%3Crect x='100' y='17' width='3' height='1' fill='%2322c55e' opacity='0.22'/%3E%3Crect x='99' y='17' width='1' height='1' fill='%234ade80' opacity='0.15'/%3E%3Crect x='30' y='15' width='1' height='1' fill='white' opacity='0.1'/%3E%3Crect x='65' y='22' width='1' height='1' fill='white' opacity='0.08'/%3E%3Crect x='82' y='14' width='1' height='1' fill='white' opacity='0.1'/%3E%3C/svg%3E") repeat-x left top / auto 100%,
|
|
rgba(0, 8, 20, 0.3);
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
border-bottom: 1px solid rgba(255,255,255,0.04);
|
|
user-select: none;
|
|
transition: opacity 0.35s ease, transform 0.35s ease;
|
|
animation: titlebar-ocean 8s linear infinite;
|
|
}
|
|
|
|
.left {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
color: rgba(255,255,255,0.6);
|
|
font: 500 10px/1 'Courier New', monospace;
|
|
}
|
|
|
|
.window-controls {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
gap: 2px;
|
|
}
|
|
|
|
.window-controls button {
|
|
width: 20px;
|
|
height: 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255,255,255,0.04);
|
|
border: 1px solid rgba(255,255,255,0.06);
|
|
border-radius: 0;
|
|
color: rgba(255,255,255,0.4);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.window-controls button:hover {
|
|
background: rgba(255,255,255,0.1);
|
|
color: rgba(255,255,255,0.8);
|
|
}
|
|
|
|
.window-controls button.active {
|
|
background: rgba(99, 102, 241, 0.2);
|
|
border-color: rgba(99, 102, 241, 0.3);
|
|
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 .collapse-all-btn {
|
|
color: #2dd4bf;
|
|
}
|
|
|
|
.window-controls .collapse-all-btn:hover {
|
|
color: #5eead4;
|
|
background: rgba(45, 212, 191, 0.15);
|
|
border-color: rgba(45, 212, 191, 0.25);
|
|
}
|
|
|
|
.window-controls .collapse-all-btn.active {
|
|
background: rgba(45, 212, 191, 0.2);
|
|
border-color: rgba(45, 212, 191, 0.3);
|
|
color: #5eead4;
|
|
}
|
|
|
|
.window-controls .size-btn {
|
|
color: #0ea5e9;
|
|
}
|
|
|
|
.window-controls .size-btn:hover {
|
|
color: #38bdf8;
|
|
background: rgba(14, 165, 233, 0.15);
|
|
border-color: rgba(14, 165, 233, 0.25);
|
|
}
|
|
|
|
.window-controls button.x:hover {
|
|
background: rgba(239, 68, 68, 0.3);
|
|
border-color: rgba(239, 68, 68, 0.4);
|
|
color: #fca5a5;
|
|
}
|
|
|
|
/* ── Error Bar ── */
|
|
.error-bar {
|
|
padding: 4px 10px;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border-bottom: 1px solid rgba(239, 68, 68, 0.2);
|
|
color: #fca5a5;
|
|
font-size: 10px;
|
|
font-family: 'Courier New', monospace;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Content ── */
|
|
.content {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
/* Background handled by AquaticBackground component */
|
|
}
|
|
|
|
/* Dark readability overlay: between ocean bg (z-index:0) and chat (z-index:1) */
|
|
.readability-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.35s ease;
|
|
}
|
|
|
|
.aero-win.chrome-visible .readability-overlay {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Override ChatContainer backgrounds: glass-transparent */
|
|
.content :deep(.chat-container) {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
border-radius: 0 !important;
|
|
position: relative;
|
|
z-index: 1;
|
|
flex: 1 !important;
|
|
min-height: 0 !important;
|
|
}
|
|
|
|
/* Chat header: absolute overlay below titlebar, floats over messages */
|
|
.content :deep(.chat-header) {
|
|
position: absolute !important;
|
|
top: 28px !important;
|
|
left: 0 !important;
|
|
right: 0 !important;
|
|
z-index: 3 !important;
|
|
background: rgba(0, 6, 18, 0.5) !important;
|
|
backdrop-filter: blur(8px) !important;
|
|
-webkit-backdrop-filter: blur(8px) !important;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06) !important;
|
|
padding: 0.3rem 0.6rem !important;
|
|
}
|
|
|
|
/* Messages: fill entire container, pad top/bottom for overlaid titlebar+header / input */
|
|
.content :deep(.messages-scroll) {
|
|
background: transparent !important;
|
|
padding-top: 5rem !important;
|
|
padding-bottom: 5rem !important;
|
|
flex: 1 !important;
|
|
scrollbar-width: none !important;
|
|
}
|
|
|
|
.content :deep(.messages-scroll)::-webkit-scrollbar {
|
|
display: none !important;
|
|
}
|
|
|
|
.content :deep(.meta-badge) {
|
|
background: rgba(255,255,255,0.04);
|
|
color: rgba(255,255,255,0.4);
|
|
border-radius: 0;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.content :deep(.meta-badge.model) {
|
|
background: rgba(99, 102, 241, 0.1);
|
|
color: #a5b4fc;
|
|
}
|
|
|
|
.content :deep(.meta-cwd),
|
|
.content :deep(.meta-duration),
|
|
.content :deep(.meta-count) {
|
|
color: rgba(255,255,255,0.25);
|
|
}
|
|
|
|
.content :deep(.copy-id-btn) {
|
|
color: rgba(255,255,255,0.25);
|
|
}
|
|
|
|
.content :deep(.copy-id-btn:hover) {
|
|
background: rgba(255,255,255,0.06);
|
|
color: rgba(255,255,255,0.6);
|
|
}
|
|
|
|
.content :deep(.select-mode-btn) {
|
|
border-color: rgba(255,255,255,0.08);
|
|
color: rgba(255,255,255,0.35);
|
|
border-radius: 0;
|
|
}
|
|
|
|
.content :deep(.select-mode-btn:hover) {
|
|
background: rgba(255,255,255,0.06);
|
|
color: rgba(255,255,255,0.7);
|
|
}
|
|
|
|
.content :deep(.select-mode-btn.active) {
|
|
background: rgba(99, 102, 241, 0.25);
|
|
border-color: rgba(99, 102, 241, 0.35);
|
|
color: #c7d2fe;
|
|
}
|
|
|
|
|
|
/* Status bar: absolute overlay at very bottom */
|
|
.content :deep(.status-bar) {
|
|
position: absolute !important;
|
|
bottom: 0 !important;
|
|
left: 0 !important;
|
|
right: 0 !important;
|
|
z-index: 4 !important;
|
|
background: rgba(0, 6, 18, 0.6) !important;
|
|
backdrop-filter: blur(8px) !important;
|
|
-webkit-backdrop-filter: blur(8px) !important;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.04) !important;
|
|
border-bottom: none !important;
|
|
padding: 0.15rem 0.5rem !important;
|
|
}
|
|
|
|
.content :deep(.status-id) {
|
|
color: rgba(255,255,255,0.35) !important;
|
|
}
|
|
|
|
.content :deep(.status-bar .copy-id-btn) {
|
|
color: rgba(255,255,255,0.25) !important;
|
|
}
|
|
|
|
.content :deep(.status-bar .copy-id-btn:hover) {
|
|
color: rgba(255,255,255,0.6) !important;
|
|
}
|
|
|
|
.content :deep(.status-bar .meta-badge) {
|
|
border-radius: 0 !important;
|
|
font-family: 'Courier New', monospace !important;
|
|
}
|
|
|
|
.content :deep(.status-bar .meta-badge.model) {
|
|
background: rgba(99, 102, 241, 0.1) !important;
|
|
color: #a5b4fc !important;
|
|
}
|
|
|
|
.content :deep(.status-bar .meta-badge.version) {
|
|
background: rgba(255,255,255,0.04) !important;
|
|
color: rgba(255,255,255,0.3) !important;
|
|
}
|
|
|
|
.content :deep(.status-bar .meta-count),
|
|
.content :deep(.status-bar .meta-duration) {
|
|
color: rgba(255,255,255,0.2) !important;
|
|
}
|
|
|
|
/* UserInput: absolute overlay above status bar */
|
|
.content :deep(.user-input) {
|
|
position: absolute !important;
|
|
bottom: 20px !important;
|
|
left: 0 !important;
|
|
right: 0 !important;
|
|
z-index: 3 !important;
|
|
background: rgba(0, 6, 18, 0.5) !important;
|
|
backdrop-filter: blur(8px) !important;
|
|
-webkit-backdrop-filter: blur(8px) !important;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.06) !important;
|
|
padding: 0.3rem 0.5rem !important;
|
|
}
|
|
|
|
/* Dark overlay on input-container so text is readable */
|
|
.content :deep(.input-container) {
|
|
background: rgba(0, 6, 18, 0.8) !important;
|
|
border-color: rgba(14, 165, 233, 0.1) !important;
|
|
border-radius: 0 !important;
|
|
padding: 0.3rem 0.4rem !important;
|
|
}
|
|
|
|
.content :deep(.input-container:focus-within) {
|
|
border-color: rgba(14, 165, 233, 0.25) !important;
|
|
background: rgba(0, 6, 18, 0.85) !important;
|
|
}
|
|
|
|
.content :deep(.input-field) {
|
|
color: rgba(255,255,255,0.85);
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Send button: pixel art daytime ocean, no border */
|
|
.content :deep(.send-btn) {
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28' shape-rendering='crispEdges'%3E%3Crect width='28' height='6' fill='%2387ceeb'/%3E%3Crect y='6' width='28' height='4' fill='%2356b3d9'/%3E%3Crect y='10' width='28' height='4' fill='%232d9abf'/%3E%3Crect y='14' width='28' height='4' fill='%231a7fa5'/%3E%3Crect y='18' width='28' height='4' fill='%23106888'/%3E%3Crect y='22' width='28' height='6' fill='%23c2b280'/%3E%3Crect x='24' y='4' width='3' height='3' fill='%23fffde0' opacity='0.8'/%3E%3Crect x='25' y='3' width='2' height='1' fill='%23fffde0' opacity='0.5'/%3E%3Crect x='2' y='5' width='4' height='2' fill='white' opacity='0.35'/%3E%3Crect x='10' y='4' width='6' height='2' fill='white' opacity='0.25'/%3E%3Crect x='20' y='6' width='3' height='1' fill='white' opacity='0.2'/%3E%3Crect x='5' y='12' width='3' height='2' fill='%23f97316' opacity='0.7'/%3E%3Crect x='4' y='13' width='1' height='1' fill='%23fdba74' opacity='0.5'/%3E%3Crect x='18' y='16' width='2' height='1' fill='%232563eb' opacity='0.5'/%3E%3Crect x='20' y='16' width='1' height='1' fill='%2393c5fd' opacity='0.4'/%3E%3Crect x='8' y='18' width='1' height='1' fill='white' opacity='0.2'/%3E%3Crect x='22' y='12' width='1' height='1' fill='white' opacity='0.2'/%3E%3Crect x='14' y='20' width='1' height='1' fill='white' opacity='0.15'/%3E%3Crect x='3' y='23' width='4' height='3' fill='%23059669' opacity='0.5'/%3E%3Crect x='4' y='22' width='2' height='1' fill='%2310b981' opacity='0.4'/%3E%3Crect x='20' y='24' width='3' height='2' fill='%23059669' opacity='0.4'/%3E%3Crect x='12' y='25' width='3' height='2' fill='%23ec4899' opacity='0.45'/%3E%3Crect x='13' y='24' width='2' height='1' fill='%23f472b6' opacity='0.35'/%3E%3C/svg%3E") !important;
|
|
border: none !important;
|
|
border-radius: 0 !important;
|
|
width: 28px !important;
|
|
height: 28px !important;
|
|
color: white !important;
|
|
image-rendering: pixelated;
|
|
box-shadow: none !important;
|
|
transition: color 0.15s ease !important;
|
|
}
|
|
|
|
.content :deep(.send-btn:hover:not(:disabled)) {
|
|
color: #fffde0 !important;
|
|
filter: none !important;
|
|
box-shadow: 0 0 12px rgba(135, 206, 235, 0.5), 0 0 4px rgba(255, 253, 224, 0.3) !important;
|
|
}
|
|
|
|
.content :deep(.send-btn:disabled) {
|
|
opacity: 0.25 !important;
|
|
filter: saturate(0.3) !important;
|
|
}
|
|
|
|
/* Selection bar */
|
|
.content :deep(.selection-bar) {
|
|
background: rgba(8, 8, 12, 0.92);
|
|
border-color: rgba(255,255,255,0.06);
|
|
border-radius: 0;
|
|
}
|
|
|
|
.content :deep(.selection-count) {
|
|
color: rgba(255,255,255,0.4);
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.content :deep(.selection-btn.toggle-all) {
|
|
background: rgba(255,255,255,0.06);
|
|
color: rgba(255,255,255,0.5);
|
|
border-radius: 0;
|
|
}
|
|
|
|
.content :deep(.message-wrapper.selected) {
|
|
background: rgba(99, 102, 241, 0.06);
|
|
}
|
|
|
|
/* ── Empty State ── */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 1;
|
|
gap: 8px;
|
|
color: rgba(255,255,255,0.3);
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.empty-state svg {
|
|
opacity: 0.2;
|
|
}
|
|
|
|
.empty-state span {
|
|
font-size: 11px;
|
|
font-family: 'Courier New', monospace;
|
|
color: rgba(255,255,255,0.35);
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.empty-state small {
|
|
font-size: 10px;
|
|
font-family: 'Courier New', monospace;
|
|
color: rgba(255,255,255,0.2);
|
|
}
|
|
|
|
/* ── Resize Handle ── */
|
|
.resize-handle {
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 16px;
|
|
height: 16px;
|
|
cursor: nwse-resize;
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' shape-rendering='crispEdges'%3E%3Crect x='12' y='12' width='2' height='2' fill='%23818cf8' opacity='0.25'/%3E%3Crect x='8' y='12' width='2' height='2' fill='%23818cf8' opacity='0.15'/%3E%3Crect x='12' y='8' width='2' height='2' fill='%23818cf8' opacity='0.15'/%3E%3Crect x='4' y='12' width='2' height='2' fill='%23818cf8' opacity='0.08'/%3E%3Crect x='12' y='4' width='2' height='2' fill='%23818cf8' opacity='0.08'/%3E%3Crect x='8' y='8' width='2' height='2' fill='%23818cf8' opacity='0.08'/%3E%3C/svg%3E") no-repeat center center;
|
|
border-radius: 0 0 10px 0;
|
|
}
|
|
|
|
.resize-handle:hover {
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' shape-rendering='crispEdges'%3E%3Crect x='12' y='12' width='2' height='2' fill='%23a5b4fc' opacity='0.4'/%3E%3Crect x='8' y='12' width='2' height='2' fill='%23a5b4fc' opacity='0.25'/%3E%3Crect x='12' y='8' width='2' height='2' fill='%23a5b4fc' opacity='0.25'/%3E%3Crect x='4' y='12' width='2' height='2' fill='%23a5b4fc' opacity='0.15'/%3E%3Crect x='12' y='4' width='2' height='2' fill='%23a5b4fc' opacity='0.15'/%3E%3Crect x='8' y='8' width='2' height='2' fill='%23a5b4fc' opacity='0.15'/%3E%3C/svg%3E") no-repeat center center;
|
|
}
|
|
|
|
.aero-win.resizing {
|
|
user-select: none;
|
|
}
|
|
|
|
.aero-win.resizing .content {
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ── Transitions ── */
|
|
.win-slide-enter-active,
|
|
.win-slide-leave-active {
|
|
transition: all .2s ease;
|
|
}
|
|
|
|
.win-slide-enter-from,
|
|
.win-slide-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(16px) scale(0.98);
|
|
}
|
|
|
|
|
|
/* ══════════════════════════════════════════════════════════════════════════
|
|
OCEAN LIFE ECOSYSTEM
|
|
══════════════════════════════════════════════════════════════════════════ */
|
|
|
|
.ocean-life {
|
|
position: absolute;
|
|
inset: 0;
|
|
overflow: hidden;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
.cr {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
background: transparent;
|
|
font-style: normal;
|
|
}
|
|
|
|
/* ── 1. Fish School (3 orange fish, swim left) ── */
|
|
.fish-school {
|
|
top: 16px;
|
|
animation: swim-left 10s linear infinite;
|
|
box-shadow:
|
|
0 0 #f97316, 1px 0 #f97316, -1px 0 #fb923c,
|
|
5px 3px #f97316, 6px 3px #f97316, 4px 3px #fb923c,
|
|
3px -2px #f97316, 4px -2px #f97316, 2px -2px #fb923c;
|
|
}
|
|
|
|
/* ── 2. Fish School 2 (2 blue fish, swim right) ── */
|
|
.fish-school-2 {
|
|
top: 20px;
|
|
animation: swim-right 14s linear infinite;
|
|
box-shadow:
|
|
0 0 #818cf8, -1px 0 #818cf8, 1px 0 #a5b4fc,
|
|
-4px 2px #818cf8, -5px 2px #818cf8, -3px 2px #a5b4fc;
|
|
}
|
|
|
|
/* ── 3. Dolphin (grey-blue, arc jump) ── */
|
|
.dolphin {
|
|
top: 14px;
|
|
animation: dolphin-arc 12s linear infinite;
|
|
box-shadow:
|
|
0 0 #94a3b8, 1px 0 #94a3b8, 2px 0 #94a3b8, 3px 0 #cbd5e1, -1px 0 #64748b,
|
|
0 1px #64748b, 1px 1px #64748b, 2px 1px #94a3b8,
|
|
-1px 2px #94a3b8;
|
|
}
|
|
|
|
/* ── 4. Jellyfish (purple, bob) ── */
|
|
.jellyfish {
|
|
top: 18px;
|
|
animation: swim-left 25s linear infinite, jelly-bob 3s ease-in-out infinite;
|
|
box-shadow:
|
|
0 -1px #c084fc,
|
|
-1px 0 #a855f7, 0 0 #a855f7, 1px 0 #a855f7,
|
|
0 1px #c084fc,
|
|
-1px 2px #7c3aed, 1px 2px #7c3aed,
|
|
0 3px #7c3aed;
|
|
}
|
|
|
|
/* ── 5. Seahorse (yellow-orange, slow drift) ── */
|
|
.seahorse {
|
|
top: 15px;
|
|
animation: swim-left 35s linear infinite, jelly-bob 4s ease-in-out infinite;
|
|
box-shadow:
|
|
0 -1px #fbbf24,
|
|
0 0 #f59e0b, 1px 0 #f59e0b,
|
|
0 1px #d97706,
|
|
0 2px #d97706,
|
|
1px 3px #b45309;
|
|
}
|
|
|
|
/* ── 6. Turtle (green, slow) ── */
|
|
.turtle {
|
|
top: 14px;
|
|
animation: swim-left 50s linear infinite;
|
|
box-shadow:
|
|
2px -1px #4ade80,
|
|
-1px 0 #16a34a, 0 0 #16a34a, 1px 0 #22c55e, 2px 0 #22c55e, 3px 0 #16a34a,
|
|
-1px 1px #4ade80, 3px 1px #4ade80;
|
|
}
|
|
|
|
/* ── 7. Pirate Ship (dark silhouette, very slow) ── */
|
|
.pirate-ship {
|
|
top: 7px;
|
|
animation: swim-right 180s linear infinite;
|
|
box-shadow:
|
|
3px -3px #334155, 3px -2px #334155, 3px -1px #475569,
|
|
1px 0 #1e293b, 2px 0 #1e293b, 3px 0 #1e293b, 4px 0 #1e293b, 5px 0 #1e293b,
|
|
0 1px #1e293b, 1px 1px #1e293b, 2px 1px #1e293b, 3px 1px #1e293b, 4px 1px #1e293b, 5px 1px #1e293b, 6px 1px #1e293b,
|
|
1px 2px #0f172a, 2px 2px #0f172a, 3px 2px #0f172a, 4px 2px #0f172a, 5px 2px #0f172a;
|
|
}
|
|
|
|
/* ── 8-10. Bubbles (rising white dots) ── */
|
|
.bubble {
|
|
animation: bubble-rise 6s linear infinite;
|
|
box-shadow: 0 0 rgba(255,255,255,0.45);
|
|
}
|
|
.bubble.b1 { left: 25%; animation-duration: 6s; }
|
|
.bubble.b2 { left: 55%; animation-duration: 9s; }
|
|
.bubble.b3 { left: 80%; animation-duration: 11s; }
|
|
|
|
/* ── 11. Satellite (night, metallic + blue panels) ── */
|
|
.satellite {
|
|
top: 3px;
|
|
animation: swim-left 15s linear infinite;
|
|
box-shadow:
|
|
-2px 0 #3b82f6, -1px 0 #3b82f6, 0 0 #94a3b8, 1px 0 #94a3b8, 2px 0 #3b82f6, 3px 0 #3b82f6;
|
|
}
|
|
|
|
/* ── 12. Anglerfish (night, dark body + glowing lure) ── */
|
|
.anglerfish {
|
|
top: 22px;
|
|
animation: swim-left 40s linear infinite;
|
|
box-shadow:
|
|
-2px -1px #fbbf24, -2px -2px rgba(251,191,36,0.4),
|
|
0 0 #1e293b, 1px 0 #1e293b, 2px 0 #1e293b, 3px 0 #334155,
|
|
0 1px #0f172a, 1px 1px #0f172a, 2px 1px #1e293b;
|
|
}
|
|
|
|
/* ── 13-15. Bioluminescence (night, pulsing cyan dots) ── */
|
|
.biolum {
|
|
box-shadow: 0 0 #22d3ee;
|
|
animation: bio-pulse 3.5s ease-in-out infinite;
|
|
}
|
|
.biolum.bl1 { top: 19px; left: 20%; animation-duration: 3.5s; }
|
|
.biolum.bl2 { top: 23px; left: 50%; animation-duration: 4.5s; }
|
|
.biolum.bl3 { top: 17px; left: 75%; animation-duration: 5.5s; }
|
|
|
|
/* ── 16. Comet (rare, fast diagonal streak) ── */
|
|
.comet {
|
|
top: 2px;
|
|
right: 0;
|
|
animation: comet-streak 2.5s linear forwards;
|
|
box-shadow:
|
|
0 0 #fef3c7, -1px 0 #fef3c7,
|
|
-2px 1px #fde68a, -3px 1px #fde68a,
|
|
-4px 2px rgba(251,191,36,0.6),
|
|
-5px 3px rgba(251,191,36,0.3),
|
|
-6px 4px rgba(251,191,36,0.15);
|
|
}
|
|
|
|
/* ── 17. Whale (rare, large blue silhouette) ── */
|
|
.whale {
|
|
top: 14px;
|
|
animation: swim-left 90s linear infinite;
|
|
box-shadow:
|
|
4px -1px #2563eb, 5px -1px #2563eb,
|
|
1px 0 #1d4ed8, 2px 0 #1d4ed8, 3px 0 #1d4ed8, 4px 0 #2563eb, 5px 0 #2563eb, 6px 0 #2563eb, 7px 0 #1d4ed8,
|
|
0 1px #1e40af, 1px 1px #1e40af, 2px 1px #1d4ed8, 3px 1px #1d4ed8, 4px 1px #1d4ed8, 5px 1px #1d4ed8, 6px 1px #1d4ed8, 7px 1px #1e40af, 8px 1px #1e40af,
|
|
2px 2px #1e40af, 3px 2px #1e40af, 4px 2px #1e40af, 5px 2px #1e40af, 6px 2px #1e40af,
|
|
7px 3px #1e40af;
|
|
}
|
|
|
|
/* ── 18. Bottle (rare, message in a bottle) ── */
|
|
.bottle {
|
|
top: 11px;
|
|
animation: swim-left 120s linear infinite, jelly-bob 5s ease-in-out infinite;
|
|
box-shadow:
|
|
0 -1px #d97706,
|
|
-1px 0 #92400e, 0 0 #92400e, 1px 0 #fef3c7,
|
|
-1px 1px #92400e, 0 1px #92400e;
|
|
}
|
|
|
|
/* ── 19. Leviathan (rare, enormous faint shadow) ── */
|
|
.leviathan {
|
|
top: 20px;
|
|
animation: swim-left 300s linear infinite;
|
|
opacity: 0.15;
|
|
box-shadow:
|
|
2px -1px #1e293b, 3px -1px #1e293b, 4px -1px #1e293b, 5px -1px #1e293b, 6px -1px #1e293b, 7px -1px #1e293b, 8px -1px #1e293b, 9px -1px #1e293b,
|
|
0 0 #0f172a, 1px 0 #0f172a, 2px 0 #0f172a, 3px 0 #0f172a, 4px 0 #0f172a, 5px 0 #0f172a, 6px 0 #0f172a, 7px 0 #0f172a, 8px 0 #0f172a, 9px 0 #0f172a, 10px 0 #0f172a, 11px 0 #0f172a, 12px 0 #0f172a,
|
|
1px 1px #0f172a, 2px 1px #0f172a, 3px 1px #0f172a, 4px 1px #0f172a, 5px 1px #0f172a, 6px 1px #0f172a, 7px 1px #0f172a, 8px 1px #0f172a, 9px 1px #0f172a, 10px 1px #0f172a, 11px 1px #0f172a,
|
|
3px 2px #0f172a, 4px 2px #0f172a, 5px 2px #0f172a, 6px 2px #0f172a, 7px 2px #0f172a, 8px 2px #0f172a, 9px 2px #0f172a;
|
|
}
|
|
|
|
/* ── 20. Sun Ray (noon, golden beam) ── */
|
|
.sun-ray {
|
|
top: 0;
|
|
left: 40%;
|
|
width: 20px;
|
|
height: 100%;
|
|
background: linear-gradient(180deg, rgba(251,191,36,0.12) 0%, rgba(251,191,36,0.04) 40%, transparent 100%);
|
|
animation: sun-ray-pulse 5s ease-in-out infinite;
|
|
}
|
|
|
|
/* ── 21. Dawn Glow (dawn, warm orange) ── */
|
|
.dawn-glow {
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, rgba(251,146,60,0.1) 0%, transparent 40%);
|
|
animation: sun-ray-pulse 8s ease-in-out infinite;
|
|
}
|
|
|
|
/* ── 22. Party Fish (Fridays, rainbow nyan) ── */
|
|
.party-fish {
|
|
top: 15px;
|
|
animation: swim-right 8s linear infinite;
|
|
box-shadow:
|
|
-4px 0 #ef4444, -3px 0 #f97316, -2px 0 #eab308, -1px 0 #22c55e, 0 0 #3b82f6, 1px 0 #8b5cf6,
|
|
-3px 1px #f97316, -2px 1px #eab308, -1px 1px #22c55e, 0 1px #3b82f6;
|
|
}
|
|
|
|
/* ── 23. Ghost Ship (midnight, ethereal) ── */
|
|
.ghost-ship {
|
|
top: 7px;
|
|
animation: swim-left 240s linear infinite;
|
|
opacity: 0.3;
|
|
box-shadow:
|
|
3px -3px rgba(255,255,255,0.6), 3px -2px rgba(255,255,255,0.5), 3px -1px rgba(255,255,255,0.4),
|
|
1px 0 rgba(255,255,255,0.3), 2px 0 rgba(255,255,255,0.3), 3px 0 rgba(255,255,255,0.3), 4px 0 rgba(255,255,255,0.3), 5px 0 rgba(255,255,255,0.3),
|
|
0 1px rgba(255,255,255,0.2), 1px 1px rgba(255,255,255,0.2), 2px 1px rgba(255,255,255,0.2), 3px 1px rgba(255,255,255,0.2), 4px 1px rgba(255,255,255,0.2), 5px 1px rgba(255,255,255,0.2), 6px 1px rgba(255,255,255,0.2),
|
|
1px 2px rgba(255,255,255,0.15), 2px 2px rgba(255,255,255,0.15), 3px 2px rgba(255,255,255,0.15), 4px 2px rgba(255,255,255,0.15), 5px 2px rgba(255,255,255,0.15);
|
|
}
|
|
|
|
/* ── Ocean Life Keyframes ── */
|
|
|
|
@keyframes swim-left {
|
|
from { left: calc(100% + 15px); }
|
|
to { left: -15px; }
|
|
}
|
|
|
|
@keyframes swim-right {
|
|
from { left: -15px; }
|
|
to { left: calc(100% + 15px); }
|
|
}
|
|
|
|
@keyframes dolphin-arc {
|
|
0% { left: calc(100% + 15px); top: 14px; }
|
|
35% { top: 14px; }
|
|
50% { top: 4px; }
|
|
65% { top: 14px; }
|
|
100% { left: -15px; top: 14px; }
|
|
}
|
|
|
|
@keyframes jelly-bob {
|
|
0%, 100% { transform: translateY(0); }
|
|
50% { transform: translateY(-2px); }
|
|
}
|
|
|
|
@keyframes bubble-rise {
|
|
0% { top: 28px; opacity: 0.5; }
|
|
80% { opacity: 0.3; }
|
|
100% { top: -2px; opacity: 0; }
|
|
}
|
|
|
|
@keyframes bio-pulse {
|
|
0%, 100% { opacity: 0; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
@keyframes comet-streak {
|
|
0% { top: 1px; left: 100%; opacity: 0.9; }
|
|
100% { top: 18px; left: -20px; opacity: 0; }
|
|
}
|
|
|
|
@keyframes sun-ray-pulse {
|
|
0%, 100% { opacity: 0.05; }
|
|
50% { opacity: 0.18; }
|
|
}
|
|
|
|
@keyframes titlebar-ocean {
|
|
from { background-position: 0 0, 0 0; }
|
|
to { background-position: 120px 0, 0 0; }
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* ── Mobile ── */
|
|
.aero-win.mobile {
|
|
min-width: unset;
|
|
min-height: unset;
|
|
max-width: 100%;
|
|
transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
|
bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.aero-win.mobile .glass {
|
|
border-radius: 16px 16px 0 0;
|
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
}
|
|
|
|
|
|
.aero-win.mobile .resize-handle {
|
|
display: none;
|
|
}
|
|
|
|
.aero-win.mobile .content {
|
|
flex: 1;
|
|
min-height: 100px;
|
|
touch-action: pan-y;
|
|
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%) !important;
|
|
opacity: 1 !important;
|
|
}
|
|
|
|
/* ── Scroll arrows (aquatic pixel art) ── */
|
|
.scroll-arrows {
|
|
position: absolute;
|
|
right: 6px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
z-index: 5;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.scroll-arrow {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 30px;
|
|
min-height: 24px;
|
|
border: 1px solid rgba(14, 165, 233, 0.12);
|
|
background: rgba(0, 10, 30, 0.45);
|
|
backdrop-filter: blur(4px);
|
|
-webkit-backdrop-filter: blur(4px);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.scroll-arrow:hover {
|
|
background: rgba(14, 165, 233, 0.08);
|
|
border-color: rgba(14, 165, 233, 0.3);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.scroll-arrow:active {
|
|
transform: scale(0.92);
|
|
}
|
|
|
|
.scroll-arrow.sa-surface svg { animation: sa-float 3s ease-in-out infinite; }
|
|
.scroll-arrow.sa-up svg { animation: sa-bob-up 2.5s ease-in-out infinite; }
|
|
.scroll-arrow.sa-down svg { animation: sa-bob-down 2.5s ease-in-out infinite; }
|
|
.scroll-arrow.sa-seabed svg { animation: sa-sway 4s ease-in-out infinite; }
|
|
|
|
@keyframes sa-float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-1px); } }
|
|
@keyframes sa-bob-up { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-1.5px); } }
|
|
@keyframes sa-bob-down { 0%,100% { transform: translateY(0); } 50% { transform: translateY(1.5px); } }
|
|
@keyframes sa-sway { 0%,100% { transform: translateX(0); } 50% { transform: translateX(0.5px); } }
|
|
|
|
/* Transition for scroll arrows appear/disappear */
|
|
.scroll-arrows-enter-active,
|
|
.scroll-arrows-leave-active {
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.scroll-arrows-enter-from,
|
|
.scroll-arrows-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(-50%) translateX(8px);
|
|
}
|
|
</style>
|