libra/docs/stories/story-9.7-navigation-footer...

14 KiB

Story 9.7: Navigation & Footer Styling

Epic Reference

Epic 9: Design & Branding Implementation

Story Dependencies

  • Story 9.1: Color System Implementation (provides bg-navy, text-gold, text-cream classes)
  • Story 9.3: Logo Integration (provides <x-logo> component with variants)
  • Story 1.3: Bilingual Infrastructure (provides language toggle mechanism)
  • Story 1.4: Base UI Navigation (provides existing header structure)

User Story

As a user, I want professional navigation and footer styling, So that I can easily navigate the site and find information.

Acceptance Criteria

Navigation Bar

  • Fixed top position with z-50
  • Navy blue background (bg-navy)
  • Logo left (desktop), centered (mobile)
  • Gold text for links (text-gold)
  • Active link indicator (gold underline or background)
  • Language toggle styled consistently
  • Responsive mobile menu

Mobile Menu

  • Full-width dropdown/slide from left
  • Navy background (bg-navy)
  • Clear touch targets (44px+ minimum)
  • Smooth animation (200-300ms)
  • Close button visible
  • Navy blue background (bg-navy)
  • Logo and firm info (using <x-logo variant="reversed">)
  • Contact details section
  • Links to Terms/Privacy pages
  • Copyright notice with dynamic year
  • Sticky footer (always at bottom even on short pages)

Edge Cases

  • Guest navigation: Show Home, Posts, Contact, Login/Register
  • Authenticated navigation: Show Home, Dashboard, Posts, user menu
  • RTL layout: Logo moves to right, menu items reverse order
  • Empty page content: Footer stays at viewport bottom

Technical Implementation

Files to Modify

File Action Purpose
resources/views/components/layouts/app/header.blade.php Modify Update nav styling to brand colors
resources/views/components/layouts/app.blade.php Modify Add footer component, ensure sticky footer
resources/views/components/layouts/auth.blade.php Modify Add footer for auth pages
resources/css/app.css Modify Add nav-specific utilities if needed

Files to Create

File Purpose
resources/views/components/footer.blade.php Reusable footer component
resources/views/components/nav-link.blade.php Styled navigation link with active state
resources/views/components/mobile-menu.blade.php Mobile navigation drawer (optional - can inline)

Color Reference (from Story 9.1)

