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>
This commit is contained in:
905
docs/MOBILE_PATTERNS.md
Normal file
905
docs/MOBILE_PATTERNS.md
Normal file
@@ -0,0 +1,905 @@
|
|||||||
|
# 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
|
||||||
@@ -274,8 +274,8 @@
|
|||||||
"Secțiune Quick Start pentru devs noi",
|
"Secțiune Quick Start pentru devs noi",
|
||||||
"Diagrame ASCII pentru layout mobile"
|
"Diagrame ASCII pentru layout mobile"
|
||||||
],
|
],
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed in iteration 3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-115",
|
"id": "US-115",
|
||||||
|
|||||||
@@ -538,3 +538,9 @@ Mon Jan 12 09:44:54 AM UTC 2026
|
|||||||
[2026-01-12 11:10:10] Working on story: US-113
|
[2026-01-12 11:10:10] Working on story: US-113
|
||||||
[2026-01-12 11:10:10] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_2_US-113.log)
|
[2026-01-12 11:10:10] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_2_US-113.log)
|
||||||
[2026-01-12 11:12:51] SUCCESS: Story US-113 passed!
|
[2026-01-12 11:12:51] SUCCESS: Story US-113 passed!
|
||||||
|
[2026-01-12 11:12:51] Changes committed
|
||||||
|
[2026-01-12 11:12:51] Progress: 15/20 stories completed
|
||||||
|
[2026-01-12 11:12:53] === Iteration 3/50 ===
|
||||||
|
[2026-01-12 11:12:53] Working on story: US-118
|
||||||
|
[2026-01-12 11:12:53] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_3_US-118.log)
|
||||||
|
[2026-01-12 11:16:15] SUCCESS: Story US-118 passed!
|
||||||
|
|||||||
Reference in New Issue
Block a user