Add components and improve error page

This commit is contained in:
2026-03-15 01:22:46 +00:00
parent 54ea6ebf59
commit 061598a0ad
12 changed files with 687 additions and 205 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import { slide, fade } from 'svelte/transition';
import '$lib/global.css';
@@ -7,20 +8,50 @@
import logoPlain from '$lib/assets/round-logo.svg';
import favicon from '$lib/assets/round-logo.svg';
import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte';
import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte';
let { children } = $props();
const navItems = [
{ label: 'Home', path: '/', icon: IconHome},
{ label: 'PIS', path:'/pis', icon: IconDialpad},
{ label: 'Options', path:'/preferences', icon: IconSettings},
{ label: 'About', path: '/about', icon: IconHelp},
]
const activePath = $derived(page.url?.pathname ?? '');
// Navigation State
const moreIcon = IconDots;
const navItems = [
{ label: 'Home', path: '/', icon: IconHome },
{ label: 'PIS', path: '/pis', icon: IconDialpad },
{ label: 'Options', path: '/preferences', icon: IconSettings },
{ label: 'About', path: '/about', icon: IconHelp }
];
let navWidth = $state(0);
let menuOpen = $state(false);
const ITEM_WIDTH = 70;
const MORE_BUTTON_WIDTH = 70;
const activePath = $derived(page.url?.pathname ?? '');
const visibleCount = $derived.by(() => {
if (navWidth === 0) return navItems.length;
const available = navWidth;
const totalItems = navItems.length;
const countWithoutMore = Math.floor(available/ ITEM_WIDTH);
if (countWithoutMore >= totalItems) return totalItems;
const availableWithMore = navWidth - MORE_BUTTON_WIDTH;
const countWithMore = Math.floor(availableWithMore / ITEM_WIDTH);
if (totalItems - countWithMore === 1) {
if (available >= (countWithMore + 1) * ITEM_WIDTH) {
return totalItems;
}
}
return countWithMore;
});
const visibleItems = $derived(navItems.slice(0, visibleCount));
const hiddenItems = $derived(navItems.slice(visibleCount));
</script>
<svelte:head>
@@ -41,37 +72,76 @@
</header>
<main>
{@render children()}
{@render children()}
</main>
<nav>
<nav bind:clientWidth={navWidth}>
<!-- Dynamic Nav Elements Here! -->
{#each navItems as item}
{@const isActive = activePath === item.path}
<a
{#each visibleItems as item}
{@const isActive = activePath === item.path}
<a
href={item.path}
class="nav-item"
class:active={isActive}
aria-current={isActive ? 'page' : undefined}
>
<item.icon
size={24}
stroke={isActive ? 2 : 1.5}
class="nav-icon"
/>
onclick={() => (menuOpen = false)}
>
<item.icon size={24} stroke={isActive ? 2 : 1.5} class="nav-icon" />
<span class="label">{item.label}</span>
</a>
</a>
{/each}
{#if hiddenItems.length > 0}
<div class="more-menu-wrapper">
<button
class="nav-item more-btn"
onclick={() => (menuOpen = !menuOpen)}
aria-expanded={menuOpen}
>
<IconDots size={24} />
<span class="label">More</span>
</button>
</div>
{#if menuOpen}
<div
class="backdrop"
onclick={() => (menuOpen = false)}
transition:fade={{ duration: 150 }}
></div>
<div class="menu-popover" transition:slide={{ axis: 'y', duration: 250 }}>
{#each hiddenItems as item}
{@const isActive = activePath === item.path}
<a
href={item.path}
class="menu-popover-item"
onclick={() => (menuOpen = false)}
class:active={isActive}
aria-current={isActive ? 'page' : undefined}
>
<item.icon size={20} />
<span>{item.label}</span>
</a>
{/each}
</div>
{/if}
{/if}
</nav>
<div class="viewport-guard">
<img src={logoPlain} alt="OwlBoard Logo" width=100 height=100>
<h1 class="viewport-guard-title">Narrow Gauge Detected</h1>
<p>
Just as trains need the right track width, our data needs a bit more room to stay on the rails. Please expand your view to at least 300px to view the app.
</p>
</div>
<style>
header {
header {
top: 0;
height: 80px;
box-shadow: var(--shadow-std);
}
.logo-link {
.logo-link {
width: auto;
display: block;
height: 55px;
@@ -91,7 +161,8 @@
padding-bottom: 2px;
color: var(--color-title);
}
header, nav {
header,
nav {
flex-shrink: 0;
width: 100%;
background-color: var(--color-accent);
@@ -106,13 +177,6 @@
gap: 0rem;
}
nav {
bottom: 0;
height: 60px;
box-shadow: var(--shadow-up);
}
main {
padding-top: 80px;
padding-bottom: 60px;
@@ -122,22 +186,42 @@
background-image: radial-gradient(var(--color-bg-dark), var(--color-bg-light));
}
.nav-item {
nav {
display: flex;
bottom: 0;
height: 60px;
box-shadow: var(--shadow-up);
}
.nav-item, .more-menu-wrapper {
display: flex;
flex: 1;
flex-direction: column;
width: 70px;
align-items: center;
justify-content: center;
text-decoration: none;
position: relative;
position: relative;
cursor: pointer;
height: 100%;
border: 0;
color: var(--color-title);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
background: none;
padding: 0;
border: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-item.active {
.nav-item:hover,
.menu-popover-item:hover {
filter: brightness(1.9);
background-color: #ffffff1a;
}
.nav-item.active,
.menu-popover-item.active {
color: #ffffff;
background: rgba(255,255,255,0.05);
background: rgba(255, 255, 255, 0.05);
}
.nav-item.active::before {
@@ -151,7 +235,8 @@
box-shadow: var(--shadow-std);
}
.nav-item.active .label {
.nav-item.active .label,
.menu-popover-item.active .label {
font-weight: 600;
}
@@ -162,4 +247,79 @@
margin-top: 4px;
letter-spacing: 0.05rem;
}
.backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 60px;
width: 100vw;
height: calc(100dvh - 60px);
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
z-index: 999;
}
.menu-popover {
position: absolute;
bottom: 100%;
right: 10px;
background-color: var(--color-accent);
border-radius: 12px 12px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
min-width: 140px;
z-index: 1000;
overflow: hidden;
}
.menu-popover-item {
display: flex;
align-items: center;
gap: 12px;
position: relative;
min-height: 48px;
padding: 12px 16px;
text-decoration: none;
color: var(--color-title);
}
.menu-popover-item.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 60%;
background-color: var(--color-brand);
border-radius: 0 4px 4px 0;
box-shadow: var(--shadow-right);
}
.viewport-guard {
display: none;
}
@media (max-width: 299px) {
.viewport-guard {
display: block;
position: fixed;
text-align: center;
font-family: 'URW Gothic', sans-serif;
margin-top: 1rem;
font-size: 1.2rem;
color: var(--color-title);
}
.viewport-guard p {
width: 90%;
margin: auto;
padding-top: 30px;
}
header, main, nav {
display: none;
}
}
</style>