All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m3s
Agregar sistema completo de catación de café con las siguientes características: - Tipos TypeScript completos para sesiones, muestras, intensidades y notas - Composable useIndexedDB para gestión de sesión activa en cliente - Composable useCatacion con lógica de negocio para actualización de muestras - Componentes reutilizables: * SliderIntensidad: Slider dual para valores descriptivos (1-10) y afectivos (1-15) * SelectorFamilia: Selector jerárquico de familias de notas (3 niveles) * SelectorTazas: Selector de tazas (1-5) para uniformidad y defectos * ResumenMuestra: Header de accordion con progreso y estadísticas * FormularioMuestra: Formulario completo con 3 tabs (Fragancia/Aroma, Sabor, Impresión Global) - Páginas: * /cata: Gestión de sesiones (crear nueva o continuar existente) * /cata/sesion: Interfaz principal de catación con accordions y tabs - Tema dual: * Modo claro: Fondo blanco, texto negro, outlines azules * Modo oscuro: Fondo negro, texto verde terminal, estilo monospace - Diseño mobile-first responsive con CSS vanilla (sin @apply de Tailwind) - Configuración PWA con almacenamiento en IndexedDB
228 lines
4.5 KiB
Vue
228 lines
4.5 KiB
Vue
<template>
|
|
<div class="selector-tazas cata-fade-in">
|
|
<!-- Label -->
|
|
<label v-if="label" class="block text-sm font-medium mb-3 cata-text">
|
|
{{ label }}
|
|
<span v-if="required" class="text-error">*</span>
|
|
</label>
|
|
|
|
<!-- Grid de tazas -->
|
|
<div class="tazas-grid">
|
|
<button
|
|
v-for="numeroTaza in [1, 2, 3, 4, 5]"
|
|
:key="numeroTaza"
|
|
type="button"
|
|
:class="[
|
|
'taza-item',
|
|
'cata-checkbox',
|
|
{
|
|
'cata-checkbox-checked': esTazaSeleccionada(numeroTaza),
|
|
'disabled': disabled,
|
|
},
|
|
]"
|
|
:disabled="disabled"
|
|
@click="toggleTaza(numeroTaza)"
|
|
>
|
|
<!-- Número de taza -->
|
|
<span class="taza-numero cata-text">{{ numeroTaza }}</span>
|
|
|
|
<!-- Indicador de selección -->
|
|
<div
|
|
v-if="esTazaSeleccionada(numeroTaza)"
|
|
class="taza-check"
|
|
>
|
|
<UIcon name="i-lucide-check" class="w-4 h-4" />
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Descripción -->
|
|
<p v-if="showDescription" class="text-xs mt-2 cata-text opacity-75">
|
|
{{ tipoDescription }}
|
|
</p>
|
|
|
|
<!-- Selección actual -->
|
|
<p v-if="modelValue.length > 0" class="text-xs mt-2 cata-text">
|
|
Seleccionadas: {{ modelValue.join(', ') }}
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
interface SelectorTazasProps {
|
|
/** Tipo de selector */
|
|
tipo: 'uniformes' | 'defectuosas'
|
|
/** Tazas seleccionadas (números 1-5) */
|
|
modelValue: number[]
|
|
/** Etiqueta del selector */
|
|
label?: string
|
|
/** Deshabilitar el selector */
|
|
disabled?: boolean
|
|
/** Marcar como requerido */
|
|
required?: boolean
|
|
/** Mostrar descripción del tipo */
|
|
showDescription?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<SelectorTazasProps>(), {
|
|
disabled: false,
|
|
required: false,
|
|
showDescription: true,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: number[]]
|
|
}>()
|
|
|
|
// Descripción según el tipo
|
|
const tipoDescription = computed(() => {
|
|
return props.tipo === 'uniformes'
|
|
? 'Selecciona las tazas que NO son uniformes'
|
|
: 'Selecciona las tazas que presentan defectos'
|
|
})
|
|
|
|
// Verifica si una taza está seleccionada
|
|
const esTazaSeleccionada = (numeroTaza: number): boolean => {
|
|
return props.modelValue.includes(numeroTaza)
|
|
}
|
|
|
|
// Toggle de selección de taza
|
|
const toggleTaza = (numeroTaza: number) => {
|
|
if (props.disabled) return
|
|
|
|
const seleccionadas = [...props.modelValue]
|
|
const index = seleccionadas.indexOf(numeroTaza)
|
|
|
|
if (index > -1) {
|
|
// Deseleccionar
|
|
seleccionadas.splice(index, 1)
|
|
} else {
|
|
// Seleccionar
|
|
seleccionadas.push(numeroTaza)
|
|
}
|
|
|
|
// Ordenar las tazas seleccionadas
|
|
seleccionadas.sort((a, b) => a - b)
|
|
|
|
emit('update:modelValue', seleccionadas)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.selector-tazas {
|
|
width: 100%;
|
|
}
|
|
|
|
.tazas-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.taza-item {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 60px;
|
|
transition-property: all;
|
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
transition-duration: 150ms;
|
|
}
|
|
|
|
.taza-item:focus {
|
|
outline: 2px solid transparent;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.taza-item:focus-visible {
|
|
box-shadow: 0 0 0 2px var(--cata-primary);
|
|
}
|
|
|
|
.taza-item.disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.taza-numero {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.taza-check {
|
|
position: absolute;
|
|
top: 0.25rem;
|
|
right: 0.25rem;
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
border-radius: 9999px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: var(--cata-primary);
|
|
color: white;
|
|
}
|
|
|
|
.dark .taza-check {
|
|
color: black;
|
|
}
|
|
|
|
/* Animación al seleccionar */
|
|
.taza-item.cata-checkbox-checked {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
/* Hover effect */
|
|
.taza-item:not(.disabled):hover {
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
/* Active effect */
|
|
.taza-item:not(.disabled):active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 640px) {
|
|
.tazas-grid {
|
|
gap: 0.375rem;
|
|
}
|
|
|
|
.taza-item {
|
|
min-height: 50px;
|
|
}
|
|
|
|
.taza-numero {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.taza-check {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
}
|
|
|
|
.taza-check :deep(svg) {
|
|
width: 0.75rem;
|
|
height: 0.75rem;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 641px) {
|
|
.taza-item {
|
|
min-height: 70px;
|
|
}
|
|
|
|
.taza-numero {
|
|
font-size: 1.25rem;
|
|
}
|
|
}
|
|
|
|
/* Touch-friendly */
|
|
@media (hover: none) and (pointer: coarse) {
|
|
.taza-item {
|
|
min-height: 60px;
|
|
}
|
|
}
|
|
</style>
|