-128
frontend/src/lib/components/EndOfList.svelte
-128
frontend/src/lib/components/EndOfList.svelte
···
1
-
<script lang="ts">
2
-
import { fade } from "svelte/transition";
3
-
4
-
interface Props {
5
-
message?: string;
6
-
}
7
-
8
-
let { message = "you've reached the end!" }: Props = $props();
9
-
10
-
const count = 16;
11
-
const ghostCount = 12; // trailing ghosts per dot
12
-
</script>
13
-
14
-
<div class="end-of-list" in:fade={{ duration: 400 }}>
15
-
<div class="canvas">
16
-
{#each Array(count) as _, i}
17
-
{#each Array(ghostCount) as _, g}
18
-
<i
19
-
style="--i:{i}; --g:{g}"
20
-
class:ghost={g > 0}
21
-
></i>
22
-
{/each}
23
-
{/each}
24
-
<!-- second wave: 1/3 phase offset -->
25
-
{#each Array(count) as _, i}
26
-
{#each Array(ghostCount) as _, g}
27
-
<i
28
-
style="--i:{i}; --g:{g}"
29
-
class:ghost={g > 0}
30
-
class="phase2"
31
-
></i>
32
-
{/each}
33
-
{/each}
34
-
<!-- third wave: 2/3 phase offset -->
35
-
{#each Array(count) as _, i}
36
-
{#each Array(ghostCount) as _, g}
37
-
<i
38
-
style="--i:{i}; --g:{g}"
39
-
class:ghost={g > 0}
40
-
class="phase3"
41
-
></i>
42
-
{/each}
43
-
{/each}
44
-
</div>
45
-
<p class="message">{message}</p>
46
-
</div>
47
-
48
-
<style>
49
-
.end-of-list {
50
-
display: flex;
51
-
flex-direction: column;
52
-
align-items: center;
53
-
gap: 1rem;
54
-
padding: 2rem 1rem;
55
-
margin-top: 1rem;
56
-
}
57
-
58
-
.canvas {
59
-
position: relative;
60
-
width: 100px;
61
-
height: 32px;
62
-
}
63
-
64
-
i {
65
-
--wave-color: var(--accent);
66
-
position: absolute;
67
-
left: calc(var(--i) * 6px);
68
-
top: 50%;
69
-
width: 2px;
70
-
height: 2px;
71
-
background: var(--wave-color);
72
-
border-radius: 50%;
73
-
opacity: 0.6;
74
-
box-shadow: 0 0 2px var(--wave-color);
75
-
animation: drift 2s ease-in-out infinite;
76
-
/* base delay for wave + ghost offset for trail */
77
-
animation-delay: calc((var(--i) * -0.15s) + (var(--g) * 0.07s));
78
-
}
79
-
80
-
i.ghost {
81
-
opacity: calc(0.35 - (var(--g) * 0.025));
82
-
width: 1px;
83
-
height: 1px;
84
-
border-radius: 50%;
85
-
filter: blur(calc(var(--g) * 0.2px));
86
-
box-shadow: none;
87
-
}
88
-
89
-
@keyframes drift {
90
-
0%,
91
-
100% {
92
-
transform: translateY(8px);
93
-
}
94
-
50% {
95
-
transform: translateY(-8px);
96
-
}
97
-
}
98
-
99
-
/* 2s duration, so 0.667s = 1/3 phase, 1.333s = 2/3 phase */
100
-
/* each wave gets a color variation mixed from the accent */
101
-
i.phase2 {
102
-
--wave-color: color-mix(in oklch, var(--accent) 70%, #fff);
103
-
animation-delay: calc(
104
-
(var(--i) * -0.15s) + (var(--g) * 0.07s) - 0.667s
105
-
);
106
-
}
107
-
108
-
i.phase3 {
109
-
--wave-color: color-mix(in oklch, var(--accent) 60%, #000);
110
-
animation-delay: calc(
111
-
(var(--i) * -0.15s) + (var(--g) * 0.07s) - 1.333s
112
-
);
113
-
}
114
-
115
-
.message {
116
-
margin: 0;
117
-
color: var(--text-tertiary);
118
-
font-size: 0.8rem;
119
-
letter-spacing: 0.02em;
120
-
}
121
-
122
-
@media (prefers-reduced-motion: reduce) {
123
-
i {
124
-
animation: none;
125
-
opacity: 0.5;
126
-
}
127
-
}
128
-
</style>
-2
frontend/src/routes/+page.svelte
-2
frontend/src/routes/+page.svelte
···
4
4
import TrackItem from '$lib/components/TrackItem.svelte';
5
5
import Header from '$lib/components/Header.svelte';
6
6
import WaveLoading from '$lib/components/WaveLoading.svelte';
7
-
import EndOfList from '$lib/components/EndOfList.svelte';
8
7
import { player } from '$lib/player.svelte';
9
8
import { queue } from '$lib/queue.svelte';
10
9
import { tracksCache } from '$lib/tracks.svelte';
···
107
106
/>
108
107
{/each}
109
108
</div>
110
-
<EndOfList />
111
109
{/if}
112
110
</section>
113
111
</main>