Update UI components and enhance controller navigation for improved user experience

- Updated styles in various components to change color themes from cyan to yellow for better visual consistency.
- Enhanced focus management in controller navigation to improve accessibility and user interaction.
- Added new data attributes for controller navigation in multiple views to streamline user interactions with app containers.
- Improved audio handling by removing unused functions in useLoginSounds.ts, optimizing the codebase.
This commit is contained in:
Dorian
2026-02-17 21:10:16 +00:00
parent 5a04875dcc
commit 316dfee2fc
12 changed files with 428 additions and 55 deletions
+16 -44
View File
@@ -1,20 +1,13 @@
<template>
<div class="logo-gradient-border flex-shrink-0 inline-block overflow-hidden">
<!-- Neode logo - white or coloured -->
<div class="logo-gradient-border flex-shrink-0 inline-block overflow-hidden" :class="sizeClass">
<!-- Neode logo - always white -->
<svg
class="w-14 h-14 block logo-svg"
class="block logo-svg"
viewBox="0 0 1024 1024"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="Neode"
>
<defs>
<linearGradient :id="gradientId" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f9aa4b" />
<stop offset="50%" stop-color="#f7931a" />
<stop offset="100%" stop-color="#e68a19" />
</linearGradient>
</defs>
<rect width="1024" height="1024" fill="#030202" />
<rect
v-for="(r, i) in rects"
@@ -23,9 +16,8 @@
:y="r.y"
:width="r.w"
:height="r.h"
:fill="mode === 'coloured' ? `url(#${gradientId})` : 'white'"
fill="white"
class="logo-square"
:class="{ 'logo-square-coloured': mode === 'coloured' }"
:style="{ '--delay': delays[i] + 'ms' }"
/>
</svg>
@@ -33,9 +25,11 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
const props = withDefaults(defineProps<{
size?: 'sm' | 'lg' | 'xl'
}>(), { size: 'sm' })
const gradientId = 'logo-color-' + Math.random().toString(36).slice(2, 11)
const sizeClass = props.size === 'xl' ? 'w-48 h-48 sm:w-64 sm:h-64 md:w-80 md:h-80' : props.size === 'lg' ? 'w-32 h-32 sm:w-48 sm:h-48' : 'w-14 h-14'
// Parsed from favico-black.svg path - 20 rects
const rects = [
@@ -61,26 +55,9 @@ const rects = [
{ x: 595.379, y: 633.989, w: 71.007, h: 72.011 },
]
// Stagger delays (ms) - spread over ~4.5s load phase
const delays = [0, 2341, 467, 1890, 312, 3456, 123, 2789, 567, 4123, 901, 1456, 234, 3789, 2678, 456, 847, 2912, 1891, 423]
type Mode = 'normal' | 'coloured'
const mode = ref<Mode>('normal')
let cycleCount = 0
let intervalId: ReturnType<typeof setInterval> | null = null
const CYCLE_MS = 10000
onMounted(() => {
intervalId = setInterval(() => {
cycleCount++
mode.value = cycleCount % 4 === 3 ? 'coloured' : 'normal'
}, CYCLE_MS)
})
onBeforeUnmount(() => {
if (intervalId) clearInterval(intervalId)
})
// Stagger delays (ms) - row-by-row top-to-bottom, left-to-right for a clean reveal
// Row 1 (y~318): 0,1,2,3 | Row 2 (y~396): 4,5 | Row 3 (y~476): 6-11 | Row 4 (y~555): 12-15 | Row 5 (y~634): 16-19
const delays = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900]
</script>
<style scoped>
@@ -90,20 +67,15 @@ onBeforeUnmount(() => {
.logo-square {
opacity: 0;
animation: logo-square-in 10s cubic-bezier(0.4, 0, 0.2, 1) infinite;
animation: logo-square-in 3s ease-out infinite;
animation-delay: var(--delay, 0ms);
animation-fill-mode: both;
}
.logo-square-coloured {
filter: drop-shadow(0 0 2px rgba(247, 147, 26, 0.4));
}
/* 045%: squares load in. 45100%: full logo visible */
/* Fade in only - no scale/position change. Squares stay fixed. */
@keyframes logo-square-in {
0% { opacity: 0; transform: scale(0.95); }
4% { opacity: 1; transform: scale(1); }
45% { opacity: 1; transform: scale(1); }
100% { opacity: 1; transform: scale(1); }
0% { opacity: 0; }
15% { opacity: 1; }
100% { opacity: 1; }
}
</style>
@@ -0,0 +1,111 @@
<template>
<Teleport to="body">
<Transition name="app-launcher">
<div
v-if="store.isOpen"
class="fixed inset-0 z-[2400] flex items-center justify-center p-6 md:p-10"
@click.self="store.close()"
>
<!-- Backdrop - blur like spotlight -->
<div class="absolute inset-0 bg-black/60 backdrop-blur-md"></div>
<!-- Panel - inset with margins, glass style like spotlight -->
<div
class="app-launcher-panel relative z-10 flex flex-col overflow-hidden rounded-2xl shadow-2xl"
:class="panelClasses"
>
<!-- Header bar - drag handle + title + close -->
<div class="flex items-center gap-3 border-b border-white/10 px-4 py-3">
<div class="flex items-center justify-center w-8 h-8 shrink-0 rounded cursor-grab hover:bg-white/10 transition-colors">
<svg class="w-4 h-4 text-white/50" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 6h2v2H8V6zm0 5h2v2H8v-2zm0 5h2v2H8v-2zm5-10h2v2h-2V6zm0 5h2v2h-2v-2zm0 5h2v2h-2v-2z" />
</svg>
</div>
<span class="flex-1 truncate text-sm font-medium text-white/90">{{ store.title || 'App' }}</span>
<button
ref="closeBtnRef"
type="button"
class="flex items-center justify-center w-9 h-9 rounded-lg hover:bg-white/15 text-white/70 hover:text-white transition-colors"
aria-label="Close"
@click="store.close()"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<kbd class="hidden sm:inline-flex px-2 py-1 text-xs text-white/50 bg-white/10 rounded">Esc</kbd>
</div>
<!-- Iframe container -->
<div class="relative flex-1 min-h-0 bg-black/40">
<iframe
v-if="store.url"
:src="store.url"
class="absolute inset-0 w-full h-full border-0"
title="App content"
/>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { useAppLauncherStore } from '@/stores/appLauncher'
const store = useAppLauncherStore()
const closeBtnRef = ref<HTMLButtonElement | null>(null)
const panelClasses = [
'glass-card',
'w-full h-full max-w-[calc(100vw-3rem)] max-h-[calc(100vh-5rem)]',
'md:max-w-[calc(100vw-5rem)] md:max-h-[calc(100vh-5rem)]',
]
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape' && store.isOpen) {
store.close()
e.preventDefault()
e.stopPropagation()
}
}
watch(
() => store.isOpen,
(open) => {
if (open) {
closeBtnRef.value?.focus()
}
}
)
onMounted(() => {
window.addEventListener('keydown', onKeyDown, true)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeyDown, true)
})
</script>
<style scoped>
.app-launcher-enter-active,
.app-launcher-leave-active {
transition: opacity 0.25s ease;
}
.app-launcher-enter-active .app-launcher-panel,
.app-launcher-leave-active .app-launcher-panel {
transition: transform 0.25s ease, opacity 0.25s ease;
}
.app-launcher-enter-from,
.app-launcher-leave-to {
opacity: 0;
}
.app-launcher-enter-from .app-launcher-panel,
.app-launcher-leave-to .app-launcher-panel {
transform: scale(0.96);
opacity: 0;
}
</style>
+166
View File
@@ -0,0 +1,166 @@
<template>
<Teleport to="body">
<Transition name="screensaver">
<div
v-if="store.isActive"
class="screensaver-container fixed inset-0 z-[3000] bg-black cursor-pointer"
@click="store.deactivate()"
@keydown.escape="store.deactivate()"
>
<!-- Logo with audio viz ring - explicitly centered in viewport -->
<div class="screensaver-content">
<!-- Radial audio visualization - bars around the logo -->
<div class="screensaver-viz-ring">
<div
v-for="(_, i) in segmentCount"
:key="i"
class="screensaver-viz-segment"
:style="getSegmentStyle(i)"
/>
</div>
<!-- Logo in center -->
<div class="screensaver-logo-wrapper relative z-10">
<AnimatedLogo size="xl" />
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import AnimatedLogo from '@/components/AnimatedLogo.vue'
import { useScreensaverStore } from '@/stores/screensaver'
const store = useScreensaverStore()
const segmentCount = 48
function getSegmentStyle(i: number) {
const deg = (i / segmentCount) * 360
return {
'--segment-index': i,
'--segment-deg': `${deg}deg`,
}
}
// Dismiss on any key (except when typing)
function onKeyDown(e: KeyboardEvent) {
if (store.isActive) {
store.deactivate()
e.preventDefault()
}
}
onMounted(() => {
window.addEventListener('keydown', onKeyDown)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeyDown)
})
</script>
<style scoped>
.screensaver-enter-active,
.screensaver-leave-active {
transition: opacity 0.5s ease;
}
.screensaver-enter-from,
.screensaver-leave-to {
opacity: 0;
}
/* Explicit viewport centering */
.screensaver-container {
position: fixed;
inset: 0;
display: grid;
place-items: center;
}
.screensaver-content {
position: relative;
width: 280px;
height: 280px;
flex-shrink: 0;
}
@media (min-width: 640px) {
.screensaver-content {
width: 360px;
height: 360px;
}
}
@media (min-width: 768px) {
.screensaver-content {
width: 400px;
height: 400px;
}
}
/* Ring of segments around the logo - audio viz style */
.screensaver-viz-ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
--viz-radius: 140px;
}
@media (min-width: 640px) {
.screensaver-viz-ring {
--viz-radius: 180px;
}
}
@media (min-width: 768px) {
.screensaver-viz-ring {
--viz-radius: 200px;
}
}
.screensaver-viz-segment {
position: absolute;
left: 50%;
top: 50%;
width: 4px;
height: 24px;
margin-left: -2px;
margin-top: -12px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1));
border-radius: 2px;
/* Origin at segment center = ring center (segment is centered via left/top 50%) */
transform-origin: center center;
transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius)));
animation: segment-pulse 2s ease-in-out infinite;
animation-delay: calc(var(--segment-index) * 0.02s);
}
@media (min-width: 768px) {
.screensaver-viz-segment {
height: 28px;
margin-top: -14px;
}
}
@keyframes segment-pulse {
0%, 100% {
opacity: 0.3;
transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4);
}
50% {
opacity: 0.9;
transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1);
}
}
.screensaver-logo-wrapper {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
filter: drop-shadow(0 0 40px rgba(255, 255, 255, 0.15));
}
</style>