Navy Blue:    bg-navy     (#0A1F44) - nav/footer background
Gold:         text-gold   (#D4AF37) - links, accents
Light Gold:   text-gold-light (#F4E4B8) - hover states
Cream:        text-cream  (#F9F7F4) - footer text

Navigation Structure

<!-- resources/views/components/layouts/app/header.blade.php -->
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}">
    <head>
        @include('partials.head')
    </head>
    <body class="min-h-screen flex flex-col bg-cream">
        <!-- Fixed Navigation -->
        <nav class="fixed top-0 inset-x-0 bg-navy z-50" x-data="{ mobileMenu: false }">
            <div class="container mx-auto px-4">
                <div class="flex items-center justify-between h-16">
                    <!-- Logo -->
                    <a href="{{ route('home') }}" class="flex items-center" wire:navigate>
                        <x-logo size="small" />
                    </a>

                    <!-- Desktop Links -->
                    <div class="hidden md:flex items-center gap-6">
                        <x-nav-link href="{{ route('home') }}" :active="request()->routeIs('home')">
                            {{ __('nav.home') }}
                        </x-nav-link>
                        <x-nav-link href="{{ route('posts.index') }}" :active="request()->routeIs('posts.*')">
                            {{ __('nav.posts') }}
                        </x-nav-link>
                        @guest
                            <x-nav-link href="{{ route('login') }}" :active="request()->routeIs('login')">
                                {{ __('nav.login') }}
                            </x-nav-link>
                        @else
                            <x-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
                                {{ __('nav.dashboard') }}
                            </x-nav-link>
                        @endguest

                        <!-- Language Toggle -->
                        <x-language-toggle />
                    </div>

                    <!-- Mobile Toggle -->
                    <button
                        class="md:hidden text-gold p-2 min-h-[44px] min-w-[44px] flex items-center justify-center"
                        x-on:click="mobileMenu = !mobileMenu"
                        aria-label="{{ __('Toggle menu') }}"
                    >
                        <flux:icon name="bars-3" class="w-6 h-6" x-show="!mobileMenu" />
                        <flux:icon name="x-mark" class="w-6 h-6" x-show="mobileMenu" x-cloak />
                    </button>
                </div>
            </div>

            <!-- Mobile Menu -->
            <div
                class="md:hidden bg-navy border-t border-gold/20"
                x-show="mobileMenu"
                x-transition:enter="transition ease-out duration-200"
                x-transition:enter-start="opacity-0 -translate-y-2"
                x-transition:enter-end="opacity-100 translate-y-0"
                x-transition:leave="transition ease-in duration-150"
                x-transition:leave-start="opacity-100 translate-y-0"
                x-transition:leave-end="opacity-0 -translate-y-2"
                x-cloak
            >
                <div class="container mx-auto px-4 py-4 space-y-2">
                    <x-nav-link href="{{ route('home') }}" :active="request()->routeIs('home')" mobile>
                        {{ __('nav.home') }}
                    </x-nav-link>
                    <x-nav-link href="{{ route('posts.index') }}" :active="request()->routeIs('posts.*')" mobile>
                        {{ __('nav.posts') }}
                    </x-nav-link>
                    @guest
                        <x-nav-link href="{{ route('login') }}" :active="request()->routeIs('login')" mobile>
                            {{ __('nav.login') }}
                        </x-nav-link>
                        <x-nav-link href="{{ route('register') }}" :active="request()->routeIs('register')" mobile>
                            {{ __('nav.register') }}
                        </x-nav-link>
                    @else
                        <x-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')" mobile>
                            {{ __('nav.dashboard') }}
                        </x-nav-link>
                    @endguest
                </div>
            </div>
        </nav>

        <!-- Spacer for fixed nav -->
        <div class="h-16"></div>

        <!-- Main Content -->
        <main class="flex-1">
            {{ $slot }}
        </main>

        <!-- Footer -->
        <x-footer />

        @fluxScripts
    </body>
</html>
<!-- resources/views/components/nav-link.blade.php -->
@props(['active' => false, 'mobile' => false])

@php
$baseClasses = $mobile
    ? 'block px-4 py-3 min-h-[44px] text-base font-medium transition-colors duration-150'
    : 'text-sm font-medium transition-colors duration-150';

$activeClasses = $active
    ? 'text-gold-light'
    : 'text-gold hover:text-gold-light';
@endphp

<a {{ $attributes->merge(['class' => "$baseClasses $activeClasses"]) }} wire:navigate>
    {{ $slot }}
</a>
<!-- resources/views/components/footer.blade.php -->
<footer class="bg-navy text-cream mt-auto">
    <div class="container mx-auto px-4 py-12">
        <div class="grid md:grid-cols-3 gap-8">
            <!-- Logo & Description -->
            <div>
                <x-logo variant="reversed" />
                <p class="mt-4 text-sm opacity-80">
                    {{ __('footer.description') }}
                </p>
            </div>

            <!-- Quick Links -->
            <div>
                <h4 class="font-semibold text-gold mb-4">{{ __('footer.links') }}</h4>
                <ul class="space-y-2 text-sm">
                    <li>
                        <a href="{{ route('page.show', 'terms') }}" class="hover:text-gold transition-colors duration-150" wire:navigate>
                            {{ __('footer.terms') }}
                        </a>
                    </li>
                    <li>
                        <a href="{{ route('page.show', 'privacy') }}" class="hover:text-gold transition-colors duration-150" wire:navigate>
                            {{ __('footer.privacy') }}
                        </a>
                    </li>
                </ul>
            </div>

            <!-- Contact Info -->
            <div>
                <h4 class="font-semibold text-gold mb-4">{{ __('footer.contact') }}</h4>
                <ul class="space-y-2 text-sm opacity-80">
                    <li class="flex items-center gap-2 rtl:flex-row-reverse">
                        <flux:icon name="envelope" class="w-4 h-4 text-gold" />
                        <span>{{ config('libra.contact.email', 'info@libra.ps') }}</span>
                    </li>
                    <li class="flex items-center gap-2 rtl:flex-row-reverse">
                        <flux:icon name="phone" class="w-4 h-4 text-gold" />
                        <span dir="ltr">{{ config('libra.contact.phone', '+970 2 123 4567') }}</span>
                    </li>
                    <li class="flex items-start gap-2 rtl:flex-row-reverse">
                        <flux:icon name="map-pin" class="w-4 h-4 text-gold mt-0.5" />
                        <span>{{ config('libra.contact.address', 'Ramallah, Palestine') }}</span>
                    </li>
                </ul>
            </div>
        </div>

        <!-- Copyright -->
        <div class="border-t border-cream/20 mt-8 pt-8 text-sm text-center opacity-60">
            &copy; {{ date('Y') }} {{ __('footer.copyright', ['name' => config('app.name')]) }}
        </div>
    </div>
</footer>
/* resources/css/app.css - add if flex approach insufficient */
body {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

main {
    flex: 1;
}

RTL Considerations

  1. Logo Position: Use rtl:order-last if logo should move to right in RTL
  2. Menu Items: Flex containers with rtl:flex-row-reverse or use logical properties
  3. Icons: Some icons (arrows, chevrons) may need flipping with rtl:scale-x-[-1]
  4. Spacing: Use ms-*/me-* (margin-start/end) instead of ml-*/mr-*
  5. Text Alignment: Use text-start/text-end instead of text-left/text-right

Translation Keys Required

// resources/lang/en/nav.php
return [
    'home' => 'Home',
    'posts' => 'Posts',
    'login' => 'Login',
    'register' => 'Register',
    'dashboard' => 'Dashboard',
];

// resources/lang/en/footer.php
return [
    'description' => 'Professional legal services in Palestine.',
    'links' => 'Quick Links',
    'terms' => 'Terms of Service',
    'privacy' => 'Privacy Policy',
    'contact' => 'Contact Us',
    'copyright' => 'All rights reserved. :name',
];

Definition of Done

  • Navigation styled with navy background and gold links
  • Mobile menu works with smooth animation
  • Footer styled and positioned correctly
  • Sticky footer works on short-content pages
  • All navigation links functional
  • RTL layout mirrors correctly (logo right, reversed order)
  • Guest vs authenticated nav items display correctly
  • Language toggle integrated and styled
  • Touch targets meet 44px minimum on mobile
  • Tests pass

Testing Requirements

Feature Tests

// tests/Feature/NavigationTest.php
test('navigation displays home link', function () {
    $this->get('/')
        ->assertSee(__('nav.home'));
});

test('guest sees login link in navigation', function () {
    $this->get('/')
        ->assertSee(__('nav.login'));
});

test('authenticated user sees dashboard link', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->get('/')
        ->assertSee(__('nav.dashboard'))
        ->assertDontSee(__('nav.login'));
});

test('footer displays on all pages', function () {
    $this->get('/')
        ->assertSee(__('footer.copyright'));
});

test('footer links to terms and privacy', function () {
    $this->get('/')
        ->assertSee(__('footer.terms'))
        ->assertSee(__('footer.privacy'));
});

Browser Tests (Pest v4)

// tests/Browser/NavigationTest.php
it('mobile menu toggles correctly', function () {
    visit('/')
        ->resize(375, 812) // iPhone viewport
        ->assertNotVisible('[x-show="mobileMenu"]')
        ->click('[aria-label="Toggle menu"]')
        ->assertVisible('[x-show="mobileMenu"]')
        ->click('[aria-label="Toggle menu"]')
        ->assertNotVisible('[x-show="mobileMenu"]');
});

it('navigation renders correctly in RTL', function () {
    visit('/?lang=ar')
        ->assertAttribute('html', 'dir', 'rtl');
});

Manual Testing Checklist

  • Desktop: Nav links visible, hover states work
  • Mobile: Menu toggle works, touch targets adequate
  • RTL (Arabic): Layout mirrors correctly
  • LTR (English): Standard layout
  • Short page: Footer at viewport bottom
  • Long page: Footer at content bottom
  • Guest user: Login/Register visible
  • Logged in: Dashboard/user menu visible

Estimation

Complexity: Medium | Effort: 4 hours

References

  • Epic 9 Definition: docs/epics/epic-9-design-branding.md
  • Color System: Story 9.1
  • Logo Component: Story 9.3
  • Existing Header: resources/views/components/layouts/app/header.blade.php
  • PRD Design Section: docs/prd.md#design-requirements