Implemented by Ralph autonomous loop. Iteration: 9 Co-Authored-By: Claude <noreply@anthropic.com>
1909 lines
59 KiB
Markdown
1909 lines
59 KiB
Markdown
# ROA2WEB Mobile Patterns Library
|
||
|
||
**Version:** 3.0.0
|
||
**Last Updated:** 2026-01-12
|
||
**Status:** ✅ Complete (Phase 3 - Mobile Navigation Overhaul)
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Quick Start](#quick-start)
|
||
2. [Mobile Layout Overview](#mobile-layout-overview)
|
||
3. [Navigation Architecture](#navigation-architecture)
|
||
4. [MobileTopBar](#mobiletopbar)
|
||
5. [MobileBottomNav](#mobilebottomnav) *(UPDATED - Phase 3)*
|
||
6. [MobileDrawerMenu](#mobiledrawermenu) *(UPDATED - Phase 3)*
|
||
7. [Mobile Tab Pattern](#mobile-tab-pattern) *(NEW - Phase 3)*
|
||
8. [FAB Pattern](#fab-pattern) *(NEW - Phase 3)*
|
||
9. [MobileActionBar](#mobileactionbar)
|
||
10. [MobileSelectionFooter](#mobileselectionfooter)
|
||
11. [BottomSheet](#bottomsheet)
|
||
12. [SwipeableCards](#swipeablecards)
|
||
13. [Design Tokens for Mobile](#design-tokens-for-mobile)
|
||
14. [Best Practices](#best-practices)
|
||
15. [Troubleshooting](#troubleshooting)
|
||
|
||
---
|
||
|
||
## Quick Start
|
||
|
||
### For New Developers
|
||
|
||
Get a mobile view running in **5 minutes**:
|
||
|
||
```vue
|
||
<template>
|
||
<div class="mobile-view">
|
||
<!-- 1. Top Bar -->
|
||
<MobileTopBar
|
||
title="My View"
|
||
:showMenu="true"
|
||
:actions="topBarActions"
|
||
@menu-click="showDrawer = true"
|
||
@action-click="handleAction"
|
||
/>
|
||
|
||
<!-- 2. Drawer Menu (hamburger navigation) -->
|
||
<MobileDrawerMenu
|
||
v-model="showDrawer"
|
||
:user="currentUser"
|
||
@logout="handleLogout"
|
||
/>
|
||
|
||
<!-- 3. Main Content (with padding for fixed bars) -->
|
||
<main class="mobile-content">
|
||
<!-- Your content here -->
|
||
</main>
|
||
|
||
<!-- 4. Bottom Navigation -->
|
||
<MobileBottomNav />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import MobileTopBar from '@shared/components/mobile/MobileTopBar.vue'
|
||
import MobileBottomNav from '@shared/components/mobile/MobileBottomNav.vue'
|
||
import MobileDrawerMenu from '@shared/components/mobile/MobileDrawerMenu.vue'
|
||
|
||
const showDrawer = ref(false)
|
||
const currentUser = { username: 'John Doe' }
|
||
|
||
const topBarActions = [
|
||
{ icon: 'pi pi-filter', label: 'Filter', tooltip: 'Filtrează' }
|
||
]
|
||
|
||
const handleLogout = () => {
|
||
// Handle logout
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.mobile-content {
|
||
padding-top: 56px; /* MobileTopBar height */
|
||
padding-bottom: 56px; /* MobileBottomNav height */
|
||
min-height: 100vh;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### Import Paths
|
||
|
||
All mobile components are located in `src/shared/components/mobile/`:
|
||
|
||
```javascript
|
||
import MobileTopBar from '@shared/components/mobile/MobileTopBar.vue'
|
||
import MobileBottomNav from '@shared/components/mobile/MobileBottomNav.vue'
|
||
import MobileDrawerMenu from '@shared/components/mobile/MobileDrawerMenu.vue'
|
||
import MobileActionBar from '@shared/components/mobile/MobileActionBar.vue'
|
||
import MobileSelectionFooter from '@shared/components/mobile/MobileSelectionFooter.vue'
|
||
import BottomSheet from '@shared/components/mobile/BottomSheet.vue'
|
||
import SwipeableCards from '@shared/components/mobile/SwipeableCards.vue'
|
||
```
|
||
|
||
---
|
||
|
||
## Mobile Layout Overview
|
||
|
||
### ASCII Diagram: Standard Mobile Layout (Phase 3)
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ MobileTopBar (56px) │
|
||
│ [≡] Page Title [📥] [🔍] │
|
||
├─────────────────────────────────────────┤
|
||
│ │
|
||
│ │
|
||
│ MAIN CONTENT │
|
||
│ │
|
||
│ (scrollable area) │
|
||
│ │
|
||
│ padding-top: 56px │
|
||
│ padding-bottom: 56px │
|
||
│ [+] │ ← FAB (contextual)
|
||
│ │
|
||
├─────────────────────────────────────────┤
|
||
│ MobileBottomNav (56px) │
|
||
│ 🏠 📋 📄 ⚙️ │
|
||
│ Dashboard Bonuri Facturi Setări │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
> **Phase 3 Changes:**
|
||
> - Footer Nav: 4 direct links (Dashboard, Bonuri, Facturi, Setări)
|
||
> - Upload moved to FAB on Bonuri page
|
||
> - Rapoarte removed from footer (accessible via Hamburger Menu)
|
||
|
||
### ASCII Diagram: Drawer Menu Open (Phase 3 - Grouped Categories)
|
||
|
||
```
|
||
┌──────────────────┬──────────────────────┐
|
||
│ MobileDrawer │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ Menu (280px) │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
├──────────────────┤ ░░░ OVERLAY ░░░░░░░░ │
|
||
│ 🏢 ROA2WEB │ ░░ (tap to close) ░░ │
|
||
├──────────────────┤ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ PRINCIPALE │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ 🏠 Dashboard │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ 📋 Bonuri │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ ────────────── │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ RAPOARTE │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ 📄 Facturi │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ 🔢 Balanță │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ 💰 Casa și Banca│ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ ────────────── │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ ANALIZE │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ ⏰ Scadențe │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ 📋 Facturi Det. │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ ────────────── │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ ADMINISTRARE │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ ⚙️ Setări │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
├──────────────────┤ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ 👤 Username │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
│ 🚪 Deconectare │ ░░░░░░░░░░░░░░░░░░░░ │
|
||
└──────────────────┴──────────────────────┘
|
||
```
|
||
|
||
> **Phase 3 Changes:**
|
||
> - Navigation organized into 4 category sections with visual separators
|
||
> - PRINCIPALE: Dashboard, Bonuri
|
||
> - RAPOARTE: Facturi, Balanță, Casa și Banca
|
||
> - ANALIZE: Scadențe, Facturi Detaliate
|
||
> - ADMINISTRARE: Setări
|
||
|
||
### ASCII Diagram: Selection Mode Layout
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ MobileTopBar (selection-active) │
|
||
│ [←] "3 selectate" [☑] [✕] │
|
||
├─────────────────────────────────────────┤
|
||
│ │
|
||
│ ☑ Item 1 │
|
||
│ ☐ Item 2 │
|
||
│ ☑ Item 3 │
|
||
│ ☑ Item 4 │
|
||
│ ☐ Item 5 │
|
||
│ │
|
||
│ │
|
||
├─────────────────────────────────────────┤
|
||
│ MobileSelectionFooter │
|
||
│ ┌──────────┐ ┌──────────┐ │
|
||
│ │ 🗑 Șterge │ │ 📤 Export │ │
|
||
│ └──────────┘ └──────────┘ │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### ASCII Diagram: Action Bar Layout (Edit/Create Views)
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ MobileTopBar (56px) │
|
||
│ [←] Editare Bon [⋮] │
|
||
├─────────────────────────────────────────┤
|
||
│ │
|
||
│ Form Fields... │
|
||
│ │
|
||
│ ┌─────────────────────────────┐ │
|
||
│ │ Furnizor: LIDL │ │
|
||
│ │ Total: 125.50 RON │ │
|
||
│ │ Data: 2026-01-12 │ │
|
||
│ └─────────────────────────────┘ │
|
||
│ │
|
||
├─────────────────────────────────────────┤
|
||
│ MobileActionBar (context-aware) │
|
||
│ ┌──────────┐ ┌──────────────────┐ │
|
||
│ │ Salvează │ │ Trimite aprobare │ │
|
||
│ └──────────┘ └──────────────────┘ │
|
||
├─────────────────────────────────────────┤
|
||
│ MobileBottomNav (56px) │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### ASCII Diagram: BottomSheet Open
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ MobileTopBar (56px) │
|
||
├─────────────────────────────────────────┤
|
||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||
│ ░░░░░░░░░░░░ OVERLAY ░░░░░░░░░░░░░░░░░░ │
|
||
│ ░░░░░░░░░░ (tap to close) ░░░░░░░░░░░░░ │
|
||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||
├─────────────────────────────────────────┤ ←─ BottomSheet
|
||
│ ─────────── │ slides up
|
||
│ (drag handle) │
|
||
│ │
|
||
│ Filter Options: │
|
||
│ ☐ Option A │
|
||
│ ☑ Option B │
|
||
│ ☐ Option C │
|
||
│ │
|
||
│ [ Apply Filters ] │
|
||
│ │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### ASCII Diagram: SwipeableCards
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ │
|
||
│ ┌─────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ KPI Card Content │ │ ← Swipe left/right
|
||
│ │ │ │ to navigate
|
||
│ │ $125,430 │ │
|
||
│ │ Total Sales │ │
|
||
│ │ │ │
|
||
│ └─────────────────────────────────────┘ │
|
||
│ │
|
||
│ ●━━━━●───● │ ← Dots indicator
|
||
│ │ (active = expanded)
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### ASCII Diagram: Mobile Tab Pattern (Phase 3)
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ MobileTopBar (56px) │
|
||
│ [≡] Facturi [📥] [🔍] │
|
||
├─────────────────────────────────────────┤
|
||
│┌──────────────────┬────────────────────┐│
|
||
││ Clienți │ Furnizori ││ ← Full-width tabs
|
||
││ ═══════════ │ ││ (Material Design 3)
|
||
│└──────────────────┴────────────────────┘│
|
||
├─────────────────────────────────────────┤
|
||
│ │
|
||
│ TAB CONTENT │
|
||
│ (changes based on tab) │
|
||
│ │
|
||
│ padding-top: 104px (56+48) │
|
||
│ │
|
||
├─────────────────────────────────────────┤
|
||
│ MobileBottomNav (56px) │
|
||
│ 🏠 📋 📄 ⚙️ │
|
||
│ Dashboard Bonuri Facturi Setări │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
> **Used in:** Facturi (`/reports/invoices`), Scadențe (`/reports/maturity-analysis`)
|
||
|
||
### ASCII Diagram: FAB Pattern (Phase 3)
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ MobileTopBar (56px) │
|
||
│ [≡] Bonuri [📥] [🔍] │
|
||
├─────────────────────────────────────────┤
|
||
│ │
|
||
│ MAIN CONTENT │
|
||
│ │
|
||
│ ┌─────────────┐ │
|
||
│ │ Upload │ │ ← FAB Popup Menu
|
||
│ │ Bulk │ │ (appears on tap)
|
||
│ ├─────────────┤ │
|
||
│ │ Bon Nou │ │
|
||
│ └─────────────┘ │
|
||
│ [+] │ ← FAB (56x56px)
|
||
│ │ bottom: 72px
|
||
├─────────────────────────────────────────┤
|
||
│ MobileBottomNav (56px) │
|
||
│ 🏠 📋 📄 ⚙️ │
|
||
│ Dashboard Bonuri Facturi Setări │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
> **Used in:** Bonuri page (`/data-entry`) - Upload moved from footer to FAB
|
||
|
||
---
|
||
|
||
## Navigation Architecture
|
||
|
||
### Route Map (Phase 3 - Updated)
|
||
|
||
```
|
||
┌─────────────────────────────────────┐
|
||
│ / (root) │
|
||
│ Redirects to /dashboard │
|
||
└───────────────┬─────────────────────┘
|
||
│
|
||
┌───────────────────────────┼───────────────────────────┐
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌───────────────────┐ ┌───────────────────────┐ ┌─────────────────┐
|
||
│ /data-entry │ │ /reports │ │ /settings │
|
||
│ │ │ │ │ │
|
||
│ 📋 Bonuri List │ │ 📄 invoices [TABS] │ │ ⚙️ Settings Hub │
|
||
│ 📝 Bon Create │ │ 💰 bank-cash │ │ (centralized │
|
||
│ ✏️ Bon Edit │ │ 🔢 trial-balance │ │ settings) │
|
||
│ 📊 OCR Metrics │ │ 📉 maturity [TABS] │ │ │
|
||
│ ☁️ Bulk Upload │ │ 📋 detailed-invoices │ │ Links to: │
|
||
│ │ │ 📈 cache-stats │ │ - OCR Settings │
|
||
│ [FAB: +] │ │ 📝 server-logs │ │ - Cache Stats │
|
||
│ - Bon Nou │ │ 📱 telegram │ │ - Server Logs │
|
||
│ - Upload Bulk │ │ │ │ - Telegram │
|
||
└───────────────────┘ └───────────────────────┘ └─────────────────┘
|
||
|
||
/dashboard
|
||
│
|
||
▼
|
||
┌─────────────────────┐
|
||
│ 🏠 Dashboard │ ← Primary entry point (Footer Nav)
|
||
│ SwipeableCards KPIs│
|
||
│ (No quick-links │
|
||
│ on mobile) │
|
||
└─────────────────────┘
|
||
```
|
||
|
||
> **Phase 3 Navigation Changes:**
|
||
> - `/dashboard` is now a direct route in Footer Nav (not under `/reports`)
|
||
> - Invoices and Maturity Analysis have Clienți/Furnizori tabs
|
||
> - FAB on Bonuri page provides Bon Nou + Upload Bulk actions
|
||
|
||
### Navigation Flow: Settings Hub Pattern
|
||
|
||
The Settings Hub (`/settings`) is the centralized entry point for all administrative pages:
|
||
|
||
```
|
||
MobileBottomNav "Setări" button
|
||
│
|
||
▼
|
||
┌───────────────────────────────────┐
|
||
│ Settings Hub │
|
||
│ /settings │
|
||
│ │
|
||
│ ┌─────────┐ ┌─────────┐ │
|
||
│ │ OCR │ │ Cache │ │
|
||
│ │ Setări │ │ Stats │ │
|
||
│ └────┬────┘ └────┬────┘ │
|
||
│ │ │ │
|
||
│ ┌────┴────┐ ┌────┴────┐ │
|
||
│ │ Server │ │Telegram │ │
|
||
│ │ Logs │ │ Bot │ │
|
||
│ └─────────┘ └─────────┘ │
|
||
└───────────────────────────────────┘
|
||
│
|
||
▼ (click on card)
|
||
┌───────────────────────────────────┐
|
||
│ /data-entry/ocr-metrics │
|
||
│ /reports/cache-stats │
|
||
│ /reports/server-logs │
|
||
│ /reports/telegram │
|
||
└───────────────────────────────────┘
|
||
```
|
||
|
||
### MobileBottomNav Default Items (Phase 3 - 4 Direct Links)
|
||
|
||
```javascript
|
||
// Default navigation items - Phase 3 (4 links, no action button)
|
||
[
|
||
{ to: '/dashboard', icon: 'pi pi-home', label: 'Dashboard' },
|
||
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri' },
|
||
{ to: '/reports/invoices', icon: 'pi pi-file-edit', label: 'Facturi' },
|
||
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări' }
|
||
]
|
||
```
|
||
|
||
> **Phase 3 Changes:**
|
||
> - Upload button removed from footer (moved to FAB on Bonuri page)
|
||
> - Dashboard added as direct link
|
||
> - Facturi added as direct link (with Clienți/Furnizori tabs)
|
||
> - All 4 items are now router links (no action buttons)
|
||
|
||
### MobileDrawerMenu Navigation Links (Phase 3 - Grouped Categories)
|
||
|
||
```javascript
|
||
// PRINCIPALE: Primary navigation
|
||
const principaleItems = [
|
||
{ to: '/dashboard', icon: 'pi pi-home', label: 'Dashboard', exactMatch: true },
|
||
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri', exactMatch: false }
|
||
]
|
||
|
||
// RAPOARTE: Reports section
|
||
const rapoarteItems = [
|
||
{ to: '/reports/invoices', icon: 'pi pi-file', label: 'Facturi', exactMatch: true },
|
||
{ to: '/reports/trial-balance', icon: 'pi pi-calculator', label: 'Balanță', exactMatch: true },
|
||
{ to: '/reports/bank-cash', icon: 'pi pi-money-bill', label: 'Casa și Banca', exactMatch: true }
|
||
]
|
||
|
||
// ANALIZE: Analysis section
|
||
const analizeItems = [
|
||
{ to: '/reports/maturity-analysis', icon: 'pi pi-clock', label: 'Scadențe', exactMatch: true },
|
||
{ to: '/reports/detailed-invoices', icon: 'pi pi-list', label: 'Facturi Detaliate', exactMatch: true }
|
||
]
|
||
|
||
// ADMINISTRARE: Admin section
|
||
const administrareItems = [
|
||
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări', exactMatch: false }
|
||
]
|
||
```
|
||
|
||
> **Phase 3 Changes:**
|
||
> - Navigation items organized into 4 visual sections with headers
|
||
> - Visual dividers between sections (`.drawer-divider`)
|
||
> - Section headers styled as uppercase labels (PRINCIPALE, RAPOARTE, ANALIZE, ADMINISTRARE)
|
||
> - `exactMatch` flag for precise active state highlighting
|
||
|
||
### Dashboard Mobile Layout (Phase 3 - KPIs Only)
|
||
|
||
On mobile, Dashboard shows ONLY KPI cards (quick-links removed in Phase 3):
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ MobileTopBar: Dashboard │
|
||
│ [≡] Dashboard [📥] [🔍] │
|
||
├─────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────┐ │
|
||
│ │ SwipeableCards (KPIs) │ │
|
||
│ │ ← Swipe → │ │
|
||
│ │ │ │
|
||
│ │ Total Facturi: 125,430 RON │ │
|
||
│ │ Scadențe: 5 restante │ │
|
||
│ │ ... │ │
|
||
│ │ │ │
|
||
│ └─────────────────────────────────┘ │
|
||
│ │
|
||
│ ●━━━━●───● │
|
||
│ │
|
||
│ (No quick-links on mobile - use │
|
||
│ Footer Nav or Hamburger Menu) │
|
||
│ │
|
||
├─────────────────────────────────────────┤
|
||
│ MobileBottomNav (56px) │
|
||
│ 🏠 📋 📄 ⚙️ │
|
||
│ Dashboard Bonuri Facturi Setări │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
> **Phase 3 Changes:**
|
||
> - Quick-link cards removed from mobile Dashboard
|
||
> - Desktop Dashboard keeps quick-links (unchanged)
|
||
> - Navigation to other pages via Footer Nav or Hamburger Menu
|
||
|
||
---
|
||
|
||
## MobileTopBar
|
||
|
||
Material Design 3 inspired top navigation bar for mobile views.
|
||
|
||
### Props
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `title` | String | `''` | Center title text |
|
||
| `showBack` | Boolean | `false` | Show back arrow button on left |
|
||
| `showMenu` | Boolean | `false` | Show hamburger menu on left (ignored if showBack is true) |
|
||
| `actions` | Array | `[]` | Right-side action buttons |
|
||
| `selectionActive` | Boolean | `false` | Enable selection mode styling |
|
||
|
||
### Events
|
||
|
||
| Event | Payload | Description |
|
||
|-------|---------|-------------|
|
||
| `menu-click` | - | Hamburger menu clicked |
|
||
| `back-click` | - | Back arrow clicked |
|
||
| `action-click` | `action` | Action button clicked |
|
||
|
||
### Action Object Structure
|
||
|
||
```typescript
|
||
interface TopBarAction {
|
||
icon: string // PrimeIcons class (e.g., 'pi pi-filter')
|
||
label?: string // Accessibility label
|
||
tooltip?: string // Tooltip text
|
||
active?: boolean // Highlight when active
|
||
}
|
||
```
|
||
|
||
### Basic Usage
|
||
|
||
```vue
|
||
<template>
|
||
<MobileTopBar
|
||
title="Bonuri Fiscale"
|
||
:showMenu="true"
|
||
:actions="topBarActions"
|
||
@menu-click="showSidebar = true"
|
||
@action-click="handleAction"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
const topBarActions = [
|
||
{ icon: 'pi pi-filter', label: 'Filtrează', tooltip: 'Deschide filtre' },
|
||
{ icon: 'pi pi-search', label: 'Caută', tooltip: 'Căutare' }
|
||
]
|
||
|
||
const handleAction = (action) => {
|
||
if (action.icon === 'pi pi-filter') {
|
||
openFilterSheet()
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### With Back Navigation
|
||
|
||
```vue
|
||
<MobileTopBar
|
||
title="Detalii Bon"
|
||
:showBack="true"
|
||
@back-click="router.back()"
|
||
/>
|
||
```
|
||
|
||
### Selection Mode
|
||
|
||
```vue
|
||
<MobileTopBar
|
||
:title="selectedCount > 0 ? `${selectedCount} selectate` : 'Bonuri'"
|
||
:showBack="selectedCount > 0"
|
||
:selectionActive="selectedCount > 0"
|
||
:actions="selectionActions"
|
||
@back-click="clearSelection"
|
||
/>
|
||
```
|
||
|
||
### CSS Classes
|
||
|
||
| Class | Description |
|
||
|-------|-------------|
|
||
| `.mobile-top-bar` | Base container (fixed, 56px height) |
|
||
| `.selection-active` | Blue background for selection mode |
|
||
| `.top-bar-left` | Left button container |
|
||
| `.top-bar-right` | Right action buttons container |
|
||
| `.top-bar-title` | Center title (ellipsis on overflow) |
|
||
| `.top-bar-btn` | Individual button (48x48px touch target) |
|
||
| `.top-bar-btn.active` | Highlighted state |
|
||
|
||
---
|
||
|
||
## MobileBottomNav
|
||
|
||
Material Design 3 inspired bottom navigation bar with router integration.
|
||
|
||
### Props
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `items` | Array | Default nav items | Navigation items array |
|
||
|
||
### Events
|
||
|
||
| Event | Payload | Description |
|
||
|-------|---------|-------------|
|
||
| `item-click` | `item` | Non-router item clicked |
|
||
|
||
### Item Object Structure
|
||
|
||
```typescript
|
||
interface NavItem {
|
||
to?: string // Route path (uses router-link when provided)
|
||
icon: string // PrimeIcons class (e.g., 'pi pi-receipt')
|
||
label: string // Display text
|
||
active?: boolean // Force active state (for non-router items)
|
||
}
|
||
```
|
||
|
||
### Default Items
|
||
|
||
```javascript
|
||
[
|
||
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri' },
|
||
{ icon: 'pi pi-cloud-upload', label: 'Upload' }, // Action button
|
||
{ to: '/reports/dashboard', icon: 'pi pi-chart-bar', label: 'Rapoarte' },
|
||
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări' } // → Settings Hub
|
||
]
|
||
```
|
||
|
||
### Basic Usage
|
||
|
||
```vue
|
||
<template>
|
||
<MobileBottomNav />
|
||
</template>
|
||
```
|
||
|
||
### Custom Items
|
||
|
||
```vue
|
||
<template>
|
||
<MobileBottomNav
|
||
:items="customItems"
|
||
@item-click="handleNavClick"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
const customItems = [
|
||
{ to: '/home', icon: 'pi pi-home', label: 'Acasă' },
|
||
{ to: '/reports', icon: 'pi pi-chart-bar', label: 'Rapoarte' },
|
||
{ icon: 'pi pi-plus', label: 'Adaugă' }, // Button, not route
|
||
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări' }
|
||
]
|
||
|
||
const handleNavClick = (item) => {
|
||
if (item.label === 'Adaugă') {
|
||
showAddDialog()
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### CSS Classes
|
||
|
||
| Class | Description |
|
||
|-------|-------------|
|
||
| `.mobile-bottom-nav` | Base container (fixed, 56px height) |
|
||
| `.bottom-nav-item` | Individual nav item (flex: 1) |
|
||
| `.bottom-nav-item.active` | Active state (primary color) |
|
||
| `.bottom-nav-item.router-link-active` | Auto-active via Vue Router |
|
||
|
||
---
|
||
|
||
## MobileDrawerMenu
|
||
|
||
*(UPDATED - Phase 3)*
|
||
|
||
Material Design 3 inspired navigation drawer that slides in from the left. Replaces the desktop sidebar on mobile devices. **Phase 3** reorganized navigation into grouped categories with visual separators.
|
||
|
||
### Props
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `modelValue` | Boolean | `false` | Controls visibility (v-model support) |
|
||
| `user` | Object | `null` | User object for profile display `{ username: string }` |
|
||
| `onLogout` | Function | `null` | Optional logout callback; emits `logout` event if not provided |
|
||
|
||
### Events
|
||
|
||
| Event | Payload | Description |
|
||
|-------|---------|-------------|
|
||
| `update:modelValue` | `boolean` | v-model update when visibility changes |
|
||
| `logout` | - | Emitted when logout is clicked (if no `onLogout` prop) |
|
||
|
||
### Navigation Items (Phase 3 - Grouped Categories)
|
||
|
||
The drawer navigation is organized into 4 sections with visual separators:
|
||
|
||
```javascript
|
||
// PRINCIPALE Section
|
||
const principaleItems = [
|
||
{ to: '/dashboard', icon: 'pi pi-home', label: 'Dashboard', exactMatch: true },
|
||
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri', exactMatch: false }
|
||
]
|
||
|
||
// RAPOARTE Section
|
||
const rapoarteItems = [
|
||
{ to: '/reports/invoices', icon: 'pi pi-file', label: 'Facturi', exactMatch: true },
|
||
{ to: '/reports/trial-balance', icon: 'pi pi-calculator', label: 'Balanță', exactMatch: true },
|
||
{ to: '/reports/bank-cash', icon: 'pi pi-money-bill', label: 'Casa și Banca', exactMatch: true }
|
||
]
|
||
|
||
// ANALIZE Section
|
||
const analizeItems = [
|
||
{ to: '/reports/maturity-analysis', icon: 'pi pi-clock', label: 'Scadențe', exactMatch: true },
|
||
{ to: '/reports/detailed-invoices', icon: 'pi pi-list', label: 'Facturi Detaliate', exactMatch: true }
|
||
]
|
||
|
||
// ADMINISTRARE Section
|
||
const administrareItems = [
|
||
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări', exactMatch: false }
|
||
]
|
||
```
|
||
|
||
### Section Structure
|
||
|
||
```
|
||
┌──────────────────┐
|
||
│ 🏢 ROA2WEB │ ← Header with logo
|
||
├──────────────────┤
|
||
│ PRINCIPALE │ ← Section header (uppercase)
|
||
│ 🏠 Dashboard │
|
||
│ 📋 Bonuri │
|
||
│ ────────────── │ ← Divider
|
||
│ RAPOARTE │
|
||
│ 📄 Facturi │
|
||
│ 🔢 Balanță │
|
||
│ 💰 Casa și Banca│
|
||
│ ────────────── │
|
||
│ ANALIZE │
|
||
│ ⏰ Scadențe │
|
||
│ 📋 Facturi Det. │
|
||
│ ────────────── │
|
||
│ ADMINISTRARE │
|
||
│ ⚙️ Setări │
|
||
├──────────────────┤
|
||
│ 👤 Username │ ← Profile section (footer)
|
||
│ 🚪 Deconectare │
|
||
└──────────────────┘
|
||
```
|
||
|
||
### Basic Usage
|
||
|
||
```vue
|
||
<template>
|
||
<MobileTopBar
|
||
title="Dashboard"
|
||
:showMenu="true"
|
||
@menu-click="showDrawer = true"
|
||
/>
|
||
|
||
<MobileDrawerMenu
|
||
v-model="showDrawer"
|
||
:user="authStore.user"
|
||
@logout="handleLogout"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import MobileDrawerMenu from '@shared/components/mobile/MobileDrawerMenu.vue'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
|
||
const showDrawer = ref(false)
|
||
const authStore = useAuthStore()
|
||
|
||
const handleLogout = async () => {
|
||
await authStore.logout()
|
||
router.push('/login')
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### With Logout Callback
|
||
|
||
```vue
|
||
<MobileDrawerMenu
|
||
v-model="showDrawer"
|
||
:user="{ username: 'John Doe' }"
|
||
:onLogout="async () => {
|
||
await api.logout()
|
||
router.push('/login')
|
||
}"
|
||
/>
|
||
```
|
||
|
||
### Features
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| **Slide Animation** | Slides in from left with overlay fade |
|
||
| **Close on Tap Outside** | Tapping the overlay closes the drawer |
|
||
| **Close on Navigation** | Drawer closes automatically when a link is clicked |
|
||
| **Active State** | Current route is highlighted |
|
||
| **Profile Section** | Displays username and logout button at bottom |
|
||
| **Teleported** | Rendered to `<body>` to avoid z-index issues |
|
||
| **Dark Mode** | Full support for light/dark themes |
|
||
|
||
### CSS Classes
|
||
|
||
| Class | Description |
|
||
|-------|-------------|
|
||
| `.drawer-overlay` | Full-screen backdrop (50% black, 70% in dark mode) |
|
||
| `.drawer-menu` | Main drawer container (280px, max 85vw) |
|
||
| `.drawer-header` | Header with logo |
|
||
| `.drawer-sections` | Scrollable container for all navigation sections |
|
||
| `.drawer-section` | Individual section container |
|
||
| `.section-header` | Section header label (PRINCIPALE, RAPOARTE, etc.) - uppercase, small text |
|
||
| `.drawer-nav` | List container for navigation links |
|
||
| `.drawer-link` | Individual navigation link (48px min-height) |
|
||
| `.drawer-link.active` | Active route styling (blue background) |
|
||
| `.drawer-divider` | Visual separator between sections |
|
||
| `.drawer-profile` | Profile section at bottom |
|
||
| `.profile-header` | User avatar and name row |
|
||
| `.profile-avatar` | Circular avatar with user icon |
|
||
| `.logout-link` | Red logout button styling |
|
||
|
||
### Section Header Styling
|
||
|
||
```css
|
||
.section-header {
|
||
padding: var(--space-sm) var(--space-lg);
|
||
font-size: var(--text-xs);
|
||
font-weight: var(--font-semibold);
|
||
color: var(--text-color-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
.drawer-divider {
|
||
height: 1px;
|
||
background: var(--surface-border);
|
||
margin: var(--space-xs) var(--space-md);
|
||
}
|
||
```
|
||
|
||
### Animation
|
||
|
||
Uses Vue `<Transition name="drawer">`:
|
||
- Overlay fades in/out
|
||
- Drawer slides from left (`translateX(-100%)` → `translateX(0)`)
|
||
- Duration: `var(--transition-normal)` (250ms)
|
||
|
||
---
|
||
|
||
## Mobile Tab Pattern
|
||
|
||
*(NEW - Phase 3)*
|
||
|
||
Material Design 3 inspired full-width tabs for switching between Clienți and Furnizori views. Used in pages that display partner-related data.
|
||
|
||
### When to Use
|
||
|
||
Use the Mobile Tab Pattern when:
|
||
- A page shows data that can be filtered by partner type (Clienți/Furnizori)
|
||
- The tab switch should be persistent via URL query parameter
|
||
- Both tabs share the same filters (except type)
|
||
|
||
### Implementation
|
||
|
||
```vue
|
||
<template>
|
||
<!-- US-304/US-305: Mobile Tabs for Clienți/Furnizori -->
|
||
<div v-if="isMobile" class="mobile-tabs-container">
|
||
<div class="mobile-tabs">
|
||
<button
|
||
class="mobile-tab"
|
||
:class="{ active: activeTab === 'clients' }"
|
||
@click="switchTab('clients')"
|
||
>
|
||
<span class="tab-label">Clienți</span>
|
||
</button>
|
||
<button
|
||
class="mobile-tab"
|
||
:class="{ active: activeTab === 'suppliers' }"
|
||
@click="switchTab('suppliers')"
|
||
>
|
||
<span class="tab-label">Furnizori</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
// Tab state synced with URL query param
|
||
const activeTab = ref(route.query.tab === 'suppliers' ? 'suppliers' : 'clients')
|
||
|
||
// Switch tab and update URL
|
||
const switchTab = (tab) => {
|
||
if (tab === activeTab.value) return
|
||
activeTab.value = tab
|
||
|
||
// Update URL query param without full navigation
|
||
router.replace({
|
||
query: { ...route.query, tab: tab === 'clients' ? undefined : tab }
|
||
})
|
||
|
||
// Trigger data reload with new type
|
||
loadData()
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### CSS Classes
|
||
|
||
```css
|
||
/* Tab container - positioned below MobileTopBar */
|
||
.mobile-tabs-container {
|
||
position: fixed;
|
||
top: 56px; /* Below MobileTopBar */
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 999;
|
||
background: var(--surface-card);
|
||
border-bottom: 1px solid var(--surface-border);
|
||
}
|
||
|
||
.mobile-tabs {
|
||
display: flex;
|
||
width: 100%;
|
||
}
|
||
|
||
.mobile-tab {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: var(--space-md) var(--space-sm);
|
||
background: transparent;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
cursor: pointer;
|
||
transition: all var(--transition-fast);
|
||
color: var(--text-color-secondary);
|
||
min-height: 48px;
|
||
font-size: var(--text-base);
|
||
font-weight: var(--font-medium);
|
||
}
|
||
|
||
.mobile-tab:active {
|
||
background: var(--surface-hover);
|
||
}
|
||
|
||
.mobile-tab.active {
|
||
color: var(--color-primary);
|
||
border-bottom-color: var(--color-primary);
|
||
font-weight: var(--font-semibold);
|
||
}
|
||
```
|
||
|
||
### Content Padding
|
||
|
||
When using tabs, increase top padding to account for both bars:
|
||
|
||
```css
|
||
.mobile-content-with-tabs {
|
||
padding-top: 104px; /* 56px (TopBar) + 48px (Tabs) */
|
||
padding-bottom: 56px; /* BottomNav */
|
||
}
|
||
```
|
||
|
||
### Used In
|
||
|
||
| Page | Route | Description |
|
||
|------|-------|-------------|
|
||
| **Facturi** | `/reports/invoices` | Switch between client/supplier invoices |
|
||
| **Scadențe** | `/reports/maturity-analysis` | Switch between client/supplier maturity |
|
||
|
||
---
|
||
|
||
## FAB Pattern
|
||
|
||
*(NEW - Phase 3)*
|
||
|
||
Material Design 3 inspired Floating Action Button (FAB) for contextual actions. Positioned above MobileBottomNav with popup menu support.
|
||
|
||
### When to Use
|
||
|
||
Use the FAB Pattern when:
|
||
- A page has primary actions that benefit from prominent placement
|
||
- Multiple related actions can be grouped (popup menu)
|
||
- Actions were previously in footer/header but need better mobile UX
|
||
|
||
### Design Tokens
|
||
|
||
```javascript
|
||
// From PRD cssRules.mobileLayoutTokens
|
||
{
|
||
fabSize: '56px', // FAB diameter
|
||
fabBottomOffset: '72px', // 56px (nav) + 16px spacing
|
||
touchTargetMin: '48px' // Minimum touch target
|
||
}
|
||
```
|
||
|
||
### Implementation
|
||
|
||
```vue
|
||
<template>
|
||
<!-- FAB Button -->
|
||
<Transition name="fab-slide">
|
||
<button
|
||
v-if="isMobile && !selectionMode && fabVisible"
|
||
class="mobile-fab"
|
||
:class="{ 'fab-active': fabMenuOpen }"
|
||
@click="toggleFabMenu"
|
||
aria-label="Meniu acțiuni"
|
||
aria-haspopup="true"
|
||
:aria-expanded="fabMenuOpen"
|
||
>
|
||
<i class="pi pi-plus" :class="{ 'fab-icon-rotate': fabMenuOpen }"></i>
|
||
</button>
|
||
</Transition>
|
||
|
||
<!-- FAB Popup Menu -->
|
||
<Menu
|
||
ref="fabMenuRef"
|
||
:model="fabMenuItems"
|
||
:popup="true"
|
||
class="fab-popup-menu"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import Menu from 'primevue/menu'
|
||
|
||
const router = useRouter()
|
||
const fabMenuRef = ref(null)
|
||
const fabMenuOpen = ref(false)
|
||
const fabVisible = ref(true)
|
||
|
||
// FAB Menu Items
|
||
const fabMenuItems = [
|
||
{
|
||
label: 'Bon Nou',
|
||
icon: 'pi pi-plus',
|
||
command: () => router.push('/data-entry/receipts/new')
|
||
},
|
||
{
|
||
label: 'Upload Bulk',
|
||
icon: 'pi pi-cloud-upload',
|
||
command: () => openBulkUpload()
|
||
}
|
||
]
|
||
|
||
// Toggle FAB Menu
|
||
const toggleFabMenu = (event) => {
|
||
fabMenuOpen.value = !fabMenuOpen.value
|
||
fabMenuRef.value?.toggle(event)
|
||
}
|
||
|
||
// Hide FAB during scroll, show when scroll stops
|
||
let lastScrollY = 0
|
||
const handleScroll = () => {
|
||
const currentScrollY = window.scrollY
|
||
const scrollDelta = currentScrollY - lastScrollY
|
||
|
||
if (scrollDelta > 10) {
|
||
fabVisible.value = false // Scrolling down - hide
|
||
} else if (scrollDelta < -10) {
|
||
fabVisible.value = true // Scrolling up - show
|
||
}
|
||
|
||
lastScrollY = currentScrollY
|
||
|
||
// Show after scroll stops (300ms)
|
||
clearTimeout(scrollTimeout)
|
||
scrollTimeout = setTimeout(() => {
|
||
fabVisible.value = true
|
||
}, 300)
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### CSS Classes
|
||
|
||
```css
|
||
/* FAB Button */
|
||
.mobile-fab {
|
||
position: fixed;
|
||
bottom: 72px; /* 56px nav + 16px spacing */
|
||
right: var(--space-md);
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: var(--radius-full);
|
||
background: var(--color-primary);
|
||
color: white;
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: var(--shadow-lg);
|
||
cursor: pointer;
|
||
z-index: 999;
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
.mobile-fab:active {
|
||
transform: scale(0.95);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.mobile-fab i {
|
||
font-size: var(--text-2xl);
|
||
transition: transform var(--transition-fast);
|
||
}
|
||
|
||
/* FAB active state when menu open */
|
||
.mobile-fab.fab-active {
|
||
background: var(--color-primary-dark);
|
||
}
|
||
|
||
.mobile-fab .fab-icon-rotate {
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
/* FAB Popup Menu positioning */
|
||
.fab-popup-menu {
|
||
position: fixed !important;
|
||
bottom: 140px !important; /* 72px + 56px + 12px */
|
||
right: var(--space-md) !important;
|
||
left: auto !important;
|
||
top: auto !important;
|
||
min-width: 180px;
|
||
z-index: 1000 !important;
|
||
}
|
||
|
||
/* FAB slide animation */
|
||
.fab-slide-enter-active,
|
||
.fab-slide-leave-active {
|
||
transition: transform var(--transition-normal), opacity var(--transition-normal);
|
||
}
|
||
|
||
.fab-slide-enter-from,
|
||
.fab-slide-leave-to {
|
||
transform: translateY(100px);
|
||
opacity: 0;
|
||
}
|
||
```
|
||
|
||
### Behavior
|
||
|
||
| Interaction | Result |
|
||
|-------------|--------|
|
||
| **Tap FAB** | Opens popup menu with actions |
|
||
| **Tap menu item** | Executes action, closes menu |
|
||
| **Tap outside** | Closes menu |
|
||
| **Scroll down** | FAB hides (slide down) |
|
||
| **Scroll up** | FAB shows (slide up) |
|
||
| **Scroll stops** | FAB reappears after 300ms |
|
||
| **Selection mode** | FAB hidden (replaced by selection footer) |
|
||
|
||
### Used In
|
||
|
||
| Page | Route | Actions |
|
||
|------|-------|---------|
|
||
| **Bonuri** | `/data-entry` | Bon Nou, Upload Bulk |
|
||
|
||
> **Phase 3 Note:** Upload was moved from Footer Nav to FAB to reduce footer clutter and provide better contextual placement.
|
||
|
||
---
|
||
|
||
## MobileActionBar
|
||
|
||
Context-aware bottom action bar for forms and edit views. Positions above MobileBottomNav and displays action buttons based on current state.
|
||
|
||
### Props
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `visible` | Boolean | `false` | Controls visibility (triggers slide-up animation) |
|
||
| `actions` | Array | `[]` | Array of action buttons to display |
|
||
|
||
### Action Object Structure
|
||
|
||
```typescript
|
||
interface ActionBarAction {
|
||
label: string // Button text (e.g., 'Salvează')
|
||
icon: string // PrimeIcons class (e.g., 'pi pi-save')
|
||
severity?: string // PrimeVue severity ('primary', 'secondary', 'danger', etc.)
|
||
handler: () => void // Click handler function
|
||
disabled?: boolean // Disable the button
|
||
}
|
||
```
|
||
|
||
### Layout Behavior
|
||
|
||
| Actions Count | Layout |
|
||
|---------------|--------|
|
||
| 1 button | Full-width |
|
||
| 2 buttons | Side-by-side, equal width |
|
||
| 3+ buttons | Equal distribution |
|
||
|
||
### Basic Usage
|
||
|
||
```vue
|
||
<template>
|
||
<MobileActionBar
|
||
:visible="isMobile"
|
||
:actions="formActions"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
import MobileActionBar from '@shared/components/mobile/MobileActionBar.vue'
|
||
|
||
const formActions = [
|
||
{
|
||
label: 'Salvează',
|
||
icon: 'pi pi-save',
|
||
severity: 'primary',
|
||
handler: handleSave
|
||
},
|
||
{
|
||
label: 'Anulează',
|
||
icon: 'pi pi-times',
|
||
severity: 'secondary',
|
||
handler: handleCancel
|
||
}
|
||
]
|
||
</script>
|
||
```
|
||
|
||
### Context-Aware Actions (Receipt Edit Example)
|
||
|
||
```vue
|
||
<template>
|
||
<MobileActionBar
|
||
:visible="isMobile"
|
||
:actions="contextAwareActions"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed } from 'vue'
|
||
|
||
const receiptStatus = ref('draft') // 'draft' | 'pending' | 'approved' | 'rejected'
|
||
|
||
const contextAwareActions = computed(() => {
|
||
switch (receiptStatus.value) {
|
||
case 'draft':
|
||
return [
|
||
{ label: 'Salvează', icon: 'pi pi-save', severity: 'secondary', handler: handleSave },
|
||
{ label: 'Trimite', icon: 'pi pi-send', severity: 'primary', handler: handleSubmit }
|
||
]
|
||
case 'pending':
|
||
return [
|
||
{ label: 'Salvează', icon: 'pi pi-save', severity: 'secondary', handler: handleSave },
|
||
{ label: 'Aprobă', icon: 'pi pi-check', severity: 'success', handler: handleApprove },
|
||
{ label: 'Respinge', icon: 'pi pi-times', severity: 'danger', handler: handleReject }
|
||
]
|
||
case 'approved':
|
||
return [] // No actions for approved receipts
|
||
case 'rejected':
|
||
return [
|
||
{ label: 'Salvează', icon: 'pi pi-save', severity: 'secondary', handler: handleSave },
|
||
{ label: 'Re-trimite', icon: 'pi pi-refresh', severity: 'primary', handler: handleResubmit }
|
||
]
|
||
default:
|
||
return []
|
||
}
|
||
})
|
||
</script>
|
||
```
|
||
|
||
### Hiding During Overlays
|
||
|
||
Hide the action bar when BottomSheet or other overlays are open:
|
||
|
||
```vue
|
||
<template>
|
||
<MobileActionBar
|
||
:visible="isMobile && !isBottomSheetOpen"
|
||
:actions="formActions"
|
||
/>
|
||
|
||
<BottomSheet v-model="isBottomSheetOpen">
|
||
<!-- Filter content -->
|
||
</BottomSheet>
|
||
</template>
|
||
```
|
||
|
||
### CSS Classes
|
||
|
||
| Class | Description |
|
||
|-------|-------------|
|
||
| `.mobile-action-bar` | Base container (fixed, bottom: 56px) |
|
||
| `.action-bar-content` | Button container (max-width: 500px) |
|
||
| `.layout-single` | Single button layout (full-width) |
|
||
| `.layout-dual` | Two button layout (50% each) |
|
||
| `.layout-multi` | Three+ button layout (equal distribution) |
|
||
| `.action-bar-btn` | Individual button (48px min-height) |
|
||
|
||
### Animation
|
||
|
||
Uses Vue `<Transition name="slide-up">`:
|
||
- Slides up from below screen (`translateY(100%)` → `translateY(0)`)
|
||
- Duration: `var(--transition-normal)` (250ms)
|
||
- Fades in/out simultaneously
|
||
|
||
### Positioning
|
||
|
||
```css
|
||
.mobile-action-bar {
|
||
position: fixed;
|
||
bottom: 56px; /* Above MobileBottomNav */
|
||
left: 0;
|
||
right: 0;
|
||
/* Safe area support for iPhone X+ */
|
||
bottom: calc(56px + env(safe-area-inset-bottom));
|
||
}
|
||
```
|
||
|
||
### Content Padding Adjustment
|
||
|
||
When MobileActionBar is visible, increase bottom padding:
|
||
|
||
```css
|
||
.mobile-content {
|
||
padding-bottom: 120px; /* 56px (nav) + 64px (action bar) */
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## MobileSelectionFooter
|
||
|
||
Slide-up action bar for batch operations on selected items.
|
||
|
||
### Props
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `visible` | Boolean | `false` | Controls visibility (triggers animation) |
|
||
| `actions` | Array | `[]` | Action buttons to display |
|
||
|
||
### Action Object Structure
|
||
|
||
```typescript
|
||
interface SelectionAction {
|
||
label: string // Button text
|
||
icon: string // PrimeIcons class
|
||
severity?: string // PrimeVue severity ('secondary', 'danger', etc.)
|
||
handler: () => void // Click handler function
|
||
}
|
||
```
|
||
|
||
### Basic Usage
|
||
|
||
```vue
|
||
<template>
|
||
<MobileSelectionFooter
|
||
:visible="selectedItems.length > 0"
|
||
:actions="selectionActions"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
const selectedItems = ref([])
|
||
|
||
const selectionActions = computed(() => [
|
||
{
|
||
label: 'Șterge',
|
||
icon: 'pi pi-trash',
|
||
severity: 'danger',
|
||
handler: deleteSelected
|
||
},
|
||
{
|
||
label: 'Export',
|
||
icon: 'pi pi-download',
|
||
severity: 'secondary',
|
||
handler: exportSelected
|
||
}
|
||
])
|
||
|
||
const deleteSelected = () => {
|
||
// Delete logic
|
||
selectedItems.value = []
|
||
}
|
||
|
||
const exportSelected = () => {
|
||
// Export logic
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### Single Action
|
||
|
||
```vue
|
||
<MobileSelectionFooter
|
||
:visible="hasSelection"
|
||
:actions="[
|
||
{ label: 'Confirmă Selectate', icon: 'pi pi-check', severity: 'success', handler: confirmAll }
|
||
]"
|
||
/>
|
||
```
|
||
|
||
### CSS Classes
|
||
|
||
| Class | Description |
|
||
|-------|-------------|
|
||
| `.mobile-selection-footer` | Base container (fixed, z-index 1030) |
|
||
| `.selection-actions` | Button container (max-width: 400px) |
|
||
| `.selection-action-btn` | Individual button (48px min-height) |
|
||
|
||
### Animation
|
||
|
||
Uses Vue `<Transition name="slide-up">`:
|
||
- Slides up from bottom when `visible` becomes `true`
|
||
- Slides down when `visible` becomes `false`
|
||
- Duration: `var(--transition-normal)` (250ms)
|
||
|
||
---
|
||
|
||
## BottomSheet
|
||
|
||
Modal-style sheet that slides up from the bottom, ideal for filters and forms.
|
||
|
||
### Props
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `modelValue` | Boolean | `false` | v-model for visibility |
|
||
| `closeOnOverlay` | Boolean | `true` | Close when tapping overlay |
|
||
| `closeOnSwipeDown` | Boolean | `true` | Close when swiping down on handle |
|
||
|
||
### Events
|
||
|
||
| Event | Payload | Description |
|
||
|-------|---------|-------------|
|
||
| `update:modelValue` | `boolean` | v-model update |
|
||
|
||
### Slots
|
||
|
||
| Slot | Description |
|
||
|------|-------------|
|
||
| `default` | Content inside the sheet |
|
||
|
||
### Basic Usage
|
||
|
||
```vue
|
||
<template>
|
||
<Button label="Open Filters" @click="showFilters = true" />
|
||
|
||
<BottomSheet v-model="showFilters">
|
||
<h3>Filtre</h3>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Status</label>
|
||
<Dropdown v-model="filters.status" :options="statusOptions" />
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Data</label>
|
||
<Calendar v-model="filters.date" />
|
||
</div>
|
||
|
||
<div class="flex gap-sm mt-md">
|
||
<Button label="Resetează" severity="secondary" @click="resetFilters" />
|
||
<Button label="Aplică" @click="applyFilters" />
|
||
</div>
|
||
</BottomSheet>
|
||
</template>
|
||
|
||
<script setup>
|
||
const showFilters = ref(false)
|
||
|
||
const applyFilters = () => {
|
||
// Apply filter logic
|
||
showFilters.value = false
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### Prevent Close on Overlay
|
||
|
||
```vue
|
||
<BottomSheet
|
||
v-model="showImportantForm"
|
||
:closeOnOverlay="false"
|
||
>
|
||
<!-- User must explicitly close this -->
|
||
</BottomSheet>
|
||
```
|
||
|
||
### CSS Classes
|
||
|
||
| Class | Description |
|
||
|-------|-------------|
|
||
| `.bottom-sheet-overlay` | Full-screen overlay (50% opacity black) |
|
||
| `.bottom-sheet` | Sheet container (max-height: 90vh) |
|
||
| `.bottom-sheet-handle` | Handle area at top (32px min-height) |
|
||
| `.handle-bar` | Visual drag indicator (40px × 4px) |
|
||
| `.bottom-sheet-content` | Scrollable content area |
|
||
|
||
### Touch Gestures
|
||
|
||
| Gesture | Action |
|
||
|---------|--------|
|
||
| Tap overlay | Close (if `closeOnOverlay: true`) |
|
||
| Tap handle | Close |
|
||
| Swipe down > 100px | Close (if `closeOnSwipeDown: true`) |
|
||
|
||
---
|
||
|
||
## SwipeableCards
|
||
|
||
Touch-swipeable carousel for KPI cards with dots indicator.
|
||
|
||
### Props
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `totalCards` | Number | **required** | Number of cards in carousel |
|
||
| `showDots` | Boolean | `true` | Show navigation dots |
|
||
| `autoPlay` | Boolean | `false` | Auto-advance cards |
|
||
| `autoPlayInterval` | Number | `5000` | Auto-play interval (ms) |
|
||
|
||
### Events
|
||
|
||
| Event | Payload | Description |
|
||
|-------|---------|-------------|
|
||
| `update:currentIndex` | `number` | Current card index changed |
|
||
|
||
### Slots
|
||
|
||
Named slots for each card: `card-0`, `card-1`, `card-2`, etc.
|
||
|
||
### Exposed Methods
|
||
|
||
| Method | Description |
|
||
|--------|-------------|
|
||
| `goToCard(index)` | Navigate to specific card |
|
||
| `nextCard()` | Go to next card |
|
||
| `prevCard()` | Go to previous card |
|
||
| `currentIndex` | Current card index (ref) |
|
||
|
||
### Basic Usage
|
||
|
||
```vue
|
||
<template>
|
||
<SwipeableCards :totalCards="3">
|
||
<template #card-0>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value">$125,430</div>
|
||
<div class="kpi-label">Total Sales</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #card-1>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value">1,234</div>
|
||
<div class="kpi-label">Orders</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #card-2>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value">98.5%</div>
|
||
<div class="kpi-label">Success Rate</div>
|
||
</div>
|
||
</template>
|
||
</SwipeableCards>
|
||
</template>
|
||
```
|
||
|
||
### With Auto-Play
|
||
|
||
```vue
|
||
<SwipeableCards
|
||
:totalCards="4"
|
||
:autoPlay="true"
|
||
:autoPlayInterval="3000"
|
||
>
|
||
<!-- Cards -->
|
||
</SwipeableCards>
|
||
```
|
||
|
||
### Programmatic Navigation
|
||
|
||
```vue
|
||
<template>
|
||
<SwipeableCards ref="carousel" :totalCards="3">
|
||
<!-- Cards -->
|
||
</SwipeableCards>
|
||
|
||
<Button label="Next" @click="carousel.nextCard()" />
|
||
</template>
|
||
|
||
<script setup>
|
||
const carousel = ref(null)
|
||
</script>
|
||
```
|
||
|
||
### CSS Classes
|
||
|
||
| Class | Description |
|
||
|-------|-------------|
|
||
| `.swipeable-cards-container` | Base container with overflow hidden |
|
||
| `.cards-track` | Horizontal flex container (will-change: transform) |
|
||
| `.card-slide` | Individual card (flex: 0 0 100%) |
|
||
| `.dots-indicator` | Navigation dots container |
|
||
| `.dot` | Individual dot (8px, expands to 24px when active) |
|
||
| `.dot.active` | Active dot (primary color) |
|
||
|
||
### Touch Thresholds
|
||
|
||
| Threshold | Value | Description |
|
||
|-----------|-------|-------------|
|
||
| Swipe distance | 50px | Minimum swipe to change card |
|
||
| Velocity | 0.3 | Quick swipe detection |
|
||
| Angle | 30° | Max angle for horizontal detection |
|
||
|
||
---
|
||
|
||
## Design Tokens for Mobile
|
||
|
||
### Layout Tokens
|
||
|
||
| Token | Value | Use |
|
||
|-------|-------|-----|
|
||
| `--header-height` | 56px | MobileTopBar & MobileBottomNav height |
|
||
| `--z-fixed` | 1030 | Fixed elements z-index |
|
||
| `--z-modal-backdrop` | 1040 | BottomSheet overlay |
|
||
| `--z-modal` | 1050 | BottomSheet content |
|
||
|
||
### Touch Target Sizes
|
||
|
||
```css
|
||
/* Minimum touch target: 48x48px */
|
||
.touch-target {
|
||
min-width: 48px;
|
||
min-height: 48px;
|
||
}
|
||
|
||
/* Button in top bar */
|
||
.top-bar-btn {
|
||
width: 48px;
|
||
height: 48px;
|
||
}
|
||
```
|
||
|
||
### Spacing for Mobile
|
||
|
||
| Token | Value | Mobile Use |
|
||
|-------|-------|------------|
|
||
| `var(--space-xs)` | 4px | Icon gaps, tight spacing |
|
||
| `var(--space-sm)` | 8px | Between nav items, card gaps |
|
||
| `var(--space-md)` | 16px | Card padding, content margins |
|
||
| `var(--space-lg)` | 24px | Section spacing |
|
||
|
||
### Mobile Content Padding
|
||
|
||
```css
|
||
/* Standard mobile content area */
|
||
.mobile-content {
|
||
padding-top: 56px; /* MobileTopBar */
|
||
padding-bottom: 56px; /* MobileBottomNav */
|
||
padding-left: var(--space-md);
|
||
padding-right: var(--space-md);
|
||
}
|
||
|
||
/* When selection footer is visible */
|
||
.mobile-content-with-selection {
|
||
padding-bottom: 80px; /* Higher for selection footer */
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Best Practices
|
||
|
||
### 1. Always Use Design Tokens
|
||
|
||
```css
|
||
/* ✅ CORRECT */
|
||
.mobile-card {
|
||
padding: var(--space-md);
|
||
border-radius: var(--radius-md);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
/* ❌ WRONG */
|
||
.mobile-card {
|
||
padding: 16px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||
}
|
||
```
|
||
|
||
### 2. Account for Fixed Headers/Footers
|
||
|
||
```vue
|
||
<template>
|
||
<div class="view-container">
|
||
<MobileTopBar title="My View" />
|
||
|
||
<main class="view-content">
|
||
<!-- Content that scrolls -->
|
||
</main>
|
||
|
||
<MobileBottomNav />
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.view-content {
|
||
padding-top: 56px; /* TopBar height */
|
||
padding-bottom: 56px; /* BottomNav height */
|
||
min-height: 100vh;
|
||
overflow-y: auto;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### 3. Test Both Light and Dark Mode
|
||
|
||
All mobile components support:
|
||
- Manual dark mode: `[data-theme="dark"]`
|
||
- System preference: `@media (prefers-color-scheme: dark)`
|
||
|
||
```javascript
|
||
// Toggle theme in DevTools:
|
||
document.documentElement.setAttribute('data-theme', 'dark')
|
||
```
|
||
|
||
### 4. Handle Safe Areas (Notch/Home Indicator)
|
||
|
||
```css
|
||
.mobile-bottom-nav {
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.mobile-content {
|
||
padding-bottom: calc(56px + env(safe-area-inset-bottom));
|
||
}
|
||
```
|
||
|
||
### 5. Selection Mode Pattern
|
||
|
||
```vue
|
||
<script setup>
|
||
const selectedItems = ref([])
|
||
const isSelectionMode = computed(() => selectedItems.value.length > 0)
|
||
|
||
const toggleSelection = (item) => {
|
||
const index = selectedItems.value.findIndex(i => i.id === item.id)
|
||
if (index === -1) {
|
||
selectedItems.value.push(item)
|
||
} else {
|
||
selectedItems.value.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
const clearSelection = () => {
|
||
selectedItems.value = []
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 6. Combine Components for Complex Views
|
||
|
||
```vue
|
||
<template>
|
||
<!-- Top Bar adapts to selection mode -->
|
||
<MobileTopBar
|
||
:title="isSelectionMode ? `${selectedCount} selectate` : 'Bonuri'"
|
||
:showBack="isSelectionMode"
|
||
:selectionActive="isSelectionMode"
|
||
:actions="currentActions"
|
||
@back-click="clearSelection"
|
||
@action-click="handleAction"
|
||
/>
|
||
|
||
<!-- Main Content -->
|
||
<main class="content">
|
||
<SwipeableCards :totalCards="kpis.length">
|
||
<template v-for="(kpi, i) in kpis" #[`card-${i}`]>
|
||
<KPICard :data="kpi" />
|
||
</template>
|
||
</SwipeableCards>
|
||
|
||
<ItemList :items="items" @select="toggleSelection" />
|
||
</main>
|
||
|
||
<!-- Bottom Nav (hidden in selection mode) -->
|
||
<MobileBottomNav v-if="!isSelectionMode" />
|
||
|
||
<!-- Selection Footer (shown in selection mode) -->
|
||
<MobileSelectionFooter
|
||
:visible="isSelectionMode"
|
||
:actions="selectionActions"
|
||
/>
|
||
|
||
<!-- Filter Sheet -->
|
||
<BottomSheet v-model="showFilters">
|
||
<FilterForm @apply="applyFilters" />
|
||
</BottomSheet>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
### Top/Bottom bars not fixed
|
||
|
||
**Problem**: Bars scroll with content.
|
||
|
||
**Solution**: Ensure `.mobile-top-bar` and `.mobile-bottom-nav` have `position: fixed`. Check for `overflow: hidden` on ancestors.
|
||
|
||
### Content hidden behind bars
|
||
|
||
**Problem**: Content is cut off at top/bottom.
|
||
|
||
**Solution**: Add padding to content area:
|
||
```css
|
||
.content {
|
||
padding-top: 56px;
|
||
padding-bottom: 56px;
|
||
}
|
||
```
|
||
|
||
### BottomSheet z-index issues
|
||
|
||
**Problem**: BottomSheet appears behind other elements.
|
||
|
||
**Solution**: BottomSheet uses `<Teleport to="body">` to avoid stacking context issues. Check for `z-index` wars in your CSS.
|
||
|
||
### Swipe conflicts with scrolling
|
||
|
||
**Problem**: SwipeableCards interferes with vertical scroll.
|
||
|
||
**Solution**: Component uses angle detection (30° threshold). Ensure cards have sufficient content area. Check `touch-action: pan-y` is set.
|
||
|
||
### Dark mode not working
|
||
|
||
**Problem**: Components don't respond to theme changes.
|
||
|
||
**Solution**: All components support both mechanisms:
|
||
1. Manual: `[data-theme="dark"]` on `<html>`
|
||
2. System: `@media (prefers-color-scheme: dark)`
|
||
|
||
Check that `data-theme` attribute is set correctly.
|
||
|
||
### Selection footer overlaps content
|
||
|
||
**Problem**: When selection footer appears, it covers list items.
|
||
|
||
**Solution**: Use dynamic padding:
|
||
```css
|
||
.content {
|
||
padding-bottom: v-bind(isSelectionMode ? '96px' : '56px');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Related Documentation
|
||
|
||
- [Design Tokens](./DESIGN_TOKENS.md) - Color, spacing, typography tokens
|
||
- [CSS Patterns](./CSS_PATTERNS.md) - General UI patterns
|
||
- [Onboarding CSS](./ONBOARDING_CSS.md) - Quick start for CSS
|
||
|
||
---
|
||
|
||
## Changelog
|
||
|
||
### Version 3.0.0 (2026-01-12) - Phase 3: Mobile Navigation Overhaul
|
||
|
||
- **NEW**: Added `Mobile Tab Pattern` section for Clienți/Furnizori switching (US-304, US-305)
|
||
- **NEW**: Added `FAB Pattern` section for contextual actions (US-303)
|
||
- **UPDATED**: `MobileBottomNav` - 4 direct links: Dashboard, Bonuri, Facturi, Setări (US-307)
|
||
- **UPDATED**: `MobileDrawerMenu` - Grouped categories: PRINCIPALE, RAPOARTE, ANALIZE, ADMINISTRARE (US-308)
|
||
- **UPDATED**: Dashboard mobile layout - KPIs only, quick-links removed (US-309)
|
||
- **UPDATED**: ASCII diagrams with new navigation structure and FAB/Tab patterns
|
||
- **UPDATED**: Navigation Architecture with Phase 3 route map
|
||
- **REMOVED**: Upload button from footer nav (moved to FAB on Bonuri page)
|
||
|
||
### Version 2.0.0 (2026-01-12)
|
||
|
||
- **NEW**: Added `MobileDrawerMenu` component documentation
|
||
- **NEW**: Added `MobileActionBar` component documentation
|
||
- **NEW**: Added Navigation Architecture section with route maps
|
||
- **UPDATED**: MobileBottomNav default items now point to Settings Hub (`/settings`)
|
||
- **UPDATED**: ASCII diagrams with new routes and component layouts
|
||
- **UPDATED**: Quick Start section to include drawer menu integration
|
||
|
||
### Version 1.0.0 (2026-01-11)
|
||
|
||
- Initial release with MobileTopBar, MobileBottomNav, MobileSelectionFooter, BottomSheet, SwipeableCards
|
||
|
||
---
|
||
|
||
**Last Updated:** 2026-01-12
|
||
**Version:** 3.0.0
|
||
**Maintained By:** Frontend Team
|