From 87fb92d2100aa9ce135a13c9ef03713aca75b9fd Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 18 Oct 2025 01:39:27 -0600 Subject: [PATCH] =?UTF-8?q?Feat:=20Implementar=20UI=20completa=20de=20RioC?= =?UTF-8?q?ata=20-=20Sistema=20de=20cataci=C3=B3n=20de=20caf=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- nuxt4/app.config.ts | 165 ++++++ nuxt4/app/assets/css/main.css | 353 +++++++++++++ .../app/components/cata/FormularioMuestra.vue | 471 ++++++++++++++++++ nuxt4/app/components/cata/ResumenMuestra.vue | 309 ++++++++++++ nuxt4/app/components/cata/SelectorFamilia.vue | 374 ++++++++++++++ nuxt4/app/components/cata/SelectorTazas.vue | 227 +++++++++ .../app/components/cata/SliderIntensidad.vue | 166 ++++++ nuxt4/app/composables/useCatacion.ts | 346 +++++++++++++ nuxt4/app/composables/useIndexedDB.ts | 288 +++++++++++ nuxt4/app/pages/cata/index.vue | 332 ++++++++++++ nuxt4/app/pages/cata/sesion.vue | 430 ++++++++++++++++ nuxt4/app/types/catacion.ts | 315 ++++++++++++ 12 files changed, 3776 insertions(+) create mode 100644 nuxt4/app.config.ts create mode 100644 nuxt4/app/components/cata/FormularioMuestra.vue create mode 100644 nuxt4/app/components/cata/ResumenMuestra.vue create mode 100644 nuxt4/app/components/cata/SelectorFamilia.vue create mode 100644 nuxt4/app/components/cata/SelectorTazas.vue create mode 100644 nuxt4/app/components/cata/SliderIntensidad.vue create mode 100644 nuxt4/app/composables/useCatacion.ts create mode 100644 nuxt4/app/composables/useIndexedDB.ts create mode 100644 nuxt4/app/pages/cata/index.vue create mode 100644 nuxt4/app/pages/cata/sesion.vue create mode 100644 nuxt4/app/types/catacion.ts diff --git a/nuxt4/app.config.ts b/nuxt4/app.config.ts new file mode 100644 index 0000000..36f618e --- /dev/null +++ b/nuxt4/app.config.ts @@ -0,0 +1,165 @@ +export default defineAppConfig({ + ui: { + // ======================================================================== + // COLORES PERSONALIZADOS - MODO CLARO Y OSCURO + // ======================================================================== + colors: { + // Modo claro: Paleta de azules (celeste a oscuro) + primary: { + 50: '#e0f2fe', // Azul muy claro + 100: '#bae6fd', // Azul celeste claro + 200: '#87CEEB', // Azul celeste (SkyBlue) + 300: '#7dd3fc', // Azul celeste medio + 400: '#38bdf8', // Azul medio + 500: '#4682B4', // Azul acero (SteelBlue) + 600: '#0284c7', // Azul más intenso + 700: '#0369a1', // Azul oscuro + 800: '#075985', // Azul muy oscuro + 900: '#00008B', // Azul oscuro intenso (DarkBlue) + 950: '#000066', // Azul casi negro + }, + }, + + // ======================================================================== + // CONFIGURACIÓN DE VARIABLES CSS PERSONALIZADAS + // ======================================================================== + variables: { + light: { + background: '0 0% 100%', // Blanco puro + foreground: '0 0% 0%', // Negro puro + + // Colores primarios para modo claro (azul) + primary: '203 92% 54%', // #4682B4 (SteelBlue) + 'primary-foreground': '0 0% 0%', // Texto negro sobre azul + + // Bordes y outlines en azul + border: '203 50% 70%', // Azul celeste para bordes + muted: '203 30% 40%', // Azul oscuro para texto secundario + 'muted-foreground': '0 0% 20%', // Gris oscuro + + // Superficies con outline + elevated: '0 0% 100%', // Blanco (sin relleno) + accented: '203 92% 94%', // Azul muy claro para hover + }, + dark: { + background: '0 0% 0%', // Negro puro + foreground: '120 100% 50%', // Verde terminal (#00FF00) + + // Colores primarios para modo oscuro (verde terminal) + primary: '120 100% 50%', // #00FF00 (Verde terminal) + 'primary-foreground': '0 0% 0%', // Negro sobre verde + + // Bordes y outlines en verde + border: '120 100% 40%', // Verde oscuro para bordes + muted: '120 80% 30%', // Verde apagado + 'muted-foreground': '120 50% 60%', // Verde claro para texto secundario + + // Superficies con outline + elevated: '0 0% 0%', // Negro (sin relleno) + accented: '120 100% 10%', // Verde muy oscuro para hover + }, + }, + + // ======================================================================== + // PERSONALIZACIÓN DE COMPONENTES + // ======================================================================== + + // Accordion: Solo outlines, sin rellenos + accordion: { + slots: { + root: 'w-full', + item: 'border border-primary/30 last:border-b dark:border-primary/50', + header: 'flex', + trigger: 'group flex-1 flex items-center gap-1.5 font-medium text-sm py-3.5 px-4 focus-visible:outline-primary min-w-0 dark:font-mono', + content: 'data-[state=open]:animate-[accordion-down_200ms_ease-out] data-[state=closed]:animate-[accordion-up_200ms_ease-out] overflow-hidden focus:outline-none', + body: 'text-sm pb-3.5 px-4', + leadingIcon: 'shrink-0 size-5', + trailingIcon: 'shrink-0 size-5 ms-auto group-data-[state=open]:rotate-180 transition-transform duration-200', + label: 'text-start break-words dark:font-mono', + }, + }, + + // Tabs: Estilo minimalista con outlines + tabs: { + slots: { + root: 'flex items-center gap-2', + list: 'relative flex p-0 group border-b border-primary/30 dark:border-primary/50', + indicator: 'absolute transition-[translate,width] duration-200 border-b-2 border-primary dark:border-primary', + trigger: [ + 'group relative inline-flex items-center min-w-0 px-4 py-2', + 'data-[state=inactive]:text-muted hover:data-[state=inactive]:not-disabled:text-default', + 'font-medium dark:font-mono', + 'transition-colors', + 'border-b-2 border-transparent', + 'data-[state=active]:text-primary data-[state=active]:border-primary', + ], + leadingIcon: 'shrink-0', + label: 'truncate dark:font-mono', + content: 'focus:outline-none w-full py-4', + }, + variants: { + orientation: { + horizontal: { + root: 'flex-col', + list: 'w-full', + indicator: 'left-0 w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position) bottom-0', + }, + vertical: { + list: 'flex-col border-r border-b-0', + indicator: 'top-0 h-(--reka-tabs-indicator-size) translate-y-(--reka-tabs-indicator-position) right-0 border-r-2 border-b-0', + }, + }, + }, + }, + + // Slider: Personalización para intensidades + slider: { + slots: { + root: 'relative flex items-center select-none touch-none', + track: 'relative bg-transparent border border-primary/30 dark:border-primary/50 overflow-hidden rounded-full grow', + range: 'absolute rounded-full bg-primary/20 dark:bg-primary/30', + thumb: 'rounded-full bg-background ring-2 ring-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:ring-primary', + }, + }, + + // CheckboxGroup: Estilo outline + checkboxGroup: { + slots: { + root: 'relative', + fieldset: 'flex gap-x-2', + legend: 'mb-1 block font-medium text-default dark:font-mono', + item: 'border border-primary/30 dark:border-primary/50 px-3 py-2 rounded-md has-data-[state=checked]:border-primary dark:has-data-[state=checked]:border-primary', + }, + }, + + // Button: Estilo outline + button: { + slots: { + base: 'border border-primary/50 dark:border-primary dark:font-mono', + }, + variants: { + variant: { + outline: { + base: 'bg-transparent border-2 border-primary text-primary hover:bg-primary/10 dark:hover:bg-primary/20', + }, + }, + }, + }, + + // Card: Solo outlines + card: { + slots: { + root: 'bg-transparent border border-primary/30 dark:border-primary/50 rounded-lg', + header: 'border-b border-primary/30 dark:border-primary/50 px-4 py-3', + body: 'px-4 py-3', + }, + }, + + // Input: Outlines con focus + input: { + slots: { + root: 'bg-transparent border border-primary/50 dark:border-primary focus:ring-2 focus:ring-primary dark:font-mono', + }, + }, + }, +}) diff --git a/nuxt4/app/assets/css/main.css b/nuxt4/app/assets/css/main.css index 7c95c6f..9c1ba6e 100644 --- a/nuxt4/app/assets/css/main.css +++ b/nuxt4/app/assets/css/main.css @@ -1,2 +1,355 @@ @import "tailwindcss"; @import "@nuxt/ui"; + +@tailwind utilities; + +/* ========================================================================== */ +/* ESTILOS PERSONALIZADOS PARA RIOCATA */ +/* ========================================================================== */ + +/* -------------------------------------------------------------------------- */ +/* MODO CLARO: Estilo papel blanco con outlines azules */ +/* -------------------------------------------------------------------------- */ +@media (prefers-color-scheme: light) { + :root { + --cata-bg: #ffffff; + --cata-fg: #000000; + --cata-primary: #4682b4; /* Steel Blue */ + --cata-primary-light: #87ceeb; /* Sky Blue */ + --cata-primary-dark: #00008b; /* Dark Blue */ + --cata-border-width: 1px; + --cata-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + } +} + +:root:not(.dark) { + --cata-bg: #ffffff; + --cata-fg: #000000; + --cata-primary: #4682b4; + --cata-primary-light: #87ceeb; + --cata-primary-dark: #00008b; + --cata-border-width: 1px; + --cata-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +/* -------------------------------------------------------------------------- */ +/* MODO OSCURO: Estilo terminal verde antiguo */ +/* -------------------------------------------------------------------------- */ +@media (prefers-color-scheme: dark) { + :root { + --cata-bg: #000000; + --cata-fg: #00ff00; /* Terminal Green */ + --cata-primary: #00ff00; /* Terminal Green */ + --cata-primary-light: #00cc00; /* Medium Green */ + --cata-primary-dark: #009900; /* Dark Green */ + --cata-border-width: 0.75px; + --cata-font-family: 'Courier New', Courier, 'Lucida Console', Monaco, monospace; + } +} + +:root.dark { + --cata-bg: #000000; + --cata-fg: #00ff00; + --cata-primary: #00ff00; + --cata-primary-light: #00cc00; + --cata-primary-dark: #009900; + --cata-border-width: 0.75px; + --cata-font-family: 'Courier New', Courier, 'Lucida Console', Monaco, monospace; +} + +/* -------------------------------------------------------------------------- */ +/* UTILIDADES GLOBALES */ +/* -------------------------------------------------------------------------- */ + +/* Outline containers sin fondo relleno */ +.cata-outline-box { + background-color: transparent; + border: var(--cata-border-width) solid var(--cata-primary); + border-color: color-mix(in srgb, var(--cata-primary) 50%, transparent); +} + +.dark .cata-outline-box { + border-color: color-mix(in srgb, var(--cata-primary) 70%, transparent); +} + +/* Líneas delgadas horizontales */ +.cata-divider { + width: 100%; + height: 1px; + background-color: color-mix(in srgb, var(--cata-primary) 30%, transparent); + border: none; +} + +.dark .cata-divider { + background-color: color-mix(in srgb, var(--cata-primary) 50%, transparent); +} + +/* Texto con estilo terminal en modo oscuro */ +.cata-text { + color: var(--cata-fg); + font-family: var(--cata-font-family); +} + +.dark .cata-text { + text-shadow: 0 0 2px currentColor; + font-weight: 300; + letter-spacing: 0.02em; +} + +/* -------------------------------------------------------------------------- */ +/* COMPONENTES ESPECÍFICOS */ +/* -------------------------------------------------------------------------- */ + +/* Páginas de catación: fondo completo */ +.cata-page { + min-height: 100vh; + background-color: var(--cata-bg); + color: var(--cata-fg); +} + +/* Headers de accordions */ +.cata-accordion-header { + background-color: transparent; + border: var(--cata-border-width) solid var(--cata-primary); + border-color: color-mix(in srgb, var(--cata-primary) 50%, transparent); + padding: 0.75rem 1rem; + border-radius: 0.375rem; + transition: all 200ms; +} + +.cata-accordion-header:hover { + background-color: color-mix(in srgb, var(--cata-primary) 5%, transparent); +} + +.dark .cata-accordion-header:hover { + background-color: color-mix(in srgb, var(--cata-primary) 10%, transparent); +} + +/* Tabs personalizados */ +.cata-tab { + border-bottom: 2px solid transparent; + padding: 0.5rem 1rem; + transition: all 200ms; + color: color-mix(in srgb, var(--cata-fg) 60%, transparent); +} + +.cata-tab-active { + border-bottom: 2px solid var(--cata-primary); + border-color: var(--cata-primary); + color: var(--cata-primary); + font-weight: 600; +} + +.dark .cata-tab-active { + text-shadow: 0 0 4px currentColor; +} + +/* Inputs con outline */ +.cata-input { + background-color: transparent; + border: var(--cata-border-width) solid var(--cata-primary); + border-color: color-mix(in srgb, var(--cata-primary) 50%, transparent); + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + color: var(--cata-fg); + font-family: var(--cata-font-family); +} + +.cata-input:focus { + outline: 2px solid var(--cata-primary); + outline-offset: 2px; +} + +.dark .cata-input { + font-weight: 300; +} + +/* Sliders personalizados */ +.cata-slider-track { + background-color: transparent; + border: var(--cata-border-width) solid; + border-color: color-mix(in srgb, var(--cata-primary) 50%, transparent); +} + +.cata-slider-thumb { + background-color: white; + border: 2px solid var(--cata-primary); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.dark .cata-slider-thumb { + background-color: black; +} + +.dark .cata-slider-thumb { + box-shadow: 0 0 6px var(--cata-primary); +} + +/* Checkboxes con outline */ +.cata-checkbox { + background-color: transparent; + border: var(--cata-border-width) solid var(--cata-primary); + border-color: color-mix(in srgb, var(--cata-primary) 50%, transparent); + border-radius: 0.375rem; + padding: 0.5rem 0.75rem; + cursor: pointer; + transition: all 150ms; +} + +.cata-checkbox:hover { + background-color: color-mix(in srgb, var(--cata-primary) 5%, transparent); +} + +.cata-checkbox-checked { + border-color: var(--cata-primary) !important; + border-width: 2px; + background-color: color-mix(in srgb, var(--cata-primary) 10%, transparent); +} + +.dark .cata-checkbox-checked { + background-color: color-mix(in srgb, var(--cata-primary) 15%, transparent); + box-shadow: 0 0 8px color-mix(in srgb, var(--cata-primary) 30%, transparent); +} + +/* Botones con outline */ +.cata-button { + background-color: transparent; + border: var(--cata-border-width) solid var(--cata-primary); + border-color: color-mix(in srgb, var(--cata-primary) 50%, transparent); + padding: 0.5rem 1rem; + border-radius: 0.375rem; + transition: all 200ms; + cursor: pointer; + color: var(--cata-fg); + font-family: var(--cata-font-family); +} + +.cata-button:hover { + background-color: color-mix(in srgb, var(--cata-primary) 10%, transparent); + border-width: 2px; +} + +.cata-button:active { + transform: scale(0.98); +} + +.dark .cata-button:hover { + box-shadow: 0 0 12px color-mix(in srgb, var(--cata-primary) 40%, transparent); +} + +/* -------------------------------------------------------------------------- */ +/* ANIMACIONES Y TRANSICIONES */ +/* -------------------------------------------------------------------------- */ + +/* Fade in suave para contenido */ +@keyframes cata-fade-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.cata-fade-in { + animation: cata-fade-in 0.3s ease-out; +} + +/* Glow effect para modo oscuro */ +@keyframes cata-glow-pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } +} + +.dark .cata-glow { + animation: cata-glow-pulse 2s ease-in-out infinite; +} + +/* -------------------------------------------------------------------------- */ +/* RESPONSIVE Y MOBILE-FIRST */ +/* -------------------------------------------------------------------------- */ + +/* Optimizaciones para móvil */ +@media (max-width: 640px) { + .cata-accordion-header { + padding: 0.5rem 0.75rem; + } + + .cata-tab { + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + } + + .cata-button { + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + } +} + +/* Optimizaciones para tablet */ +@media (min-width: 641px) and (max-width: 1024px) { + .cata-accordion-header { + padding: 0.75rem 1rem; + } +} + +/* Ajustes para orientación landscape en móviles */ +@media (max-height: 500px) and (orientation: landscape) { + .cata-page { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + + .cata-accordion-header { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } +} + +/* Touch-friendly: Aumentar áreas de toque */ +@media (hover: none) and (pointer: coarse) { + .cata-button, + .cata-checkbox, + .cata-tab { + min-height: 44px; + min-width: 44px; + } +} + +/* -------------------------------------------------------------------------- */ +/* ACCESIBILIDAD */ +/* -------------------------------------------------------------------------- */ + +/* Focus visible mejorado */ +*:focus-visible { + outline: 2px solid var(--cata-primary); + outline-offset: 2px; + border-radius: 4px; +} + +.dark *:focus-visible { + outline-width: 3px; + box-shadow: 0 0 8px var(--cata-primary); +} + +/* Modo de alto contraste */ +@media (prefers-contrast: high) { + :root { + --cata-border-width: 2px; + } + + .cata-outline-box { + border-width: 2px; + } + + .dark .cata-text { + text-shadow: none; + font-weight: 400; + } +} diff --git a/nuxt4/app/components/cata/FormularioMuestra.vue b/nuxt4/app/components/cata/FormularioMuestra.vue new file mode 100644 index 0000000..1f5d0cf --- /dev/null +++ b/nuxt4/app/components/cata/FormularioMuestra.vue @@ -0,0 +1,471 @@ +