Disponer de una solución de «data-Calendar», es decir, poder representar datos en un calendario es básico en casi todos los desarrollo, dado que mucha de nuestras informaciones están asociadas a una echa.
La solución «histórica» de Open Source es FullCalendar que es la que hemos utilizado en PHPRunner en mis ejemplos y en los Templates de XlineSoft.
Este producto, también, ha creado una «saga» de seguidores y de creadores de solución similar en diversas plataformas.
Objetivo
Disponer de solución para incorporar FullCalendar en plataforma Svelte 5 y disponer de una alternativa nativa de Svelte, que disponga de la funcionalidad de FullCalendar (svelte-calendar).
DEMOS:
- FullCalendar: https://fhumanes.com/svelte-fullcalendar/
- Svelte-Calendar: https://fhumanes.com/svelte-calendar/
Solución Técnica
Estos ejemplos simples los he hecho son la IA Gemini. Me ha costado mucho llegar a ellos, porque en esta sesión Gemini nada más que hacía deducir erróneamente los parámetros de configuración por más información de la documentación que le facilitaba, no era capaz de interpretar adecuadamente los contenidos de los manuales.
Como veréis, los ejemplos son muy simples, más en el caso de FullCalendar. El motivo es que no tiene implementación nativa en Svelte 5, pero se ha hecho un WRAPPER, para ser utilizado. Me ha gustado conocer esto, porque significa que prácticamente todo lo que sea JavaScript se puede utilizar en Svelte 5 con esta técnica.
La solución de «svelte-calendar», al ser nativa, me atrae mucho más para esta plataforma y al ser una «copia» funcional de FullCalendar, se ajusta a lo que quería probar.
Esta solución incorpora TOOLTIP y al hacer CLIC la apertura de una ventana modal para presentar o actualizar datos.
A nivel de referencia os dejo los ficheros más importantes del ejemplo.
- FullCalendar
App.svelteFullCalendarWrapper.svelte<script> import FullCalendarWrapper from './FullCalendarWrapper.svelte'; // import '@fullcalendar/core/main.css'; // import '@fullcalendar/daygrid/main.css'; const calendarOptions = { initialView: 'dayGridMonth', events: [ { title: 'Evento 1', date: '2025-07-01' }, { title: 'Evento 2', date: '2025-07-07' } ], // Otras opciones de FullCalendar aquí }; </script> <h1>Mi Calendario de Svelte</h1>
<script> import { onMount, onDestroy } from 'svelte'; import { Calendar } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; // Importa otros plugins que necesites export let options = {}; // Propiedad para pasar las opciones de FullCalendar let calendarEl; // Referencia al elemento DOM donde FullCalendar se montará let calendar; // Instancia de FullCalendar onMount(() => { calendar = new Calendar(calendarEl, { plugins: [dayGridPlugin, timeGridPlugin], // Asegúrate de incluir los plugins ...options, // Pasa las opciones que vienen del componente padre }); calendar.render(); }); onDestroy(() => { if (calendar) { calendar.destroy(); // Limpia FullCalendar cuando el componente Svelte se destruye } }); </script> <div bind:this={calendarEl}></div> <style> /* Puedes añadir estilos específicos para FullCalendar aquí o importarlos @import '@fullcalendar/core/main.css'; @import '@fullcalendar/daygrid/main.css'; */ /* @import '@fullcalendar/timegrid/main.css'; */ </style>
- Svelte-Calendar
<script> import MyCalendar from './MyCalendar.svelte'; </script> <main> <h1>Calendario de Proyectos y Ocupación de Recursos</h1> <MyCalendar /> </main> <style> main { text-align: center; padding: 2em; max-width: 1400px; margin: 0 auto; background-color: #f8f9fa; min-height: 100vh; } h1 { color: #2c3e50; margin-bottom: 2.5em; font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0,0,0,0.1); } </style>
<script> import { Calendar, DayGrid, // Ahora solo importamos DayGrid // Eliminamos TimeGrid, ResourceTimeline, ResourceTimeGrid, Interaction } from '@event-calendar/core'; // Si estás cargando el CSS completo del CDN en index.html, puedes comentar o eliminar esta línea // para evitar posibles conflictos o redundancias. // import '@event-calendar/core/index.css'; let showModal = $state(false); let selectedTask = $state(null); // NOTA: Con la vista DayGrid, los 'resources' ya no son directamente aplicables a la visualización. // Los eventos se mostrarán en la cuadrícula del día, sin asignación a recursos visibles. const resources = [ { id: 'recursoA', title: 'Recurso A (Desarrollador)' }, { id: 'recursoB', title: 'Recurso B (Diseñador)' }, { id: 'recursoC', title: 'Recurso C (QA)' }, ]; const events = [ { id: '1', resourceId: 'recursoA', title: 'Diseño de Base de Datos', start: '2025-07-14T09:00:00', end: '2025-07-14T17:00:00', description: 'Definición de tablas y relaciones para el proyecto X.' }, { id: '2', resourceId: 'recursoA', title: 'Desarrollo API REST', start: '2025-07-15T09:00:00', end: '2025-07-17T17:00:00', description: 'Implementación de endpoints para la gestión de usuarios y productos.' }, { id: '3', resourceId: 'recursoB', title: 'Maquetación Home Page', start: '2025-07-14T10:00:00', end: '2025-07-14T18:00:00', description: 'Creación del diseño visual principal de la página de inicio.' }, { id: '4', resourceId: 'recursoC', title: 'Test de Integración', start: '2025-07-16T09:00:00', end: '2025-07-16T13:00:00', description: 'Verificación de la comunicación entre los módulos del sistema.' }, { id: '5', resourceId: 'recursoA', title: 'Implementar Autenticación', start: '2025-07-18T09:00:00', end: '2025-07-19T17:00:00', description: 'Integración de sistema de login/registro con OAuth2.' }, { id: '6', resourceId: 'recursoB', title: 'Diseño de Interfaz Móvil', start: '2025-07-15T09:00:00', end: '2025-07-16T17:00:00', description: 'Adaptación del diseño UX/UI para dispositivos móviles y tablets.' }, { id: '7', resourceId: 'recursoC', title: 'Pruebas de Rendimiento', start: '2025-07-17T14:00:00', end: '2025-07-17T18:00:00', description: 'Análisis de la velocidad y escalabilidad de la aplicación bajo carga.' }, { id: '8', resourceId: 'recursoA', title: 'Refactorización Código', start: '2025-07-22T09:00:00', end: '2025-07-22T17:00:00', description: 'Mejora de la calidad, legibilidad y eficiencia del código base.' }, { id: '9', resourceId: 'recursoB', title: 'Creación de Iconos', start: '2025-07-17T10:00:00', end: '2025-07-17T15:00:00', description: 'Diseño de nuevos elementos gráficos e iconos para la interfaz de usuario.' }, { id: '10', resourceId: 'recursoC', title: 'Preparación de Release', start: '2025-07-23T09:00:00', end: '2025-07-23T12:00:00', description: 'Últimas comprobaciones, empaquetado y preparación para el despliegue.' }, ]; let options = $state({ // Solo incluimos DayGrid ahora plugins: [ DayGrid, // Eliminamos los demás plugins para simplificar ], initialDate: '2025-07-14', initialView: 'dayGridMonth', // Mantenemos la vista de mes headerToolbar: { start: 'today prev,next', // Solo botones de navegación y hoy center: 'title', // En la vista de mes (DayGrid), solo "dayGridMonth" tiene sentido aquí // Quitamos las vistas de recursos y tiempo ya que sus plugins no están cargados. end: 'dayGridMonth', // Solo DayGridMonth }, buttonText: { today: 'Hoy', dayGridMonth: 'Mes', // No necesitamos botones para las vistas de recursos o tiempo }, // Eliminamos las definiciones de 'views' para resourceTimelineWeek y resourceTimelineDay // ya que no estamos usando esos plugins. views: { // Si DayGridMonth necesita personalización, la añadiríamos aquí. // Por defecto, con DayGrid plugin, 'dayGridMonth' funciona sin definición explícita. }, // Con DayGrid, `resources` no se visualizan como columnas. // Los eventos seguirán referenciando sus resourceId, pero la UI no los usará para agrupar. resources: resources, // Lo mantenemos por si lo vuelves a usar, pero no afecta DayGrid visualmente events: events, locale: 'es', eventMouseEnter: function(arg) { const eventTitle = arg.event.title; const eventDescription = arg.event.extendedProps.description || 'Sin descripción'; arg.el.setAttribute('title', `Tarea: ${eventTitle}\nDescripción: ${eventDescription}`); arg.el.style.cursor = 'pointer'; }, eventMouseLeave: function(arg) { arg.el.removeAttribute('title'); }, eventClick: function(arg) { selectedTask = { id: arg.event.id, title: arg.event.title, // Al mostrar el modal, puedes seguir buscando el recurso si lo necesitas resource: resources.find(r => r.id === arg.event.resourceId)?.title || 'Recurso desconocido', start: arg.event.startStr, end: arg.event.endStr, description: arg.event.extendedProps.description || 'No hay descripción disponible para esta tarea.', }; showModal = true; }, }); function closeModal() { showModal = false; selectedTask = null; } function handleModalKeydown(event) { if (event.key === 'Escape') { closeModal(); } } </script> <style> /* Tu CSS permanece igual */ :global(.ec-calendar) { font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; max-width: 1200px; margin: 40px auto; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); } .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.65); display: flex; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3); max-width: 550px; width: 90%; position: relative; color: #333; animation: fadeInScale 0.3s ease-out forwards; } @keyframes fadeInScale { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } .modal-content h3 { margin-top: 0; color: #0056b3; font-size: 1.6em; border-bottom: 2px solid #f0f0f0; padding-bottom: 10px; margin-bottom: 20px; } .modal-content p { margin-bottom: 12px; line-height: 1.7; font-size: 1.05em; } .modal-content strong { color: #007bff; } .modal-close-button { position: absolute; top: 15px; right: 15px; background: none; border: none; font-size: 1.8em; cursor: pointer; color: #888; line-height: 1; transition: color 0.2s ease; } .modal-close-button:hover { color: #333; } </style> <Calendar plugins={[DayGrid]} {options} /> {#if showModal && selectedTask} <div class="modal-overlay" onclick={closeModal} self role="dialog" aria-modal="true" tabindex="-1" onkeydown={handleModalKeydown} > <div class="modal-content"> <button type="button" class="modal-close-button" onclick={closeModal} aria-label="Cerrar modal">×</button> <h3>Detalles de la Tarea: {selectedTask.title}</h3> <p><strong>Recurso Asignado:</strong> {selectedTask.resource}</p> <p><strong>Inicio:</strong> {new Date(selectedTask.start).toLocaleString('es-ES', { dateStyle: 'full', timeStyle: 'short' })}</p> <p><strong>Fin:</strong> {new Date(selectedTask.end).toLocaleString('es-ES', { dateStyle: 'full', timeStyle: 'short' })}</p> <p><strong>Descripción:</strong> {selectedTask.description}</p> </div> </div> {/if}
Como hago en todos los casos, os dejo en ficheros adjuntos los proyectos para que los descarguéis y podáis hacer cualquier prueba.