VoiceMicButton component, PixelLife aquatic layer, improved UserMessageBubble with voice display, AgentBadge terminal switcher, ChatContainer voice integration, FloatingTranscriptDebug ocean life enhancements, and terminal registry support. Remove traefik config.
1637 lines
58 KiB
Vue
1637 lines
58 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 } 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,
|
|
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 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))
|
|
}
|
|
|
|
// 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)
|
|
|
|
// Virtual keyboard detection
|
|
const keyboardVisible = ref(false)
|
|
const keyboardHeight = ref(0)
|
|
|
|
function onVisualViewportResize() {
|
|
if (!window.visualViewport) return
|
|
const vv = window.visualViewport
|
|
const kbH = window.innerHeight - (vv.offsetTop + vv.height)
|
|
keyboardHeight.value = Math.max(0, kbH)
|
|
keyboardVisible.value = kbH > 100
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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%',
|
|
}
|
|
|
|
if (keyboardVisible.value) {
|
|
// Body is position:fixed so no browser auto-scroll — simple offset works
|
|
style.bottom = `${keyboardHeight.value}px`
|
|
const available = window.innerHeight - keyboardHeight.value - headerH
|
|
if (isFullScreen) {
|
|
style.height = `${Math.max(0, available)}px`
|
|
} else {
|
|
const originalH = window.innerHeight * sheetHeight.value / 100
|
|
style.height = `${Math.max(0, Math.min(originalH, available))}px`
|
|
}
|
|
} else {
|
|
// No keyboard
|
|
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() {
|
|
createNewSession()
|
|
}
|
|
|
|
// ============================================================================
|
|
// WATCHERS
|
|
// ============================================================================
|
|
|
|
watch(isOpen, async (open) => {
|
|
if (open && !initialized) {
|
|
initialized = true
|
|
await nextTick()
|
|
init()
|
|
}
|
|
})
|
|
|
|
// ============================================================================
|
|
// ZOOM KEYBOARD HANDLER
|
|
// ============================================================================
|
|
|
|
function handleZoomKey(e: KeyboardEvent) {
|
|
if (!isOpen.value || !e.ctrlKey) 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', handleZoomKey)
|
|
if (window.visualViewport) {
|
|
window.visualViewport.addEventListener('resize', onVisualViewportResize)
|
|
}
|
|
oceanLifeTimer = setInterval(tickOceanLife, 20000)
|
|
tickOceanLife()
|
|
await voice.init()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (oceanLifeTimer) clearInterval(oceanLifeTimer)
|
|
disconnectRealtime()
|
|
voice.cleanup()
|
|
document.removeEventListener('keydown', handleZoomKey)
|
|
document.removeEventListener('mousemove', onDrag)
|
|
document.removeEventListener('mouseup', stopDrag)
|
|
document.removeEventListener('mousemove', onResize)
|
|
document.removeEventListener('mouseup', stopResize)
|
|
window.removeEventListener('resize', checkMobile)
|
|
window.visualViewport?.removeEventListener('resize', onVisualViewportResize)
|
|
})
|
|
</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"
|
|
@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?.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"
|
|
@send="handleSend"
|
|
@switch-agent="handleAgentSwitch"
|
|
@select-session="handleSessionSelect"
|
|
@create-session="handleCreateSession"
|
|
@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"
|
|
/>
|
|
<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>
|
|
|
|
<!-- Resize handle -->
|
|
<div v-if="!effectiveMobile" class="resize-handle" @mousedown="startResize"></div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</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 .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-gutter: stable !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;
|
|
}
|
|
|
|
/* Pixel art scrollbar */
|
|
.content :deep(.messages-scroll)::-webkit-scrollbar {
|
|
width: 8px;
|
|
transition: opacity 0.35s ease;
|
|
}
|
|
|
|
/* Idle: hide scrollbar visuals (gutter stays via scrollbar-gutter: stable) */
|
|
.aero-win:not(.chrome-visible) .content :deep(.messages-scroll)::-webkit-scrollbar-track {
|
|
background: transparent !important;
|
|
}
|
|
|
|
.aero-win:not(.chrome-visible) .content :deep(.messages-scroll)::-webkit-scrollbar-thumb {
|
|
background: transparent !important;
|
|
border-color: transparent !important;
|
|
}
|
|
|
|
.content :deep(.messages-scroll)::-webkit-scrollbar-track {
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='8' height='8' fill='%230c2d4a' opacity='0.4'/%3E%3Crect x='2' y='2' width='2' height='2' fill='%23075985' opacity='0.15'/%3E%3Crect x='6' y='6' width='2' height='2' fill='%23075985' opacity='0.1'/%3E%3C/svg%3E") repeat;
|
|
}
|
|
|
|
.content :deep(.messages-scroll)::-webkit-scrollbar-thumb {
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='16' viewBox='0 0 8 16' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='8' height='16' fill='%230ea5e9' opacity='0.3'/%3E%3Crect x='2' y='2' width='4' height='2' fill='%2322d3ee' opacity='0.25'/%3E%3Crect x='2' y='6' width='4' height='2' fill='%2367e8f9' opacity='0.2'/%3E%3Crect x='2' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.25'/%3E%3Crect x='2' y='14' width='4' height='2' fill='%2367e8f9' opacity='0.15'/%3E%3C/svg%3E") repeat;
|
|
border: 1px solid rgba(14, 165, 233, 0.15);
|
|
}
|
|
|
|
.content :deep(.messages-scroll)::-webkit-scrollbar-thumb:hover {
|
|
background:
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='16' viewBox='0 0 8 16' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='8' height='16' fill='%230ea5e9' opacity='0.45'/%3E%3Crect x='2' y='2' width='4' height='2' fill='%2322d3ee' opacity='0.35'/%3E%3Crect x='2' y='6' width='4' height='2' fill='%2367e8f9' opacity='0.3'/%3E%3Crect x='2' y='10' width='4' height='2' fill='%2322d3ee' opacity='0.35'/%3E%3Crect x='2' y='14' width='4' height='2' fill='%2367e8f9' opacity='0.25'/%3E%3C/svg%3E") repeat;
|
|
border-color: rgba(14, 165, 233, 0.3);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
</style>
|