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

24 KiB
Raw Blame History

ROA2WEB Mobile Patterns Library

Version: 1.0.0 Last Updated: 2026-01-12 Status: Complete


Table of Contents

  1. Quick Start
  2. Mobile Layout Overview
  3. MobileTopBar
  4. MobileBottomNav
  5. MobileSelectionFooter
  6. BottomSheet
  7. SwipeableCards
  8. Design Tokens for Mobile
  9. Best Practices
  10. 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="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/:

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

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: '/data-entry/ocr-metrics', icon: 'pi pi-cog', label: 'Setări' }
]

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

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 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

<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:

  1. Manual: [data-theme="dark"] on <html>
  2. System: @media (prefers-color-scheme: dark)

Check that data-theme attribute is set correctly.

Problem: When selection footer appears, it covers list items.

Solution: Use dynamic padding:

.content {
  padding-bottom: v-bind(isSelectionMode ? '96px' : '56px');
}


Last Updated: 2026-01-12 Version: 1.0.0 Maintained By: Frontend Team