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:
@@ -199,8 +199,8 @@
|
|||||||
"Animație: slide-up la mount",
|
"Animație: slide-up la mount",
|
||||||
"npm run build passes"
|
"npm run build passes"
|
||||||
],
|
],
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed in iteration 11"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-209",
|
"id": "US-209",
|
||||||
|
|||||||
@@ -64,3 +64,9 @@ User Stories: 14 (US-201 to US-214)
|
|||||||
[2026-01-12 12:28:05] Working on story: US-213
|
[2026-01-12 12:28:05] Working on story: US-213
|
||||||
[2026-01-12 12:28:05] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_10_US-213.log)
|
[2026-01-12 12:28:05] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_10_US-213.log)
|
||||||
[2026-01-12 12:36:45] SUCCESS: Story US-213 passed!
|
[2026-01-12 12:36:45] SUCCESS: Story US-213 passed!
|
||||||
|
[2026-01-12 12:36:45] Changes committed
|
||||||
|
[2026-01-12 12:36:45] Progress: 10/14 stories completed
|
||||||
|
[2026-01-12 12:36:47] === Iteration 11/100 ===
|
||||||
|
[2026-01-12 12:36:47] Working on story: US-210
|
||||||
|
[2026-01-12 12:36:47] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_11_US-210.log)
|
||||||
|
[2026-01-12 12:38:10] SUCCESS: Story US-210 passed!
|
||||||
|
|||||||
221
src/shared/components/mobile/MobileActionBar.vue
Normal file
221
src/shared/components/mobile/MobileActionBar.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user