Files
cataRio/nuxt4/app/components/cata/SelectorIntensidad.vue
josedario87 cec471bc32
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m5s
Fix: Mejorar comportamiento de selectores de puntaje en Descriptiva/Afectiva
- Evitar deselección al presionar el mismo corazón/círculo ya seleccionado
- Cambiar iconos de corazón de Lucide a Heroicons para mejor distinción visual
  - No seleccionado: i-heroicons-heart (outline)
  - Seleccionado: i-heroicons-heart-solid (relleno)
- Eliminar clase CSS icono-filled innecesaria
2025-10-19 00:34:44 -06:00

252 lines
6.6 KiB
Vue

<template>
<div class="selector-intensidad cata-fade-in" :style="customColorStyle">
<!-- Contenedor de iconos -->
<div class="iconos-container">
<button
v-for="valor in iconos"
:key="valor"
type="button"
class="icono-item"
:class="{
'icono-activo': modelValue !== null && valor <= modelValue,
'icono-hover': valorHover !== null && valor <= valorHover,
'icono-descriptiva': tipo === 'descriptiva',
'icono-afectiva': tipo === 'afectiva',
}"
:disabled="disabled"
@click="handleClick(valor)"
@mouseenter="valorHover = valor"
@mouseleave="valorHover = null"
>
<!-- Icono para descriptiva: círculo -->
<UIcon
v-if="tipo === 'descriptiva'"
:name="(modelValue !== null && valor <= modelValue) || (valorHover !== null && valor <= valorHover)
? 'i-lucide-circle-dot'
: 'i-lucide-circle'"
class="icono"
/>
<!-- Icono para afectiva: corazón -->
<UIcon
v-else
:name="(modelValue !== null && valor <= modelValue) || (valorHover !== null && valor <= valorHover)
? 'i-heroicons-heart-solid'
: 'i-heroicons-heart'"
class="icono"
/>
</button>
</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">
interface SelectorIntensidadProps {
/** Tipo de intensidad: descriptiva (1-10) o afectiva (1-15) */
tipo: 'descriptiva' | 'afectiva'
/** Valor actual del selector */
modelValue: number | null
/** Etiqueta del selector */
label?: string
/** ID para el input (auto-generado si no se provee) */
id?: string
/** Deshabilitar el selector */
disabled?: boolean
/** Marcar como requerido */
required?: boolean
/** Mostrar descripción del tipo */
showDescription?: boolean
/** Color personalizado para el selector */
color?: string
}
const props = withDefaults(defineProps<SelectorIntensidadProps>(), {
disabled: false,
required: false,
showDescription: false,
})
const emit = defineEmits<{
'update:modelValue': [value: number | null]
}>()
// Estado para el hover
const valorHover = ref<number | null>(null)
// Configuración según el tipo
const min = computed(() => 1)
const max = computed(() => props.tipo === 'descriptiva' ? 10 : 15)
// Array de valores para los iconos
const iconos = computed(() => {
return Array.from({ length: max.value }, (_, i) => i + 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'
})
// Manejar click en un icono
const handleClick = (valor: number) => {
if (props.disabled) return
// Si clickean el mismo valor, no hace nada (mantiene el valor)
if (props.modelValue === valor) {
return
}
emit('update:modelValue', valor)
}
// Estilo dinámico para el color personalizado
const customColorStyle = computed(() => {
if (!props.color) return {}
return {
'--selector-custom-color': props.color,
}
})
</script>
<style scoped>
.selector-intensidad {
width: 100%;
}
.iconos-container {
display: flex;
gap: 0.15rem;
justify-content: center;
flex-wrap: wrap;
padding: 0.25rem 0;
}
.icono-item {
background: transparent;
border: none;
padding: 0.15rem;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
}
.icono-item:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.icono-item:not(:disabled):hover {
transform: scale(1.15);
}
.icono-item:not(:disabled):active {
transform: scale(1.05);
}
/* Iconos */
.icono {
width: 1rem;
height: 1rem;
transition: all 0.2s ease;
}
/* Iconos descriptiva (círculos) */
.icono-descriptiva .icono {
color: color-mix(in srgb, var(--cata-primary) 40%, transparent);
}
.icono-descriptiva.icono-activo .icono,
.icono-descriptiva.icono-hover .icono {
color: var(--cata-primary);
}
/* Iconos afectiva (corazones) */
.icono-afectiva .icono {
color: color-mix(in srgb, var(--cata-primary) 40%, transparent);
stroke-width: 2;
}
.icono-afectiva.icono-activo .icono,
.icono-afectiva.icono-hover .icono {
color: var(--cata-primary);
}
/* Color personalizado */
.selector-intensidad[style*="--selector-custom-color"] .icono-descriptiva .icono {
color: color-mix(in srgb, var(--selector-custom-color) 40%, transparent);
}
.selector-intensidad[style*="--selector-custom-color"] .icono-descriptiva.icono-activo .icono,
.selector-intensidad[style*="--selector-custom-color"] .icono-descriptiva.icono-hover .icono {
color: var(--selector-custom-color);
}
.selector-intensidad[style*="--selector-custom-color"] .icono-afectiva .icono {
color: color-mix(in srgb, var(--selector-custom-color) 40%, transparent);
}
.selector-intensidad[style*="--selector-custom-color"] .icono-afectiva.icono-activo .icono,
.selector-intensidad[style*="--selector-custom-color"] .icono-afectiva.icono-hover .icono {
color: var(--selector-custom-color);
}
/* Modo oscuro */
.dark .icono-descriptiva .icono {
color: color-mix(in srgb, var(--cata-primary) 30%, transparent);
}
.dark .icono-descriptiva.icono-activo .icono,
.dark .icono-descriptiva.icono-hover .icono {
color: var(--cata-primary);
filter: drop-shadow(0 0 4px var(--cata-primary));
}
.dark .icono-afectiva .icono {
color: color-mix(in srgb, var(--cata-primary) 30%, transparent);
}
.dark .icono-afectiva.icono-activo .icono,
.dark .icono-afectiva.icono-hover .icono {
color: var(--cata-primary);
filter: drop-shadow(0 0 4px var(--cata-primary));
}
/* Color personalizado en modo oscuro */
.dark .selector-intensidad[style*="--selector-custom-color"] .icono-descriptiva.icono-activo .icono,
.dark .selector-intensidad[style*="--selector-custom-color"] .icono-descriptiva.icono-hover .icono {
filter: drop-shadow(0 0 4px var(--selector-custom-color));
}
.dark .selector-intensidad[style*="--selector-custom-color"] .icono-afectiva.icono-activo .icono,
.dark .selector-intensidad[style*="--selector-custom-color"] .icono-afectiva.icono-hover .icono {
filter: drop-shadow(0 0 4px var(--selector-custom-color));
}
/* Responsive */
@media (max-width: 640px) {
.icono {
width: 0.875rem;
height: 0.875rem;
}
.iconos-container {
gap: 0.125rem;
}
}
/* Animación de fade in */
.selector-intensidad.cata-fade-in {
animation: cata-fade-in 0.3s ease-out;
}
</style>