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>
|
<template>
|
||||||
<div>
|
<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
|
<button
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab.key"
|
: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">
|
<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">
|
<div class="text-sm breadcrumbs py-0">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(crumb, i) in crumbs" :key="i">
|
<!-- First crumb (always visible) -->
|
||||||
<NuxtLink
|
<li>
|
||||||
v-if="i < crumbs.length - 1"
|
<NuxtLink :to="crumbs[0].path" class="text-base-content/60 hover:text-primary transition-colors">
|
||||||
:to="crumb.path"
|
{{ crumbs[0].label }}
|
||||||
class="text-base-content/60 hover:text-primary transition-colors"
|
</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 }}
|
{{ crumb.label }}
|
||||||
</NuxtLink>
|
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -144,9 +144,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.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 { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import {
|
import {
|
||||||
formatSize,
|
formatSize,
|
||||||
@@ -154,6 +151,9 @@ import {
|
|||||||
downloadDocument,
|
downloadDocument,
|
||||||
} from '~/shared/utils/documentDisplayUtils'
|
} from '~/shared/utils/documentDisplayUtils'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const machineId = computed(() => route.params.id as string | undefined)
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
products: Array<{
|
products: Array<{
|
||||||
id?: string | null
|
id?: string | null
|
||||||
|
|||||||
@@ -68,34 +68,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-else-if="filteredSites.length === 0" class="text-center py-16">
|
<CommonEmptyState
|
||||||
<div class="max-w-sm mx-auto">
|
v-else-if="filteredSites.length === 0"
|
||||||
<div class="w-16 h-16 rounded-2xl bg-base-200 grid place-items-center mx-auto mb-5">
|
:icon="IconLucideFactory"
|
||||||
<IconLucideFactory
|
title="Aucune machine trouvée"
|
||||||
class="w-8 h-8 text-base-content/30"
|
description="Commencez par ajouter des sites et des machines."
|
||||||
aria-hidden="true"
|
class="py-16"
|
||||||
/>
|
>
|
||||||
</div>
|
<div v-if="canEdit" class="flex gap-2 justify-center">
|
||||||
<h3 class="text-lg font-semibold text-base-content mb-1">
|
<button class="btn btn-primary btn-sm" @click="showAddSiteModal = true">
|
||||||
Aucune machine trouvée
|
Ajouter un site
|
||||||
</h3>
|
</button>
|
||||||
<p class="text-sm text-base-content/50 mb-6">
|
<button class="btn btn-ghost btn-sm" @click="showAddMachineModal = true">
|
||||||
Commencez par ajouter des sites et des machines.
|
Ajouter une machine
|
||||||
</p>
|
</button>
|
||||||
<div class="flex gap-2 justify-center">
|
|
||||||
<button v-if="canEdit" 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"
|
|
||||||
>
|
|
||||||
Ajouter une machine
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CommonEmptyState>
|
||||||
|
|
||||||
<!-- Sites List -->
|
<!-- Sites List -->
|
||||||
<div v-else class="space-y-5">
|
<div v-else class="space-y-5">
|
||||||
|
|||||||
@@ -173,18 +173,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
<div v-else class="text-center py-12">
|
<CommonEmptyState
|
||||||
<div class="max-w-md mx-auto">
|
v-else
|
||||||
<div class="w-16 h-16 rounded-2xl bg-base-200 grid place-items-center mx-auto mb-5">
|
:icon="IconLucideAlertTriangle"
|
||||||
<IconLucideAlertTriangle class="w-8 h-8 text-base-content/30" aria-hidden="true" />
|
title="Machine non trouvée"
|
||||||
</div>
|
:description="`La machine avec l'ID « ${machineId} » n'existe pas ou a été supprimée.`"
|
||||||
<h3 class="text-lg font-semibold text-base-content mb-1">Machine non trouvée</h3>
|
action-label="Retour aux machines"
|
||||||
<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>
|
@action="$router.back()"
|
||||||
<button type="button" class="btn btn-primary" @click="$router.back()">
|
/>
|
||||||
Retour aux machines
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<MachinePrintSelectionModal
|
<MachinePrintSelectionModal
|
||||||
|
|||||||
@@ -53,20 +53,14 @@
|
|||||||
<span class="loading loading-spinner loading-lg" />
|
<span class="loading loading-spinner loading-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="filteredMachines.length === 0" class="text-center py-12">
|
<CommonEmptyState
|
||||||
<div class="max-w-md mx-auto">
|
v-else-if="filteredMachines.length === 0"
|
||||||
<IconLucideFactory class="w-16 h-16 mx-auto text-gray-400 mb-4" aria-hidden="true" />
|
:icon="IconLucideFactory"
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
title="Aucune machine trouvée"
|
||||||
Aucune machine trouvée
|
description="Commencez par ajouter votre première machine."
|
||||||
</h3>
|
action-label="Ajouter une machine"
|
||||||
<p class="text-gray-500 mb-4">
|
action-to="/machines/new"
|
||||||
Commencez par ajouter votre première machine.
|
/>
|
||||||
</p>
|
|
||||||
<NuxtLink to="/machines/new" class="btn btn-primary">
|
|
||||||
Ajouter une machine
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user