feat(ui) : improve mobile responsiveness — breadcrumb truncation, tabs scroll, form grids
This commit is contained in:
33
frontend/app/components/common/EmptyState.vue
Normal file
33
frontend/app/components/common/EmptyState.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="text-center py-12">
|
||||
<div v-if="icon" class="w-16 h-16 rounded-2xl bg-base-200 grid place-items-center mx-auto mb-5">
|
||||
<component :is="icon" class="w-8 h-8 text-base-content/30" aria-hidden="true" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-base-content mb-1">{{ title }}</h3>
|
||||
<p v-if="description" class="text-sm text-base-content/50 mb-6">{{ description }}</p>
|
||||
<slot>
|
||||
<NuxtLink v-if="actionTo" :to="actionTo" class="btn btn-primary btn-sm">
|
||||
{{ actionLabel }}
|
||||
</NuxtLink>
|
||||
<button v-else-if="actionLabel" type="button" class="btn btn-primary btn-sm" @click="$emit('action')">
|
||||
{{ actionLabel }}
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
icon?: Component
|
||||
actionLabel?: string
|
||||
actionTo?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
action: []
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav class="tabs tabs-bordered mb-6" role="tablist" :aria-label="ariaLabel">
|
||||
<nav class="tabs tabs-bordered mb-6 overflow-x-auto flex-nowrap" role="tablist" :aria-label="ariaLabel">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
|
||||
@@ -2,15 +2,29 @@
|
||||
<nav v-if="crumbs.length > 1" class="container mx-auto px-6 pt-4" aria-label="Fil d'Ariane">
|
||||
<div class="text-sm breadcrumbs py-0">
|
||||
<ul>
|
||||
<li v-for="(crumb, i) in crumbs" :key="i">
|
||||
<NuxtLink
|
||||
v-if="i < crumbs.length - 1"
|
||||
:to="crumb.path"
|
||||
class="text-base-content/60 hover:text-primary transition-colors"
|
||||
<!-- First crumb (always visible) -->
|
||||
<li>
|
||||
<NuxtLink :to="crumbs[0].path" class="text-base-content/60 hover:text-primary transition-colors">
|
||||
{{ crumbs[0].label }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<!-- Ellipsis on mobile when there are middle crumbs -->
|
||||
<li v-if="crumbs.length > 2" class="sm:hidden">
|
||||
<span class="text-base-content/40">…</span>
|
||||
</li>
|
||||
<!-- Middle crumbs: hidden on mobile, visible sm+ -->
|
||||
<li
|
||||
v-for="(crumb, i) in crumbs.slice(1, crumbs.length - 1)"
|
||||
:key="i"
|
||||
class="hidden sm:list-item"
|
||||
>
|
||||
<NuxtLink :to="crumb.path" class="text-base-content/60 hover:text-primary transition-colors">
|
||||
{{ crumb.label }}
|
||||
</NuxtLink>
|
||||
<span v-else class="text-base-content font-medium">{{ crumb.label }}</span>
|
||||
</li>
|
||||
<!-- Last crumb (always visible, current page) -->
|
||||
<li v-if="crumbs.length > 1">
|
||||
<span class="text-base-content font-medium">{{ crumbs[crumbs.length - 1].label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -144,9 +144,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const machineId = computed(() => route.params.id as string | undefined)
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import {
|
||||
formatSize,
|
||||
@@ -154,6 +151,9 @@ import {
|
||||
downloadDocument,
|
||||
} from '~/shared/utils/documentDisplayUtils'
|
||||
|
||||
const route = useRoute()
|
||||
const machineId = computed(() => route.params.id as string | undefined)
|
||||
|
||||
defineProps<{
|
||||
products: Array<{
|
||||
id?: string | null
|
||||
|
||||
@@ -68,34 +68,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="filteredSites.length === 0" class="text-center py-16">
|
||||
<div class="max-w-sm mx-auto">
|
||||
<div class="w-16 h-16 rounded-2xl bg-base-200 grid place-items-center mx-auto mb-5">
|
||||
<IconLucideFactory
|
||||
class="w-8 h-8 text-base-content/30"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-base-content mb-1">
|
||||
Aucune machine trouvée
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/50 mb-6">
|
||||
Commencez par ajouter des sites et des machines.
|
||||
</p>
|
||||
<div class="flex gap-2 justify-center">
|
||||
<button v-if="canEdit" class="btn btn-primary btn-sm" @click="showAddSiteModal = true">
|
||||
<CommonEmptyState
|
||||
v-else-if="filteredSites.length === 0"
|
||||
:icon="IconLucideFactory"
|
||||
title="Aucune machine trouvée"
|
||||
description="Commencez par ajouter des sites et des machines."
|
||||
class="py-16"
|
||||
>
|
||||
<div v-if="canEdit" class="flex gap-2 justify-center">
|
||||
<button class="btn btn-primary btn-sm" @click="showAddSiteModal = true">
|
||||
Ajouter un site
|
||||
</button>
|
||||
<button
|
||||
v-if="canEdit"
|
||||
class="btn btn-ghost btn-sm"
|
||||
@click="showAddMachineModal = true"
|
||||
>
|
||||
<button class="btn btn-ghost btn-sm" @click="showAddMachineModal = true">
|
||||
Ajouter une machine
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CommonEmptyState>
|
||||
|
||||
<!-- Sites List -->
|
||||
<div v-else class="space-y-5">
|
||||
|
||||
@@ -173,18 +173,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else class="text-center py-12">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="w-16 h-16 rounded-2xl bg-base-200 grid place-items-center mx-auto mb-5">
|
||||
<IconLucideAlertTriangle class="w-8 h-8 text-base-content/30" aria-hidden="true" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-base-content mb-1">Machine non trouvée</h3>
|
||||
<p class="text-sm text-base-content/50 mb-6">La machine avec l'ID "{{ machineId }}" n'existe pas ou a été supprimée.</p>
|
||||
<button type="button" class="btn btn-primary" @click="$router.back()">
|
||||
Retour aux machines
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<CommonEmptyState
|
||||
v-else
|
||||
:icon="IconLucideAlertTriangle"
|
||||
title="Machine non trouvée"
|
||||
:description="`La machine avec l'ID « ${machineId} » n'existe pas ou a été supprimée.`"
|
||||
action-label="Retour aux machines"
|
||||
@action="$router.back()"
|
||||
/>
|
||||
</main>
|
||||
|
||||
<MachinePrintSelectionModal
|
||||
|
||||
@@ -53,20 +53,14 @@
|
||||
<span class="loading loading-spinner loading-lg" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredMachines.length === 0" class="text-center py-12">
|
||||
<div class="max-w-md mx-auto">
|
||||
<IconLucideFactory class="w-16 h-16 mx-auto text-gray-400 mb-4" aria-hidden="true" />
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
||||
Aucune machine trouvée
|
||||
</h3>
|
||||
<p class="text-gray-500 mb-4">
|
||||
Commencez par ajouter votre première machine.
|
||||
</p>
|
||||
<NuxtLink to="/machines/new" class="btn btn-primary">
|
||||
Ajouter une machine
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<CommonEmptyState
|
||||
v-else-if="filteredMachines.length === 0"
|
||||
:icon="IconLucideFactory"
|
||||
title="Aucune machine trouvée"
|
||||
description="Commencez par ajouter votre première machine."
|
||||
action-label="Ajouter une machine"
|
||||
action-to="/machines/new"
|
||||
/>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user