feat: voice mic, pixel life layer, enhanced transcript-debug UX
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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<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'
|
||||
|
||||
@@ -32,14 +33,31 @@ const {
|
||||
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' },
|
||||
@@ -62,6 +80,46 @@ const showChrome = computed(() =>
|
||||
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
|
||||
// ============================================================================
|
||||
@@ -84,6 +142,24 @@ 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)
|
||||
@@ -117,6 +193,18 @@ function cycleSizeMode() {
|
||||
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
|
||||
// ============================================================================
|
||||
@@ -213,21 +301,34 @@ const sizeModePresets: Record<SizeMode, { w: number; h: number }> = {
|
||||
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',
|
||||
bottom: '0',
|
||||
width: '100%',
|
||||
}
|
||||
if (isFullScreen) {
|
||||
// Full height but respect app header
|
||||
const headerEl = document.querySelector('.app-header') as HTMLElement | null
|
||||
const headerH = headerEl ? headerEl.offsetHeight : 40
|
||||
style.top = `${headerH}px`
|
||||
style.height = 'auto'
|
||||
|
||||
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 {
|
||||
style.height = `${sheetHeight.value}dvh`
|
||||
// No keyboard
|
||||
style.bottom = '0'
|
||||
if (isFullScreen) {
|
||||
style.top = `${headerH}px`
|
||||
style.height = 'auto'
|
||||
} else {
|
||||
style.height = `${sheetHeight.value}dvh`
|
||||
}
|
||||
}
|
||||
return style
|
||||
}
|
||||
@@ -335,6 +436,7 @@ function handleSessionSelect(sessionId: string) {
|
||||
}
|
||||
|
||||
function handleSend(message: string) {
|
||||
voice.clearTranscript()
|
||||
sendPrompt(message)
|
||||
}
|
||||
|
||||
@@ -373,20 +475,29 @@ function handleZoomKey(e: KeyboardEvent) {
|
||||
// LIFECYCLE
|
||||
// ============================================================================
|
||||
|
||||
onMounted(() => {
|
||||
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>
|
||||
|
||||
@@ -420,8 +531,49 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 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" />
|
||||
<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
|
||||
@@ -486,7 +638,7 @@ onBeforeUnmount(() => {
|
||||
<!-- Content -->
|
||||
<div class="content">
|
||||
<AquaticBackground />
|
||||
<div class="readability-overlay" />
|
||||
<div class="readability-overlay" :style="{ background: `rgba(0, 0, 0, ${overlayOpacity})` }" />
|
||||
<ChatContainer
|
||||
ref="chatRef"
|
||||
v-if="conversation"
|
||||
@@ -500,10 +652,27 @@ onBeforeUnmount(() => {
|
||||
: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">
|
||||
@@ -733,9 +902,12 @@ onBeforeUnmount(() => {
|
||||
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;
|
||||
@@ -744,6 +916,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
@@ -831,7 +1005,6 @@ onBeforeUnmount(() => {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.35s ease;
|
||||
@@ -1148,6 +1321,275 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
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); }
|
||||
}
|
||||
@@ -1157,7 +1599,8 @@ onBeforeUnmount(() => {
|
||||
min-width: unset;
|
||||
min-height: unset;
|
||||
max-width: 100%;
|
||||
transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user