version con las correcciones de texto y preferencias por defecto. Se ha dotado de un diseño responsive a la aplicación

v2_responsive
dev 12 months ago
parent 01f8f0419a
commit 8dc5d2c722

@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"

@ -1,7 +1,7 @@
<script lang="ts">
import './app.css';
import { fade } from 'svelte/transition';
import { fade, slide } from 'svelte/transition';
import Navegacion from "./lib/components/Navegacion.svelte"
import HomeView from "./lib/views/HomeView.svelte";
import ConfiguradorView from "./lib/views/ConfiguradorView.svelte";

@ -370,17 +370,18 @@ export const proyecto : Proyecto = {
nombre : 'Ducha / Bañera',
opciones: [
{
nombre: 'Con Ducha',
value : 'didu',
material: 'ducha',
},
{
nombre : 'Bañera',
value : 'diba',
material: 'banera',
},
{
nombre: 'Con Ducha',
value : 'didu',
material: 'ducha',
precio : +1924.25
},
]
},

@ -24,8 +24,8 @@
<div class="group bg-white rounded-lg border border-gray-200 hover:border-slate-700 shadow-sm overflow-hidden flex flex-col hover:shadow-lg">
<div class="w-full rounded-lg flex flex-col justify-between group bg-white border border-gray-200 hover:border-slate-700 shadow-sm overflow-hidden hover:shadow-lg">
<!-- <div class="group bg-white rounded-lg border border-gray-200 hover:border-slate-700 shadow-sm overflow-hidden flex flex-col hover:shadow-lg"> -->
<!-- IMAGEN -->
<div
role="button"

