|
|
|
|
@@ -1,40 +1,38 @@
|
|
|
|
|
<template>
|
|
|
|
|
<Transition name="boot-fade">
|
|
|
|
|
<div v-if="visible" class="boot-screen" @click="handleClick">
|
|
|
|
|
<div v-if="visible" class="boot-screen">
|
|
|
|
|
<!-- Particle starfield -->
|
|
|
|
|
<canvas ref="canvasRef" class="boot-stars" />
|
|
|
|
|
|
|
|
|
|
<!-- Two-column layout: terminal left, orb right -->
|
|
|
|
|
<div class="boot-layout" :class="{ 'boot-layout-centered': bootDone }">
|
|
|
|
|
<!-- Left: Terminal log (fades out when done) -->
|
|
|
|
|
<Transition name="terminal-fade">
|
|
|
|
|
<div v-if="!bootDone" class="boot-left">
|
|
|
|
|
<div class="boot-terminal" ref="terminalRef">
|
|
|
|
|
<p v-for="(line, i) in logLines" :key="i" class="boot-log-line" :class="line.type">
|
|
|
|
|
<span class="boot-log-ts">{{ line.prefix }}</span>
|
|
|
|
|
<span>{{ line.text }}</span>
|
|
|
|
|
</p>
|
|
|
|
|
<span class="boot-cursor">_</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="boot-progress-wrap">
|
|
|
|
|
<svg class="boot-arc" viewBox="0 0 200 12" preserveAspectRatio="none">
|
|
|
|
|
<rect x="0" y="4" width="200" height="4" rx="2" fill="rgba(255,255,255,0.06)" />
|
|
|
|
|
<rect x="0" y="4" :width="progress * 2" height="4" rx="2" fill="url(#boot-grad)" />
|
|
|
|
|
<defs>
|
|
|
|
|
<linearGradient id="boot-grad" x1="0" y1="0" x2="200" y2="0" gradientUnits="userSpaceOnUse">
|
|
|
|
|
<stop offset="0" stop-color="#fb923c" />
|
|
|
|
|
<stop offset="1" stop-color="#f59e0b" />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
</defs>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="boot-pct">{{ Math.round(progress) }}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="boot-layout">
|
|
|
|
|
<!-- Left: Terminal log -->
|
|
|
|
|
<div class="boot-left">
|
|
|
|
|
<div class="boot-terminal" ref="terminalRef">
|
|
|
|
|
<p v-for="(line, i) in logLines" :key="i" class="boot-log-line" :class="line.type">
|
|
|
|
|
<span class="boot-log-ts">{{ line.prefix }}</span>
|
|
|
|
|
<span>{{ line.text }}</span>
|
|
|
|
|
</p>
|
|
|
|
|
<span class="boot-cursor">_</span>
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
<div class="boot-progress-wrap">
|
|
|
|
|
<svg class="boot-arc" viewBox="0 0 200 12" preserveAspectRatio="none">
|
|
|
|
|
<rect x="0" y="4" width="200" height="4" rx="2" fill="rgba(255,255,255,0.06)" />
|
|
|
|
|
<rect x="0" y="4" :width="progress * 2" height="4" rx="2" fill="url(#boot-grad)" />
|
|
|
|
|
<defs>
|
|
|
|
|
<linearGradient id="boot-grad" x1="0" y1="0" x2="200" y2="0" gradientUnits="userSpaceOnUse">
|
|
|
|
|
<stop offset="0" stop-color="rgba(255,255,255,0.5)" />
|
|
|
|
|
<stop offset="1" stop-color="rgba(255,255,255,0.9)" />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
</defs>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="boot-pct">{{ Math.round(progress) }}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Right (or center when done): The orb / screensaver -->
|
|
|
|
|
<!-- Right: The orb -->
|
|
|
|
|
<div class="boot-right">
|
|
|
|
|
<div class="boot-orb" :class="{ 'boot-orb-screensaver': bootDone }">
|
|
|
|
|
<div class="boot-orb">
|
|
|
|
|
<!-- Viz ring segments -->
|
|
|
|
|
<div class="boot-viz-ring">
|
|
|
|
|
<div
|
|
|
|
|
@@ -46,23 +44,17 @@
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Center: screensaver-style bordered frame with pixel icon / logo -->
|
|
|
|
|
<!-- Center: gradient-bordered frame with cycling icons -->
|
|
|
|
|
<div class="boot-center-icon">
|
|
|
|
|
<div class="logo-gradient-border boot-icon-frame">
|
|
|
|
|
<div class="boot-icon-inner">
|
|
|
|
|
<Transition name="icon-morph" mode="out-in">
|
|
|
|
|
<div v-if="!bootDone" :key="currentIcon" class="boot-pixel-wrap" :class="{ 'boot-glitch': glitching }">
|
|
|
|
|
<div v-html="sanitizedIcon" />
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else key="logo" class="boot-logo-inner-logo">
|
|
|
|
|
<AnimatedLogo size="xl" no-border fit />
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="boot-icon-frame boot-gradient-border">
|
|
|
|
|
<Transition name="icon-morph" mode="out-in">
|
|
|
|
|
<div :key="currentIcon" class="boot-pixel-wrap" :class="{ 'boot-glitch': glitching }">
|
|
|
|
|
<img :src="iconSources[currentIcon]" class="boot-icon-img" />
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -70,9 +62,7 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
|
|
|
|
|
import DOMPurify from 'dompurify'
|
|
|
|
|
import AnimatedLogo from '@/components/AnimatedLogo.vue'
|
|
|
|
|
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{ visible: boolean }>()
|
|
|
|
|
const emit = defineEmits<{ ready: [] }>()
|
|
|
|
|
@@ -80,61 +70,19 @@ const emit = defineEmits<{ ready: [] }>()
|
|
|
|
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
|
|
|
const terminalRef = ref<HTMLElement | null>(null)
|
|
|
|
|
|
|
|
|
|
const bootDone = ref(false)
|
|
|
|
|
const currentIcon = ref(0)
|
|
|
|
|
const progress = ref(0)
|
|
|
|
|
const litBars = ref(0)
|
|
|
|
|
const glitching = ref(false)
|
|
|
|
|
|
|
|
|
|
// 16x16 pixel art icons
|
|
|
|
|
const icons = [
|
|
|
|
|
// Big smiley — warm and friendly
|
|
|
|
|
`<svg viewBox="0 0 16 16" class="boot-svg">
|
|
|
|
|
<rect x="4" y="5" width="2" height="3" fill="white"/>
|
|
|
|
|
<rect x="10" y="5" width="2" height="3" fill="white"/>
|
|
|
|
|
<rect x="4" y="5" width="2" height="1" fill="rgba(255,255,255,0.5)"/>
|
|
|
|
|
<rect x="10" y="5" width="2" height="1" fill="rgba(255,255,255,0.5)"/>
|
|
|
|
|
<rect x="3" y="10" width="2" height="1" fill="white"/>
|
|
|
|
|
<rect x="11" y="10" width="2" height="1" fill="white"/>
|
|
|
|
|
<rect x="5" y="11" width="6" height="1" fill="white"/>
|
|
|
|
|
</svg>`,
|
|
|
|
|
// Bitcoin
|
|
|
|
|
`<svg viewBox="0 0 16 16" class="boot-svg">
|
|
|
|
|
<rect x="5" y="1" width="2" height="2" fill="#f7931a"/><rect x="9" y="1" width="2" height="2" fill="#f7931a"/>
|
|
|
|
|
<rect x="4" y="3" width="2" height="10" fill="#f7931a"/><rect x="6" y="3" width="4" height="2" fill="#f7931a"/>
|
|
|
|
|
<rect x="10" y="4" width="2" height="3" fill="#f7931a"/><rect x="6" y="7" width="4" height="2" fill="#f7931a"/>
|
|
|
|
|
<rect x="10" y="8" width="2" height="4" fill="#f7931a"/><rect x="6" y="11" width="4" height="2" fill="#f7931a"/>
|
|
|
|
|
<rect x="5" y="13" width="2" height="2" fill="#f7931a"/><rect x="9" y="13" width="2" height="2" fill="#f7931a"/>
|
|
|
|
|
</svg>`,
|
|
|
|
|
// Lightning
|
|
|
|
|
`<svg viewBox="0 0 16 16" class="boot-svg">
|
|
|
|
|
<rect x="8" y="0" width="3" height="3" fill="#fbbf24"/><rect x="6" y="3" width="3" height="3" fill="#fbbf24"/>
|
|
|
|
|
<rect x="4" y="6" width="8" height="2" fill="#fbbf24"/><rect x="7" y="8" width="3" height="3" fill="#fbbf24"/>
|
|
|
|
|
<rect x="5" y="11" width="3" height="3" fill="#fbbf24"/><rect x="3" y="14" width="3" height="2" fill="#fbbf24"/>
|
|
|
|
|
</svg>`,
|
|
|
|
|
// Shield
|
|
|
|
|
`<svg viewBox="0 0 16 16" class="boot-svg">
|
|
|
|
|
<rect x="3" y="1" width="10" height="2" fill="#60a5fa"/><rect x="2" y="3" width="2" height="7" fill="#60a5fa"/>
|
|
|
|
|
<rect x="12" y="3" width="2" height="7" fill="#60a5fa"/><rect x="4" y="10" width="2" height="2" fill="#60a5fa"/>
|
|
|
|
|
<rect x="10" y="10" width="2" height="2" fill="#60a5fa"/><rect x="6" y="12" width="4" height="2" fill="#60a5fa"/>
|
|
|
|
|
<rect x="7" y="5" width="2" height="4" fill="white" opacity="0.5"/><rect x="6" y="6" width="4" height="2" fill="white" opacity="0.5"/>
|
|
|
|
|
</svg>`,
|
|
|
|
|
// Key
|
|
|
|
|
`<svg viewBox="0 0 16 16" class="boot-svg">
|
|
|
|
|
<circle cx="5" cy="6" r="3" fill="none" stroke="#4ade80" stroke-width="1.5"/>
|
|
|
|
|
<rect x="7" y="5" width="7" height="2" fill="#4ade80"/>
|
|
|
|
|
<rect x="12" y="7" width="2" height="2" fill="#4ade80"/><rect x="10" y="7" width="2" height="2" fill="#4ade80"/>
|
|
|
|
|
</svg>`,
|
|
|
|
|
// Mesh nodes
|
|
|
|
|
`<svg viewBox="0 0 16 16" class="boot-svg">
|
|
|
|
|
<circle cx="8" cy="8" r="2" fill="white"/>
|
|
|
|
|
<circle cx="3" cy="3" r="1.5" fill="#a78bfa"/><circle cx="13" cy="3" r="1.5" fill="#a78bfa"/>
|
|
|
|
|
<circle cx="3" cy="13" r="1.5" fill="#a78bfa"/><circle cx="13" cy="13" r="1.5" fill="#a78bfa"/>
|
|
|
|
|
<line x1="8" y1="8" x2="3" y2="3" stroke="#a78bfa" stroke-width="0.5" opacity="0.5"/>
|
|
|
|
|
<line x1="8" y1="8" x2="13" y2="3" stroke="#a78bfa" stroke-width="0.5" opacity="0.5"/>
|
|
|
|
|
<line x1="8" y1="8" x2="3" y2="13" stroke="#a78bfa" stroke-width="0.5" opacity="0.5"/>
|
|
|
|
|
<line x1="8" y1="8" x2="13" y2="13" stroke="#a78bfa" stroke-width="0.5" opacity="0.5"/>
|
|
|
|
|
</svg>`,
|
|
|
|
|
// Boot screen icons — from /assets/icon/ directory
|
|
|
|
|
const iconSources = [
|
|
|
|
|
'/assets/icon/bitcoin.svg',
|
|
|
|
|
'/assets/icon/cloud-done.svg',
|
|
|
|
|
'/assets/icon/github.svg',
|
|
|
|
|
'/assets/icon/save.svg',
|
|
|
|
|
'/assets/icon/batteries.svg',
|
|
|
|
|
'/assets/icon/barbarian.svg',
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
interface LogLine { prefix: string; text: string; type: string }
|
|
|
|
|
@@ -159,8 +107,6 @@ const bootMessages = [
|
|
|
|
|
{ delay: 23500, prefix: '***', text: 'ALL SYSTEMS OPERATIONAL', type: 'ready' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const sanitizedIcon = computed(() => DOMPurify.sanitize(icons[currentIcon.value] || '', { USE_PROFILES: { svg: true } }))
|
|
|
|
|
|
|
|
|
|
// Starfield
|
|
|
|
|
let animFrame = 0
|
|
|
|
|
const stars: { x: number; y: number; z: number }[] = []
|
|
|
|
|
@@ -191,15 +137,6 @@ function drawStars(c: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
|
|
|
|
|
|
|
|
|
|
function triggerGlitch() { glitching.value = true; setTimeout(() => { glitching.value = false }, 200) }
|
|
|
|
|
|
|
|
|
|
function handleClick() {
|
|
|
|
|
if (!bootDone.value) return
|
|
|
|
|
// Clear intro flag so App.vue's SplashScreen plays the full intro sequence
|
|
|
|
|
localStorage.removeItem('neode_intro_seen')
|
|
|
|
|
// Also clear onboarding flag so it goes through onboarding after intro
|
|
|
|
|
localStorage.removeItem('neode_onboarding_complete')
|
|
|
|
|
emit('ready')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Health check
|
|
|
|
|
async function checkHealth(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
@@ -221,10 +158,11 @@ let logTimeouts: ReturnType<typeof setTimeout>[] = []
|
|
|
|
|
|
|
|
|
|
function startPolling() {
|
|
|
|
|
iconInterval = setInterval(() => {
|
|
|
|
|
if (!bootDone.value) { currentIcon.value = (currentIcon.value + 1) % icons.length; triggerGlitch() }
|
|
|
|
|
currentIcon.value = (currentIcon.value + 1) % iconSources.length
|
|
|
|
|
triggerGlitch()
|
|
|
|
|
}, 2500)
|
|
|
|
|
|
|
|
|
|
// Feed boot log messages — the visual sequence drives the timeline
|
|
|
|
|
// Feed boot log messages
|
|
|
|
|
const lastMsgDelay = bootMessages[bootMessages.length - 1]!.delay
|
|
|
|
|
for (const msg of bootMessages) {
|
|
|
|
|
logTimeouts.push(setTimeout(() => {
|
|
|
|
|
@@ -237,23 +175,29 @@ function startPolling() {
|
|
|
|
|
}, msg.delay))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// After the last message, start polling for real server readiness
|
|
|
|
|
// (visual sequence must complete before we transition)
|
|
|
|
|
// After the last message, poll for server readiness then immediately transition
|
|
|
|
|
let finished = false
|
|
|
|
|
logTimeouts.push(setTimeout(() => {
|
|
|
|
|
// In dev/mock mode the server may already be ready — check and complete
|
|
|
|
|
const finishBoot = () => {
|
|
|
|
|
if (finished) return
|
|
|
|
|
finished = true
|
|
|
|
|
stopPolling()
|
|
|
|
|
progress.value = 100
|
|
|
|
|
litBars.value = 48
|
|
|
|
|
setTimeout(() => { bootDone.value = true }, 1200)
|
|
|
|
|
if (import.meta.env.DEV) console.log('[Boot] finishBoot — emitting ready in 800ms')
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (import.meta.env.DEV) console.log('[Boot] emitting ready now')
|
|
|
|
|
emit('ready')
|
|
|
|
|
}, 800)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check immediately
|
|
|
|
|
checkHealth().then(r => {
|
|
|
|
|
if (import.meta.env.DEV) console.log('[Boot] health check result:', r)
|
|
|
|
|
if (r) { finishBoot(); return }
|
|
|
|
|
// Not ready yet — poll until it is
|
|
|
|
|
pollInterval = setInterval(async () => {
|
|
|
|
|
if (await checkHealth()) finishBoot()
|
|
|
|
|
const healthy = await checkHealth()
|
|
|
|
|
if (import.meta.env.DEV) console.log('[Boot] poll health:', healthy)
|
|
|
|
|
if (healthy) finishBoot()
|
|
|
|
|
}, 2000)
|
|
|
|
|
})
|
|
|
|
|
}, lastMsgDelay + 1500))
|
|
|
|
|
@@ -280,10 +224,16 @@ function initCanvas() {
|
|
|
|
|
if (ctx) { initStars(c); drawStars(c, ctx) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(() => props.visible, v => { if (v) { startPolling(); nextTick(initCanvas) } })
|
|
|
|
|
onMounted(() => { if (props.visible) { startPolling(); nextTick(initCanvas) } })
|
|
|
|
|
let started = false
|
|
|
|
|
function startIfNeeded() {
|
|
|
|
|
if (started) return
|
|
|
|
|
started = true
|
|
|
|
|
startPolling()
|
|
|
|
|
nextTick(initCanvas)
|
|
|
|
|
}
|
|
|
|
|
watch(() => props.visible, v => { if (v) startIfNeeded() })
|
|
|
|
|
onMounted(() => { if (props.visible) startIfNeeded() })
|
|
|
|
|
onBeforeUnmount(() => { stopPolling(); cancelAnimationFrame(animFrame) })
|
|
|
|
|
defineExpose({ startPolling })
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
@@ -292,7 +242,6 @@ defineExpose({ startPolling })
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
cursor: default; overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
.boot-screen:has(.boot-click-prompt) { cursor: pointer; }
|
|
|
|
|
.boot-stars { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
|
|
|
|
|
|
|
|
/* Two-column layout */
|
|
|
|
|
@@ -300,9 +249,7 @@ defineExpose({ startPolling })
|
|
|
|
|
position: relative; z-index: 1;
|
|
|
|
|
display: flex; align-items: center; gap: 3rem;
|
|
|
|
|
max-width: 900px; width: 90%; padding: 0 1rem;
|
|
|
|
|
transition: justify-content 0.8s ease;
|
|
|
|
|
}
|
|
|
|
|
.boot-layout-centered { justify-content: center; }
|
|
|
|
|
|
|
|
|
|
/* Left column: terminal */
|
|
|
|
|
.boot-left {
|
|
|
|
|
@@ -318,19 +265,19 @@ defineExpose({ startPolling })
|
|
|
|
|
}
|
|
|
|
|
.boot-log-line { white-space: nowrap; overflow: hidden; animation: log-in 0.3s ease both; }
|
|
|
|
|
.boot-log-line.info { color: rgba(255,255,255,0.35); }
|
|
|
|
|
.boot-log-line.success { color: #4ade80; }
|
|
|
|
|
.boot-log-line.ready { color: #fb923c; font-weight: 600; text-shadow: 0 0 10px rgba(251,146,60,0.5); }
|
|
|
|
|
.boot-log-line.success { color: rgba(255,255,255,0.7); }
|
|
|
|
|
.boot-log-line.ready { color: white; font-weight: 600; text-shadow: 0 0 10px rgba(255,255,255,0.4); }
|
|
|
|
|
.boot-log-ts { color: rgba(255,255,255,0.15); margin-right: 8px; font-weight: 500; }
|
|
|
|
|
.boot-log-line.success .boot-log-ts { color: rgba(74,222,128,0.4); }
|
|
|
|
|
.boot-log-line.ready .boot-log-ts { color: rgba(251,146,60,0.6); }
|
|
|
|
|
.boot-log-line.success .boot-log-ts { color: rgba(255,255,255,0.35); }
|
|
|
|
|
.boot-log-line.ready .boot-log-ts { color: rgba(255,255,255,0.5); }
|
|
|
|
|
@keyframes log-in { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
|
|
|
|
|
|
|
|
|
|
.boot-cursor { color: rgba(251,146,60,0.7); animation: blink 1s step-end infinite; font-family: monospace; font-size: 12px; }
|
|
|
|
|
.boot-cursor { color: rgba(255,255,255,0.5); animation: blink 1s step-end infinite; font-family: monospace; font-size: 12px; }
|
|
|
|
|
@keyframes blink { 50% { opacity: 0; } }
|
|
|
|
|
|
|
|
|
|
.boot-progress-wrap { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
|
|
|
|
|
.boot-arc { flex: 1; height: 12px; }
|
|
|
|
|
.boot-pct { font-family: 'SF Mono', monospace; font-size: 10px; color: rgba(255,255,255,0.25); min-width: 28px; text-align: right; }
|
|
|
|
|
.boot-pct { font-family: 'SF Mono', monospace; font-size: 10px; color: rgba(255,255,255,0.3); min-width: 28px; text-align: right; }
|
|
|
|
|
|
|
|
|
|
/* Right column: orb */
|
|
|
|
|
.boot-right {
|
|
|
|
|
@@ -339,24 +286,14 @@ defineExpose({ startPolling })
|
|
|
|
|
|
|
|
|
|
.boot-orb {
|
|
|
|
|
position: relative; width: 220px; height: 220px;
|
|
|
|
|
transition: width 0.8s ease, height 0.8s ease;
|
|
|
|
|
}
|
|
|
|
|
@media (min-width: 640px) { .boot-orb { width: 280px; height: 280px; } }
|
|
|
|
|
@media (min-width: 768px) { .boot-orb { width: 320px; height: 320px; } }
|
|
|
|
|
|
|
|
|
|
.boot-orb-screensaver {
|
|
|
|
|
width: 280px; height: 280px;
|
|
|
|
|
}
|
|
|
|
|
@media (min-width: 640px) { .boot-orb-screensaver { width: 360px; height: 360px; } }
|
|
|
|
|
@media (min-width: 768px) { .boot-orb-screensaver { width: 400px; height: 400px; } }
|
|
|
|
|
|
|
|
|
|
/* Viz ring */
|
|
|
|
|
.boot-viz-ring { position: absolute; inset: 0; --vr: 100px; }
|
|
|
|
|
@media (min-width: 640px) { .boot-viz-ring { --vr: 130px; } }
|
|
|
|
|
@media (min-width: 768px) { .boot-viz-ring { --vr: 150px; } }
|
|
|
|
|
.boot-orb-screensaver .boot-viz-ring { --vr: 130px; }
|
|
|
|
|
@media (min-width: 640px) { .boot-orb-screensaver .boot-viz-ring { --vr: 170px; } }
|
|
|
|
|
@media (min-width: 768px) { .boot-orb-screensaver .boot-viz-ring { --vr: 190px; } }
|
|
|
|
|
|
|
|
|
|
.boot-viz-seg {
|
|
|
|
|
position: absolute; left: 50%; top: 50%;
|
|
|
|
|
@@ -367,22 +304,10 @@ defineExpose({ startPolling })
|
|
|
|
|
transition: background 0.4s ease, height 0.4s ease, box-shadow 0.4s ease;
|
|
|
|
|
}
|
|
|
|
|
.boot-seg-lit {
|
|
|
|
|
background: linear-gradient(to bottom, rgba(251,146,60,0.8), rgba(245,158,11,0.3));
|
|
|
|
|
box-shadow: 0 0 5px rgba(251,146,60,0.2);
|
|
|
|
|
background: linear-gradient(to bottom, rgba(255,255,255,0.7), rgba(255,255,255,0.2));
|
|
|
|
|
box-shadow: 0 0 6px rgba(255,255,255,0.15);
|
|
|
|
|
height: 22px; margin-top: -11px;
|
|
|
|
|
}
|
|
|
|
|
/* When done, all segments pulse like screensaver */
|
|
|
|
|
.boot-orb-screensaver .boot-viz-seg {
|
|
|
|
|
background: linear-gradient(to bottom, rgba(255,255,255,0.4), rgba(255,255,255,0.1));
|
|
|
|
|
box-shadow: none; height: 24px; margin-top: -12px;
|
|
|
|
|
animation: seg-pulse 14s ease-in-out infinite;
|
|
|
|
|
animation-delay: calc(var(--si) * 0.02s);
|
|
|
|
|
}
|
|
|
|
|
@keyframes seg-pulse {
|
|
|
|
|
0%,14.3%,28.6%,42.9%,57.1%,71.4%,92.9%,100% { opacity:0.3; transform: rotate(var(--sd)) translateY(calc(-1 * var(--vr))) scaleY(0.4); }
|
|
|
|
|
7.1%,21.4%,35.7%,50%,64.3% { opacity:0.9; transform: rotate(var(--sd)) translateY(calc(-1 * var(--vr))) scaleY(1); }
|
|
|
|
|
78.6%,85.7% { opacity:1; transform: rotate(var(--sd)) translateY(calc(-1 * var(--vr))) scaleY(1.5); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Center icon */
|
|
|
|
|
.boot-center-icon {
|
|
|
|
|
@@ -390,29 +315,39 @@ defineExpose({ startPolling })
|
|
|
|
|
filter: drop-shadow(0 0 30px rgba(255,255,255,0.1));
|
|
|
|
|
}
|
|
|
|
|
.boot-icon-frame {
|
|
|
|
|
width: 140px; height: 140px;
|
|
|
|
|
display: flex; align-items: center; justify-content: center; overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
@media (min-width: 640px) { .boot-icon-frame { width: 180px; height: 180px; } }
|
|
|
|
|
@media (min-width: 768px) { .boot-icon-frame { width: 220px; height: 220px; } }
|
|
|
|
|
.boot-orb-screensaver .boot-icon-frame {
|
|
|
|
|
width: 192px; height: 192px;
|
|
|
|
|
}
|
|
|
|
|
@media (min-width: 640px) { .boot-orb-screensaver .boot-icon-frame { width: 256px; height: 256px; } }
|
|
|
|
|
@media (min-width: 768px) { .boot-orb-screensaver .boot-icon-frame { width: 320px; height: 320px; } }
|
|
|
|
|
|
|
|
|
|
.boot-icon-inner {
|
|
|
|
|
position: absolute; inset: 3px;
|
|
|
|
|
width: 120px; height: 120px;
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
background: rgba(0,0,0,0.85); border-radius: inherit;
|
|
|
|
|
}
|
|
|
|
|
@media (min-width: 640px) { .boot-icon-frame { width: 160px; height: 160px; } }
|
|
|
|
|
@media (min-width: 768px) { .boot-icon-frame { width: 200px; height: 200px; } }
|
|
|
|
|
|
|
|
|
|
/* Gradient border — circular, matches logo-gradient-border style */
|
|
|
|
|
.boot-gradient-border {
|
|
|
|
|
position: relative;
|
|
|
|
|
border-radius: 9999px;
|
|
|
|
|
padding: 3px;
|
|
|
|
|
background: linear-gradient(135deg, rgba(255,255,255,0.6) 0%, rgba(0,0,0,0.8) 100%);
|
|
|
|
|
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
|
|
|
|
}
|
|
|
|
|
.boot-gradient-border::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 3px;
|
|
|
|
|
border-radius: 9999px;
|
|
|
|
|
background: #000;
|
|
|
|
|
z-index: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.boot-pixel-wrap { width: 72px; height: 72px; }
|
|
|
|
|
@media (min-width: 640px) { .boot-pixel-wrap { width: 90px; height: 90px; } }
|
|
|
|
|
.boot-pixel-wrap {
|
|
|
|
|
width: 100%; height: 100%;
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
position: relative; z-index: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.boot-logo-inner-logo { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
|
|
|
|
|
:deep(.boot-svg) { width: 100%; height: 100%; image-rendering: pixelated; image-rendering: crisp-edges; }
|
|
|
|
|
.boot-icon-img {
|
|
|
|
|
width: 55%; height: 55%; object-fit: contain;
|
|
|
|
|
filter: brightness(0) invert(1) drop-shadow(0 0 8px rgba(255,255,255,0.15));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Glitch */
|
|
|
|
|
.boot-glitch { animation: glitch 0.2s steps(3) both; }
|
|
|
|
|
@@ -430,22 +365,6 @@ defineExpose({ startPolling })
|
|
|
|
|
.icon-morph-enter-from { opacity:0; transform: scale(0.5) rotate(-10deg); filter: blur(4px); }
|
|
|
|
|
.icon-morph-leave-to { opacity:0; transform: scale(1.4) rotate(10deg); filter: blur(4px); }
|
|
|
|
|
|
|
|
|
|
/* Click prompt */
|
|
|
|
|
.boot-click-prompt {
|
|
|
|
|
color: rgba(255,255,255,0.4); font-size: 0.8rem; font-weight: 500;
|
|
|
|
|
letter-spacing: 0.1em; text-transform: uppercase;
|
|
|
|
|
animation: prompt-breathe 3s ease-in-out infinite;
|
|
|
|
|
}
|
|
|
|
|
@keyframes prompt-breathe {
|
|
|
|
|
0%,100% { opacity: 0.3; } 50% { opacity: 0.7; }
|
|
|
|
|
}
|
|
|
|
|
.prompt-fade-enter-active { transition: opacity 1s ease 0.5s; }
|
|
|
|
|
.prompt-fade-enter-from { opacity: 0; }
|
|
|
|
|
|
|
|
|
|
/* Terminal fade out */
|
|
|
|
|
.terminal-fade-leave-active { transition: opacity 0.8s ease, transform 0.8s ease; }
|
|
|
|
|
.terminal-fade-leave-to { opacity: 0; transform: translateX(-30px); }
|
|
|
|
|
|
|
|
|
|
/* Boot screen fade out */
|
|
|
|
|
.boot-fade-leave-active { transition: opacity 1.2s ease; }
|
|
|
|
|
.boot-fade-leave-to { opacity: 0; }
|
|
|
|
|
@@ -455,6 +374,5 @@ defineExpose({ startPolling })
|
|
|
|
|
.boot-layout { flex-direction: column-reverse; gap: 2rem; }
|
|
|
|
|
.boot-left { max-width: 100%; }
|
|
|
|
|
.boot-orb { width: 200px; height: 200px; }
|
|
|
|
|
.boot-orb-screensaver { width: 260px; height: 260px; }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|