Implemented by Ralph autonomous loop. Iteration: 14 Co-Authored-By: Claude <noreply@anthropic.com>
42 KiB
ROA2WEB Mobile Patterns Library
Version: 2.0.0 Last Updated: 2026-01-12 Status: ✅ Complete
Table of Contents
- Quick Start
- Mobile Layout Overview
- Navigation Architecture
- MobileTopBar
- MobileBottomNav
- MobileDrawerMenu (NEW)
- MobileActionBar (NEW)
- MobileSelectionFooter
- BottomSheet
- SwipeableCards
- Design Tokens for Mobile
- Best Practices
- Troubleshooting
Quick Start
For New Developers
Get a mobile view running in 5 minutes:
<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/:
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
┌─────────────────────────────────────────┐
│ MobileTopBar (56px) │
│ [≡] Page Title [🔍] [⋮] │
├─────────────────────────────────────────┤
│ │
│ │
│ MAIN CONTENT │
│ │
│ (scrollable area) │
│ │
│ padding-top: 56px │
│ padding-bottom: 56px │
│ │
│ │
├─────────────────────────────────────────┤
│ MobileBottomNav (56px) │
│ 📋 ☁️ 📊 ⚙️ │
│ Bonuri Upload Rapoarte Setări │
└─────────────────────────────────────────┘
ASCII Diagram: Drawer Menu Open
┌──────────────────┬──────────────────────┐
│ MobileDrawer │ ░░░░░░░░░░░░░░░░░░░░ │
│ Menu (280px) │ ░░░░░░░░░░░░░░░░░░░░ │
├──────────────────┤ ░░░ OVERLAY ░░░░░░░░ │
│ 🏢 ROA2WEB │ ░░ (tap to close) ░░ │
├──────────────────┤ ░░░░░░░░░░░░░░░░░░░░ │
│ 🏠 Dashboard │ ░░░░░░░░░░░░░░░░░░░░ │
│ 📋 Bonuri │ ░░░░░░░░░░░░░░░░░░░░ │
│ 📄 Facturi │ ░░░░░░░░░░░░░░░░░░░░ │
│ 🔢 Balanță │ ░░░░░░░░░░░░░░░░░░░░ │
│ 💰 Trezorerie │ ░░░░░░░░░░░░░░░░░░░░ │
│ ⚙️ Setări │ ░░░░░░░░░░░░░░░░░░░░ │
├──────────────────┤ ░░░░░░░░░░░░░░░░░░░░ │
│ 👤 Username │ ░░░░░░░░░░░░░░░░░░░░ │
│ 🚪 Deconectare │ ░░░░░░░░░░░░░░░░░░░░ │
└──────────────────┴──────────────────────┘
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)
└─────────────────────────────────────────┘
Navigation Architecture
Route Map
┌─────────────────────────────────────┐
│ / (root) │
│ Redirects to /reports/dashboard │
└───────────────┬─────────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────────┐ ┌─────────────────┐
│ /data-entry │ │ /reports │ │ /settings │
│ │ │ │ │ │
│ 📋 Bonuri List │ │ 📊 dashboard │ │ ⚙️ Settings Hub │
│ 📝 Bon Create │ │ 📄 invoices │ │ (centralized │
│ ✏️ Bon Edit │ │ 💰 bank-cash │ │ settings) │
│ 📊 OCR Metrics │ │ 🔢 trial-balance │ │ │
│ │ │ 📉 maturity-analysis │ │ Links to: │
│ │ │ 📋 detailed-invoices │ │ - OCR Settings │
│ │ │ 📈 cache-stats │ │ - Cache Stats │
│ │ │ 📝 server-logs │ │ - Server Logs │
│ │ │ 📱 telegram │ │ - Telegram │
└───────────────────┘ └───────────────────────┘ └─────────────────┘
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 (Updated)
// Default navigation items
[
{ 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
]
MobileDrawerMenu Navigation Links
// Drawer menu navigation items
[
{ to: '/reports/dashboard', icon: 'pi pi-home', label: 'Dashboard' },
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri' },
{ to: '/reports/invoices', icon: 'pi pi-file', label: 'Facturi' },
{ to: '/reports/trial-balance', icon: 'pi pi-calculator', label: 'Balanță' },
{ to: '/reports/bank-cash', icon: 'pi pi-money-bill', label: 'Trezorerie' },
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări' }
]
Dashboard Mobile Quick Links
On mobile, Dashboard shows KPI cards plus quick-link cards:
┌─────────────────────────────────────────┐
│ MobileTopBar: Dashboard │
├─────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐ │
│ │ SwipeableCards (KPIs) │ │
│ │ ← Swipe → │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 📉 Analiză │ │ 📋 Facturi │ │
│ │ Scadențe → │ │ Detaliate → │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ (Quick links to dedicated pages) │
│ │
├─────────────────────────────────────────┤
│ MobileBottomNav │
└─────────────────────────────────────────┘
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
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
<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
<MobileTopBar
title="Detalii Bon"
:showBack="true"
@back-click="router.back()"
/>
Selection Mode
<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
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
[
{ 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
<template>
<MobileBottomNav />
</template>
Custom Items
<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
Material Design 3 inspired navigation drawer that slides in from the left. Replaces the desktop sidebar on mobile devices.
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 (Built-in)
The drawer contains the following navigation links:
[
{ to: '/reports/dashboard', icon: 'pi pi-home', label: 'Dashboard' },
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri' },
{ to: '/reports/invoices', icon: 'pi pi-file', label: 'Facturi' },
{ to: '/reports/trial-balance', icon: 'pi pi-calculator', label: 'Balanță' },
{ to: '/reports/bank-cash', icon: 'pi pi-money-bill', label: 'Trezorerie' },
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări' }
]
Basic Usage
<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
<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) |
.drawer-menu |
Main drawer container (280px, max 85vw) |
.drawer-header |
Header with logo |
.drawer-section |
Navigation links container |
.drawer-link |
Individual navigation link (48px min-height) |
.drawer-link.active |
Active route styling (blue background) |
.drawer-profile |
Profile section at bottom |
.logout-link |
Red logout button styling |
Animation
Uses Vue <Transition name="drawer">:
- Overlay fades in/out
- Drawer slides from left (
translateX(-100%)→translateX(0)) - Duration:
var(--transition-normal)(250ms)
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
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
<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)
<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:
<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
.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:
.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
interface SelectionAction {
label: string // Button text
icon: string // PrimeIcons class
severity?: string // PrimeVue severity ('secondary', 'danger', etc.)
handler: () => void // Click handler function
}
Basic Usage
<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
<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
visiblebecomestrue - Slides down when
visiblebecomesfalse - 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
<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
<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
<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
<SwipeableCards
:totalCards="4"
:autoPlay="true"
:autoPlayInterval="3000"
>
<!-- Cards -->
</SwipeableCards>
Programmatic Navigation
<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
/* 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
/* 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
/* ✅ 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
<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)
// Toggle theme in DevTools:
document.documentElement.setAttribute('data-theme', 'dark')
4. Handle Safe Areas (Notch/Home Indicator)
.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
<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
<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:
.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:
- Manual:
[data-theme="dark"]on<html> - 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:
.content {
padding-bottom: v-bind(isSelectionMode ? '96px' : '56px');
}
Related Documentation
- Design Tokens - Color, spacing, typography tokens
- CSS Patterns - General UI patterns
- Onboarding CSS - Quick start for CSS
Changelog
Version 2.0.0 (2026-01-12)
- NEW: Added
MobileDrawerMenucomponent documentation - NEW: Added
MobileActionBarcomponent 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: 2.0.0 Maintained By: Frontend Team