@ -20,13 +20,12 @@
</script>
{#if popupImagen.isVisible }
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
{#if popupImagen.isVisible}
<!-- Overlay principal -->
<div
class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 transition-opacity duration-300"
class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 transition-opacity duration-300 p-4 md:p-6"
role="button"
tabindex="0"
aria-label="Cerrar modal"
@ -35,35 +34,36 @@
>
<!-- Contenedor del contenido -->
<div
class="bg-white p-6 rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-auto transform transition-transform duration-300 scale-100"
class="bg-white p-4 md:p-6 rounded-xl shadow-2xl w-full max-w-md sm:max-w-lg md:max-w-4xl max-h-[90vh] overflow-auto transform transition-transform duration-300 scale-100"
onclick={(e) => e.stopPropagation()}
role="document"
>
<h2 class="text-2xl font-bold text-center mb-4 text-gray-800">
<h2 class="text-xl sm:text-2xl font-bold text-center mb-4 text-gray-800">
Opción de {popupImagen.texto}
</h2>
{#if isLoading}
<div class="flex justify-center items-center h-[70vh]">
<div class="animate-spin rounded-full h-16 w-16 border-4 border-blue-500 border-t-transparent"></div>
<div class="flex justify-center items-center h-[50vh] sm:h-[70vh]">
<div class="animate-spin rounded-full h-12 w-12 sm:h-16 sm:w-16 border-4 border-blue-500 border-t-transparent"></div>
</div>
{/if}
<img
src={popupImagen.imagen}
alt="Imagen ampliada"
class="w-full max-h-[70vh] object-contain rounded-lg mb-4 transition-opacity duration-500"
class="w-full max-h-[50vh] sm:max-h-[70vh] object-contain rounded-lg mb-4 transition-opacity duration-500"
onload={handleImageLoad}
style:opacity={isLoading ? 0 : 1}
/>
<button
type="button"
class="w-full px-4 py-2 bg-azul-s text-white rounded-lg hover:bg-blue-900 focus:ring-4 focus:ring-blue-300 font-medium transition-all duration-200"
class="mt-4 bg-blue-900 w-full px-4 py-3 md:px-6 md:py-2 text-white rounded-lg hover:bg-blue-900 focus:ring-4 focus:ring-blue-300 font-medium transition-all duration-200"
onclick={() => popupImagen.close()}
>
Cerrar
</button>
</div>
</div>
{/if}
{/if}

@ -4,65 +4,76 @@
import {dataApp} from "../../data/data_app";
import { navegationState, type PageId } from "../stores/navegation.svelte.js";
import PdfBoton from "./PdfBoton.svelte";
import { fade, slide } from 'svelte/transition';
let mobileMenuOpen = $state(false);
function toggleMobileMenu () : void { mobileMenuOpen = !mobileMenuOpen; }
function toggleMobileMenu () : void {
mobileMenuOpen = !mobileMenuOpen;
}
function handleOnclick (sectionId: PageId) {
navegationState.activePage = sectionId;
mobileMenuOpen = false;
mobileMenuOpen = false; // Cierra menú móvil al seleccionar
}
// Nueva función para cerrar menú al hacer click fuera (opcional)
function handleBackdropClick(event: MouseEvent) {
if ((event.target as HTMLElement).classList.contains('mobile-menu-backdrop')) {
mobileMenuOpen = false;
}
}
</script>
<nav class="bg-white shadow-md border-b border-gray-200 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex-shrink-0">
<div class="flex items-center">
<div role="button"
tabindex="0"
class="text-white px-3 py-2 rounded-md font-bold text-lg">
<a href={empresaData.url} target="_blank">
<img src={empresaData.logos.default} alt={empresaData.nombre} class="h-16 p-1.5 object-contain" />
</a>
</div>
<span class="ml-2 sm:ml-4 text-gray-500 font-bold uppercase tracking-widest text-xl sm:text-1+xl truncate max-w-32 sm:max-w-none">
{dataApp.titulo}
</span>
<div
role="button"
tabindex="0"
class="ml-2 sm:ml-4 text-gray-600 font-medium text-sm sm:text-base truncate max-w-32 sm:max-w-none cursor-pointer"
onclick={() => handleOnclick('ini') }
onkeydown={(e) => { if (e.key === 'Enter' || e.key === 'Space') handleOnclick('ini'); } }
>
<img src={proyecto.logos.default} alt={proyecto.nombre} class="h-16 p-1.5 object-contain" />
</div>
<!-- Logo y branding - Más compacto en mobile -->
<div class="flex-shrink-0 flex items-center space-x-2 sm:space-x-4">
<!-- Logo empresa - Más pequeño en mobile -->
<div role="button" tabindex="0" class="flex-shrink-0">
<a href={empresaData.url} target="_blank" rel="noopener noreferrer">
<img
src={empresaData.logos.default}
alt={empresaData.nombre}
class="h-10 sm:h-12 lg:h-14 p-1 object-contain transition-all duration-200 hover:scale-105"
/>
</a>
</div>
<!-- Título app - Más compacto y truncado en mobile -->
<span class="hidden sm:block text-black-600 font-bold uppercase tracking-wider text-center
sm:text-sx lg:text-xl truncate max-w-xs sm:max-w-none">
{dataApp.titulo}
</span>
<!-- Logo proyecto - Oculto en mobile muy pequeño, visible en sm+ -->
<!-- CORRECTO - Clases combinadas -->
<div class="sm:flex sm:ml-2 lg:ml-4 items-center text-gray-600 font-medium cursor-pointer"
role="button" tabindex="0"
onclick={() => handleOnclick('ini')}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === 'Space') handleOnclick('ini'); }}>
<img
src={proyecto.logos.default}
alt={proyecto.nombre}
class="h-10 sm:h-12 lg:h-14 p-1 object-contain hover:scale-105 transition-transform"
/>
</div>
</div>
<div class="hidden md:flex space-x-1">
<!-- Menú desktop -->
<div class="hidden md:flex items-center space-x-1 lg:space-x-2">
{#each navegationState.sectionsInfo as section}
{#if section.menu }
<button type="button"
class="px-3 lg:px-4 mr-4 py-2 rounded-md text-base font-medium transition-colors duration-200 font-sans {
navegationState.activePage === section.id
? 'bg-blue-100 text-primary-700 border border-blue-200'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100 cursor-pointer'
}"
onclick={() => handleOnclick(section.id) } >
<button
type="button"
class="px-3 py-2 lg:px-4 lg:py-2 rounded-md text-sm lg:text-base font-medium
transition-all duration-200 font-sans hover:shadow-sm
{navegationState.activePage === section.id
? 'bg-blue-100 text-blue-700 border border-blue-200 shadow-sm'
: 'text-gray-600 hover:text-gray-900 hover:bg-blue-50 cursor-pointer'
}"
onclick={() => handleOnclick(section.id)}>
{section.label}
</button>
{/if}
@ -70,19 +81,26 @@
<PdfBoton />
</div>
<div class="md:hidden">
<button type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500"
<!-- Botón hamburguesa - Mejoras visuales -->
<div class="md:hidden flex items-center">
<button
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400
hover:text-gray-600 hover:bg-gray-100 focus:outline-none focus:ring-2
focus:ring-inset focus:ring-blue-500 transition-colors duration-200
{mobileMenuOpen ? 'bg-gray-100 text-gray-600' : ''}"
onclick={toggleMobileMenu}
aria-expanded="false" >
<span class="sr-only">Abrir menú principal</span>
aria-expanded={mobileMenuOpen}
aria-label={mobileMenuOpen ? "Cerrar menú" : "Abrir menú"}>
{#if !mobileMenuOpen}
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<!-- Icono hamburguesa -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
{:else}
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<!-- Icono X -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
{/if}
</button>
@ -90,31 +108,63 @@
</div>
</div>
<!-- Menú móvil mejorado -->
<!-- Menú móvil con slide funcionando -->
{#if mobileMenuOpen}
<div class="md:hidden border-t border-gray-200">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-white">
{#each navegationState.sectionsInfo as section}
<button
type="button"
class="w-full text-left px-3 py-2 rounded-md text-base font-medium transition-colors duration-200 {
navegationState.activePage === section.id
? 'bg-green-100 text-primary-700 border border-primary-200'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100 cursor-pointer'
}"
onclick={() => handleOnclick(section.id)}
>
{section.label}
</button>
{/each}
<div
class="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
transition:fade={{ duration: 150 }}
onclick={() => mobileMenuOpen = false}>
<!-- Menú que desliza desde arriba -->
<div
class="absolute top-16 left-0 right-0 bg-white shadow-xl border-t border-gray-200
max-h-[calc(100vh-4rem)] overflow-y-auto"
transition:slide={{
duration: 300,
axis: 'Y',
easing: 'cubic-out'
}}>
<div class="px-2 pt-2 pb-4 space-y-1 sm:px-3">
{#each navegationState.sectionsInfo as section}
{#if section.menu }
<button
type="button"
class="w-full text-left px-4 py-3 rounded-md text-base font-medium
transition-all duration-200 border-l-4
{navegationState.activePage === section.id
? 'bg-blue-50 text-blue-700 border-blue-500'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50 border-transparent'
}"
onclick={() => handleOnclick(section.id)}>
<div class="flex items-center justify-between">
<span>{section.mome ?? section.label}</span>
{#if navegationState.activePage === section.id}
<svg class="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
{/if}
</div>
</button>
{/if}
{/each}
<div class="px-4 py-3 border-t border-gray-200 mt-2">
<PdfBoton />
</div>
</div>
</div>
</div>
{/if}
</nav>
<style>
button:disabled {
pointer-events: none;
opacity: 0.5;
}
</style>
/* Smooth scroll para navegación */
html {
scroll-behavior: smooth;
}
</style>

@ -384,9 +384,11 @@
<!-- class="w-full md:w-auto px-3 lg:px-4 py-2 rounded-md text-base font-medium transition-colors duration-200 text-white bg-green-900 hover:bg-green-700 cursor-pointer" -->
<button type="button"
class="px-3 lg:px-4 py-2 rounded-md text-base font-medium transition-colors duration-200 text-white bg-green-900 hover:bg-green-700 cursor-pointer transition"
class="w-full md:w-auto px-4 py-3 md:px-3 lg:px-4 md:py-2 rounded-md text-base font-medium transition-colors duration-200 text-white bg-green-900 hover:bg-green-700 cursor-pointer truncate"
onclick={() => generatePdf()}>
Descargar Selección
</button>

@ -5,6 +5,7 @@ interface PageInfo {
id : PageId,
label: string,
menu : boolean,
mome?: string
}
@ -27,8 +28,8 @@ export const navegationState = {
},
sectionsInfo: [
{ id: 'ini', label: 'Inicio', menu: false },
{ id: 'cfg', label: 'Personalizar', menu: true },
{ id: 'sel', label: 'Mi Selección', menu: true },
{ id: 'cfg', label: 'Personalizar', mome: 'Personalizar Estancias', menu: true },
{ id: 'sel', label: 'Mi Selección', mome: 'Acabados y opciones seleccionadas', menu: true },
{ id: 'at' , label: 'Active Thing', menu: false },
] as PageInfo[],
}

@ -29,7 +29,7 @@ export const filtros : Record <string, Filtro> = {
'lavadora' : {
tipo: 'notify',
txt: proyecto.textos.aviso_puerta,
txt: proyecto.textos.aviso_lavadora,
check: (value: string): boolean => true,
},

@ -7,29 +7,61 @@
import {proyecto} from '../../data/data_proyecto';
import EstanciaOpciones from "../components/EstanciaOpciones.svelte";
import {filtros} from "../utils/filtros";
import {writable} from "svelte/store";
let isLoading = $state(false);
$effect(() => {
// Las runas se leen automáticamente aquí
const currentImage = imageSrc()[appState.estanciaSeleccionada];
isLoading = false;
const img = new Image();
img.src = currentImage;
img.onload = () => {
isLoading = true;
};
});
</script>
<div class="bg-gray-50 flex justify-center h-full">
<div class="bg-gray-50 flex flex-col lg:flex-row min-h-[calc(100vh-4rem)] items-start lg:fixed lg:inset-x-0 lg:bottom-0 lg:top-16">
<!-- IMAGEN CENTRAL -->
<div class="flex-1 h-full p-4 sm:p-4 bg-white rounded-l-xl shadow-lg border-r border-gray-200">
<div class="sticky top-1 z-10 flex-1 p-3 h-full bg-white shadow-lg lg:static flex lg:items-start justify-center overflow-hidden">
{#if !isLoading}
<div class="absolute inset-0 flex items-center justify-center bg-gray-100">
<svg class="animate-spin h-8 w-8 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
</svg>
</div>
{/if}
<img
src={imageSrc()[appState.estanciaSeleccionada]}
alt={`Vista de ${proyecto.estancias[appState.estanciaSeleccionada].nombre}`}
class="w-full h-auto max-h-[90vh] object-contain"
onload={() => isLoading=true}
/>
</div>
<!-- ASIDE DERECHA CON BARRA SCROOL -->
<div class="hidden lg:block h-full min-h-0">
<div class="w-80 bg-white shadow-xl border-l border-gray-200 flex flex-col h-full overflow-hidden">
<div class="flex-1 lg:flex-none lg:w-[380px] flex flex-col bg-gray-50 border-gray-200 overflow-y-auto lg:h-full">
<div class="w-full bg-white shadow-xl border-l border-gray-200 flex flex-col h-full overflow-hidden">
<div class="p-4 border-b bg-gray-50 border-gray-300">
<div class="p-4 border-b w border-gray-300 bg-gray-50">
<div>
<h2 class="text-lg font-semibold text-gray-900 mb-1">Estancias</h2>
<p class="text-sm mb-4 text-gray-600">Seleccione la estancia a configurar</p>
@ -61,11 +93,11 @@
<p class="text-sm pb-4">{grupo.texto}</p>
{/if}
{#if grupo.aviso }
<div class="p-4 rounded-l-xl bg-amber-50 mb-3">
<p class="text-sm">{grupo.aviso}</p>
<div class="p-4 rounded-xl border-1 border-amber-200 bg-amber-50 mb-3 text-sm text-gray-700">
<p>{grupo.aviso}</p>
</div>
{/if}
<div class="grid grid-cols-2 gap-2">
<div class="grid grid-cols-2 gap-4">
{#each grupo.opciones as opcion }
<BotonAcabado opcion={opcion}
filtro={grupo.filtro ? filtros[grupo.filtro] : undefined }
@ -82,8 +114,8 @@
{#if proyecto.estancias[appState.estanciaSeleccionada].adds}
{@const adds = proyecto.estancias[appState.estanciaSeleccionada].adds }
{#if adds && typeof adds === 'object' && !Array.isArray(adds) }
<div class="mt-8 border-t border-gray-200 pt-6 pb-6">
<h3 class="text-sm font-medium text-gray-700 mb-4 uppercase tracking-wide">Otras Opciones</h3>
<div class="border-gray-200 pt-6 pb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 uppercase tracking-wide">Otras Opciones</h3>
<div class="space-y-2">
{#each Object.entries(adds) as [addKey, add]}
{@const seleccionada = selectedAdds[appState.estanciaSeleccionada]?.[addKey] ?? false}

@ -14,6 +14,7 @@
import {formatearPrecio} from "../utils/formaters";
import Incidencias from "../components/Incidencias.svelte";
import {navegationState} from "../stores/navegation.svelte";
import PdfBoton from "../components/PdfBoton.svelte";
const incidencias = getIncidencias();
@ -39,16 +40,17 @@
<div class="max-w-7xl w-full mx-auto pt-4 pb-8">
<div class="p-4 sm:p-6 text-center">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 ">
{#each Object.entries(proyecto.estancias) as [estanciaId, estancia]}
<EstanciaFicha estancia={estancia} />
{/each}
<!-- INCIDENCIAS -->
{#if incidencias}
<div class="col-span-3" >
{#if true && incidencias}
<div class="col-span-1 md:col-span-2 lg:col-span-3">
<div class="bg-white rounded-xl border border-red-200 shadow-sm overflow-hidden flex flex-col">
<div class="p-4 text-left">
<h2 class="text-lg font-bold text-red-700 mb-4">
<h2 class="text-sm font-bold text-red-700 mb-4">
⚠️ Se han detectado incidencias en la selección de acabados / opciones
</h2>
@ -60,9 +62,9 @@
onclick={() => goToConfigurador(estanciaId)}
onkeydown={(e) => e.key === 'Enter' && goToConfigurador(estanciaId) }
>
<h3 class="font-semibold text-red-800 mb-2">
<span class="font-semibold text-red-800 mb-2 text-sm">
{proyecto.estancias[estanciaId]?.nombre || estanciaId}
</h3>
</span>
<div class="grid grid-cols-[20%_80%] gap-2 text-sm text-gray-700 text-left">
{#each lista as inc}
@ -79,7 +81,8 @@
<!-- TOTALES -->
<div class="col-span-1 col-start-4">
{#if true}
<div class="col-span-1 md-col-span-2 lg-col-span-1 lg:col-start-4">
<div class="bg-white rounded-xl border border-gray-300 shadow-sm overflow-hidden flex flex-col max-w-sm mx-auto">
<div class="bg-green-800 text-white p-2">
@ -107,12 +110,17 @@
</div>
</div>
{/if}
<!-- TOTALES END -->
</div>
</div>
<div class="p-4 sm:p-6 text-center sm:hidden">
<PdfBoton />
</div>
<div class="p-4 sm:p-6 text-center">
<GalletaMensaje texto={empresaData.textos.legal_imagenes} titulo="AVISO" />

Loading…
Cancel
Save

Powered by TurnKey Linux.