- Layout: mobile hamburger + drawer nav (backdrop button + sibling nav), desktop sidebar hidden on small screens, p-4 md:p-8 main padding - Products: card list view on mobile, flex-wrap filters - Lab results: card list view on mobile - ProductForm: responsive grids (grid-cols-1 sm:grid-cols-2), skin profile checkboxes 2→3 cols, active ingredient row restructured (name+✕ in flex row, percent/strength/irritation in 3-col grid), section headers stack on mobile - Skin snapshots: date+icons on one row, badges on separate row below - Product [id] header: back link stacked above title, redundant badge removed - Routines header: flex-col on mobile, sm:flex-row Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
124 lines
4.4 KiB
Svelte
124 lines
4.4 KiB
Svelte
<script lang="ts">
|
|
import '../app.css';
|
|
import { page } from '$app/state';
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte';
|
|
|
|
let { children } = $props();
|
|
|
|
let mobileMenuOpen = $state(false);
|
|
|
|
const navItems = $derived([
|
|
{ href: '/', label: m.nav_dashboard(), icon: '🏠' },
|
|
{ href: '/products', label: m.nav_products(), icon: '🧴' },
|
|
{ href: '/routines', label: m.nav_routines(), icon: '📋' },
|
|
{ href: '/routines/grooming-schedule', label: m.nav_grooming(), icon: '🪒' },
|
|
{ href: '/health/medications', label: m.nav_medications(), icon: '💊' },
|
|
{ href: '/health/lab-results', label: m["nav_labResults"](), icon: '🔬' },
|
|
{ href: '/skin', label: m.nav_skin(), icon: '✨' }
|
|
]);
|
|
|
|
function isActive(href: string) {
|
|
if (href === '/') return page.url.pathname === '/';
|
|
const pathname = page.url.pathname;
|
|
if (!pathname.startsWith(href)) return false;
|
|
// Don't mark parent as active if a more-specific nav item also matches
|
|
const moreSpecific = navItems.some(
|
|
(item) => item.href !== href && item.href.startsWith(href) && pathname.startsWith(item.href)
|
|
);
|
|
return !moreSpecific;
|
|
}
|
|
</script>
|
|
|
|
<div class="flex min-h-screen flex-col bg-background md:flex-row">
|
|
<!-- Mobile header -->
|
|
<header class="flex items-center justify-between border-b border-border bg-card px-4 py-3 md:hidden">
|
|
<div>
|
|
<span class="text-sm font-semibold tracking-tight">{m["nav_appName"]()}</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
|
class="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
aria-label="Toggle menu"
|
|
>
|
|
{#if mobileMenuOpen}
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
{:else}
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
{/if}
|
|
</button>
|
|
</header>
|
|
|
|
<!-- Mobile drawer overlay -->
|
|
{#if mobileMenuOpen}
|
|
<!-- Backdrop: closes drawer on click -->
|
|
<button
|
|
type="button"
|
|
class="fixed inset-0 z-50 bg-black/50 md:hidden"
|
|
onclick={() => (mobileMenuOpen = false)}
|
|
aria-label={m.common_cancel()}
|
|
></button>
|
|
<!-- Drawer (same z-50 but later in DOM → on top of backdrop) -->
|
|
<nav
|
|
class="fixed inset-y-0 left-0 z-50 w-64 overflow-y-auto bg-card px-3 py-6 md:hidden"
|
|
>
|
|
<div class="mb-8 px-3">
|
|
<h1 class="text-lg font-semibold tracking-tight">{m["nav_appName"]()}</h1>
|
|
<p class="text-xs text-muted-foreground">{m["nav_appSubtitle"]()}</p>
|
|
</div>
|
|
<ul class="space-y-1">
|
|
{#each navItems as item}
|
|
<li>
|
|
<a
|
|
href={item.href}
|
|
onclick={() => (mobileMenuOpen = false)}
|
|
class="flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors
|
|
{isActive(item.href)
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}"
|
|
>
|
|
<span class="text-base">{item.icon}</span>
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<div class="mt-6 px-3">
|
|
<LanguageSwitcher />
|
|
</div>
|
|
</nav>
|
|
{/if}
|
|
|
|
<!-- Desktop Sidebar -->
|
|
<nav class="hidden w-56 shrink-0 flex-col border-r border-border bg-card px-3 py-6 md:flex">
|
|
<div class="mb-8 px-3">
|
|
<h1 class="text-lg font-semibold tracking-tight">{m["nav_appName"]()}</h1>
|
|
<p class="text-xs text-muted-foreground">{m["nav_appSubtitle"]()}</p>
|
|
</div>
|
|
<ul class="space-y-1">
|
|
{#each navItems as item}
|
|
<li>
|
|
<a
|
|
href={item.href}
|
|
class="flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors
|
|
{isActive(item.href)
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}"
|
|
>
|
|
<span class="text-base">{item.icon}</span>
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<div class="mt-6 px-3">
|
|
<LanguageSwitcher />
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main content -->
|
|
<main class="flex-1 overflow-auto p-4 md:p-8">
|
|
{@render children()}
|
|
</main>
|
|
</div>
|