Navbars are still messed up
This commit is contained in:
38
src/lib/components/util/iconButton.svelte
Normal file
38
src/lib/components/util/iconButton.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
export let disabled = false
|
||||
export let halo = false
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<button class:disabled class:halo class="relative grid aspect-square h-full place-items-center transition-transform duration-75 active:scale-90" on:click|preventDefault={() => dispatch('click')} {disabled}>
|
||||
<slot name="icon" />
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button.disabled {
|
||||
color: rgb(82, 82, 82);
|
||||
}
|
||||
button:not(.disabled).halo::before {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: color-mix(in srgb, var(--lazuli-primary) 20%, transparent);
|
||||
border-radius: 100%;
|
||||
transition-property: width height;
|
||||
transition-duration: 200ms;
|
||||
position: absolute;
|
||||
}
|
||||
button:not(.disabled).halo:hover::before {
|
||||
width: 130%;
|
||||
height: 130%;
|
||||
}
|
||||
button :global(> :first-child) {
|
||||
transition: color 200ms;
|
||||
}
|
||||
button:not(.disabled):hover :global(> :first-child) {
|
||||
color: var(--lazuli-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script context="module" lang="ts">
|
||||
export interface NavTab {
|
||||
pathname: string
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let navTabs: NavTab[]
|
||||
export let currentPathname: string
|
||||
export let transitionTime: number = 200
|
||||
|
||||
import { fade } from 'svelte/transition'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
type PageTransitionDirection = 'left' | 'right'
|
||||
let direction: PageTransitionDirection = 'right'
|
||||
|
||||
const calculateDirection = (newPage: string, currentPage: string): PageTransitionDirection => {
|
||||
const newPageIndex = navTabs.findIndex((tab) => tab.pathname === newPage)
|
||||
const currentPageIndex = navTabs.findIndex((tab) => tab.pathname === currentPage)
|
||||
return newPageIndex > currentPageIndex ? 'right' : 'left'
|
||||
}
|
||||
|
||||
let activeTab: HTMLButtonElement, indicatorBar: HTMLDivElement, tabList: HTMLDivElement
|
||||
$: calculateBar(activeTab)
|
||||
|
||||
const calculateBar = (activeTab: HTMLButtonElement) => {
|
||||
if (activeTab && indicatorBar && tabList) {
|
||||
const listRect = tabList.getBoundingClientRect()
|
||||
const tabRec = activeTab.getBoundingClientRect()
|
||||
if (direction === 'right') {
|
||||
indicatorBar.style.right = `${listRect.right - tabRec.right}px`
|
||||
setTimeout(() => (indicatorBar.style.left = `${tabRec.left - listRect.left}px`), transitionTime)
|
||||
} else {
|
||||
indicatorBar.style.left = `${tabRec.left - listRect.left}px`
|
||||
setTimeout(() => (indicatorBar.style.right = `${listRect.right - tabRec.right}px`), transitionTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={tabList} id="bottom-tab-list" class="relative flex w-full items-center justify-around bg-black">
|
||||
{#each navTabs as tabData}
|
||||
{#if currentPathname === tabData.pathname}
|
||||
<button bind:this={activeTab} class="pointer-events-none text-white transition-colors" disabled>
|
||||
<i class={tabData.icon} />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="text-neutral-400 transition-colors hover:text-lazuli-primary"
|
||||
on:click={() => {
|
||||
direction = calculateDirection(tabData.pathname, currentPathname)
|
||||
dispatch('navigate', { direction, pathname: tabData.pathname })
|
||||
}}
|
||||
>
|
||||
<i class={tabData.icon} />
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if navTabs.some((tab) => tab.pathname === currentPathname)}
|
||||
<div bind:this={indicatorBar} transition:fade class="absolute bottom-0 h-1 rounded-full bg-white transition-all duration-300 ease-in-out" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#bottom-tab-list {
|
||||
padding: 16px 0px;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
type PageTransitionDirection = 'up' | 'down'
|
||||
let direction: PageTransitionDirection = 'down'
|
||||
|
||||
const calculateDirection = (newPage: string, currentPage: string): void => {
|
||||
const calculateDirection = (newPage: string, currentPage: string): PageTransitionDirection => {
|
||||
const newPageIndex = navTabs.findIndex((tab) => tab.pathname === newPage)
|
||||
const currentPageIndex = navTabs.findIndex((tab) => tab.pathname === currentPage)
|
||||
newPageIndex > currentPageIndex ? (direction = 'down') : (direction = 'up')
|
||||
return newPageIndex > currentPageIndex ? 'down' : 'up'
|
||||
}
|
||||
|
||||
let activeTab: HTMLButtonElement, indicatorBar: HTMLDivElement, tabList: HTMLDivElement
|
||||
@@ -56,7 +56,7 @@
|
||||
<button
|
||||
class="grid aspect-square w-14 place-items-center text-neutral-400 transition-colors hover:text-lazuli-primary"
|
||||
on:click={() => {
|
||||
calculateDirection(tabData.pathname, currentPathname)
|
||||
direction = calculateDirection(tabData.pathname, currentPathname)
|
||||
dispatch('navigate', { direction, pathname: tabData.pathname })
|
||||
}}
|
||||
>
|
||||
|
||||
52
src/lib/components/util/slider.svelte
Normal file
52
src/lib/components/util/slider.svelte
Normal file
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
export let value = 0
|
||||
|
||||
let sliderThumb: HTMLSpanElement, sliderTrail: HTMLSpanElement
|
||||
|
||||
const trackThumb = (sliderPos: number): void => {
|
||||
if (sliderThumb) sliderThumb.style.left = `${sliderPos}%`
|
||||
if (sliderTrail) sliderTrail.style.right = `${100 - sliderPos}%`
|
||||
}
|
||||
|
||||
$: trackThumb(value)
|
||||
|
||||
const handleKeyPress = (key: string) => {
|
||||
if ((key === 'ArrowRight' || key === 'ArrowUp') && value < 100) return (value += 1)
|
||||
if ((key === 'ArrowLeft' || key === 'ArrowDown') && value > 0) return (value -= 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="slider-track"
|
||||
class="relative isolate h-1 w-full rounded-full bg-neutral-600"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuenow={value}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
on:keydown={(event) => handleKeyPress(event.key)}
|
||||
>
|
||||
<input type="range" class="absolute z-10 h-1 w-full" step="any" min="0" max="100" bind:value tabindex="-1" aria-hidden="true" aria-disabled="true" />
|
||||
<span bind:this={sliderTrail} id="slider-trail" class="absolute left-0 h-1 rounded-full bg-white transition-colors" />
|
||||
<span bind:this={sliderThumb} id="slider-thumb" class="absolute top-1/2 aspect-square h-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-0 transition-opacity duration-300" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
input[type='range'] {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
}
|
||||
#slider-track:hover > #slider-trail {
|
||||
background-color: var(--lazuli-primary);
|
||||
}
|
||||
#slider-track:focus > #slider-trail {
|
||||
background-color: var(--lazuli-primary);
|
||||
}
|
||||
#slider-track:hover > #slider-thumb {
|
||||
opacity: 1;
|
||||
}
|
||||
#slider-track:focus > #slider-thumb {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
29
src/lib/components/util/toggle.svelte
Normal file
29
src/lib/components/util/toggle.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
export let toggled = false
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const handleToggle = (): void => {
|
||||
toggled = !toggled
|
||||
dispatch('toggled', { toggled })
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class:toggled aria-checked={toggled} role="checkbox" class="relative flex h-6 w-10 items-center rounded-full bg-neutral-500 transition-colors" on:click={handleToggle}>
|
||||
<div class:toggled class="absolute left-0 aspect-square h-full p-1 transition-all">
|
||||
<div class="grid h-full w-full place-items-center rounded-full bg-white text-xs">
|
||||
<i class={toggled ? 'fa-solid fa-check text-lazuli-primary' : 'fa-solid fa-xmark text-neutral-500'} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button.toggled {
|
||||
background-color: var(--lazuli-primary);
|
||||
}
|
||||
div.toggled {
|
||||
left: 100%;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
</style>
|
||||
50
src/lib/components/util/volumeSlider.svelte
Normal file
50
src/lib/components/util/volumeSlider.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import Slider from '$lib/components/util/slider.svelte'
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let volume = 0
|
||||
|
||||
let muted = false
|
||||
let storedVolume: number
|
||||
|
||||
const getVolume = (): number => {
|
||||
const currentVolume = localStorage.getItem('volume')
|
||||
if (currentVolume) return Number(currentVolume)
|
||||
|
||||
const defaultVolume = 100
|
||||
localStorage.setItem('volume', defaultVolume.toString())
|
||||
return defaultVolume
|
||||
}
|
||||
|
||||
const setVolume = (volume: number): void => {
|
||||
if (Number.isFinite(volume)) localStorage.setItem('volume', Math.round(volume).toString())
|
||||
}
|
||||
|
||||
onMount(() => (storedVolume = getVolume()))
|
||||
|
||||
$: changeVolume(storedVolume)
|
||||
const changeVolume = (newVolume: number) => {
|
||||
if (typeof newVolume === 'number' && !isNaN(newVolume)) setVolume(newVolume)
|
||||
}
|
||||
|
||||
$: volume = muted ? 0 : storedVolume
|
||||
</script>
|
||||
|
||||
<div id="volume-slider" class="flex h-10 w-fit flex-shrink-0 flex-row-reverse items-center gap-2">
|
||||
<IconButton halo={false} on:click={() => (muted = !muted)}>
|
||||
<i slot="icon" class="fa-solid {volume > 50 ? 'fa-volume-high' : volume > 0 ? 'fa-volume-low' : 'fa-volume-xmark'} w-full text-center text-base" />
|
||||
</IconButton>
|
||||
<div id="slider-wrapper" class="w-0 transition-all duration-500">
|
||||
<Slider bind:value={storedVolume} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#volume-slider:hover > #slider-wrapper {
|
||||
width: 6rem;
|
||||
}
|
||||
#slider-wrapper:focus-within {
|
||||
width: 6rem;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
@@ -1,11 +1,11 @@
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import type { AlertType } from '$lib/components/util/alert.svelte'
|
||||
|
||||
export const pageWidth: Writable<number> = writable(0)
|
||||
export const pageWidth: Writable<number> = writable()
|
||||
|
||||
export const newestAlert: Writable<[AlertType, string] | null> = writable(null)
|
||||
export const newestAlert: Writable<[AlertType, string]> = writable()
|
||||
|
||||
export const currentlyPlaying = writable(null)
|
||||
export const currentlyPlaying = writable()
|
||||
|
||||
const youtubeMusicBackground: string = 'https://www.gstatic.com/youtube/media/ytm/images/sbg/wsbg@4000x2250.png' // Default Youtube music background
|
||||
export const backgroundImage: Writable<string> = writable(youtubeMusicBackground)
|
||||
|
||||
Reference in New Issue
Block a user