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
167 lines
4.3 KiB
Vue
167 lines
4.3 KiB
Vue
<template>
|
|
<div class="slider-intensidad cata-fade-in">
|
|
<!-- Label -->
|
|
<label
|
|
v-if="label"
|
|
:for="inputId"
|
|
class="block text-sm font-medium mb-2 cata-text"
|
|
>
|
|
{{ label }}
|
|
<span v-if="required" class="text-error">*</span>
|
|
</label>
|
|
|
|
<!-- Slider Container -->
|
|
<div class="relative">
|
|
<USlider
|
|
:id="inputId"
|
|
:model-value="modelValue ?? undefined"
|
|
:min="min"
|
|
:max="max"
|
|
:step="step"
|
|
:disabled="disabled"
|
|
:tooltip="tooltipConfig"
|
|
:ui="sliderUi"
|
|
@update:model-value="handleChange"
|
|
/>
|
|
|
|
<!-- Indicadores de rango -->
|
|
<div class="flex justify-between mt-2 text-xs cata-text opacity-60">
|
|
<span>{{ min }}</span>
|
|
<span v-if="modelValue !== null" class="font-semibold opacity-100">
|
|
{{ modelValue }}
|
|
</span>
|
|
<span>{{ max }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Descripción del tipo -->
|
|
<p v-if="showDescription" class="text-xs mt-1 cata-text opacity-75">
|
|
{{ tipoDescription }}
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { TooltipProps } from '@nuxt/ui'
|
|
|
|
interface SliderIntensidadProps {
|
|
/** Tipo de intensidad: descriptiva (1-10) o afectiva (1-15) */
|
|
tipo: 'descriptiva' | 'afectiva'
|
|
/** Valor actual del slider */
|
|
modelValue: number | null
|
|
/** Etiqueta del slider */
|
|
label?: string
|
|
/** ID para el input (auto-generado si no se provee) */
|
|
id?: string
|
|
/** Deshabilitar el slider */
|
|
disabled?: boolean
|
|
/** Marcar como requerido */
|
|
required?: boolean
|
|
/** Mostrar descripción del tipo */
|
|
showDescription?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<SliderIntensidadProps>(), {
|
|
disabled: false,
|
|
required: false,
|
|
showDescription: true,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: number | null]
|
|
}>()
|
|
|
|
// ID único para el input
|
|
const inputId = computed(() => props.id || `slider-${Math.random().toString(36).substring(2, 9)}`)
|
|
|
|
// Configuración según el tipo
|
|
const min = computed(() => 1)
|
|
const max = computed(() => props.tipo === 'descriptiva' ? 10 : 15)
|
|
const step = computed(() => 1)
|
|
|
|
// Descripción del tipo de intensidad
|
|
const tipoDescription = computed(() => {
|
|
return props.tipo === 'descriptiva'
|
|
? 'Intensidad descriptiva: qué tan intensa es la característica (sin importar si es buena o mala)'
|
|
: 'Intensidad afectiva: qué tan buena o mala consideras esta característica'
|
|
})
|
|
|
|
// Configuración del tooltip
|
|
const tooltipConfig = computed<boolean | TooltipProps>(() => ({
|
|
disableClosingTrigger: true,
|
|
text: props.modelValue !== null ? String(props.modelValue) : '',
|
|
}))
|
|
|
|
// Configuración UI personalizada del slider
|
|
const sliderUi = computed(() => ({
|
|
root: 'cata-slider-root',
|
|
track: 'cata-slider-track',
|
|
range: props.tipo === 'descriptiva'
|
|
? 'bg-primary/20 dark:bg-primary/30'
|
|
: 'bg-primary/40 dark:bg-primary/50',
|
|
thumb: 'cata-slider-thumb',
|
|
}))
|
|
|
|
// Manejar cambio de valor
|
|
const handleChange = (value: number | number[] | undefined) => {
|
|
if (value === undefined || value === null) {
|
|
emit('update:modelValue', null)
|
|
return
|
|
}
|
|
const newValue = Array.isArray(value) ? (value[0] ?? null) : value
|
|
emit('update:modelValue', newValue)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.slider-intensidad {
|
|
width: 100%;
|
|
}
|
|
|
|
/* Personalización adicional para el slider */
|
|
:deep(.cata-slider-root) {
|
|
padding-top: 0.5rem;
|
|
padding-bottom: 0.5rem;
|
|
}
|
|
|
|
:deep(.cata-slider-track) {
|
|
height: 0.5rem;
|
|
border-radius: 9999px;
|
|
background: transparent;
|
|
border: var(--cata-border-width) solid;
|
|
border-color: color-mix(in srgb, var(--cata-primary) 50%, transparent);
|
|
}
|
|
|
|
:deep(.cata-slider-thumb) {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
border-radius: 9999px;
|
|
background-color: var(--cata-bg);
|
|
border: 2px solid var(--cata-primary);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
:deep(.cata-slider-thumb:hover) {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
:deep(.cata-slider-thumb:active) {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.dark :deep(.cata-slider-thumb) {
|
|
box-shadow: 0 0 6px var(--cata-primary);
|
|
}
|
|
|
|
.dark :deep(.cata-slider-thumb:hover) {
|
|
box-shadow: 0 0 12px var(--cata-primary);
|
|
}
|
|
|
|
/* Animación de fade in */
|
|
.slider-intensidad.cata-fade-in {
|
|
animation: cata-fade-in 0.3s ease-out;
|
|
}
|
|
</style>
|