Files
cataRio/nuxt4/app/components/cata/SliderIntensidad.vue
josedario87 87fb92d210
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m3s
Feat: Implementar UI completa de RioCata - Sistema de catación de café
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
2025-10-18 01:39:27 -06:00

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>