feat(mobile-navigation-improvements): Complete US-210 - Creare MobileActionBar Component

Implemented by Ralph autonomous loop.
Iteration: 11

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-12 12:38:10 +00:00
parent 4c5d2956d8
commit 8cb806e45f
3 changed files with 229 additions and 2 deletions

View File

@@ -0,0 +1,221 @@
<template>
<Transition name="slide-up">
<div v-if="visible" class="mobile-action-bar">
<div class="action-bar-content" :class="layoutClass">
<Button
v-for="(action, index) in actions"
:key="index"
:label="action.label"
:icon="action.icon"
:severity="action.severity || 'primary'"
:disabled="action.disabled"
class="action-bar-btn"
@click="handleAction(action)"
/>
</div>
</div>
</Transition>
</template>
<script setup>
import { computed } from 'vue'
import Button from 'primevue/button'
/**
* MobileActionBar - Material Design 3 inspired bottom action bar for mobile views
*
* A reusable component for displaying context-aware action buttons on mobile.
* Positioned fixed at the bottom, above MobileBottomNav (56px offset).
*
* Props:
* - visible: Whether the action bar is visible (controls slide-up animation)
* - actions: Array of action buttons to display
* Each action: { label: string, icon: string, severity?: string, handler: Function, disabled?: boolean }
*
* Layout behavior:
* - Single action: Full-width button
* - Two actions: Side-by-side buttons (50% each)
* - Three or more: Side-by-side with equal distribution
*
* Usage example:
* <MobileActionBar
* :visible="isMobile"
* :actions="[
* { label: 'Salvează', icon: 'pi pi-save', severity: 'primary', handler: handleSave },
* { label: 'Anulează', icon: 'pi pi-times', severity: 'secondary', handler: handleCancel }
* ]"
* />
*/
const props = defineProps({
/**
* Controls visibility of the action bar (triggers slide-up animation)
*/
visible: {
type: Boolean,
default: false
},
/**
* Array of action buttons to display
* Each action should have:
* - label: string - Button text
* - icon: string - PrimeIcons class (e.g., 'pi pi-save')
* - severity?: string - PrimeVue button severity ('primary', 'secondary', 'danger', etc.)
* - handler: Function - Click handler
* - disabled?: boolean - Disable the button
*/
actions: {
type: Array,
default: () => [],
validator: (actions) => {
return Array.isArray(actions) && actions.every(
action => typeof action.label === 'string' &&
typeof action.icon === 'string' &&
typeof action.handler === 'function'
)
}
}
})
/**
* Computed class for layout based on number of actions
* - single: Full-width for 1 button
* - dual: Side-by-side for 2 buttons
* - multi: Equal distribution for 3+ buttons
*/
const layoutClass = computed(() => {
const count = props.actions.length
if (count === 1) return 'layout-single'
if (count === 2) return 'layout-dual'
return 'layout-multi'
})
/**
* Handle action button click
* Calls the action's handler function if defined and not disabled
*/
const handleAction = (action) => {
if (action.disabled) return
if (action.handler && typeof action.handler === 'function') {
action.handler()
}
}
</script>
<style scoped>
/* ================================================
MobileActionBar Component Styles
Material Design 3 inspired bottom action bar
Position: fixed above MobileBottomNav (56px offset)
================================================ */
.mobile-action-bar {
position: fixed;
bottom: 56px; /* Above MobileBottomNav */
left: 0;
right: 0;
background: var(--surface-card);
border-top: 1px solid var(--surface-border);
padding: var(--space-sm) var(--space-md);
z-index: var(--z-fixed);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
/* Action bar content container */
.action-bar-content {
display: flex;
gap: var(--space-sm);
width: 100%;
max-width: 500px;
margin: 0 auto;
}
/* ================================================
Layout Variants
================================================ */
/* Single button: Full width */
.layout-single .action-bar-btn {
flex: 1;
width: 100%;
}
/* Two buttons: Side by side, equal width */
.layout-dual .action-bar-btn {
flex: 1;
min-width: 0; /* Allow shrinking */
}
/* Three or more buttons: Equal distribution */
.layout-multi .action-bar-btn {
flex: 1;
min-width: 0;
}
/* ================================================
Button Styles
================================================ */
.action-bar-btn {
height: 48px;
font-size: var(--text-base);
font-weight: var(--font-semibold);
/* Ensure minimum touch target size */
min-height: 48px;
/* Allow text to wrap if needed */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ================================================
Slide-up Animation (Vue Transition)
================================================ */
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform var(--transition-normal), opacity var(--transition-normal);
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
opacity: 0;
}
.slide-up-enter-to,
.slide-up-leave-from {
transform: translateY(0);
opacity: 1;
}
/* ================================================
Safe Area Support (iPhone X+ notch)
================================================ */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.mobile-action-bar {
bottom: calc(56px + env(safe-area-inset-bottom));
}
}
/* ================================================
Dark Mode Support
================================================ */
/* Manual dark mode via data-theme attribute */
[data-theme="dark"] .mobile-action-bar {
background: var(--surface-card);
border-top-color: var(--surface-border);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
}
/* Auto dark mode (when no manual theme is set) */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .mobile-action-bar {
background: var(--surface-card);
border-top-color: var(--surface-border);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
}
}
</style>