Files
roa2web-service-auto/docs/MOBILE_PATTERNS.md
Claude Agent 8d1d1d643d feat(unified-mobile-material-design): Complete US-118 - Creare MOBILE_PATTERNS.md documentație
Implemented by Ralph autonomous loop.
Iteration: 3

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 11:16:15 +00:00

906 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ROA2WEB Mobile Patterns Library
**Version:** 1.0.0
**Last Updated:** 2026-01-12
**Status:** ✅ Complete
---
## Table of Contents
1. [Quick Start](#quick-start)
2. [Mobile Layout Overview](#mobile-layout-overview)
3. [MobileTopBar](#mobiletopbar)
4. [MobileBottomNav](#mobilebottomnav)
5. [MobileSelectionFooter](#mobileselectionfooter)
6. [BottomSheet](#bottomsheet)
7. [SwipeableCards](#swipeablecards)
8. [Design Tokens for Mobile](#design-tokens-for-mobile)
9. [Best Practices](#best-practices)
10. [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="toggleSidebar"
@action-click="handleAction"
/>
<!-- 2. Main Content (with padding for fixed bars) -->
<main class="mobile-content">
<!-- Your content here -->
</main>
<!-- 3. Bottom Navigation -->
<MobileBottomNav :items="navItems" />
</div>
</template>
<script setup>
import MobileTopBar from '@shared/components/mobile/MobileTopBar.vue'
import MobileBottomNav from '@shared/components/mobile/MobileBottomNav.vue'
const topBarActions = [
{ icon: 'pi pi-filter', label: 'Filter', tooltip: 'Filtrează' }
]
const navItems = [
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri' },
{ to: '/reports', icon: 'pi pi-chart-bar', label: 'Rapoarte' }
]
</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 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) │
│ 🏠 📋 📊 ⚙️ │
│ Home Upload Reports Settings │
└─────────────────────────────────────────┘
```
### 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: 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)
└─────────────────────────────────────────┘
```
---
## 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: '/data-entry/ocr-metrics', icon: 'pi pi-cog', label: 'Setări' }
]
```
### 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 |
---
## 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
---
**Last Updated:** 2026-01-12
**Version:** 1.0.0
**Maintained By:** Frontend Team