mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-28 21:40:58 +01:00
style(nav): add bottom nav, hamburger page nav
This commit is contained in:
14
src/lib/client/stores/mobileNav.ts
Normal file
14
src/lib/client/stores/mobileNav.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
function createMobileNavStore() {
|
||||
const { subscribe, set, update } = writable(false);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
open: () => set(true),
|
||||
close: () => set(false),
|
||||
toggle: () => update((v) => !v)
|
||||
};
|
||||
}
|
||||
|
||||
export const mobileNavOpen = createMobileNavStore();
|
||||
68
src/lib/client/ui/navigation/bottomNav/BottomNav.svelte
Normal file
68
src/lib/client/ui/navigation/bottomNav/BottomNav.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { navIconStore } from '$stores/navIcons';
|
||||
import {
|
||||
Database,
|
||||
MonitorPlay,
|
||||
Sliders,
|
||||
Palette,
|
||||
Settings,
|
||||
Microscope,
|
||||
Tag,
|
||||
Clock
|
||||
} from 'lucide-svelte';
|
||||
|
||||
type NavItem = {
|
||||
href: string;
|
||||
label: string;
|
||||
shortLabel?: string;
|
||||
icon: typeof Database;
|
||||
emoji: string;
|
||||
priority: 'always' | 'medium' | 'low';
|
||||
};
|
||||
|
||||
const items: NavItem[] = [
|
||||
{ href: '/databases', label: 'Databases', icon: Database, emoji: '💾', priority: 'always' },
|
||||
{ href: '/arr', label: 'Arrs', icon: MonitorPlay, emoji: '📺', priority: 'always' },
|
||||
{ href: '/quality-profiles', label: 'Profiles', icon: Sliders, emoji: '⚡', priority: 'always' },
|
||||
{ href: '/custom-formats', label: 'Formats', icon: Palette, emoji: '🎨', priority: 'always' },
|
||||
{ href: '/settings', label: 'Settings', icon: Settings, emoji: '⚙️', priority: 'always' },
|
||||
{ href: '/regular-expressions', label: 'Regex', icon: Microscope, emoji: '🔬', priority: 'medium' },
|
||||
{ href: '/media-management', label: 'Media', icon: Tag, emoji: '🏷️', priority: 'low' },
|
||||
{ href: '/delay-profiles', label: 'Delay', icon: Clock, emoji: '⏱️', priority: 'low' }
|
||||
];
|
||||
|
||||
$: useEmoji = $navIconStore === 'emoji';
|
||||
$: pathname = $page.url.pathname;
|
||||
|
||||
function isActive(href: string, currentPath: string): boolean {
|
||||
if (href === '/') return currentPath === '/';
|
||||
return currentPath.startsWith(href);
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class="fixed bottom-0 left-0 right-0 z-50 border-t border-neutral-200 bg-neutral-50 pb-[env(safe-area-inset-bottom)] md:hidden dark:border-neutral-800 dark:bg-neutral-900"
|
||||
>
|
||||
<div class="flex items-center justify-around px-1">
|
||||
{#each items as item}
|
||||
{@const active = isActive(item.href, pathname)}
|
||||
<a
|
||||
href={item.href}
|
||||
class="flex flex-col items-center justify-center py-2 transition-colors
|
||||
{item.priority === 'medium' ? 'hidden sm:flex' : ''}
|
||||
{item.priority === 'low' ? 'hidden' : ''}
|
||||
{active
|
||||
? 'text-accent-600 dark:text-accent-400'
|
||||
: 'text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200'}"
|
||||
>
|
||||
{#if useEmoji}
|
||||
<span class="text-xl">{item.emoji}</span>
|
||||
{:else}
|
||||
<svelte:component this={item.icon} size={20} strokeWidth={active ? 2.5 : 2} />
|
||||
{/if}
|
||||
<span class="mt-0.5 text-[10px] font-medium">{item.shortLabel ?? item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,24 +1,33 @@
|
||||
<script lang="ts">
|
||||
import AccentPicker from './accentPicker.svelte';
|
||||
import ThemeToggle from './themeToggle.svelte';
|
||||
import { Menu } from 'lucide-svelte';
|
||||
import { mobileNavOpen } from '$stores/mobileNav';
|
||||
import logo from '$assets/logo-512.png';
|
||||
|
||||
export let collapsed: boolean = false;
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class="fixed top-0 left-0 z-50 w-72 border-r border-b border-neutral-200 bg-neutral-50 transition-transform duration-200 dark:border-neutral-800 dark:bg-neutral-900"
|
||||
class:-translate-x-[calc(100%-24px)]={collapsed}
|
||||
class="fixed top-0 left-0 z-50 w-full border-b border-neutral-200 bg-neutral-50 md:w-80 md:border-r md:transition-transform md:duration-200 dark:border-neutral-800 dark:bg-neutral-900 {collapsed ? 'md:-translate-x-[calc(100%-24px)]' : ''}"
|
||||
>
|
||||
<div class="flex items-center justify-between px-4 py-4">
|
||||
<!-- Left: Brand name with logo -->
|
||||
<div class="ml-4 flex items-center gap-2">
|
||||
<img src={logo} alt="Profilarr logo" class="h-5 w-5 translate-y-[2px]" />
|
||||
<div class="text-xl font-bold text-neutral-900 dark:text-neutral-100">profilarr</div>
|
||||
<!-- Left: Hamburger (mobile) + Brand name with logo (desktop) -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => mobileNavOpen.open()}
|
||||
class="rounded-md p-1.5 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-700 md:hidden dark:hover:bg-neutral-800 dark:hover:text-neutral-300"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu size={20} />
|
||||
</button>
|
||||
<img src={logo} alt="Profilarr logo" class="hidden h-5 w-5 translate-y-[2px] md:ml-4 md:block" />
|
||||
<div class="hidden text-xl font-bold text-neutral-900 md:block dark:text-neutral-100">profilarr</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Accent picker and Theme toggle -->
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Right: Accent picker and Theme toggle (desktop only) -->
|
||||
<div class="hidden items-center gap-1 md:flex">
|
||||
<AccentPicker />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
@@ -2,19 +2,58 @@
|
||||
import Group from './group.svelte';
|
||||
import GroupItem from './groupItem.svelte';
|
||||
import Version from './version.svelte';
|
||||
import { Home, Sliders, Palette, Microscope, Tag, Clock, Settings } from 'lucide-svelte';
|
||||
import { Home, Sliders, Palette, Microscope, Tag, Clock, Settings, X } from 'lucide-svelte';
|
||||
import { navIconStore } from '$stores/navIcons';
|
||||
import { mobileNavOpen } from '$stores/mobileNav';
|
||||
import { page } from '$app/stores';
|
||||
import logo from '$assets/logo-512.png';
|
||||
|
||||
export let collapsed: boolean = false;
|
||||
export let version: string = '';
|
||||
|
||||
$: useEmoji = $navIconStore === 'emoji';
|
||||
|
||||
// Close mobile nav when page changes
|
||||
$: $page.url.pathname, mobileNavOpen.close();
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && $mobileNavOpen) mobileNavOpen.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<!-- Mobile backdrop -->
|
||||
{#if $mobileNavOpen}
|
||||
<button
|
||||
type="button"
|
||||
class="fixed inset-0 z-[60] bg-black/50 md:hidden"
|
||||
on:click={() => mobileNavOpen.close()}
|
||||
aria-label="Close menu"
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<nav
|
||||
class="fixed top-16 left-0 flex h-[calc(100vh-4rem)] w-72 flex-col border-r border-neutral-200 bg-neutral-50 transition-transform duration-200 dark:border-neutral-800 dark:bg-neutral-900"
|
||||
class:-translate-x-[calc(100%-24px)]={collapsed}
|
||||
class="fixed top-0 left-0 z-[70] flex h-full w-[90vw] flex-col border-r border-neutral-200 bg-neutral-50 transition-transform duration-200 dark:border-neutral-800 dark:bg-neutral-900
|
||||
{$mobileNavOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||
md:top-16 md:h-[calc(100vh-4rem)] md:w-80 md:translate-x-0 {collapsed ? 'md:-translate-x-[calc(100%-24px)]' : ''}"
|
||||
>
|
||||
<!-- Mobile header with logo and close button -->
|
||||
<div class="flex items-center justify-between border-b border-neutral-200 py-4 pl-8 pr-4 md:hidden dark:border-neutral-800">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src={logo} alt="Profilarr logo" class="h-5 w-5" />
|
||||
<span class="text-xl font-bold text-neutral-900 dark:text-neutral-100">profilarr</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => mobileNavOpen.close()}
|
||||
class="rounded-md p-1.5 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-700 dark:hover:bg-neutral-800 dark:hover:text-neutral-300"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<Group
|
||||
label={useEmoji ? '🏠 Home' : 'Home'}
|
||||
@@ -85,7 +124,7 @@
|
||||
<GroupItem label="About" href="/settings/about" />
|
||||
<GroupItem label="Log Out" href="/auth/logout" />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<Version {version} />
|
||||
<Version {version} />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
const showVersion = shouldShowVersion();
|
||||
</script>
|
||||
|
||||
<div class="p-4">
|
||||
<div
|
||||
class="flex items-center gap-2.5 rounded-lg border border-neutral-300 px-3 py-2 dark:border-neutral-700"
|
||||
>
|
||||
<div
|
||||
class="mt-2 flex items-center gap-2.5 rounded-lg border border-neutral-300 px-3 py-2 dark:border-neutral-700"
|
||||
>
|
||||
<img src={logo} alt="Profilarr logo" class="h-5 w-5 flex-shrink-0" />
|
||||
|
||||
<div class="flex-1">
|
||||
@@ -24,4 +23,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import logo from '$assets/logo-512.png';
|
||||
import Navbar from '$ui/navigation/navbar/navbar.svelte';
|
||||
import PageNav from '$ui/navigation/pageNav/pageNav.svelte';
|
||||
import AlertContainer from '$alerts/AlertContainer.svelte';
|
||||
import BottomNav from '$ui/navigation/bottomNav/BottomNav.svelte';
|
||||
import AlertContainer from '$alerts/AlertContainer.svelte';
|
||||
import { sidebarCollapsed } from '$lib/client/stores/sidebar';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
@@ -21,16 +22,17 @@
|
||||
{#if !isAuthPage}
|
||||
<Navbar collapsed={$sidebarCollapsed} />
|
||||
<PageNav collapsed={$sidebarCollapsed} version={data.version} />
|
||||
<BottomNav />
|
||||
{/if}
|
||||
<AlertContainer />
|
||||
|
||||
{#if !isAuthPage}
|
||||
<!-- Sidebar collapse toggle button -->
|
||||
<!-- Sidebar collapse toggle button (desktop only) -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => sidebarCollapsed.toggle()}
|
||||
class="fixed top-16 z-50 flex h-6 w-6 -translate-x-1/2 -translate-y-1/3 items-center justify-center rounded-md border border-neutral-300 bg-white shadow-sm transition-all hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||
style="left: {$sidebarCollapsed ? '24px' : '288px'}"
|
||||
class="fixed top-16 z-50 hidden h-6 w-6 -translate-x-1/2 -translate-y-1/3 items-center justify-center rounded-md border border-neutral-300 bg-white shadow-sm transition-all hover:bg-neutral-50 md:flex dark:border-neutral-700 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||
style="left: {$sidebarCollapsed ? '24px' : '320px'}"
|
||||
aria-label={$sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
<div class="flex flex-col gap-[3px]">
|
||||
@@ -41,6 +43,6 @@
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<main class={isAuthPage ? '' : `transition-all duration-200 ${$sidebarCollapsed ? 'pl-[24px]' : 'pl-72'}`}>
|
||||
<main class={isAuthPage ? '' : `pt-16 pb-16 transition-all duration-200 md:pb-0 md:pt-0 ${$sidebarCollapsed ? 'md:pl-[24px]' : 'md:pl-80'}`}>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user